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  }