github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/pkg/vcs/git.go (about) 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 4 package vcs 5 6 import ( 7 "bufio" 8 "bytes" 9 "errors" 10 "fmt" 11 "net/mail" 12 "os" 13 "os/exec" 14 "regexp" 15 "sort" 16 "strings" 17 "time" 18 19 "github.com/google/syzkaller/pkg/debugtracer" 20 "github.com/google/syzkaller/pkg/hash" 21 "github.com/google/syzkaller/pkg/log" 22 "github.com/google/syzkaller/pkg/osutil" 23 ) 24 25 type gitRepo struct { 26 *Git 27 } 28 29 func newGitRepo(dir string, ignoreCC map[string]bool, opts []RepoOpt) *gitRepo { 30 git := &gitRepo{ 31 Git: &Git{ 32 Dir: dir, 33 Sandbox: true, 34 Env: filterEnv(), 35 ignoreCC: ignoreCC, 36 }, 37 } 38 for _, opt := range opts { 39 switch opt { 40 case OptPrecious: 41 git.precious = true 42 case OptDontSandbox: 43 git.Sandbox = false 44 } 45 } 46 return git 47 } 48 49 func filterEnv() []string { 50 // We have to filter various git environment variables - if 51 // these variables are set (e.g. if a test is being run as 52 // part of a rebase) we're going to be acting on some other 53 // repository (e.g the syzkaller tree itself) rather than the 54 // intended repo. 55 env := os.Environ() 56 for i := 0; i < len(env); i++ { 57 if strings.HasPrefix(env[i], "GIT_DIR") || 58 strings.HasPrefix(env[i], "GIT_WORK_TREE") || 59 strings.HasPrefix(env[i], "GIT_INDEX_FILE") || 60 strings.HasPrefix(env[i], "GIT_OBJECT_DIRECTORY") { 61 env = append(env[:i], env[i+1:]...) 62 i-- 63 } 64 } 65 66 return env 67 } 68 69 func (git *gitRepo) Poll(repo, branch string) (*Commit, error) { 70 git.Reset() 71 origin, err := git.Run("remote", "get-url", "origin") 72 if err != nil || strings.TrimSpace(string(origin)) != repo { 73 // The repo is here, but it has wrong origin (e.g. repo in config has changed), re-clone. 74 if err := git.clone(repo, branch); err != nil { 75 return nil, err 76 } 77 } 78 // Use origin/branch for the case the branch was force-pushed, 79 // in such case branch is not the same is origin/branch and we will 80 // stuck with the local version forever (git checkout won't fail). 81 if _, err := git.Run("checkout", "origin/"+branch); err != nil { 82 // No such branch (e.g. branch in config has changed), re-clone. 83 if err := git.clone(repo, branch); err != nil { 84 return nil, err 85 } 86 } 87 if output, err := git.Run("fetch", "--force"); err != nil { 88 if git.isNetworkError(output) { 89 // The clone operation will fail as well, so no sense to re-clone. 90 return nil, err 91 } 92 if err := git.clone(repo, branch); err != nil { 93 return nil, err 94 } 95 } 96 if _, err := git.Run("checkout", "origin/"+branch); err != nil { 97 return nil, err 98 } 99 if _, err := git.Run("submodule", "update", "--init"); err != nil { 100 return nil, err 101 } 102 return git.Commit(HEAD) 103 } 104 105 func (git *gitRepo) isNetworkError(output []byte) bool { 106 // The list is not exhaustive and is meant to be extended over time. 107 return bytes.Contains(output, []byte("fatal: read error: Connection reset by peer")) 108 } 109 110 func (git *gitRepo) CheckoutBranch(repo, branch string) (*Commit, error) { 111 if err := git.repair(); err != nil { 112 return nil, err 113 } 114 repoHash := hash.String([]byte(repo)) 115 // Because the HEAD is detached, submodules assumes "origin" to be the default 116 // remote when initializing. 117 // This sets "origin" to be the current remote. 118 // Ignore errors as we can double add or remove the same remote and that will fail. 119 git.Run("remote", "rm", "origin") 120 git.Run("remote", "add", "origin", repo) 121 git.Run("remote", "add", repoHash, repo) 122 _, err := git.Run("fetch", "--force", repoHash, branch) 123 if err != nil { 124 return nil, err 125 } 126 if _, err := git.Run("checkout", "FETCH_HEAD", "--force"); err != nil { 127 return nil, err 128 } 129 if _, err := git.Run("submodule", "update", "--init"); err != nil { 130 return nil, err 131 } 132 // If the branch checkout had to be "forced" the directory may 133 // contain remaining untracked files. 134 // Clean again to ensure the new branch is in a clean state. 135 if err := git.repair(); err != nil { 136 return nil, err 137 } 138 return git.Commit(HEAD) 139 } 140 141 func (git *gitRepo) CheckoutCommit(repo, commit string) (*Commit, error) { 142 if err := git.repair(); err != nil { 143 return nil, err 144 } 145 if err := git.fetchRemote(repo, commit); err != nil { 146 return nil, err 147 } 148 return git.SwitchCommit(commit) 149 } 150 151 func (git *gitRepo) fetchRemote(repo, commit string) error { 152 repoHash := hash.String([]byte(repo)) 153 // Ignore error as we can double add the same remote and that will fail. 154 git.Run("remote", "add", repoHash, repo) 155 fetchArgs := []string{"fetch", "--force", "--tags", repoHash} 156 if commit != "" && gitFullHashRe.MatchString(commit) { 157 // This trick only works with full commit hashes. 158 fetchArgs = append(fetchArgs, commit) 159 } 160 _, err := git.Run(fetchArgs...) 161 if err != nil { 162 var verbose *osutil.VerboseError 163 if errors.As(err, &verbose) && 164 bytes.Contains(verbose.Output, []byte("error: cannot lock ref")) { 165 // It can happen that the fetched repo has tags names that conflict 166 // with the ones already present in the repository. 167 // Try to fetch more, but this time prune tags, it should help. 168 // The --prune-tags option will remove all tags that are not present 169 // in this remote repo, so don't do it always. Only when necessary. 170 _, err = git.Run("fetch", "--force", "--tags", "--prune", "--prune-tags", repoHash) 171 } 172 } 173 return err 174 } 175 176 func (git *gitRepo) SwitchCommit(commit string) (*Commit, error) { 177 if !git.precious { 178 git.Run("reset", "--hard") 179 git.Run("clean", "-fdx") 180 } 181 if _, err := git.Run("checkout", commit); err != nil { 182 return nil, err 183 } 184 if _, err := git.Run("submodule", "update", "--init"); err != nil { 185 return nil, err 186 } 187 return git.Commit(HEAD) 188 } 189 190 func (git *gitRepo) clone(repo, branch string) error { 191 if git.precious { 192 return fmt.Errorf("won't reinit precious repo") 193 } 194 if err := git.initRepo(nil); err != nil { 195 return err 196 } 197 if _, err := git.Run("remote", "add", "origin", repo); err != nil { 198 return err 199 } 200 if _, err := git.Run("fetch", "origin", branch); err != nil { 201 return err 202 } 203 return nil 204 } 205 206 func (git *gitRepo) repair() error { 207 if err := git.Reset(); err != nil { 208 return git.initRepo(err) 209 } 210 return nil 211 } 212 213 func (git *gitRepo) initRepo(reason error) error { 214 if reason != nil { 215 log.Logf(1, "git: initializing repo at %v: %v", git.Dir, reason) 216 } 217 if err := os.RemoveAll(git.Dir); err != nil { 218 return fmt.Errorf("failed to remove repo dir: %w", err) 219 } 220 if err := osutil.MkdirAll(git.Dir); err != nil { 221 return fmt.Errorf("failed to create repo dir: %w", err) 222 } 223 if git.Sandbox { 224 if err := osutil.SandboxChown(git.Dir); err != nil { 225 return err 226 } 227 } 228 if _, err := git.Run("init"); err != nil { 229 return err 230 } 231 return nil 232 } 233 234 func (git *gitRepo) Contains(commit string) (bool, error) { 235 _, err := git.Run("merge-base", "--is-ancestor", commit, HEAD) 236 return err == nil, nil 237 } 238 239 const gitDateFormat = "Mon Jan 2 15:04:05 2006 -0700" 240 241 func gitParseCommit(output, user, domain []byte, ignoreCC map[string]bool) (*Commit, error) { 242 lines := bytes.Split(output, []byte{'\n'}) 243 if len(lines) < 8 || len(lines[0]) != 40 { 244 return nil, fmt.Errorf("unexpected git log output: %q", output) 245 } 246 date, err := time.Parse(gitDateFormat, string(lines[4])) 247 if err != nil { 248 return nil, fmt.Errorf("failed to parse date in git log output: %w\n%q", err, output) 249 } 250 commitDate, err := time.Parse(gitDateFormat, string(lines[6])) 251 if err != nil { 252 return nil, fmt.Errorf("failed to parse date in git log output: %w\n%q", err, output) 253 } 254 recipients := make(map[string]bool) 255 recipients[strings.ToLower(string(lines[2]))] = true 256 var tags []string 257 // Use summary line + all description lines. 258 for _, line := range append([][]byte{lines[1]}, lines[7:]...) { 259 if user != nil { 260 userPos := bytes.Index(line, user) 261 if userPos != -1 { 262 domainPos := bytes.Index(line[userPos+len(user)+1:], domain) 263 if domainPos != -1 { 264 startPos := userPos + len(user) 265 endPos := userPos + len(user) + domainPos + 1 266 tag := string(line[startPos:endPos]) 267 present := false 268 for _, tag1 := range tags { 269 if tag1 == tag { 270 present = true 271 break 272 } 273 } 274 if !present { 275 tags = append(tags, tag) 276 } 277 } 278 } 279 } 280 for _, re := range ccRes { 281 matches := re.FindSubmatchIndex(line) 282 if matches == nil { 283 continue 284 } 285 addr, err := mail.ParseAddress(string(line[matches[2]:matches[3]])) 286 if err != nil { 287 break 288 } 289 email := strings.ToLower(addr.Address) 290 if ignoreCC[email] { 291 continue 292 } 293 recipients[email] = true 294 break 295 } 296 } 297 sortedRecipients := make(Recipients, 0, len(recipients)) 298 for addr := range recipients { 299 sortedRecipients = append(sortedRecipients, RecipientInfo{mail.Address{Address: addr}, To}) 300 } 301 sort.Sort(sortedRecipients) 302 parents := strings.Split(string(lines[5]), " ") 303 com := &Commit{ 304 Hash: string(lines[0]), 305 Title: string(lines[1]), 306 Author: string(lines[2]), 307 AuthorName: string(lines[3]), 308 Parents: parents, 309 Recipients: sortedRecipients, 310 Tags: tags, 311 Date: date, 312 CommitDate: commitDate, 313 } 314 return com, nil 315 } 316 317 func (git *gitRepo) GetCommitByTitle(title string) (*Commit, error) { 318 commits, _, err := git.GetCommitsByTitles([]string{title}) 319 if err != nil || len(commits) == 0 { 320 return nil, err 321 } 322 return commits[0], nil 323 } 324 325 const ( 326 fetchCommitsMaxAgeInYears = 5 327 ) 328 329 func (git *gitRepo) GetCommitsByTitles(titles []string) ([]*Commit, []string, error) { 330 var greps []string 331 m := make(map[string]string) 332 for _, title := range titles { 333 canonical := CanonicalizeCommit(title) 334 greps = append(greps, canonical) 335 m[canonical] = title 336 } 337 since := time.Now().Add(-time.Hour * 24 * 365 * fetchCommitsMaxAgeInYears).Format("01-02-2006") 338 commits, err := git.fetchCommits(since, HEAD, "", "", greps, true) 339 if err != nil { 340 return nil, nil, err 341 } 342 var results []*Commit 343 for _, com := range commits { 344 canonical := CanonicalizeCommit(com.Title) 345 if orig := m[canonical]; orig != "" { 346 delete(m, canonical) 347 results = append(results, com) 348 com.Title = orig 349 } 350 } 351 var missing []string 352 for _, orig := range m { 353 missing = append(missing, orig) 354 } 355 return results, missing, nil 356 } 357 358 func (git *gitRepo) LatestCommits(afterCommit string, afterDate time.Time) ([]CommitShort, error) { 359 args := []string{"log", "--pretty=format:%h:%cd"} 360 if !afterDate.IsZero() { 361 args = append(args, "--since", afterDate.Format(time.RFC3339)) 362 } 363 if afterCommit != "" { 364 args = append(args, afterCommit+"..") 365 } 366 output, err := git.Run(args...) 367 if err != nil { 368 return nil, err 369 } 370 if len(output) == 0 { 371 return nil, nil 372 } 373 var ret []CommitShort 374 for _, line := range strings.Split(string(output), "\n") { 375 hash, date, _ := strings.Cut(line, ":") 376 commitDate, err := time.Parse(gitDateFormat, date) 377 if err != nil { 378 return nil, fmt.Errorf("failed to parse date in %q: %w", line, err) 379 } 380 ret = append(ret, CommitShort{Hash: hash, CommitDate: commitDate}) 381 } 382 return ret, nil 383 } 384 385 func (git *gitRepo) ExtractFixTagsFromCommits(baseCommit, email string) ([]*Commit, error) { 386 user, domain, err := splitEmail(email) 387 if err != nil { 388 return nil, fmt.Errorf("failed to parse email %q: %w", email, err) 389 } 390 grep := user + "+.*" + domain 391 since := time.Now().Add(-time.Hour * 24 * 365 * fetchCommitsMaxAgeInYears).Format("01-02-2006") 392 return git.fetchCommits(since, baseCommit, user, domain, []string{grep}, false) 393 } 394 395 func splitEmail(email string) (user, domain string, err error) { 396 addr, err := mail.ParseAddress(email) 397 if err != nil { 398 return "", "", err 399 } 400 at := strings.IndexByte(addr.Address, '@') 401 if at == -1 { 402 return "", "", fmt.Errorf("no @ in email address") 403 } 404 user = addr.Address[:at] 405 domain = addr.Address[at:] 406 if plus := strings.IndexByte(user, '+'); plus != -1 { 407 user = user[:plus] 408 } 409 return 410 } 411 412 func (git *gitRepo) Bisect(bad, good string, dt debugtracer.DebugTracer, pred func() (BisectResult, 413 error)) ([]*Commit, error) { 414 git.Reset() 415 firstBad, err := git.Commit(bad) 416 if err != nil { 417 return nil, err 418 } 419 output, err := git.Run("bisect", "start", bad, good) 420 if err != nil { 421 return nil, err 422 } 423 defer git.Reset() 424 dt.Log("# git bisect start %v %v\n%s", bad, good, output) 425 current, err := git.Commit(HEAD) 426 if err != nil { 427 return nil, err 428 } 429 var bisectTerms = [...]string{ 430 BisectBad: "bad", 431 BisectGood: "good", 432 BisectSkip: "skip", 433 } 434 for { 435 res, err := pred() 436 // Linux EnvForCommit may cherry-pick some fixes, reset these before the next step. 437 git.Run("reset", "--hard") 438 if err != nil { 439 return nil, err 440 } 441 if res == BisectBad { 442 firstBad = current 443 } 444 output, err = git.Run("bisect", bisectTerms[res]) 445 dt.Log("# git bisect %v %v\n%s", bisectTerms[res], current.Hash, output) 446 if err != nil { 447 if bytes.Contains(output, []byte("There are only 'skip'ped commits left to test")) { 448 return git.bisectInconclusive(output) 449 } 450 return nil, err 451 } 452 next, err := git.Commit(HEAD) 453 if err != nil { 454 return nil, err 455 } 456 if current.Hash == next.Hash { 457 return []*Commit{firstBad}, nil 458 } 459 current = next 460 } 461 } 462 463 var gitFullHashRe = regexp.MustCompile("[a-f0-9]{40}") 464 465 func (git *gitRepo) bisectInconclusive(output []byte) ([]*Commit, error) { 466 // For inconclusive bisection git prints the following message: 467 // 468 // There are only 'skip'ped commits left to test. 469 // The first bad commit could be any of: 470 // 1f43f400a2cbb02f3d34de8fe30075c070254816 471 // 4d96e13ee9cd1f7f801e8c7f4b12f09d1da4a5d8 472 // 5cd856a5ef9aa189df757c322be34ad735a5b17f 473 // We cannot bisect more! 474 // 475 // For conclusive bisection: 476 // 477 // 7c3850adbcccc2c6c9e7ab23a7dcbc4926ee5b96 is the first bad commit 478 var commits []*Commit 479 for _, hash := range gitFullHashRe.FindAll(output, -1) { 480 com, err := git.Commit(string(hash)) 481 if err != nil { 482 return nil, err 483 } 484 commits = append(commits, com) 485 } 486 return commits, nil 487 } 488 489 func (git *gitRepo) ReleaseTag(commit string) (string, error) { 490 tags, err := git.previousReleaseTags(commit, true, true, true) 491 if err != nil { 492 return "", err 493 } 494 if len(tags) == 0 { 495 return "", fmt.Errorf("no release tags found for commit %v", commit) 496 } 497 return tags[0], nil 498 } 499 500 func (git *gitRepo) previousReleaseTags(commit string, self, onlyTop, includeRC bool) ([]string, error) { 501 var tags []string 502 if self { 503 output, err := git.Run("tag", "--list", "--points-at", commit, "--merged", commit, "v*.*") 504 if err != nil { 505 return nil, err 506 } 507 tags = gitParseReleaseTags(output, includeRC) 508 if onlyTop && len(tags) != 0 { 509 return tags, nil 510 } 511 } 512 output, err := git.Run("tag", "--no-contains", commit, "--merged", commit, "v*.*") 513 if err != nil { 514 return nil, err 515 } 516 tags1 := gitParseReleaseTags(output, includeRC) 517 tags = append(tags, tags1...) 518 if len(tags) == 0 { 519 return nil, fmt.Errorf("no release tags found for commit %v", commit) 520 } 521 return tags, nil 522 } 523 524 func (git *gitRepo) IsRelease(commit string) (bool, error) { 525 tags1, err := git.previousReleaseTags(commit, true, false, false) 526 if err != nil { 527 return false, err 528 } 529 tags2, err := git.previousReleaseTags(commit, false, false, false) 530 if err != nil { 531 return false, err 532 } 533 return len(tags1) != len(tags2), nil 534 } 535 536 func (git *gitRepo) Object(name, commit string) ([]byte, error) { 537 return git.Run("show", fmt.Sprintf("%s:%s", commit, name)) 538 } 539 540 func (git *gitRepo) MergeBases(firstCommit, secondCommit string) ([]*Commit, error) { 541 output, err := git.Run("merge-base", firstCommit, secondCommit) 542 if err != nil { 543 return nil, err 544 } 545 ret := []*Commit{} 546 for _, hash := range strings.Fields(string(output)) { 547 commit, err := git.Commit(hash) 548 if err != nil { 549 return nil, err 550 } 551 ret = append(ret, commit) 552 } 553 return ret, nil 554 } 555 556 // CommitExists relies on 'git cat-file -e'. 557 // If object exists its exit status is 0. 558 // If object doesn't exist its exit status is 1 (not documented). 559 // Otherwise, the exit status is 128 (not documented). 560 func (git *gitRepo) CommitExists(commit string) (bool, error) { 561 _, err := git.Run("cat-file", "-e", commit) 562 var vErr *osutil.VerboseError 563 if errors.As(err, &vErr) && vErr.ExitCode == 1 { 564 return false, nil 565 } 566 if err != nil { 567 return false, err 568 } 569 return true, nil 570 } 571 572 func (git *gitRepo) PushCommit(repo, commit string) error { 573 tagName := "tag-" + commit // assign tag to guarantee remote persistence 574 git.Run("tag", tagName) // ignore errors on re-tagging 575 if _, err := git.Run("push", repo, "tag", tagName); err != nil { 576 return fmt.Errorf("git push %s tag %s: %w", repo, tagName, err) 577 } 578 return nil 579 } 580 581 var fileNameRe = regexp.MustCompile(`(?m)^diff.* b\/([^\s]+)`) 582 583 // ParseGitDiff extracts the files modified in the git patch. 584 func ParseGitDiff(patch []byte) []string { 585 var files []string 586 for _, match := range fileNameRe.FindAllStringSubmatch(string(patch), -1) { 587 files = append(files, match[1]) 588 } 589 return files 590 } 591 592 type Git struct { 593 Dir string 594 Sandbox bool 595 Env []string 596 precious bool 597 ignoreCC map[string]bool 598 } 599 600 func (git Git) Run(args ...string) ([]byte, error) { 601 cmd, err := git.command(args...) 602 if err != nil { 603 return nil, err 604 } 605 return osutil.Run(3*time.Hour, cmd) 606 } 607 608 func (git Git) command(args ...string) (*exec.Cmd, error) { 609 cmd := osutil.Command("git", args...) 610 cmd.Dir = git.Dir 611 cmd.Env = git.Env 612 if git.Sandbox { 613 if err := osutil.Sandbox(cmd, true, false); err != nil { 614 return nil, err 615 } 616 } 617 return cmd, nil 618 } 619 620 // Apply invokes git apply for a series of git patches. 621 // It is different from Patch() in that it normally handles raw patch emails. 622 func (git Git) Apply(patch []byte) error { 623 cmd, err := git.command("apply", "-") 624 if err != nil { 625 return err 626 } 627 stdin, err := cmd.StdinPipe() 628 if err != nil { 629 return err 630 } 631 go func() { 632 stdin.Write(patch) 633 stdin.Close() 634 }() 635 _, err = osutil.Run(3*time.Hour, cmd) 636 return err 637 } 638 639 // Reset resets the git repo to a known clean state. 640 func (git Git) Reset() error { 641 if git.precious { 642 return nil 643 } 644 git.Run("reset", "--hard", "--recurse-submodules") 645 git.Run("clean", "-xfdf") 646 git.Run("submodule", "foreach", "--recursive", "git", "clean", "-xfdf") 647 git.Run("bisect", "reset") 648 _, err := git.Run("reset", "--hard", "--recurse-submodules") 649 return err 650 } 651 652 // Commit extracts the information about the particular git commit. 653 func (git Git) Commit(hash string) (*Commit, error) { 654 const patchSeparator = "---===syzkaller-patch-separator===---" 655 output, err := git.Run("log", "--format=%H%n%s%n%ae%n%an%n%ad%n%P%n%cd%n%b"+patchSeparator, 656 "-n", "1", "-p", "-U0", hash) 657 if err != nil { 658 return nil, err 659 } 660 pos := bytes.Index(output, []byte(patchSeparator)) 661 if pos == -1 { 662 return nil, fmt.Errorf("git log output does not contain patch separator") 663 } 664 commit, err := gitParseCommit(output[:pos], nil, nil, git.ignoreCC) 665 if err != nil { 666 return nil, err 667 } 668 commit.Patch = output[pos+len(patchSeparator):] 669 for len(commit.Patch) != 0 && commit.Patch[0] == '\n' { 670 commit.Patch = commit.Patch[1:] 671 } 672 return commit, nil 673 } 674 675 func (git Git) fetchCommits(since, base, user, domain string, greps []string, fixedStrings bool) ([]*Commit, error) { 676 const commitSeparator = "---===syzkaller-commit-separator===---" 677 args := []string{"log", "--since", since, "--format=%H%n%s%n%ae%n%an%n%ad%n%P%n%cd%n%b%n" + commitSeparator} 678 if fixedStrings { 679 args = append(args, "--fixed-strings") 680 } 681 for _, grep := range greps { 682 args = append(args, "--grep", grep) 683 } 684 args = append(args, base) 685 cmd := exec.Command("git", args...) 686 cmd.Dir = git.Dir 687 cmd.Env = filterEnv() 688 if git.Sandbox { 689 if err := osutil.Sandbox(cmd, true, false); err != nil { 690 return nil, err 691 } 692 } 693 stdout, err := cmd.StdoutPipe() 694 if err != nil { 695 return nil, err 696 } 697 if err := cmd.Start(); err != nil { 698 return nil, err 699 } 700 defer cmd.Wait() 701 defer cmd.Process.Kill() 702 var ( 703 s = bufio.NewScanner(stdout) 704 buf = new(bytes.Buffer) 705 separator = []byte(commitSeparator) 706 commits []*Commit 707 userBytes []byte 708 domainBytes []byte 709 ) 710 if user != "" { 711 userBytes = []byte(user + "+") 712 domainBytes = []byte(domain) 713 } 714 for s.Scan() { 715 ln := s.Bytes() 716 if !bytes.Equal(ln, separator) { 717 buf.Write(ln) 718 buf.WriteByte('\n') 719 continue 720 } 721 com, err := gitParseCommit(buf.Bytes(), userBytes, domainBytes, git.ignoreCC) 722 if err != nil { 723 return nil, err 724 } 725 if user == "" || len(com.Tags) != 0 { 726 commits = append(commits, com) 727 } 728 buf.Reset() 729 } 730 return commits, s.Err() 731 }