1// Copyright 2017 syzkaller project authors. All rights reserved. 2// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. 3 4package vcs 5 6import ( 7 "bufio" 8 "bytes" 9 "fmt" 10 "io" 11 "net/mail" 12 "os" 13 "os/exec" 14 "sort" 15 "strconv" 16 "strings" 17 "time" 18 19 "github.com/google/syzkaller/pkg/osutil" 20) 21 22type git struct { 23 os string 24 vm string 25 dir string 26} 27 28func newGit(os, vm, dir string) *git { 29 return &git{ 30 os: os, 31 vm: vm, 32 dir: dir, 33 } 34} 35 36func (git *git) Poll(repo, branch string) (*Commit, error) { 37 dir := git.dir 38 runSandboxed(dir, "git", "bisect", "reset") 39 runSandboxed(dir, "git", "reset", "--hard") 40 origin, err := runSandboxed(dir, "git", "remote", "get-url", "origin") 41 if err != nil || strings.TrimSpace(string(origin)) != repo { 42 // The repo is here, but it has wrong origin (e.g. repo in config has changed), re-clone. 43 if err := git.clone(repo, branch); err != nil { 44 return nil, err 45 } 46 } 47 // Use origin/branch for the case the branch was force-pushed, 48 // in such case branch is not the same is origin/branch and we will 49 // stuck with the local version forever (git checkout won't fail). 50 if _, err := runSandboxed(dir, "git", "checkout", "origin/"+branch); err != nil { 51 // No such branch (e.g. branch in config has changed), re-clone. 52 if err := git.clone(repo, branch); err != nil { 53 return nil, err 54 } 55 } 56 if _, err := runSandboxed(dir, "git", "fetch", "--no-tags"); err != nil { 57 // Something else is wrong, re-clone. 58 if err := git.clone(repo, branch); err != nil { 59 return nil, err 60 } 61 } 62 if _, err := runSandboxed(dir, "git", "checkout", "origin/"+branch); err != nil { 63 return nil, err 64 } 65 return git.HeadCommit() 66} 67 68func (git *git) CheckoutBranch(repo, branch string) (*Commit, error) { 69 dir := git.dir 70 runSandboxed(dir, "git", "bisect", "reset") 71 if _, err := runSandboxed(dir, "git", "reset", "--hard"); err != nil { 72 if err := git.initRepo(); err != nil { 73 return nil, err 74 } 75 } 76 _, err := runSandboxed(dir, "git", "fetch", repo, branch) 77 if err != nil { 78 return nil, err 79 } 80 if _, err := runSandboxed(dir, "git", "checkout", "FETCH_HEAD"); err != nil { 81 return nil, err 82 } 83 return git.HeadCommit() 84} 85 86func (git *git) CheckoutCommit(repo, commit string) (*Commit, error) { 87 dir := git.dir 88 runSandboxed(dir, "git", "bisect", "reset") 89 if _, err := runSandboxed(dir, "git", "reset", "--hard"); err != nil { 90 if err := git.initRepo(); err != nil { 91 return nil, err 92 } 93 } 94 _, err := runSandboxed(dir, "git", "fetch", repo) 95 if err != nil { 96 return nil, err 97 } 98 return git.SwitchCommit(commit) 99} 100 101func (git *git) SwitchCommit(commit string) (*Commit, error) { 102 dir := git.dir 103 if _, err := runSandboxed(dir, "git", "checkout", commit); err != nil { 104 return nil, err 105 } 106 return git.HeadCommit() 107} 108 109func (git *git) clone(repo, branch string) error { 110 if err := git.initRepo(); err != nil { 111 return err 112 } 113 if _, err := runSandboxed(git.dir, "git", "remote", "add", "origin", repo); err != nil { 114 return err 115 } 116 if _, err := runSandboxed(git.dir, "git", "fetch", "origin", branch); err != nil { 117 return err 118 } 119 return nil 120} 121 122func (git *git) initRepo() error { 123 if err := os.RemoveAll(git.dir); err != nil { 124 return fmt.Errorf("failed to remove repo dir: %v", err) 125 } 126 if err := osutil.MkdirAll(git.dir); err != nil { 127 return fmt.Errorf("failed to create repo dir: %v", err) 128 } 129 if err := osutil.SandboxChown(git.dir); err != nil { 130 return err 131 } 132 if _, err := runSandboxed(git.dir, "git", "init"); err != nil { 133 return err 134 } 135 return nil 136} 137 138func (git *git) HeadCommit() (*Commit, error) { 139 return git.getCommit("HEAD") 140} 141 142func (git *git) getCommit(commit string) (*Commit, error) { 143 output, err := runSandboxed(git.dir, "git", "log", "--format=%H%n%s%n%ae%n%ad%n%b", "-n", "1", commit) 144 if err != nil { 145 return nil, err 146 } 147 return gitParseCommit(output) 148} 149 150func gitParseCommit(output []byte) (*Commit, error) { 151 lines := bytes.Split(output, []byte{'\n'}) 152 if len(lines) < 4 || len(lines[0]) != 40 { 153 return nil, fmt.Errorf("unexpected git log output: %q", output) 154 } 155 const dateFormat = "Mon Jan 2 15:04:05 2006 -0700" 156 date, err := time.Parse(dateFormat, string(lines[3])) 157 if err != nil { 158 return nil, fmt.Errorf("failed to parse date in git log output: %v\n%q", err, output) 159 } 160 cc := make(map[string]bool) 161 cc[strings.ToLower(string(lines[2]))] = true 162 for _, line := range lines[4:] { 163 for _, re := range ccRes { 164 matches := re.FindSubmatchIndex(line) 165 if matches == nil { 166 continue 167 } 168 addr, err := mail.ParseAddress(string(line[matches[2]:matches[3]])) 169 if err != nil { 170 break 171 } 172 cc[strings.ToLower(addr.Address)] = true 173 break 174 } 175 } 176 sortedCC := make([]string, 0, len(cc)) 177 for addr := range cc { 178 sortedCC = append(sortedCC, addr) 179 } 180 sort.Strings(sortedCC) 181 com := &Commit{ 182 Hash: string(lines[0]), 183 Title: string(lines[1]), 184 Author: string(lines[2]), 185 CC: sortedCC, 186 Date: date, 187 } 188 return com, nil 189} 190 191func (git *git) ListRecentCommits(baseCommit string) ([]string, error) { 192 // On upstream kernel this produces ~11MB of output. 193 // Somewhat inefficient to collect whole output in a slice 194 // and then convert to string, but should be bearable. 195 output, err := runSandboxed(git.dir, "git", "log", 196 "--pretty=format:%s", "--no-merges", "-n", "200000", baseCommit) 197 if err != nil { 198 return nil, err 199 } 200 return strings.Split(string(output), "\n"), nil 201} 202 203func (git *git) ExtractFixTagsFromCommits(baseCommit, email string) ([]FixCommit, error) { 204 since := time.Now().Add(-time.Hour * 24 * 365).Format("01-02-2006") 205 cmd := exec.Command("git", "log", "--no-merges", "--since", since, baseCommit) 206 cmd.Dir = git.dir 207 stdout, err := cmd.StdoutPipe() 208 if err != nil { 209 return nil, err 210 } 211 if err := cmd.Start(); err != nil { 212 return nil, err 213 } 214 defer cmd.Wait() 215 defer cmd.Process.Kill() 216 return gitExtractFixTags(stdout, email) 217} 218 219func gitExtractFixTags(r io.Reader, email string) ([]FixCommit, error) { 220 user, domain, err := splitEmail(email) 221 if err != nil { 222 return nil, fmt.Errorf("failed to parse email %q: %v", email, err) 223 } 224 var ( 225 s = bufio.NewScanner(r) 226 commits []FixCommit 227 commitTitle = "" 228 commitStart = []byte("commit ") 229 bodyPrefix = []byte(" ") 230 userBytes = []byte(user + "+") 231 domainBytes = []byte(domain) 232 ) 233 for s.Scan() { 234 ln := s.Bytes() 235 if bytes.HasPrefix(ln, commitStart) { 236 commitTitle = "" 237 continue 238 } 239 if !bytes.HasPrefix(ln, bodyPrefix) { 240 continue 241 } 242 ln = ln[len(bodyPrefix):] 243 if len(ln) == 0 { 244 continue 245 } 246 if commitTitle == "" { 247 commitTitle = string(ln) 248 continue 249 } 250 userPos := bytes.Index(ln, userBytes) 251 if userPos == -1 { 252 continue 253 } 254 domainPos := bytes.Index(ln[userPos+len(userBytes)+1:], domainBytes) 255 if domainPos == -1 { 256 continue 257 } 258 startPos := userPos + len(userBytes) 259 endPos := userPos + len(userBytes) + domainPos + 1 260 tag := string(ln[startPos:endPos]) 261 commits = append(commits, FixCommit{tag, commitTitle}) 262 } 263 return commits, s.Err() 264} 265 266func splitEmail(email string) (user, domain string, err error) { 267 addr, err := mail.ParseAddress(email) 268 if err != nil { 269 return "", "", err 270 } 271 at := strings.IndexByte(addr.Address, '@') 272 if at == -1 { 273 return "", "", fmt.Errorf("no @ in email address") 274 } 275 user = addr.Address[:at] 276 domain = addr.Address[at:] 277 if plus := strings.IndexByte(user, '+'); plus != -1 { 278 user = user[:plus] 279 } 280 return 281} 282 283func (git *git) Bisect(bad, good string, trace io.Writer, pred func() (BisectResult, error)) (*Commit, error) { 284 dir := git.dir 285 runSandboxed(dir, "git", "bisect", "reset") 286 runSandboxed(dir, "git", "reset", "--hard") 287 firstBad, err := git.getCommit(bad) 288 if err != nil { 289 return nil, err 290 } 291 output, err := runSandboxed(dir, "git", "bisect", "start", bad, good) 292 if err != nil { 293 return nil, err 294 } 295 defer runSandboxed(dir, "git", "bisect", "reset") 296 fmt.Fprintf(trace, "# git bisect start %v %v\n%s", bad, good, output) 297 current, err := git.HeadCommit() 298 if err != nil { 299 return nil, err 300 } 301 var bisectTerms = [...]string{ 302 BisectBad: "bad", 303 BisectGood: "good", 304 BisectSkip: "skip", 305 } 306 for { 307 res, err := pred() 308 if err != nil { 309 return nil, err 310 } 311 if res == BisectBad { 312 firstBad = current 313 } 314 output, err = runSandboxed(dir, "git", "bisect", bisectTerms[res]) 315 if err != nil { 316 return nil, err 317 } 318 fmt.Fprintf(trace, "# git bisect %v %v\n%s", bisectTerms[res], current.Hash, output) 319 next, err := git.HeadCommit() 320 if err != nil { 321 return nil, err 322 } 323 if current.Hash == next.Hash { 324 return firstBad, nil 325 } 326 current = next 327 } 328} 329 330// Note: linux-specific. 331func (git *git) PreviousReleaseTags(commit string) ([]string, error) { 332 output, err := runSandboxed(git.dir, "git", "tag", "--no-contains", commit, "--merged", commit, "v*.*") 333 if err != nil { 334 return nil, err 335 } 336 return gitParseReleaseTags(output) 337} 338 339func gitParseReleaseTags(output []byte) ([]string, error) { 340 var tags []string 341 for _, tag := range bytes.Split(output, []byte{'\n'}) { 342 if releaseTagRe.Match(tag) && gitReleaseTagToInt(string(tag)) != 0 { 343 tags = append(tags, string(tag)) 344 } 345 } 346 sort.Slice(tags, func(i, j int) bool { 347 return gitReleaseTagToInt(tags[i]) > gitReleaseTagToInt(tags[j]) 348 }) 349 return tags, nil 350} 351 352func gitReleaseTagToInt(tag string) uint64 { 353 matches := releaseTagRe.FindStringSubmatchIndex(tag) 354 v1, err := strconv.ParseUint(tag[matches[2]:matches[3]], 10, 64) 355 if err != nil { 356 return 0 357 } 358 v2, err := strconv.ParseUint(tag[matches[4]:matches[5]], 10, 64) 359 if err != nil { 360 return 0 361 } 362 var v3 uint64 363 if matches[6] != -1 { 364 v3, err = strconv.ParseUint(tag[matches[6]:matches[7]], 10, 64) 365 if err != nil { 366 return 0 367 } 368 } 369 return v1*1e6 + v2*1e3 + v3 370} 371