github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/codeql/github_repo_upload.go (about)

     1  package codeql
     2  
     3  import (
     4  	"archive/zip"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path"
     9  	"path/filepath"
    10  	"runtime"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/SAP/jenkins-library/pkg/command"
    15  	"github.com/SAP/jenkins-library/pkg/log"
    16  	"github.com/go-git/go-git/v5"
    17  	"github.com/go-git/go-git/v5/config"
    18  	"github.com/go-git/go-git/v5/plumbing"
    19  	"github.com/go-git/go-git/v5/plumbing/object"
    20  	"github.com/go-git/go-git/v5/plumbing/transport/http"
    21  	"github.com/go-git/go-git/v5/storage/memory"
    22  	"gopkg.in/yaml.v2"
    23  )
    24  
    25  type GitUploader interface {
    26  	UploadProjectToGithub() (string, error)
    27  }
    28  
    29  type GitUploaderInstance struct {
    30  	*command.Command
    31  
    32  	token          string
    33  	ref            string
    34  	sourceCommitId string
    35  	sourceRepo     string
    36  	targetRepo     string
    37  	dbDir          string
    38  }
    39  
    40  func NewGitUploaderInstance(token, ref, dbDir, sourceCommitId, sourceRepo, targetRepo string) (*GitUploaderInstance, error) {
    41  	dbAbsPath, err := filepath.Abs(dbDir)
    42  	if err != nil {
    43  		return nil, err
    44  	}
    45  	instance := &GitUploaderInstance{
    46  		Command:        &command.Command{},
    47  		token:          token,
    48  		ref:            ref,
    49  		sourceCommitId: sourceCommitId,
    50  		sourceRepo:     sourceRepo,
    51  		targetRepo:     targetRepo,
    52  		dbDir:          filepath.Clean(dbAbsPath),
    53  	}
    54  
    55  	instance.Stdout(log.Writer())
    56  	instance.Stderr(log.Writer())
    57  	return instance, nil
    58  }
    59  
    60  type gitUtils interface {
    61  	listRemote() ([]reference, error)
    62  	cloneRepo(dir string, opts *git.CloneOptions) (*git.Repository, error)
    63  	switchOrphan(ref string, repo *git.Repository) error
    64  }
    65  
    66  type repository interface {
    67  	Worktree() (*git.Worktree, error)
    68  	CommitObject(commit plumbing.Hash) (*object.Commit, error)
    69  	Push(o *git.PushOptions) error
    70  }
    71  
    72  type worktree interface {
    73  	RemoveGlob(pattern string) error
    74  	Clean(opts *git.CleanOptions) error
    75  	AddWithOptions(opts *git.AddOptions) error
    76  	Commit(msg string, opts *git.CommitOptions) (plumbing.Hash, error)
    77  }
    78  
    79  type reference interface {
    80  	Name() plumbing.ReferenceName
    81  }
    82  
    83  const (
    84  	CommitMessageMirroringCode = "Mirroring code for revision %s from %s"
    85  	SrcZip                     = "src.zip"
    86  	codeqlDatabaseYml          = "codeql-database.yml"
    87  )
    88  
    89  func (uploader *GitUploaderInstance) UploadProjectToGithub() (string, error) {
    90  	tmpDir, err := os.MkdirTemp("", "tmp")
    91  	if err != nil {
    92  		return "", err
    93  	}
    94  	defer os.RemoveAll(tmpDir)
    95  
    96  	refExists, err := doesRefExist(uploader, uploader.ref)
    97  	if err != nil {
    98  		return "", err
    99  	}
   100  
   101  	repo, err := clone(uploader, uploader.targetRepo, uploader.token, uploader.ref, tmpDir, refExists)
   102  	if err != nil {
   103  		return "", err
   104  	}
   105  
   106  	tree, err := repo.Worktree()
   107  	if err != nil {
   108  		return "", err
   109  	}
   110  	err = cleanDir(tree)
   111  	if err != nil {
   112  		return "", err
   113  	}
   114  
   115  	srcLocationPrefix, err := getSourceLocationPrefix(filepath.Join(uploader.dbDir, codeqlDatabaseYml))
   116  	if err != nil {
   117  		return "", err
   118  	}
   119  
   120  	zipPath := path.Join(uploader.dbDir, SrcZip)
   121  	err = unzip(zipPath, tmpDir, strings.Trim(srcLocationPrefix, fmt.Sprintf("%c", os.PathSeparator)))
   122  	if err != nil {
   123  		return "", err
   124  	}
   125  
   126  	err = add(tree)
   127  	if err != nil {
   128  		return "", err
   129  	}
   130  
   131  	newCommit, err := commit(repo, tree, uploader.sourceCommitId, uploader.sourceRepo)
   132  	if err != nil {
   133  		return "", err
   134  	}
   135  
   136  	err = push(repo, uploader.token)
   137  	if err != nil {
   138  		return "", err
   139  	}
   140  
   141  	return newCommit.ID().String(), err
   142  }
   143  
   144  func (uploader *GitUploaderInstance) listRemote() ([]reference, error) {
   145  	rem := git.NewRemote(memory.NewStorage(), &config.RemoteConfig{
   146  		Name: "origin",
   147  		URLs: []string{uploader.targetRepo},
   148  	})
   149  
   150  	list, err := rem.List(&git.ListOptions{
   151  		Auth: &http.BasicAuth{
   152  			Username: "does-not-matter",
   153  			Password: uploader.token,
   154  		},
   155  	})
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  	var convertedList []reference
   160  	for _, ref := range list {
   161  		convertedList = append(convertedList, ref)
   162  	}
   163  	return convertedList, err
   164  }
   165  
   166  func (uploader *GitUploaderInstance) cloneRepo(dir string, opts *git.CloneOptions) (*git.Repository, error) {
   167  	return git.PlainClone(dir, false, opts)
   168  }
   169  
   170  func (uploader *GitUploaderInstance) switchOrphan(ref string, r *git.Repository) error {
   171  	branchName := strings.Split(ref, "/")[2:]
   172  	newRef := plumbing.NewBranchReferenceName(strings.Join(branchName, "/"))
   173  	return r.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, newRef))
   174  }
   175  
   176  func doesRefExist(uploader gitUtils, ref string) (bool, error) {
   177  	// git ls-remote <repo>
   178  	remoteRefs, err := uploader.listRemote()
   179  	if err != nil {
   180  		return false, err
   181  	}
   182  	for _, r := range remoteRefs {
   183  		if string(r.Name()) == ref {
   184  			return true, nil
   185  		}
   186  	}
   187  	return false, nil
   188  }
   189  
   190  func clone(uploader gitUtils, url, token, ref, dir string, refExists bool) (*git.Repository, error) {
   191  	opts := &git.CloneOptions{
   192  		URL: url,
   193  		Auth: &http.BasicAuth{
   194  			Username: "does-not-matter",
   195  			Password: token,
   196  		},
   197  		SingleBranch: true,
   198  		Depth:        1,
   199  	}
   200  	if refExists {
   201  		opts.ReferenceName = plumbing.ReferenceName(ref)
   202  		// git clone -b <ref> --single-branch --depth=1 <url> <dir>
   203  		return uploader.cloneRepo(dir, opts)
   204  	}
   205  
   206  	// git clone --single-branch --depth=1 <url> <dir>
   207  	r, err := uploader.cloneRepo(dir, opts)
   208  	if err != nil {
   209  		return nil, err
   210  	}
   211  
   212  	// git switch --orphan <ref>
   213  	err = uploader.switchOrphan(ref, r)
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  	return r, nil
   218  }
   219  
   220  func cleanDir(t worktree) error {
   221  	// git rm -r
   222  	err := t.RemoveGlob("*")
   223  	if err != nil {
   224  		return err
   225  	}
   226  	// git clean -d
   227  	err = t.Clean(&git.CleanOptions{Dir: true})
   228  	return err
   229  }
   230  
   231  func add(t worktree) error {
   232  	// git add --all
   233  	return t.AddWithOptions(&git.AddOptions{
   234  		All: true,
   235  	})
   236  }
   237  
   238  func commit(r repository, t worktree, sourceCommitId, sourceRepo string) (*object.Commit, error) {
   239  	// git commit --allow-empty -m <msg>
   240  	newCommit, err := t.Commit(fmt.Sprintf(CommitMessageMirroringCode, sourceCommitId, sourceRepo), &git.CommitOptions{
   241  		AllowEmptyCommits: true,
   242  		Author: &object.Signature{
   243  			When: time.Now(),
   244  		},
   245  	})
   246  	if err != nil {
   247  		return nil, err
   248  	}
   249  	return r.CommitObject(newCommit)
   250  }
   251  
   252  func push(r repository, token string) error {
   253  	// git push
   254  	return r.Push(&git.PushOptions{
   255  		Auth: &http.BasicAuth{
   256  			Username: "does-not-matter",
   257  			Password: token,
   258  		},
   259  	})
   260  }
   261  
   262  func unzip(zipPath, targetDir, srcDir string) error {
   263  	r, err := zip.OpenReader(zipPath)
   264  	if err != nil {
   265  		return err
   266  	}
   267  	defer r.Close()
   268  
   269  	for _, f := range r.File {
   270  		fName := f.Name
   271  
   272  		if runtime.GOOS == "windows" {
   273  			fNameSplit := strings.Split(fName, "/")
   274  			if len(fNameSplit) == 0 {
   275  				continue
   276  			}
   277  			fNameSplit[0] = strings.Replace(fNameSplit[0], "_", ":", 1)
   278  			fName = strings.Join(fNameSplit, fmt.Sprintf("%c", os.PathSeparator))
   279  		}
   280  		if !strings.Contains(fName, srcDir) {
   281  			continue
   282  		}
   283  
   284  		rc, err := f.Open()
   285  		if err != nil {
   286  			return err
   287  		}
   288  
   289  		fName = strings.TrimPrefix(fName, srcDir)
   290  		fpath := filepath.Join(targetDir, fName)
   291  		if f.FileInfo().IsDir() {
   292  			os.MkdirAll(fpath, os.ModePerm)
   293  			rc.Close()
   294  			continue
   295  		}
   296  		err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm)
   297  		if err != nil {
   298  			rc.Close()
   299  			return err
   300  		}
   301  
   302  		fNew, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
   303  		if err != nil {
   304  			rc.Close()
   305  			return err
   306  		}
   307  
   308  		_, err = io.Copy(fNew, rc)
   309  		if err != nil {
   310  			rc.Close()
   311  			fNew.Close()
   312  			return err
   313  		}
   314  		rc.Close()
   315  		fNew.Close()
   316  	}
   317  	return nil
   318  }
   319  
   320  func getSourceLocationPrefix(fileName string) (string, error) {
   321  	type codeqlDatabase struct {
   322  		SourceLocation string `yaml:"sourceLocationPrefix"`
   323  	}
   324  	var db codeqlDatabase
   325  	file, err := os.ReadFile(fileName)
   326  	if err != nil {
   327  		return "", err
   328  	}
   329  	err = yaml.Unmarshal(file, &db)
   330  	if err != nil {
   331  		return "", err
   332  	}
   333  
   334  	return db.SourceLocation, nil
   335  }