diff options
Diffstat (limited to 'pkg/vcs/git.go')
| -rw-r--r-- | pkg/vcs/git.go | 396 |
1 files changed, 216 insertions, 180 deletions
diff --git a/pkg/vcs/git.go b/pkg/vcs/git.go index d8a4967ba..fd71b8d36 100644 --- a/pkg/vcs/git.go +++ b/pkg/vcs/git.go @@ -22,25 +22,25 @@ import ( "github.com/google/syzkaller/pkg/osutil" ) -type git struct { - dir string - ignoreCC map[string]bool - precious bool - sandbox bool +type gitRepo struct { + *Git } -func newGit(dir string, ignoreCC map[string]bool, opts []RepoOpt) *git { - git := &git{ - dir: dir, - ignoreCC: ignoreCC, - sandbox: true, +func newGitRepo(dir string, ignoreCC map[string]bool, opts []RepoOpt) *gitRepo { + git := &gitRepo{ + Git: &Git{ + Dir: dir, + Sandbox: true, + Env: filterEnv(), + ignoreCC: ignoreCC, + }, } for _, opt := range opts { switch opt { case OptPrecious: git.precious = true case OptDontSandbox: - git.sandbox = false + git.Sandbox = false } } return git @@ -66,9 +66,9 @@ func filterEnv() []string { return env } -func (git *git) Poll(repo, branch string) (*Commit, error) { - git.reset() - origin, err := git.git("remote", "get-url", "origin") +func (git *gitRepo) Poll(repo, branch string) (*Commit, error) { + git.Reset() + origin, err := git.Run("remote", "get-url", "origin") if err != nil || strings.TrimSpace(string(origin)) != repo { // The repo is here, but it has wrong origin (e.g. repo in config has changed), re-clone. if err := git.clone(repo, branch); err != nil { @@ -78,28 +78,28 @@ func (git *git) Poll(repo, branch string) (*Commit, error) { // Use origin/branch for the case the branch was force-pushed, // in such case branch is not the same is origin/branch and we will // stuck with the local version forever (git checkout won't fail). - if _, err := git.git("checkout", "origin/"+branch); err != nil { + if _, err := git.Run("checkout", "origin/"+branch); err != nil { // No such branch (e.g. branch in config has changed), re-clone. if err := git.clone(repo, branch); err != nil { return nil, err } } - if _, err := git.git("fetch", "--force"); err != nil { + if _, err := git.Run("fetch", "--force"); err != nil { // Something else is wrong, re-clone. if err := git.clone(repo, branch); err != nil { return nil, err } } - if _, err := git.git("checkout", "origin/"+branch); err != nil { + if _, err := git.Run("checkout", "origin/"+branch); err != nil { return nil, err } - if _, err := git.git("submodule", "update", "--init"); err != nil { + if _, err := git.Run("submodule", "update", "--init"); err != nil { return nil, err } return git.Commit(HEAD) } -func (git *git) CheckoutBranch(repo, branch string) (*Commit, error) { +func (git *gitRepo) CheckoutBranch(repo, branch string) (*Commit, error) { if err := git.repair(); err != nil { return nil, err } @@ -108,17 +108,17 @@ func (git *git) CheckoutBranch(repo, branch string) (*Commit, error) { // remote when initializing. // This sets "origin" to be the current remote. // Ignore errors as we can double add or remove the same remote and that will fail. - git.git("remote", "rm", "origin") - git.git("remote", "add", "origin", repo) - git.git("remote", "add", repoHash, repo) - _, err := git.git("fetch", "--force", repoHash, branch) + git.Run("remote", "rm", "origin") + git.Run("remote", "add", "origin", repo) + git.Run("remote", "add", repoHash, repo) + _, err := git.Run("fetch", "--force", repoHash, branch) if err != nil { return nil, err } - if _, err := git.git("checkout", "FETCH_HEAD", "--force"); err != nil { + if _, err := git.Run("checkout", "FETCH_HEAD", "--force"); err != nil { return nil, err } - if _, err := git.git("submodule", "update", "--init"); err != nil { + if _, err := git.Run("submodule", "update", "--init"); err != nil { return nil, err } // If the branch checkout had to be "forced" the directory may @@ -130,7 +130,7 @@ func (git *git) CheckoutBranch(repo, branch string) (*Commit, error) { return git.Commit(HEAD) } -func (git *git) CheckoutCommit(repo, commit string) (*Commit, error) { +func (git *gitRepo) CheckoutCommit(repo, commit string) (*Commit, error) { if err := git.repair(); err != nil { return nil, err } @@ -140,16 +140,16 @@ func (git *git) CheckoutCommit(repo, commit string) (*Commit, error) { return git.SwitchCommit(commit) } -func (git *git) fetchRemote(repo, commit string) error { +func (git *gitRepo) fetchRemote(repo, commit string) error { repoHash := hash.String([]byte(repo)) // Ignore error as we can double add the same remote and that will fail. - git.git("remote", "add", repoHash, repo) + git.Run("remote", "add", repoHash, repo) fetchArgs := []string{"fetch", "--force", "--tags", repoHash} if commit != "" && gitFullHashRe.MatchString(commit) { // This trick only works with full commit hashes. fetchArgs = append(fetchArgs, commit) } - _, err := git.git(fetchArgs...) + _, err := git.Run(fetchArgs...) if err != nil { var verbose *osutil.VerboseError if errors.As(err, &verbose) && @@ -159,110 +159,75 @@ func (git *git) fetchRemote(repo, commit string) error { // Try to fetch more, but this time prune tags, it should help. // The --prune-tags option will remove all tags that are not present // in this remote repo, so don't do it always. Only when necessary. - _, err = git.git("fetch", "--force", "--tags", "--prune", "--prune-tags", repoHash) + _, err = git.Run("fetch", "--force", "--tags", "--prune", "--prune-tags", repoHash) } } return err } -func (git *git) SwitchCommit(commit string) (*Commit, error) { +func (git *gitRepo) SwitchCommit(commit string) (*Commit, error) { if !git.precious { - git.git("reset", "--hard") - git.git("clean", "-fdx") + git.Run("reset", "--hard") + git.Run("clean", "-fdx") } - if _, err := git.git("checkout", commit); err != nil { + if _, err := git.Run("checkout", commit); err != nil { return nil, err } - if _, err := git.git("submodule", "update", "--init"); err != nil { + if _, err := git.Run("submodule", "update", "--init"); err != nil { return nil, err } return git.Commit(HEAD) } -func (git *git) clone(repo, branch string) error { +func (git *gitRepo) clone(repo, branch string) error { if git.precious { return fmt.Errorf("won't reinit precious repo") } if err := git.initRepo(nil); err != nil { return err } - if _, err := git.git("remote", "add", "origin", repo); err != nil { + if _, err := git.Run("remote", "add", "origin", repo); err != nil { return err } - if _, err := git.git("fetch", "origin", branch); err != nil { + if _, err := git.Run("fetch", "origin", branch); err != nil { return err } return nil } -func (git *git) reset() error { - // This function tries to reset git repo state to a known clean state. - if git.precious { - return nil - } - git.git("reset", "--hard", "--recurse-submodules") - git.git("clean", "-xfdf") - git.git("submodule", "foreach", "--recursive", "git", "clean", "-xfdf") - git.git("bisect", "reset") - _, err := git.git("reset", "--hard", "--recurse-submodules") - return err -} - -func (git *git) repair() error { - if err := git.reset(); err != nil { +func (git *gitRepo) repair() error { + if err := git.Reset(); err != nil { return git.initRepo(err) } return nil } -func (git *git) initRepo(reason error) error { +func (git *gitRepo) initRepo(reason error) error { if reason != nil { - log.Logf(1, "git: initializing repo at %v: %v", git.dir, reason) + log.Logf(1, "git: initializing repo at %v: %v", git.Dir, reason) } - if err := os.RemoveAll(git.dir); err != nil { + if err := os.RemoveAll(git.Dir); err != nil { return fmt.Errorf("failed to remove repo dir: %w", err) } - if err := osutil.MkdirAll(git.dir); err != nil { + if err := osutil.MkdirAll(git.Dir); err != nil { return fmt.Errorf("failed to create repo dir: %w", err) } - if git.sandbox { - if err := osutil.SandboxChown(git.dir); err != nil { + if git.Sandbox { + if err := osutil.SandboxChown(git.Dir); err != nil { return err } } - if _, err := git.git("init"); err != nil { + if _, err := git.Run("init"); err != nil { return err } return nil } -func (git *git) Contains(commit string) (bool, error) { - _, err := git.git("merge-base", "--is-ancestor", commit, HEAD) +func (git *gitRepo) Contains(commit string) (bool, error) { + _, err := git.Run("merge-base", "--is-ancestor", commit, HEAD) return err == nil, nil } -func (git *git) Commit(com string) (*Commit, error) { - const patchSeparator = "---===syzkaller-patch-separator===---" - output, err := git.git("log", "--format=%H%n%s%n%ae%n%an%n%ad%n%P%n%cd%n%b"+patchSeparator, - "-n", "1", "-p", "-U0", com) - if err != nil { - return nil, err - } - pos := bytes.Index(output, []byte(patchSeparator)) - if pos == -1 { - return nil, fmt.Errorf("git log output does not contain patch separator") - } - commit, err := gitParseCommit(output[:pos], nil, nil, git.ignoreCC) - if err != nil { - return nil, err - } - commit.Patch = output[pos+len(patchSeparator):] - for len(commit.Patch) != 0 && commit.Patch[0] == '\n' { - commit.Patch = commit.Patch[1:] - } - return commit, nil -} - func gitParseCommit(output, user, domain []byte, ignoreCC map[string]bool) (*Commit, error) { lines := bytes.Split(output, []byte{'\n'}) if len(lines) < 8 || len(lines[0]) != 40 { @@ -340,7 +305,7 @@ func gitParseCommit(output, user, domain []byte, ignoreCC map[string]bool) (*Com return com, nil } -func (git *git) GetCommitByTitle(title string) (*Commit, error) { +func (git *gitRepo) GetCommitByTitle(title string) (*Commit, error) { commits, _, err := git.GetCommitsByTitles([]string{title}) if err != nil || len(commits) == 0 { return nil, err @@ -352,7 +317,7 @@ const ( fetchCommitsMaxAgeInYears = 5 ) -func (git *git) GetCommitsByTitles(titles []string) ([]*Commit, []string, error) { +func (git *gitRepo) GetCommitsByTitles(titles []string) ([]*Commit, []string, error) { var greps []string m := make(map[string]string) for _, title := range titles { @@ -381,12 +346,12 @@ func (git *git) GetCommitsByTitles(titles []string) ([]*Commit, []string, error) return results, missing, nil } -func (git *git) ListCommitHashes(baseCommit string, from time.Time) ([]string, error) { +func (git *gitRepo) ListCommitHashes(baseCommit string, from time.Time) ([]string, error) { args := []string{"log", "--pretty=format:%h"} if !from.IsZero() { args = append(args, "--since", from.Format(time.RFC3339)) } - output, err := git.git(append(args, baseCommit)...) + output, err := git.Run(append(args, baseCommit)...) if err != nil { return nil, err } @@ -396,7 +361,7 @@ func (git *git) ListCommitHashes(baseCommit string, from time.Time) ([]string, e return strings.Split(string(output), "\n"), nil } -func (git *git) ExtractFixTagsFromCommits(baseCommit, email string) ([]*Commit, error) { +func (git *gitRepo) ExtractFixTagsFromCommits(baseCommit, email string) ([]*Commit, error) { user, domain, err := splitEmail(email) if err != nil { return nil, fmt.Errorf("failed to parse email %q: %w", email, err) @@ -406,76 +371,6 @@ func (git *git) ExtractFixTagsFromCommits(baseCommit, email string) ([]*Commit, return git.fetchCommits(since, baseCommit, user, domain, []string{grep}, false) } -func (git *git) fetchCommits(since, base, user, domain string, greps []string, fixedStrings bool) ([]*Commit, error) { - const commitSeparator = "---===syzkaller-commit-separator===---" - args := []string{"log", "--since", since, "--format=%H%n%s%n%ae%n%an%n%ad%n%P%n%cd%n%b%n" + commitSeparator} - if fixedStrings { - args = append(args, "--fixed-strings") - } - for _, grep := range greps { - args = append(args, "--grep", grep) - } - args = append(args, base) - cmd := exec.Command("git", args...) - cmd.Dir = git.dir - cmd.Env = filterEnv() - if git.sandbox { - if err := osutil.Sandbox(cmd, true, false); err != nil { - return nil, err - } - } - stdout, err := cmd.StdoutPipe() - if err != nil { - return nil, err - } - if err := cmd.Start(); err != nil { - return nil, err - } - defer cmd.Wait() - defer cmd.Process.Kill() - var ( - s = bufio.NewScanner(stdout) - buf = new(bytes.Buffer) - separator = []byte(commitSeparator) - commits []*Commit - userBytes []byte - domainBytes []byte - ) - if user != "" { - userBytes = []byte(user + "+") - domainBytes = []byte(domain) - } - for s.Scan() { - ln := s.Bytes() - if !bytes.Equal(ln, separator) { - buf.Write(ln) - buf.WriteByte('\n') - continue - } - com, err := gitParseCommit(buf.Bytes(), userBytes, domainBytes, git.ignoreCC) - if err != nil { - return nil, err - } - if user == "" || len(com.Tags) != 0 { - commits = append(commits, com) - } - buf.Reset() - } - return commits, s.Err() -} - -func (git *git) git(args ...string) ([]byte, error) { - cmd := osutil.Command("git", args...) - cmd.Dir = git.dir - cmd.Env = filterEnv() - if git.sandbox { - if err := osutil.Sandbox(cmd, true, false); err != nil { - return nil, err - } - } - return osutil.Run(3*time.Hour, cmd) -} - func splitEmail(email string) (user, domain string, err error) { addr, err := mail.ParseAddress(email) if err != nil { @@ -493,18 +388,18 @@ func splitEmail(email string) (user, domain string, err error) { return } -func (git *git) Bisect(bad, good string, dt debugtracer.DebugTracer, pred func() (BisectResult, +func (git *gitRepo) Bisect(bad, good string, dt debugtracer.DebugTracer, pred func() (BisectResult, error)) ([]*Commit, error) { - git.reset() + git.Reset() firstBad, err := git.Commit(bad) if err != nil { return nil, err } - output, err := git.git("bisect", "start", bad, good) + output, err := git.Run("bisect", "start", bad, good) if err != nil { return nil, err } - defer git.reset() + defer git.Reset() dt.Log("# git bisect start %v %v\n%s", bad, good, output) current, err := git.Commit(HEAD) if err != nil { @@ -518,14 +413,14 @@ func (git *git) Bisect(bad, good string, dt debugtracer.DebugTracer, pred func() for { res, err := pred() // Linux EnvForCommit may cherry-pick some fixes, reset these before the next step. - git.git("reset", "--hard") + git.Run("reset", "--hard") if err != nil { return nil, err } if res == BisectBad { firstBad = current } - output, err = git.git("bisect", bisectTerms[res]) + output, err = git.Run("bisect", bisectTerms[res]) dt.Log("# git bisect %v %v\n%s", bisectTerms[res], current.Hash, output) if err != nil { if bytes.Contains(output, []byte("There are only 'skip'ped commits left to test")) { @@ -546,7 +441,7 @@ func (git *git) Bisect(bad, good string, dt debugtracer.DebugTracer, pred func() var gitFullHashRe = regexp.MustCompile("[a-f0-9]{40}") -func (git *git) bisectInconclusive(output []byte) ([]*Commit, error) { +func (git *gitRepo) bisectInconclusive(output []byte) ([]*Commit, error) { // For inconclusive bisection git prints the following message: // // There are only 'skip'ped commits left to test. @@ -570,7 +465,7 @@ func (git *git) bisectInconclusive(output []byte) ([]*Commit, error) { return commits, nil } -func (git *git) ReleaseTag(commit string) (string, error) { +func (git *gitRepo) ReleaseTag(commit string) (string, error) { tags, err := git.previousReleaseTags(commit, true, true, true) if err != nil { return "", err @@ -581,10 +476,10 @@ func (git *git) ReleaseTag(commit string) (string, error) { return tags[0], nil } -func (git *git) previousReleaseTags(commit string, self, onlyTop, includeRC bool) ([]string, error) { +func (git *gitRepo) previousReleaseTags(commit string, self, onlyTop, includeRC bool) ([]string, error) { var tags []string if self { - output, err := git.git("tag", "--list", "--points-at", commit, "--merged", commit, "v*.*") + output, err := git.Run("tag", "--list", "--points-at", commit, "--merged", commit, "v*.*") if err != nil { return nil, err } @@ -593,7 +488,7 @@ func (git *git) previousReleaseTags(commit string, self, onlyTop, includeRC bool return tags, nil } } - output, err := git.git("tag", "--no-contains", commit, "--merged", commit, "v*.*") + output, err := git.Run("tag", "--no-contains", commit, "--merged", commit, "v*.*") if err != nil { return nil, err } @@ -605,7 +500,7 @@ func (git *git) previousReleaseTags(commit string, self, onlyTop, includeRC bool return tags, nil } -func (git *git) IsRelease(commit string) (bool, error) { +func (git *gitRepo) IsRelease(commit string) (bool, error) { tags1, err := git.previousReleaseTags(commit, true, false, false) if err != nil { return false, err @@ -617,12 +512,12 @@ func (git *git) IsRelease(commit string) (bool, error) { return len(tags1) != len(tags2), nil } -func (git *git) Object(name, commit string) ([]byte, error) { - return git.git("show", fmt.Sprintf("%s:%s", commit, name)) +func (git *gitRepo) Object(name, commit string) ([]byte, error) { + return git.Run("show", fmt.Sprintf("%s:%s", commit, name)) } -func (git *git) MergeBases(firstCommit, secondCommit string) ([]*Commit, error) { - output, err := git.git("merge-base", firstCommit, secondCommit) +func (git *gitRepo) MergeBases(firstCommit, secondCommit string) ([]*Commit, error) { + output, err := git.Run("merge-base", firstCommit, secondCommit) if err != nil { return nil, err } @@ -641,8 +536,8 @@ func (git *git) MergeBases(firstCommit, secondCommit string) ([]*Commit, error) // If object exists its exit status is 0. // If object doesn't exist its exit status is 1 (not documented). // Otherwise, the exit status is 128 (not documented). -func (git *git) CommitExists(commit string) (bool, error) { - _, err := git.git("cat-file", "-e", commit) +func (git *gitRepo) CommitExists(commit string) (bool, error) { + _, err := git.Run("cat-file", "-e", commit) var vErr *osutil.VerboseError if errors.As(err, &vErr) && vErr.ExitCode == 1 { return false, nil @@ -653,10 +548,10 @@ func (git *git) CommitExists(commit string) (bool, error) { return true, nil } -func (git *git) PushCommit(repo, commit string) error { +func (git *gitRepo) PushCommit(repo, commit string) error { tagName := "tag-" + commit // assign tag to guarantee remote persistence - git.git("tag", tagName) // ignore errors on re-tagging - if _, err := git.git("push", repo, "tag", tagName); err != nil { + git.Run("tag", tagName) // ignore errors on re-tagging + if _, err := git.Run("push", repo, "tag", tagName); err != nil { return fmt.Errorf("git push %s tag %s: %w", repo, tagName, err) } return nil @@ -672,3 +567,144 @@ func ParseGitDiff(patch []byte) []string { } return files } + +type Git struct { + Dir string + Sandbox bool + Env []string + precious bool + ignoreCC map[string]bool +} + +func (git Git) Run(args ...string) ([]byte, error) { + cmd, err := git.command(args...) + if err != nil { + return nil, err + } + return osutil.Run(3*time.Hour, cmd) +} + +func (git Git) command(args ...string) (*exec.Cmd, error) { + cmd := osutil.Command("git", args...) + cmd.Dir = git.Dir + cmd.Env = git.Env + if git.Sandbox { + if err := osutil.Sandbox(cmd, true, false); err != nil { + return nil, err + } + } + return cmd, nil +} + +// Apply invokes git apply for a series of git patches. +// It is different from Patch() in that it normally handles raw patch emails. +func (git Git) Apply(patch []byte) error { + cmd, err := git.command("apply", "-") + if err != nil { + return err + } + stdin, err := cmd.StdinPipe() + if err != nil { + return err + } + go func() { + stdin.Write(patch) + stdin.Close() + }() + _, err = osutil.Run(3*time.Hour, cmd) + return err +} + +// Reset resets the git repo to a known clean state. +func (git Git) Reset() error { + if git.precious { + return nil + } + git.Run("reset", "--hard", "--recurse-submodules") + git.Run("clean", "-xfdf") + git.Run("submodule", "foreach", "--recursive", "git", "clean", "-xfdf") + git.Run("bisect", "reset") + _, err := git.Run("reset", "--hard", "--recurse-submodules") + return err +} + +// Commit extracts the information about the particular git commit. +func (git Git) Commit(hash string) (*Commit, error) { + const patchSeparator = "---===syzkaller-patch-separator===---" + output, err := git.Run("log", "--format=%H%n%s%n%ae%n%an%n%ad%n%P%n%cd%n%b"+patchSeparator, + "-n", "1", "-p", "-U0", hash) + if err != nil { + return nil, err + } + pos := bytes.Index(output, []byte(patchSeparator)) + if pos == -1 { + return nil, fmt.Errorf("git log output does not contain patch separator") + } + commit, err := gitParseCommit(output[:pos], nil, nil, git.ignoreCC) + if err != nil { + return nil, err + } + commit.Patch = output[pos+len(patchSeparator):] + for len(commit.Patch) != 0 && commit.Patch[0] == '\n' { + commit.Patch = commit.Patch[1:] + } + return commit, nil +} + +func (git Git) fetchCommits(since, base, user, domain string, greps []string, fixedStrings bool) ([]*Commit, error) { + const commitSeparator = "---===syzkaller-commit-separator===---" + args := []string{"log", "--since", since, "--format=%H%n%s%n%ae%n%an%n%ad%n%P%n%cd%n%b%n" + commitSeparator} + if fixedStrings { + args = append(args, "--fixed-strings") + } + for _, grep := range greps { + args = append(args, "--grep", grep) + } + args = append(args, base) + cmd := exec.Command("git", args...) + cmd.Dir = git.Dir + cmd.Env = filterEnv() + if git.Sandbox { + if err := osutil.Sandbox(cmd, true, false); err != nil { + return nil, err + } + } + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + if err := cmd.Start(); err != nil { + return nil, err + } + defer cmd.Wait() + defer cmd.Process.Kill() + var ( + s = bufio.NewScanner(stdout) + buf = new(bytes.Buffer) + separator = []byte(commitSeparator) + commits []*Commit + userBytes []byte + domainBytes []byte + ) + if user != "" { + userBytes = []byte(user + "+") + domainBytes = []byte(domain) + } + for s.Scan() { + ln := s.Bytes() + if !bytes.Equal(ln, separator) { + buf.Write(ln) + buf.WriteByte('\n') + continue + } + com, err := gitParseCommit(buf.Bytes(), userBytes, domainBytes, git.ignoreCC) + if err != nil { + return nil, err + } + if user == "" || len(com.Tags) != 0 { + commits = append(commits, com) + } + buf.Reset() + } + return commits, s.Err() +} |
