Files
gitea/modules/git/commit.go
wxiaoguang 9dc04289aa refactor: make git package handle all git operations (#38543)
before: gitrepo vs git packages
after: git package fully handle all git operations

by the way, use `WithRepo(repo)` instead of `WithDir(repo.Path)` to hide
path details.

benefits:

1. remove all unnecessary wrappers, developers no need to struggle with
"which package should be used"
2. simplify code, RepositoryFacade can (will) be used everywhere, all
"path" details are (will be) hidden
2026-07-20 16:07:38 +00:00

277 lines
8.2 KiB
Go

// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"context"
"errors"
"io"
"os/exec"
"strings"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/util"
)
// Commit represents a git commit.
type Commit struct {
CommitMessage
ID ObjectID
TreeID ObjectID
Parents []ObjectID
Author *Signature // never nil
Committer *Signature // never nil
Signature *CommitSignature
submoduleCache *ObjectCache[*SubModule]
treeCache *Tree
}
// CommitSignature represents a git commit signature part.
type CommitSignature struct {
Signature string
Payload string
}
// ParentID returns oid of n-th parent (0-based index).
// It returns nil if no such parent exists.
func (c *Commit) ParentID(n int) (ObjectID, error) {
if n >= len(c.Parents) {
return nil, ErrNotExist{"", ""}
}
return c.Parents[n], nil
}
// Parent returns n-th parent (0-based index) of the commit.
func (c *Commit) Parent(ctx context.Context, gitRepo *Repository, n int) (*Commit, error) {
id, err := c.ParentID(n)
if err != nil {
return nil, err
}
parent, err := gitRepo.getCommit(ctx, id)
if err != nil {
return nil, err
}
return parent, nil
}
// ParentCount returns number of parents of the commit.
// 0 if this is the root commit, otherwise 1,2, etc.
func (c *Commit) ParentCount() int {
return len(c.Parents)
}
// GetCommitByPath return the commit of relative path object.
func (c *Commit) GetCommitByPath(ctx context.Context, gitRepo *Repository, relpath string) (*Commit, error) {
if gitRepo.LastCommitCache != nil {
return gitRepo.LastCommitCache.GetCommitByPath(ctx, c.ID.String(), relpath)
}
return gitRepo.getCommitByPathWithID(ctx, c.ID, relpath)
}
func (c *Commit) Tree() *Tree {
if c.treeCache == nil {
c.treeCache = newTree(c.TreeID)
}
return c.treeCache
}
func (c *Commit) GetBlobByPath(ctx context.Context, gitRepo *Repository, relpath string) (*Blob, error) {
return c.Tree().GetBlobByPath(ctx, gitRepo, relpath)
}
func (c *Commit) GetTreeEntryByPath(ctx context.Context, gitRepo *Repository, relpath string) (_ *TreeEntry, err error) {
return c.Tree().GetTreeEntryByPath(ctx, gitRepo, relpath)
}
func (c *Commit) SubTree(ctx context.Context, gitRepo *Repository, relpath string) (*Tree, error) {
return c.Tree().SubTree(ctx, gitRepo, relpath)
}
// CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize
func (c *Commit) CommitsByRange(ctx context.Context, gitRepo *Repository, page, pageSize int, not, since, until string) ([]*Commit, error) {
return gitRepo.commitsByRangeWithTime(ctx, c.ID, page, pageSize, not, since, until)
}
// CommitsBefore returns all the commits before current revision
func (c *Commit) CommitsBefore(ctx context.Context, gitRepo *Repository) ([]*Commit, error) {
return gitRepo.getCommitsBefore(ctx, c.ID)
}
// HasPreviousCommit returns true if a given commitHash is contained in commit's parents
func (c *Commit) HasPreviousCommit(ctx context.Context, gitRepo *Repository, objectID ObjectID) (bool, error) {
this := c.ID.String()
that := objectID.String()
if this == that {
return false, nil
}
_, _, err := gitcmd.NewCommand("merge-base", "--is-ancestor").
AddDynamicArguments(that, this).
WithRepo(gitRepo).
RunStdString(ctx)
if err == nil {
return true, nil
}
var exitError *exec.ExitError
if errors.As(err, &exitError) {
if exitError.ProcessState.ExitCode() == 1 && len(exitError.Stderr) == 0 {
return false, nil
}
}
return false, err
}
// IsForcePush returns true if a push from oldCommitHash to this is a force push
func (c *Commit) IsForcePush(ctx context.Context, gitRepo *Repository, oldCommitID string) (bool, error) {
objectFormat, err := gitRepo.GetObjectFormat(ctx)
if err != nil {
return false, err
}
if oldCommitID == objectFormat.EmptyObjectID().String() {
return false, nil
}
oldCommit, err := gitRepo.GetCommit(ctx, oldCommitID)
if err != nil {
return false, err
}
hasPreviousCommit, err := c.HasPreviousCommit(ctx, gitRepo, oldCommit.ID)
return !hasPreviousCommit, err
}
// CommitsBeforeLimit returns num commits before current revision
func (c *Commit) CommitsBeforeLimit(ctx context.Context, gitRepo *Repository, num int) ([]*Commit, error) {
return gitRepo.getCommitsBeforeLimit(ctx, c.ID, num)
}
// CommitsBeforeUntil returns the commits in range "[cur, ref)"
func (c *Commit) CommitsBeforeUntil(ctx context.Context, gitRepo *Repository, ref RefName) ([]*Commit, error) {
return gitRepo.CommitsBetween(ctx, c.ID.RefName(), ref, -1)
}
// SearchCommitsOptions specify the parameters for SearchCommits
type SearchCommitsOptions struct {
Keywords []string
Authors, Committers []string
After, Before string
All bool
}
// NewSearchCommitsOptions construct a SearchCommitsOption from a space-delimited search string
func NewSearchCommitsOptions(searchString string, forAllRefs bool) SearchCommitsOptions {
var keywords, authors, committers []string
var after, before string
fields := strings.FieldsSeq(searchString)
for k := range fields {
switch {
case strings.HasPrefix(k, "author:"):
authors = append(authors, strings.TrimPrefix(k, "author:"))
case strings.HasPrefix(k, "committer:"):
committers = append(committers, strings.TrimPrefix(k, "committer:"))
case strings.HasPrefix(k, "after:"):
after = strings.TrimPrefix(k, "after:")
case strings.HasPrefix(k, "before:"):
before = strings.TrimPrefix(k, "before:")
default:
keywords = append(keywords, k)
}
}
return SearchCommitsOptions{
Keywords: keywords,
Authors: authors,
Committers: committers,
After: after,
Before: before,
All: forAllRefs,
}
}
// SearchCommits returns the commits match the keyword before current revision
func (c *Commit) SearchCommits(ctx context.Context, gitRepo *Repository, opts SearchCommitsOptions) ([]*Commit, error) {
return gitRepo.searchCommits(ctx, c.ID, opts)
}
// GetFilesChangedSinceCommit get all changed file names between pastCommit to current revision
func (c *Commit) GetFilesChangedSinceCommit(ctx context.Context, gitRepo *Repository, pastCommit string) ([]string, error) {
return gitRepo.GetFilesChangedBetween(ctx, pastCommit, c.ID.String())
}
// FileChangedSinceCommit Returns true if the file given has changed since the past commit
// YOU MUST ENSURE THAT pastCommit is a valid commit ID.
func (c *Commit) FileChangedSinceCommit(ctx context.Context, gitRepo *Repository, filename, pastCommit string) (bool, error) {
return gitRepo.FileChangedBetweenCommits(ctx, filename, pastCommit, c.ID.String())
}
// GetFileContent reads a file content as a string or returns false if this was not possible
func (c *Commit) GetFileContent(ctx context.Context, gitRepo *Repository, filename string, limit int) (string, error) {
entry, err := c.GetTreeEntryByPath(ctx, gitRepo, filename)
if err != nil {
return "", err
}
r, err := entry.Blob(gitRepo).DataAsync(ctx)
if err != nil {
return "", err
}
defer r.Close()
if limit > 0 {
bs := make([]byte, limit)
n, err := util.ReadAtMost(r, bs)
if err != nil {
return "", err
}
return string(bs[:n]), nil
}
bytes, err := io.ReadAll(r)
if err != nil {
return "", err
}
return string(bytes), nil
}
// GetFullCommitID returns full length (40) of commit ID by given short SHA in a repository.
func GetFullCommitID(ctx context.Context, repo RepositoryFacade, shortID string) (string, error) {
commitID, _, err := gitcmd.NewCommand("rev-parse").
AddDynamicArguments(shortID).
WithRepo(repo).
RunStdString(ctx)
if err != nil {
if gitcmd.IsErrorExitCode(err, 128) {
return "", ErrNotExist{shortID, ""}
}
return "", err
}
return strings.TrimSpace(commitID), nil
}
func IsStringLikelyCommitID(objFmt ObjectFormat, s string, minLength ...int) bool {
maxLen := 64 // sha256
if objFmt != nil {
maxLen = objFmt.FullLength()
}
minLen := util.OptionalArg(minLength, maxLen)
if len(s) < minLen || len(s) > maxLen {
return false
}
return isStringLowerHex(s)
}
func isStringLowerHex(s string) bool {
for _, c := range s {
isHex := (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')
if !isHex {
return false
}
}
return len(s) > 0 // it accepts odd length because "shorten commit id" can be 7-chars
}