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