code.gitea.io/gitea@v1.22.3/modules/git/repo_commit.go (about)

     1  // Copyright 2015 The Gogs Authors. All rights reserved.
     2  // Copyright 2019 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package git
     6  
     7  import (
     8  	"bytes"
     9  	"io"
    10  	"os"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"code.gitea.io/gitea/modules/cache"
    15  	"code.gitea.io/gitea/modules/setting"
    16  )
    17  
    18  // GetBranchCommitID returns last commit ID string of given branch.
    19  func (repo *Repository) GetBranchCommitID(name string) (string, error) {
    20  	return repo.GetRefCommitID(BranchPrefix + name)
    21  }
    22  
    23  // GetTagCommitID returns last commit ID string of given tag.
    24  func (repo *Repository) GetTagCommitID(name string) (string, error) {
    25  	return repo.GetRefCommitID(TagPrefix + name)
    26  }
    27  
    28  // GetCommit returns commit object of by ID string.
    29  func (repo *Repository) GetCommit(commitID string) (*Commit, error) {
    30  	id, err := repo.ConvertToGitID(commitID)
    31  	if err != nil {
    32  		return nil, err
    33  	}
    34  
    35  	return repo.getCommit(id)
    36  }
    37  
    38  // GetBranchCommit returns the last commit of given branch.
    39  func (repo *Repository) GetBranchCommit(name string) (*Commit, error) {
    40  	commitID, err := repo.GetBranchCommitID(name)
    41  	if err != nil {
    42  		return nil, err
    43  	}
    44  	return repo.GetCommit(commitID)
    45  }
    46  
    47  // GetTagCommit get the commit of the specific tag via name
    48  func (repo *Repository) GetTagCommit(name string) (*Commit, error) {
    49  	commitID, err := repo.GetTagCommitID(name)
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  	return repo.GetCommit(commitID)
    54  }
    55  
    56  func (repo *Repository) getCommitByPathWithID(id ObjectID, relpath string) (*Commit, error) {
    57  	// File name starts with ':' must be escaped.
    58  	if relpath[0] == ':' {
    59  		relpath = `\` + relpath
    60  	}
    61  
    62  	stdout, _, runErr := NewCommand(repo.Ctx, "log", "-1", prettyLogFormat).AddDynamicArguments(id.String()).AddDashesAndList(relpath).RunStdString(&RunOpts{Dir: repo.Path})
    63  	if runErr != nil {
    64  		return nil, runErr
    65  	}
    66  
    67  	id, err := NewIDFromString(stdout)
    68  	if err != nil {
    69  		return nil, err
    70  	}
    71  
    72  	return repo.getCommit(id)
    73  }
    74  
    75  // GetCommitByPath returns the last commit of relative path.
    76  func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) {
    77  	stdout, _, runErr := NewCommand(repo.Ctx, "log", "-1", prettyLogFormat).AddDashesAndList(relpath).RunStdBytes(&RunOpts{Dir: repo.Path})
    78  	if runErr != nil {
    79  		return nil, runErr
    80  	}
    81  
    82  	commits, err := repo.parsePrettyFormatLogToList(stdout)
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  	if len(commits) == 0 {
    87  		return nil, ErrNotExist{ID: relpath}
    88  	}
    89  	return commits[0], nil
    90  }
    91  
    92  func (repo *Repository) commitsByRange(id ObjectID, page, pageSize int, not string) ([]*Commit, error) {
    93  	cmd := NewCommand(repo.Ctx, "log").
    94  		AddOptionFormat("--skip=%d", (page-1)*pageSize).
    95  		AddOptionFormat("--max-count=%d", pageSize).
    96  		AddArguments(prettyLogFormat).
    97  		AddDynamicArguments(id.String())
    98  
    99  	if not != "" {
   100  		cmd.AddOptionValues("--not", not)
   101  	}
   102  
   103  	stdout, _, err := cmd.RunStdBytes(&RunOpts{Dir: repo.Path})
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  
   108  	return repo.parsePrettyFormatLogToList(stdout)
   109  }
   110  
   111  func (repo *Repository) searchCommits(id ObjectID, opts SearchCommitsOptions) ([]*Commit, error) {
   112  	// add common arguments to git command
   113  	addCommonSearchArgs := func(c *Command) {
   114  		// ignore case
   115  		c.AddArguments("-i")
   116  
   117  		// add authors if present in search query
   118  		for _, v := range opts.Authors {
   119  			c.AddOptionFormat("--author=%s", v)
   120  		}
   121  
   122  		// add committers if present in search query
   123  		for _, v := range opts.Committers {
   124  			c.AddOptionFormat("--committer=%s", v)
   125  		}
   126  
   127  		// add time constraints if present in search query
   128  		if len(opts.After) > 0 {
   129  			c.AddOptionFormat("--after=%s", opts.After)
   130  		}
   131  		if len(opts.Before) > 0 {
   132  			c.AddOptionFormat("--before=%s", opts.Before)
   133  		}
   134  	}
   135  
   136  	// create new git log command with limit of 100 commits
   137  	cmd := NewCommand(repo.Ctx, "log", "-100", prettyLogFormat).AddDynamicArguments(id.String())
   138  
   139  	// pretend that all refs along with HEAD were listed on command line as <commis>
   140  	// https://git-scm.com/docs/git-log#Documentation/git-log.txt---all
   141  	// note this is done only for command created above
   142  	if opts.All {
   143  		cmd.AddArguments("--all")
   144  	}
   145  
   146  	// interpret search string keywords as string instead of regex
   147  	cmd.AddArguments("--fixed-strings")
   148  
   149  	// add remaining keywords from search string
   150  	// note this is done only for command created above
   151  	for _, v := range opts.Keywords {
   152  		cmd.AddOptionFormat("--grep=%s", v)
   153  	}
   154  
   155  	// search for commits matching given constraints and keywords in commit msg
   156  	addCommonSearchArgs(cmd)
   157  	stdout, _, err := cmd.RunStdBytes(&RunOpts{Dir: repo.Path})
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  	if len(stdout) != 0 {
   162  		stdout = append(stdout, '\n')
   163  	}
   164  
   165  	// if there are any keywords (ie not committer:, author:, time:)
   166  	// then let's iterate over them
   167  	for _, v := range opts.Keywords {
   168  		// ignore anything not matching a valid sha pattern
   169  		if id.Type().IsValid(v) {
   170  			// create new git log command with 1 commit limit
   171  			hashCmd := NewCommand(repo.Ctx, "log", "-1", prettyLogFormat)
   172  			// add previous arguments except for --grep and --all
   173  			addCommonSearchArgs(hashCmd)
   174  			// add keyword as <commit>
   175  			hashCmd.AddDynamicArguments(v)
   176  
   177  			// search with given constraints for commit matching sha hash of v
   178  			hashMatching, _, err := hashCmd.RunStdBytes(&RunOpts{Dir: repo.Path})
   179  			if err != nil || bytes.Contains(stdout, hashMatching) {
   180  				continue
   181  			}
   182  			stdout = append(stdout, hashMatching...)
   183  			stdout = append(stdout, '\n')
   184  		}
   185  	}
   186  
   187  	return repo.parsePrettyFormatLogToList(bytes.TrimSuffix(stdout, []byte{'\n'}))
   188  }
   189  
   190  // FileChangedBetweenCommits Returns true if the file changed between commit IDs id1 and id2
   191  // You must ensure that id1 and id2 are valid commit ids.
   192  func (repo *Repository) FileChangedBetweenCommits(filename, id1, id2 string) (bool, error) {
   193  	stdout, _, err := NewCommand(repo.Ctx, "diff", "--name-only", "-z").AddDynamicArguments(id1, id2).AddDashesAndList(filename).RunStdBytes(&RunOpts{Dir: repo.Path})
   194  	if err != nil {
   195  		return false, err
   196  	}
   197  	return len(strings.TrimSpace(string(stdout))) > 0, nil
   198  }
   199  
   200  // FileCommitsCount return the number of files at a revision
   201  func (repo *Repository) FileCommitsCount(revision, file string) (int64, error) {
   202  	return CommitsCount(repo.Ctx,
   203  		CommitsCountOptions{
   204  			RepoPath: repo.Path,
   205  			Revision: []string{revision},
   206  			RelPath:  []string{file},
   207  		})
   208  }
   209  
   210  type CommitsByFileAndRangeOptions struct {
   211  	Revision string
   212  	File     string
   213  	Not      string
   214  	Page     int
   215  }
   216  
   217  // CommitsByFileAndRange return the commits according revision file and the page
   218  func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) ([]*Commit, error) {
   219  	skip := (opts.Page - 1) * setting.Git.CommitsRangeSize
   220  
   221  	stdoutReader, stdoutWriter := io.Pipe()
   222  	defer func() {
   223  		_ = stdoutReader.Close()
   224  		_ = stdoutWriter.Close()
   225  	}()
   226  	go func() {
   227  		stderr := strings.Builder{}
   228  		gitCmd := NewCommand(repo.Ctx, "rev-list").
   229  			AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize*opts.Page).
   230  			AddOptionFormat("--skip=%d", skip)
   231  		gitCmd.AddDynamicArguments(opts.Revision)
   232  
   233  		if opts.Not != "" {
   234  			gitCmd.AddOptionValues("--not", opts.Not)
   235  		}
   236  
   237  		gitCmd.AddDashesAndList(opts.File)
   238  		err := gitCmd.Run(&RunOpts{
   239  			Dir:    repo.Path,
   240  			Stdout: stdoutWriter,
   241  			Stderr: &stderr,
   242  		})
   243  		if err != nil {
   244  			_ = stdoutWriter.CloseWithError(ConcatenateError(err, (&stderr).String()))
   245  		} else {
   246  			_ = stdoutWriter.Close()
   247  		}
   248  	}()
   249  
   250  	objectFormat, err := repo.GetObjectFormat()
   251  	if err != nil {
   252  		return nil, err
   253  	}
   254  
   255  	length := objectFormat.FullLength()
   256  	commits := []*Commit{}
   257  	shaline := make([]byte, length+1)
   258  	for {
   259  		n, err := io.ReadFull(stdoutReader, shaline)
   260  		if err != nil || n < length {
   261  			if err == io.EOF {
   262  				err = nil
   263  			}
   264  			return commits, err
   265  		}
   266  		objectID, err := NewIDFromString(string(shaline[0:length]))
   267  		if err != nil {
   268  			return nil, err
   269  		}
   270  		commit, err := repo.getCommit(objectID)
   271  		if err != nil {
   272  			return nil, err
   273  		}
   274  		commits = append(commits, commit)
   275  	}
   276  }
   277  
   278  // FilesCountBetween return the number of files changed between two commits
   279  func (repo *Repository) FilesCountBetween(startCommitID, endCommitID string) (int, error) {
   280  	stdout, _, err := NewCommand(repo.Ctx, "diff", "--name-only").AddDynamicArguments(startCommitID + "..." + endCommitID).RunStdString(&RunOpts{Dir: repo.Path})
   281  	if err != nil && strings.Contains(err.Error(), "no merge base") {
   282  		// git >= 2.28 now returns an error if startCommitID and endCommitID have become unrelated.
   283  		// previously it would return the results of git diff --name-only startCommitID endCommitID so let's try that...
   284  		stdout, _, err = NewCommand(repo.Ctx, "diff", "--name-only").AddDynamicArguments(startCommitID, endCommitID).RunStdString(&RunOpts{Dir: repo.Path})
   285  	}
   286  	if err != nil {
   287  		return 0, err
   288  	}
   289  	return len(strings.Split(stdout, "\n")) - 1, nil
   290  }
   291  
   292  // CommitsBetween returns a list that contains commits between [before, last).
   293  // If before is detached (removed by reset + push) it is not included.
   294  func (repo *Repository) CommitsBetween(last, before *Commit) ([]*Commit, error) {
   295  	var stdout []byte
   296  	var err error
   297  	if before == nil {
   298  		stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path})
   299  	} else {
   300  		stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(before.ID.String() + ".." + last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path})
   301  		if err != nil && strings.Contains(err.Error(), "no merge base") {
   302  			// future versions of git >= 2.28 are likely to return an error if before and last have become unrelated.
   303  			// previously it would return the results of git rev-list before last so let's try that...
   304  			stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(before.ID.String(), last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path})
   305  		}
   306  	}
   307  	if err != nil {
   308  		return nil, err
   309  	}
   310  	return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout))
   311  }
   312  
   313  // CommitsBetweenLimit returns a list that contains at most limit commits skipping the first skip commits between [before, last)
   314  func (repo *Repository) CommitsBetweenLimit(last, before *Commit, limit, skip int) ([]*Commit, error) {
   315  	var stdout []byte
   316  	var err error
   317  	if before == nil {
   318  		stdout, _, err = NewCommand(repo.Ctx, "rev-list").
   319  			AddOptionValues("--max-count", strconv.Itoa(limit)).
   320  			AddOptionValues("--skip", strconv.Itoa(skip)).
   321  			AddDynamicArguments(last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path})
   322  	} else {
   323  		stdout, _, err = NewCommand(repo.Ctx, "rev-list").
   324  			AddOptionValues("--max-count", strconv.Itoa(limit)).
   325  			AddOptionValues("--skip", strconv.Itoa(skip)).
   326  			AddDynamicArguments(before.ID.String() + ".." + last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path})
   327  		if err != nil && strings.Contains(err.Error(), "no merge base") {
   328  			// future versions of git >= 2.28 are likely to return an error if before and last have become unrelated.
   329  			// previously it would return the results of git rev-list --max-count n before last so let's try that...
   330  			stdout, _, err = NewCommand(repo.Ctx, "rev-list").
   331  				AddOptionValues("--max-count", strconv.Itoa(limit)).
   332  				AddOptionValues("--skip", strconv.Itoa(skip)).
   333  				AddDynamicArguments(before.ID.String(), last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path})
   334  		}
   335  	}
   336  	if err != nil {
   337  		return nil, err
   338  	}
   339  	return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout))
   340  }
   341  
   342  // CommitsBetweenNotBase returns a list that contains commits between [before, last), excluding commits in baseBranch.
   343  // If before is detached (removed by reset + push) it is not included.
   344  func (repo *Repository) CommitsBetweenNotBase(last, before *Commit, baseBranch string) ([]*Commit, error) {
   345  	var stdout []byte
   346  	var err error
   347  	if before == nil {
   348  		stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(&RunOpts{Dir: repo.Path})
   349  	} else {
   350  		stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(before.ID.String()+".."+last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(&RunOpts{Dir: repo.Path})
   351  		if err != nil && strings.Contains(err.Error(), "no merge base") {
   352  			// future versions of git >= 2.28 are likely to return an error if before and last have become unrelated.
   353  			// previously it would return the results of git rev-list before last so let's try that...
   354  			stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(before.ID.String(), last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(&RunOpts{Dir: repo.Path})
   355  		}
   356  	}
   357  	if err != nil {
   358  		return nil, err
   359  	}
   360  	return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout))
   361  }
   362  
   363  // CommitsBetweenIDs return commits between twoe commits
   364  func (repo *Repository) CommitsBetweenIDs(last, before string) ([]*Commit, error) {
   365  	lastCommit, err := repo.GetCommit(last)
   366  	if err != nil {
   367  		return nil, err
   368  	}
   369  	if before == "" {
   370  		return repo.CommitsBetween(lastCommit, nil)
   371  	}
   372  	beforeCommit, err := repo.GetCommit(before)
   373  	if err != nil {
   374  		return nil, err
   375  	}
   376  	return repo.CommitsBetween(lastCommit, beforeCommit)
   377  }
   378  
   379  // CommitsCountBetween return numbers of commits between two commits
   380  func (repo *Repository) CommitsCountBetween(start, end string) (int64, error) {
   381  	count, err := CommitsCount(repo.Ctx, CommitsCountOptions{
   382  		RepoPath: repo.Path,
   383  		Revision: []string{start + ".." + end},
   384  	})
   385  
   386  	if err != nil && strings.Contains(err.Error(), "no merge base") {
   387  		// future versions of git >= 2.28 are likely to return an error if before and last have become unrelated.
   388  		// previously it would return the results of git rev-list before last so let's try that...
   389  		return CommitsCount(repo.Ctx, CommitsCountOptions{
   390  			RepoPath: repo.Path,
   391  			Revision: []string{start, end},
   392  		})
   393  	}
   394  
   395  	return count, err
   396  }
   397  
   398  // commitsBefore the limit is depth, not total number of returned commits.
   399  func (repo *Repository) commitsBefore(id ObjectID, limit int) ([]*Commit, error) {
   400  	cmd := NewCommand(repo.Ctx, "log", prettyLogFormat)
   401  	if limit > 0 {
   402  		cmd.AddOptionFormat("-%d", limit)
   403  	}
   404  	cmd.AddDynamicArguments(id.String())
   405  
   406  	stdout, _, runErr := cmd.RunStdBytes(&RunOpts{Dir: repo.Path})
   407  	if runErr != nil {
   408  		return nil, runErr
   409  	}
   410  
   411  	formattedLog, err := repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout))
   412  	if err != nil {
   413  		return nil, err
   414  	}
   415  
   416  	commits := make([]*Commit, 0, len(formattedLog))
   417  	for _, commit := range formattedLog {
   418  		branches, err := repo.getBranches(os.Environ(), commit.ID.String(), 2)
   419  		if err != nil {
   420  			return nil, err
   421  		}
   422  
   423  		if len(branches) > 1 {
   424  			break
   425  		}
   426  
   427  		commits = append(commits, commit)
   428  	}
   429  
   430  	return commits, nil
   431  }
   432  
   433  func (repo *Repository) getCommitsBefore(id ObjectID) ([]*Commit, error) {
   434  	return repo.commitsBefore(id, 0)
   435  }
   436  
   437  func (repo *Repository) getCommitsBeforeLimit(id ObjectID, num int) ([]*Commit, error) {
   438  	return repo.commitsBefore(id, num)
   439  }
   440  
   441  func (repo *Repository) getBranches(env []string, commitID string, limit int) ([]string, error) {
   442  	if DefaultFeatures().CheckVersionAtLeast("2.7.0") {
   443  		stdout, _, err := NewCommand(repo.Ctx, "for-each-ref", "--format=%(refname:strip=2)").
   444  			AddOptionFormat("--count=%d", limit).
   445  			AddOptionValues("--contains", commitID, BranchPrefix).
   446  			RunStdString(&RunOpts{
   447  				Dir: repo.Path,
   448  				Env: env,
   449  			})
   450  		if err != nil {
   451  			return nil, err
   452  		}
   453  
   454  		branches := strings.Fields(stdout)
   455  		return branches, nil
   456  	}
   457  
   458  	stdout, _, err := NewCommand(repo.Ctx, "branch").AddOptionValues("--contains", commitID).RunStdString(&RunOpts{
   459  		Dir: repo.Path,
   460  		Env: env,
   461  	})
   462  	if err != nil {
   463  		return nil, err
   464  	}
   465  
   466  	refs := strings.Split(stdout, "\n")
   467  
   468  	var max int
   469  	if len(refs) > limit {
   470  		max = limit
   471  	} else {
   472  		max = len(refs) - 1
   473  	}
   474  
   475  	branches := make([]string, max)
   476  	for i, ref := range refs[:max] {
   477  		parts := strings.Fields(ref)
   478  
   479  		branches[i] = parts[len(parts)-1]
   480  	}
   481  	return branches, nil
   482  }
   483  
   484  // GetCommitsFromIDs get commits from commit IDs
   485  func (repo *Repository) GetCommitsFromIDs(commitIDs []string) []*Commit {
   486  	commits := make([]*Commit, 0, len(commitIDs))
   487  
   488  	for _, commitID := range commitIDs {
   489  		commit, err := repo.GetCommit(commitID)
   490  		if err == nil && commit != nil {
   491  			commits = append(commits, commit)
   492  		}
   493  	}
   494  
   495  	return commits
   496  }
   497  
   498  // IsCommitInBranch check if the commit is on the branch
   499  func (repo *Repository) IsCommitInBranch(commitID, branch string) (r bool, err error) {
   500  	stdout, _, err := NewCommand(repo.Ctx, "branch", "--contains").AddDynamicArguments(commitID, branch).RunStdString(&RunOpts{Dir: repo.Path})
   501  	if err != nil {
   502  		return false, err
   503  	}
   504  	return len(stdout) > 0, err
   505  }
   506  
   507  func (repo *Repository) AddLastCommitCache(cacheKey, fullName, sha string) error {
   508  	if repo.LastCommitCache == nil {
   509  		commitsCount, err := cache.GetInt64(cacheKey, func() (int64, error) {
   510  			commit, err := repo.GetCommit(sha)
   511  			if err != nil {
   512  				return 0, err
   513  			}
   514  			return commit.CommitsCount()
   515  		})
   516  		if err != nil {
   517  			return err
   518  		}
   519  		repo.LastCommitCache = NewLastCommitCache(commitsCount, fullName, repo, cache.GetCache())
   520  	}
   521  	return nil
   522  }
   523  
   524  func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID string) (string, error) {
   525  	cmd := NewCommand(repo.Ctx, "log", prettyLogFormat)
   526  	cmd.AddDynamicArguments(endCommitID)
   527  
   528  	stdout, _, runErr := cmd.RunStdBytes(&RunOpts{
   529  		Dir: repo.Path,
   530  		Env: env,
   531  	})
   532  	if runErr != nil {
   533  		return "", runErr
   534  	}
   535  
   536  	parts := bytes.Split(bytes.TrimSpace(stdout), []byte{'\n'})
   537  
   538  	var startCommitID string
   539  	for _, commitID := range parts {
   540  		branches, err := repo.getBranches(env, string(commitID), 2)
   541  		if err != nil {
   542  			return "", err
   543  		}
   544  		for _, b := range branches {
   545  			if b != branch {
   546  				return startCommitID, nil
   547  			}
   548  		}
   549  
   550  		startCommitID = string(commitID)
   551  	}
   552  
   553  	return "", nil
   554  }