From dd136858f1ea40ad3c94191d647487fa4f31926c Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 18 Oct 2024 20:33:49 +0200 Subject: Adding upstream version 9.0.0. Signed-off-by: Daniel Baumann --- modules/git/repo_commit.go | 677 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 677 insertions(+) create mode 100644 modules/git/repo_commit.go (limited to 'modules/git/repo_commit.go') diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go new file mode 100644 index 0000000..1f3d64f --- /dev/null +++ b/modules/git/repo_commit.go @@ -0,0 +1,677 @@ +// Copyright 2015 The Gogs Authors. All rights reserved. +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "bufio" + "bytes" + "errors" + "io" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/cache" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +// GetBranchCommitID returns last commit ID string of given branch. +func (repo *Repository) GetBranchCommitID(name string) (string, error) { + return repo.GetRefCommitID(BranchPrefix + name) +} + +// GetTagCommitID returns last commit ID string of given tag. +func (repo *Repository) GetTagCommitID(name string) (string, error) { + return repo.GetRefCommitID(TagPrefix + name) +} + +// GetCommit returns commit object of by ID string. +func (repo *Repository) GetCommit(commitID string) (*Commit, error) { + id, err := repo.ConvertToGitID(commitID) + if err != nil { + return nil, err + } + + return repo.getCommit(id) +} + +// GetBranchCommit returns the last commit of given branch. +func (repo *Repository) GetBranchCommit(name string) (*Commit, error) { + commitID, err := repo.GetBranchCommitID(name) + if err != nil { + return nil, err + } + return repo.GetCommit(commitID) +} + +// GetTagCommit get the commit of the specific tag via name +func (repo *Repository) GetTagCommit(name string) (*Commit, error) { + commitID, err := repo.GetTagCommitID(name) + if err != nil { + return nil, err + } + return repo.GetCommit(commitID) +} + +func (repo *Repository) getCommitByPathWithID(id ObjectID, relpath string) (*Commit, error) { + // File name starts with ':' must be escaped. + if relpath[0] == ':' { + relpath = `\` + relpath + } + + stdout, _, runErr := NewCommand(repo.Ctx, "log", "-1", prettyLogFormat).AddDynamicArguments(id.String()).AddDashesAndList(relpath).RunStdString(&RunOpts{Dir: repo.Path}) + if runErr != nil { + return nil, runErr + } + + id, err := NewIDFromString(stdout) + if err != nil { + return nil, err + } + + return repo.getCommit(id) +} + +// GetCommitByPath returns the last commit of relative path. +func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) { + stdout, _, runErr := NewCommand(repo.Ctx, "log", "-1", prettyLogFormat).AddDashesAndList(relpath).RunStdBytes(&RunOpts{Dir: repo.Path}) + if runErr != nil { + return nil, runErr + } + + commits, err := repo.parsePrettyFormatLogToList(stdout) + if err != nil { + return nil, err + } + if len(commits) == 0 { + return nil, ErrNotExist{ID: relpath} + } + return commits[0], nil +} + +func (repo *Repository) commitsByRange(id ObjectID, page, pageSize int, not string) ([]*Commit, error) { + cmd := NewCommand(repo.Ctx, "log"). + AddOptionFormat("--skip=%d", (page-1)*pageSize). + AddOptionFormat("--max-count=%d", pageSize). + AddArguments(prettyLogFormat). + AddDynamicArguments(id.String()) + + if not != "" { + cmd.AddOptionValues("--not", not) + } + + stdout, _, err := cmd.RunStdBytes(&RunOpts{Dir: repo.Path}) + if err != nil { + return nil, err + } + + return repo.parsePrettyFormatLogToList(stdout) +} + +func (repo *Repository) searchCommits(id ObjectID, opts SearchCommitsOptions) ([]*Commit, error) { + // add common arguments to git command + addCommonSearchArgs := func(c *Command) { + // ignore case + c.AddArguments("-i") + + // add authors if present in search query + for _, v := range opts.Authors { + c.AddOptionFormat("--author=%s", v) + } + + // add committers if present in search query + for _, v := range opts.Committers { + c.AddOptionFormat("--committer=%s", v) + } + + // add time constraints if present in search query + if len(opts.After) > 0 { + c.AddOptionFormat("--after=%s", opts.After) + } + if len(opts.Before) > 0 { + c.AddOptionFormat("--before=%s", opts.Before) + } + } + + // create new git log command with limit of 100 commits + cmd := NewCommand(repo.Ctx, "log", "-100", prettyLogFormat).AddDynamicArguments(id.String()) + + // pretend that all refs along with HEAD were listed on command line as + // https://git-scm.com/docs/git-log#Documentation/git-log.txt---all + // note this is done only for command created above + if opts.All { + cmd.AddArguments("--all") + } + + // interpret search string keywords as string instead of regex + cmd.AddArguments("--fixed-strings") + + // add remaining keywords from search string + // note this is done only for command created above + for _, v := range opts.Keywords { + cmd.AddOptionFormat("--grep=%s", v) + } + + // search for commits matching given constraints and keywords in commit msg + addCommonSearchArgs(cmd) + stdout, _, err := cmd.RunStdBytes(&RunOpts{Dir: repo.Path}) + if err != nil { + return nil, err + } + if len(stdout) != 0 { + stdout = append(stdout, '\n') + } + + // if there are any keywords (ie not committer:, author:, time:) + // then let's iterate over them + for _, v := range opts.Keywords { + // ignore anything not matching a valid sha pattern + if id.Type().IsValid(v) { + // create new git log command with 1 commit limit + hashCmd := NewCommand(repo.Ctx, "log", "-1", prettyLogFormat) + // add previous arguments except for --grep and --all + addCommonSearchArgs(hashCmd) + // add keyword as + hashCmd.AddDynamicArguments(v) + + // search with given constraints for commit matching sha hash of v + hashMatching, _, err := hashCmd.RunStdBytes(&RunOpts{Dir: repo.Path}) + if err != nil || bytes.Contains(stdout, hashMatching) { + continue + } + stdout = append(stdout, hashMatching...) + stdout = append(stdout, '\n') + } + } + + return repo.parsePrettyFormatLogToList(bytes.TrimSuffix(stdout, []byte{'\n'})) +} + +// FileChangedBetweenCommits Returns true if the file changed between commit IDs id1 and id2 +// You must ensure that id1 and id2 are valid commit ids. +func (repo *Repository) FileChangedBetweenCommits(filename, id1, id2 string) (bool, error) { + stdout, _, err := NewCommand(repo.Ctx, "diff", "--name-only", "-z").AddDynamicArguments(id1, id2).AddDashesAndList(filename).RunStdBytes(&RunOpts{Dir: repo.Path}) + if err != nil { + return false, err + } + return len(strings.TrimSpace(string(stdout))) > 0, nil +} + +// FileCommitsCount return the number of files at a revision +func (repo *Repository) FileCommitsCount(revision, file string) (int64, error) { + return CommitsCount(repo.Ctx, + CommitsCountOptions{ + RepoPath: repo.Path, + Revision: []string{revision}, + RelPath: []string{file}, + }) +} + +type CommitsByFileAndRangeOptions struct { + Revision string + File string + Not string + Page int +} + +// CommitsByFileAndRange return the commits according revision file and the page +func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) ([]*Commit, error) { + skip := (opts.Page - 1) * setting.Git.CommitsRangeSize + + stdoutReader, stdoutWriter := io.Pipe() + defer func() { + _ = stdoutReader.Close() + _ = stdoutWriter.Close() + }() + go func() { + stderr := strings.Builder{} + gitCmd := NewCommand(repo.Ctx, "rev-list"). + AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize*opts.Page). + AddOptionFormat("--skip=%d", skip) + gitCmd.AddDynamicArguments(opts.Revision) + + if opts.Not != "" { + gitCmd.AddOptionValues("--not", opts.Not) + } + + gitCmd.AddDashesAndList(opts.File) + err := gitCmd.Run(&RunOpts{ + Dir: repo.Path, + Stdout: stdoutWriter, + Stderr: &stderr, + }) + if err != nil { + _ = stdoutWriter.CloseWithError(ConcatenateError(err, (&stderr).String())) + } else { + _ = stdoutWriter.Close() + } + }() + + objectFormat, err := repo.GetObjectFormat() + if err != nil { + return nil, err + } + + length := objectFormat.FullLength() + commits := []*Commit{} + shaline := make([]byte, length+1) + for { + n, err := io.ReadFull(stdoutReader, shaline) + if err != nil || n < length { + if err == io.EOF { + err = nil + } + return commits, err + } + objectID, err := NewIDFromString(string(shaline[0:length])) + if err != nil { + return nil, err + } + commit, err := repo.getCommit(objectID) + if err != nil { + return nil, err + } + commits = append(commits, commit) + } +} + +// FilesCountBetween return the number of files changed between two commits +func (repo *Repository) FilesCountBetween(startCommitID, endCommitID string) (int, error) { + stdout, _, err := NewCommand(repo.Ctx, "diff", "--name-only").AddDynamicArguments(startCommitID + "..." + endCommitID).RunStdString(&RunOpts{Dir: repo.Path}) + if err != nil && strings.Contains(err.Error(), "no merge base") { + // git >= 2.28 now returns an error if startCommitID and endCommitID have become unrelated. + // previously it would return the results of git diff --name-only startCommitID endCommitID so let's try that... + stdout, _, err = NewCommand(repo.Ctx, "diff", "--name-only").AddDynamicArguments(startCommitID, endCommitID).RunStdString(&RunOpts{Dir: repo.Path}) + } + if err != nil { + return 0, err + } + return len(strings.Split(stdout, "\n")) - 1, nil +} + +// CommitsBetween returns a list that contains commits between [before, last). +// If before is detached (removed by reset + push) it is not included. +func (repo *Repository) CommitsBetween(last, before *Commit) ([]*Commit, error) { + var stdout []byte + var err error + if before == nil { + stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) + } else { + stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(before.ID.String() + ".." + last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) + if err != nil && strings.Contains(err.Error(), "no merge base") { + // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. + // previously it would return the results of git rev-list before last so let's try that... + stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(before.ID.String(), last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) + } + } + if err != nil { + return nil, err + } + return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout)) +} + +// CommitsBetweenLimit returns a list that contains at most limit commits skipping the first skip commits between [before, last) +func (repo *Repository) CommitsBetweenLimit(last, before *Commit, limit, skip int) ([]*Commit, error) { + var stdout []byte + var err error + if before == nil { + stdout, _, err = NewCommand(repo.Ctx, "rev-list"). + AddOptionValues("--max-count", strconv.Itoa(limit)). + AddOptionValues("--skip", strconv.Itoa(skip)). + AddDynamicArguments(last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) + } else { + stdout, _, err = NewCommand(repo.Ctx, "rev-list"). + AddOptionValues("--max-count", strconv.Itoa(limit)). + AddOptionValues("--skip", strconv.Itoa(skip)). + AddDynamicArguments(before.ID.String() + ".." + last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) + if err != nil && strings.Contains(err.Error(), "no merge base") { + // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. + // previously it would return the results of git rev-list --max-count n before last so let's try that... + stdout, _, err = NewCommand(repo.Ctx, "rev-list"). + AddOptionValues("--max-count", strconv.Itoa(limit)). + AddOptionValues("--skip", strconv.Itoa(skip)). + AddDynamicArguments(before.ID.String(), last.ID.String()).RunStdBytes(&RunOpts{Dir: repo.Path}) + } + } + if err != nil { + return nil, err + } + return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout)) +} + +// CommitsBetweenNotBase returns a list that contains commits between [before, last), excluding commits in baseBranch. +// If before is detached (removed by reset + push) it is not included. +func (repo *Repository) CommitsBetweenNotBase(last, before *Commit, baseBranch string) ([]*Commit, error) { + var stdout []byte + var err error + if before == nil { + stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(&RunOpts{Dir: repo.Path}) + } else { + stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(before.ID.String()+".."+last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(&RunOpts{Dir: repo.Path}) + if err != nil && strings.Contains(err.Error(), "no merge base") { + // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. + // previously it would return the results of git rev-list before last so let's try that... + stdout, _, err = NewCommand(repo.Ctx, "rev-list").AddDynamicArguments(before.ID.String(), last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(&RunOpts{Dir: repo.Path}) + } + } + if err != nil { + return nil, err + } + return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout)) +} + +// CommitsBetweenIDs return commits between twoe commits +func (repo *Repository) CommitsBetweenIDs(last, before string) ([]*Commit, error) { + lastCommit, err := repo.GetCommit(last) + if err != nil { + return nil, err + } + if before == "" { + return repo.CommitsBetween(lastCommit, nil) + } + beforeCommit, err := repo.GetCommit(before) + if err != nil { + return nil, err + } + return repo.CommitsBetween(lastCommit, beforeCommit) +} + +// CommitsCountBetween return numbers of commits between two commits +func (repo *Repository) CommitsCountBetween(start, end string) (int64, error) { + count, err := CommitsCount(repo.Ctx, CommitsCountOptions{ + RepoPath: repo.Path, + Revision: []string{start + ".." + end}, + }) + + if err != nil && strings.Contains(err.Error(), "no merge base") { + // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. + // previously it would return the results of git rev-list before last so let's try that... + return CommitsCount(repo.Ctx, CommitsCountOptions{ + RepoPath: repo.Path, + Revision: []string{start, end}, + }) + } + + return count, err +} + +// commitsBefore the limit is depth, not total number of returned commits. +func (repo *Repository) commitsBefore(id ObjectID, limit int) ([]*Commit, error) { + cmd := NewCommand(repo.Ctx, "log", prettyLogFormat) + if limit > 0 { + cmd.AddOptionFormat("-%d", limit) + } + cmd.AddDynamicArguments(id.String()) + + stdout, _, runErr := cmd.RunStdBytes(&RunOpts{Dir: repo.Path}) + if runErr != nil { + return nil, runErr + } + + formattedLog, err := repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout)) + if err != nil { + return nil, err + } + + commits := make([]*Commit, 0, len(formattedLog)) + for _, commit := range formattedLog { + branches, err := repo.getBranches(commit, 2) + if err != nil { + return nil, err + } + + if len(branches) > 1 { + break + } + + commits = append(commits, commit) + } + + return commits, nil +} + +func (repo *Repository) getCommitsBefore(id ObjectID) ([]*Commit, error) { + return repo.commitsBefore(id, 0) +} + +func (repo *Repository) getCommitsBeforeLimit(id ObjectID, num int) ([]*Commit, error) { + return repo.commitsBefore(id, num) +} + +func (repo *Repository) getBranches(commit *Commit, limit int) ([]string, error) { + if CheckGitVersionAtLeast("2.7.0") == nil { + stdout, _, err := NewCommand(repo.Ctx, "for-each-ref", "--format=%(refname:strip=2)"). + AddOptionFormat("--count=%d", limit). + AddOptionValues("--contains", commit.ID.String(), BranchPrefix). + RunStdString(&RunOpts{Dir: repo.Path}) + if err != nil { + return nil, err + } + + branches := strings.Fields(stdout) + return branches, nil + } + + stdout, _, err := NewCommand(repo.Ctx, "branch").AddOptionValues("--contains", commit.ID.String()).RunStdString(&RunOpts{Dir: repo.Path}) + if err != nil { + return nil, err + } + + refs := strings.Split(stdout, "\n") + + var max int + if len(refs) > limit { + max = limit + } else { + max = len(refs) - 1 + } + + branches := make([]string, max) + for i, ref := range refs[:max] { + parts := strings.Fields(ref) + + branches[i] = parts[len(parts)-1] + } + return branches, nil +} + +// GetCommitsFromIDs get commits from commit IDs +func (repo *Repository) GetCommitsFromIDs(commitIDs []string) []*Commit { + commits := make([]*Commit, 0, len(commitIDs)) + + for _, commitID := range commitIDs { + commit, err := repo.GetCommit(commitID) + if err == nil && commit != nil { + commits = append(commits, commit) + } + } + + return commits +} + +// IsCommitInBranch check if the commit is on the branch +func (repo *Repository) IsCommitInBranch(commitID, branch string) (r bool, err error) { + stdout, _, err := NewCommand(repo.Ctx, "branch", "--contains").AddDynamicArguments(commitID, branch).RunStdString(&RunOpts{Dir: repo.Path}) + if err != nil { + return false, err + } + return len(stdout) > 0, err +} + +func (repo *Repository) AddLastCommitCache(cacheKey, fullName, sha string) error { + if repo.LastCommitCache == nil { + commitsCount, err := cache.GetInt64(cacheKey, func() (int64, error) { + commit, err := repo.GetCommit(sha) + if err != nil { + return 0, err + } + return commit.CommitsCount() + }) + if err != nil { + return err + } + repo.LastCommitCache = NewLastCommitCache(commitsCount, fullName, repo, cache.GetCache()) + } + return nil +} + +// ResolveReference resolves a name to a reference +func (repo *Repository) ResolveReference(name string) (string, error) { + stdout, _, err := NewCommand(repo.Ctx, "show-ref", "--hash").AddDynamicArguments(name).RunStdString(&RunOpts{Dir: repo.Path}) + if err != nil { + if strings.Contains(err.Error(), "not a valid ref") { + return "", ErrNotExist{name, ""} + } + return "", err + } + stdout = strings.TrimSpace(stdout) + if stdout == "" { + return "", ErrNotExist{name, ""} + } + + return stdout, nil +} + +// GetRefCommitID returns the last commit ID string of given reference (branch or tag). +func (repo *Repository) GetRefCommitID(name string) (string, error) { + wr, rd, cancel, err := repo.CatFileBatchCheck(repo.Ctx) + if err != nil { + return "", err + } + defer cancel() + _, err = wr.Write([]byte(name + "\n")) + if err != nil { + return "", err + } + shaBs, _, _, err := ReadBatchLine(rd) + if IsErrNotExist(err) { + return "", ErrNotExist{name, ""} + } + + return string(shaBs), nil +} + +// SetReference sets the commit ID string of given reference (e.g. branch or tag). +func (repo *Repository) SetReference(name, commitID string) error { + _, _, err := NewCommand(repo.Ctx, "update-ref").AddDynamicArguments(name, commitID).RunStdString(&RunOpts{Dir: repo.Path}) + return err +} + +// RemoveReference removes the given reference (e.g. branch or tag). +func (repo *Repository) RemoveReference(name string) error { + _, _, err := NewCommand(repo.Ctx, "update-ref", "--no-deref", "-d").AddDynamicArguments(name).RunStdString(&RunOpts{Dir: repo.Path}) + return err +} + +// IsCommitExist returns true if given commit exists in current repository. +func (repo *Repository) IsCommitExist(name string) bool { + if err := ensureValidGitRepository(repo.Ctx, repo.Path); err != nil { + log.Error("IsCommitExist: %v", err) + return false + } + _, _, err := NewCommand(repo.Ctx, "cat-file", "-e").AddDynamicArguments(name).RunStdString(&RunOpts{Dir: repo.Path}) + return err == nil +} + +func (repo *Repository) getCommit(id ObjectID) (*Commit, error) { + wr, rd, cancel, err := repo.CatFileBatch(repo.Ctx) + if err != nil { + return nil, err + } + defer cancel() + + _, _ = wr.Write([]byte(id.String() + "\n")) + + return repo.getCommitFromBatchReader(rd, id) +} + +func (repo *Repository) getCommitFromBatchReader(rd *bufio.Reader, id ObjectID) (*Commit, error) { + _, typ, size, err := ReadBatchLine(rd) + if err != nil { + if errors.Is(err, io.EOF) || IsErrNotExist(err) { + return nil, ErrNotExist{ID: id.String()} + } + return nil, err + } + + switch typ { + case "missing": + return nil, ErrNotExist{ID: id.String()} + case "tag": + // then we need to parse the tag + // and load the commit + data, err := io.ReadAll(io.LimitReader(rd, size)) + if err != nil { + return nil, err + } + _, err = rd.Discard(1) + if err != nil { + return nil, err + } + tag, err := parseTagData(id.Type(), data) + if err != nil { + return nil, err + } + + commit, err := tag.Commit(repo) + if err != nil { + return nil, err + } + + return commit, nil + case "commit": + commit, err := CommitFromReader(repo, id, io.LimitReader(rd, size)) + if err != nil { + return nil, err + } + _, err = rd.Discard(1) + if err != nil { + return nil, err + } + + return commit, nil + default: + log.Debug("Unknown typ: %s", typ) + if err := DiscardFull(rd, size+1); err != nil { + return nil, err + } + return nil, ErrNotExist{ + ID: id.String(), + } + } +} + +// ConvertToGitID returns a GitHash object from a potential ID string +func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) { + objectFormat, err := repo.GetObjectFormat() + if err != nil { + return nil, err + } + if len(commitID) == objectFormat.FullLength() && objectFormat.IsValid(commitID) { + ID, err := NewIDFromString(commitID) + if err == nil { + return ID, nil + } + } + + wr, rd, cancel, err := repo.CatFileBatchCheck(repo.Ctx) + if err != nil { + return nil, err + } + defer cancel() + _, err = wr.Write([]byte(commitID + "\n")) + if err != nil { + return nil, err + } + sha, _, _, err := ReadBatchLine(rd) + if err != nil { + if IsErrNotExist(err) { + return nil, ErrNotExist{commitID, ""} + } + return nil, err + } + + return MustIDFromString(string(sha)), nil +} -- cgit v1.2.3