Files
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

276 lines
8.0 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"context"
"errors"
"io"
"os/exec"
"strings"
"github.com/hanzoai/git/modules/git/gitcmd"
"github.com/hanzoai/git/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(gitRepo *Repository, n int) (*Commit, error) {
id, err := c.ParentID(n)
if err != nil {
return nil, err
}
parent, err := gitRepo.getCommit(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(gitRepo *Repository, relpath string) (*Commit, error) {
if gitRepo.LastCommitCache != nil {
return gitRepo.LastCommitCache.GetCommitByPath(c.ID.String(), relpath)
}
return gitRepo.getCommitByPathWithID(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(gitRepo *Repository, page, pageSize int, not, since, until string) ([]*Commit, error) {
return gitRepo.commitsByRangeWithTime(c.ID, page, pageSize, not, since, until)
}
// CommitsBefore returns all the commits before current revision
func (c *Commit) CommitsBefore(gitRepo *Repository) ([]*Commit, error) {
return gitRepo.getCommitsBefore(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).
WithDir(gitRepo.Path).
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()
if err != nil {
return false, err
}
if oldCommitID == objectFormat.EmptyObjectID().String() {
return false, nil
}
oldCommit, err := gitRepo.GetCommit(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(gitRepo *Repository, num int) ([]*Commit, error) {
return gitRepo.getCommitsBeforeLimit(c.ID, num)
}
// CommitsBeforeUntil returns the commits in range "[cur, ref)"
func (c *Commit) CommitsBeforeUntil(gitRepo *Repository, ref RefName) ([]*Commit, error) {
return gitRepo.CommitsBetween(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(gitRepo *Repository, opts SearchCommitsOptions) ([]*Commit, error) {
return gitRepo.searchCommits(c.ID, opts)
}
// GetFilesChangedSinceCommit get all changed file names between pastCommit to current revision
func (c *Commit) GetFilesChangedSinceCommit(gitRepo *Repository, pastCommit string) ([]string, error) {
return gitRepo.GetFilesChangedBetween(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(gitRepo *Repository, filename, pastCommit string) (bool, error) {
return gitRepo.FileChangedBetweenCommits(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()
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, repoPath, shortID string) (string, error) {
commitID, _, err := gitcmd.NewCommand("rev-parse").
AddDynamicArguments(shortID).
WithDir(repoPath).
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
}