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 }