diff --git a/modules/git/commit.go b/modules/git/commit.go index 260b81b59022e..af09697018261 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -5,17 +5,13 @@ package git import ( - "bufio" - "bytes" "context" "errors" "io" "os/exec" - "strconv" "strings" "code.gitea.io/gitea/modules/git/gitcmd" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" ) @@ -130,65 +126,6 @@ func CommitChanges(ctx context.Context, repoPath string, opts CommitChangesOptio return err } -// AllCommitsCount returns count of all commits in repository -func AllCommitsCount(ctx context.Context, repoPath string, hidePRRefs bool, files ...string) (int64, error) { - cmd := gitcmd.NewCommand("rev-list") - if hidePRRefs { - cmd.AddArguments("--exclude=" + PullPrefix + "*") - } - cmd.AddArguments("--all", "--count") - if len(files) > 0 { - cmd.AddDashesAndList(files...) - } - - stdout, _, err := cmd.WithDir(repoPath).RunStdString(ctx) - if err != nil { - return 0, err - } - - return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64) -} - -// CommitsCountOptions the options when counting commits -type CommitsCountOptions struct { - RepoPath string - Not string - Revision []string - RelPath []string - Since string - Until string -} - -// CommitsCount returns number of total commits of until given revision. -func CommitsCount(ctx context.Context, opts CommitsCountOptions) (int64, error) { - cmd := gitcmd.NewCommand("rev-list", "--count") - - cmd.AddDynamicArguments(opts.Revision...) - - if opts.Not != "" { - cmd.AddOptionValues("--not", opts.Not) - } - - if len(opts.RelPath) > 0 { - cmd.AddDashesAndList(opts.RelPath...) - } - - stdout, _, err := cmd.WithDir(opts.RepoPath).RunStdString(ctx) - if err != nil { - return 0, err - } - - return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64) -} - -// CommitsCount returns number of total commits of until current revision. -func (c *Commit) CommitsCount() (int64, error) { - return CommitsCount(c.repo.Ctx, CommitsCountOptions{ - RepoPath: c.repo.Path, - Revision: []string{c.ID.String()}, - }) -} - // CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize func (c *Commit) CommitsByRange(page, pageSize int, not, since, until string) ([]*Commit, error) { return c.repo.commitsByRangeWithTime(c.ID, page, pageSize, not, since, until) @@ -371,85 +308,6 @@ func (c *Commit) GetBranchName() (string, error) { return strings.SplitN(strings.TrimSpace(data), "~", 2)[0], nil } -// CommitFileStatus represents status of files in a commit. -type CommitFileStatus struct { - Added []string - Removed []string - Modified []string -} - -// NewCommitFileStatus creates a CommitFileStatus -func NewCommitFileStatus() *CommitFileStatus { - return &CommitFileStatus{ - []string{}, []string{}, []string{}, - } -} - -func parseCommitFileStatus(fileStatus *CommitFileStatus, stdout io.Reader) { - rd := bufio.NewReader(stdout) - peek, err := rd.Peek(1) - if err != nil { - if err != io.EOF { - log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) - } - return - } - if peek[0] == '\n' || peek[0] == '\x00' { - _, _ = rd.Discard(1) - } - for { - modifier, err := rd.ReadString('\x00') - if err != nil { - if err != io.EOF { - log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) - } - return - } - file, err := rd.ReadString('\x00') - if err != nil { - if err != io.EOF { - log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) - } - return - } - file = file[:len(file)-1] - switch modifier[0] { - case 'A': - fileStatus.Added = append(fileStatus.Added, file) - case 'D': - fileStatus.Removed = append(fileStatus.Removed, file) - case 'M': - fileStatus.Modified = append(fileStatus.Modified, file) - } - } -} - -// GetCommitFileStatus returns file status of commit in given repository. -func GetCommitFileStatus(ctx context.Context, repoPath, commitID string) (*CommitFileStatus, error) { - stdout, w := io.Pipe() - done := make(chan struct{}) - fileStatus := NewCommitFileStatus() - go func() { - parseCommitFileStatus(fileStatus, stdout) - close(done) - }() - - stderr := new(bytes.Buffer) - err := gitcmd.NewCommand("log", "--name-status", "-m", "--pretty=format:", "--first-parent", "--no-renames", "-z", "-1"). - AddDynamicArguments(commitID). - WithDir(repoPath). - WithStdout(w). - WithStderr(stderr). - Run(ctx) - w.Close() // Close writer to exit parsing goroutine - if err != nil { - return nil, gitcmd.ConcatenateError(err, stderr.String()) - } - - <-done - return fileStatus, 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"). diff --git a/modules/git/commit_sha256_test.go b/modules/git/commit_sha256_test.go index 772f5eedb2798..0aefb30c95ea5 100644 --- a/modules/git/commit_sha256_test.go +++ b/modules/git/commit_sha256_test.go @@ -14,33 +14,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestCommitsCountSha256(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256") - - commitsCount, err := CommitsCount(t.Context(), - CommitsCountOptions{ - RepoPath: bareRepo1Path, - Revision: []string{"f004f41359117d319dedd0eaab8c5259ee2263da839dcba33637997458627fdc"}, - }) - - assert.NoError(t, err) - assert.Equal(t, int64(3), commitsCount) -} - -func TestCommitsCountWithoutBaseSha256(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256") - - commitsCount, err := CommitsCount(t.Context(), - CommitsCountOptions{ - RepoPath: bareRepo1Path, - Not: "main", - Revision: []string{"branch1"}, - }) - - assert.NoError(t, err) - assert.Equal(t, int64(2), commitsCount) -} - func TestGetFullCommitIDSha256(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256") @@ -157,39 +130,3 @@ func TestHasPreviousCommitSha256(t *testing.T) { assert.NoError(t, err) assert.False(t, selfNot) } - -func TestGetCommitFileStatusMergesSha256(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo6_merge_sha256") - - commitFileStatus, err := GetCommitFileStatus(t.Context(), bareRepo1Path, "d2e5609f630dd8db500f5298d05d16def282412e3e66ed68cc7d0833b29129a1") - assert.NoError(t, err) - - expected := CommitFileStatus{ - []string{ - "add_file.txt", - }, - []string{}, - []string{ - "to_modify.txt", - }, - } - - assert.Equal(t, expected.Added, commitFileStatus.Added) - assert.Equal(t, expected.Removed, commitFileStatus.Removed) - assert.Equal(t, expected.Modified, commitFileStatus.Modified) - - expected = CommitFileStatus{ - []string{}, - []string{ - "to_remove.txt", - }, - []string{}, - } - - commitFileStatus, err = GetCommitFileStatus(t.Context(), bareRepo1Path, "da1ded40dc8e5b7c564171f4bf2fc8370487decfb1cb6a99ef28f3ed73d09172") - assert.NoError(t, err) - - assert.Equal(t, expected.Added, commitFileStatus.Added) - assert.Equal(t, expected.Removed, commitFileStatus.Removed) - assert.Equal(t, expected.Modified, commitFileStatus.Modified) -} diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go index 688b4e294f5bb..de7b7455ebb07 100644 --- a/modules/git/commit_test.go +++ b/modules/git/commit_test.go @@ -13,33 +13,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestCommitsCount(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - - commitsCount, err := CommitsCount(t.Context(), - CommitsCountOptions{ - RepoPath: bareRepo1Path, - Revision: []string{"8006ff9adbf0cb94da7dad9e537e53817f9fa5c0"}, - }) - - assert.NoError(t, err) - assert.Equal(t, int64(3), commitsCount) -} - -func TestCommitsCountWithoutBase(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - - commitsCount, err := CommitsCount(t.Context(), - CommitsCountOptions{ - RepoPath: bareRepo1Path, - Not: "master", - Revision: []string{"branch1"}, - }) - - assert.NoError(t, err) - assert.Equal(t, int64(2), commitsCount) -} - func TestGetFullCommitID(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") @@ -212,134 +185,6 @@ func TestHasPreviousCommit(t *testing.T) { assert.False(t, selfNot) } -func TestParseCommitFileStatus(t *testing.T) { - type testcase struct { - output string - added []string - removed []string - modified []string - } - - kases := []testcase{ - { - // Merge commit - output: "MM\x00options/locale/locale_en-US.ini\x00", - modified: []string{ - "options/locale/locale_en-US.ini", - }, - added: []string{}, - removed: []string{}, - }, - { - // Spaces commit - output: "D\x00b\x00D\x00b b/b\x00A\x00b b/b b/b b/b\x00A\x00b b/b b/b b/b b/b\x00", - removed: []string{ - "b", - "b b/b", - }, - modified: []string{}, - added: []string{ - "b b/b b/b b/b", - "b b/b b/b b/b b/b", - }, - }, - { - // larger commit - output: "M\x00go.mod\x00M\x00go.sum\x00M\x00modules/ssh/ssh.go\x00M\x00vendor/github.com/gliderlabs/ssh/circle.yml\x00M\x00vendor/github.com/gliderlabs/ssh/context.go\x00A\x00vendor/github.com/gliderlabs/ssh/go.mod\x00A\x00vendor/github.com/gliderlabs/ssh/go.sum\x00M\x00vendor/github.com/gliderlabs/ssh/server.go\x00M\x00vendor/github.com/gliderlabs/ssh/session.go\x00M\x00vendor/github.com/gliderlabs/ssh/ssh.go\x00M\x00vendor/golang.org/x/sys/unix/mkerrors.sh\x00M\x00vendor/golang.org/x/sys/unix/syscall_darwin.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_darwin_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_darwin_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_386.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_arm.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_linux.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_darwin_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_darwin_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_dragonfly_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_386.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_arm.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_386.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_arm.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_arm64.go\x00M\x00vendor/modules.txt\x00", - modified: []string{ - "go.mod", - "go.sum", - "modules/ssh/ssh.go", - "vendor/github.com/gliderlabs/ssh/circle.yml", - "vendor/github.com/gliderlabs/ssh/context.go", - "vendor/github.com/gliderlabs/ssh/server.go", - "vendor/github.com/gliderlabs/ssh/session.go", - "vendor/github.com/gliderlabs/ssh/ssh.go", - "vendor/golang.org/x/sys/unix/mkerrors.sh", - "vendor/golang.org/x/sys/unix/syscall_darwin.go", - "vendor/golang.org/x/sys/unix/zerrors_darwin_amd64.go", - "vendor/golang.org/x/sys/unix/zerrors_darwin_arm64.go", - "vendor/golang.org/x/sys/unix/zerrors_freebsd_386.go", - "vendor/golang.org/x/sys/unix/zerrors_freebsd_amd64.go", - "vendor/golang.org/x/sys/unix/zerrors_freebsd_arm.go", - "vendor/golang.org/x/sys/unix/zerrors_freebsd_arm64.go", - "vendor/golang.org/x/sys/unix/zerrors_linux.go", - "vendor/golang.org/x/sys/unix/ztypes_darwin_amd64.go", - "vendor/golang.org/x/sys/unix/ztypes_darwin_arm64.go", - "vendor/golang.org/x/sys/unix/ztypes_dragonfly_amd64.go", - "vendor/golang.org/x/sys/unix/ztypes_freebsd_386.go", - "vendor/golang.org/x/sys/unix/ztypes_freebsd_amd64.go", - "vendor/golang.org/x/sys/unix/ztypes_freebsd_arm.go", - "vendor/golang.org/x/sys/unix/ztypes_freebsd_arm64.go", - "vendor/golang.org/x/sys/unix/ztypes_netbsd_386.go", - "vendor/golang.org/x/sys/unix/ztypes_netbsd_amd64.go", - "vendor/golang.org/x/sys/unix/ztypes_netbsd_arm.go", - "vendor/golang.org/x/sys/unix/ztypes_netbsd_arm64.go", - "vendor/modules.txt", - }, - added: []string{ - "vendor/github.com/gliderlabs/ssh/go.mod", - "vendor/github.com/gliderlabs/ssh/go.sum", - }, - removed: []string{}, - }, - { - // git 1.7.2 adds an unnecessary \x00 on merge commit - output: "\x00MM\x00options/locale/locale_en-US.ini\x00", - modified: []string{ - "options/locale/locale_en-US.ini", - }, - added: []string{}, - removed: []string{}, - }, - { - // git 1.7.2 adds an unnecessary \n on normal commit - output: "\nD\x00b\x00D\x00b b/b\x00A\x00b b/b b/b b/b\x00A\x00b b/b b/b b/b b/b\x00", - removed: []string{ - "b", - "b b/b", - }, - modified: []string{}, - added: []string{ - "b b/b b/b b/b", - "b b/b b/b b/b b/b", - }, - }, - } - - for _, kase := range kases { - fileStatus := NewCommitFileStatus() - parseCommitFileStatus(fileStatus, strings.NewReader(kase.output)) - - assert.Equal(t, kase.added, fileStatus.Added) - assert.Equal(t, kase.removed, fileStatus.Removed) - assert.Equal(t, kase.modified, fileStatus.Modified) - } -} - -func TestGetCommitFileStatusMerges(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo6_merge") - - commitFileStatus, err := GetCommitFileStatus(t.Context(), bareRepo1Path, "022f4ce6214973e018f02bf363bf8a2e3691f699") - assert.NoError(t, err) - - expected := CommitFileStatus{ - []string{ - "add_file.txt", - }, - []string{ - "to_remove.txt", - }, - []string{ - "to_modify.txt", - }, - } - - assert.Equal(t, expected.Added, commitFileStatus.Added) - assert.Equal(t, expected.Removed, commitFileStatus.Removed) - assert.Equal(t, expected.Modified, commitFileStatus.Modified) -} - func Test_GetCommitBranchStart(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") repo, err := OpenRepository(t.Context(), bareRepo1Path) diff --git a/modules/git/repo.go b/modules/git/repo.go index 29e70d94c86f6..7e86b10de9bd3 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -32,11 +32,6 @@ type GPGSettings struct { const prettyLogFormat = `--pretty=format:%H` -// GetAllCommitsCount returns count of all commits in repository -func (repo *Repository) GetAllCommitsCount() (int64, error) { - return AllCommitsCount(repo.Ctx, repo.Path, false) -} - func (repo *Repository) ShowPrettyFormatLogToList(ctx context.Context, revisionRange string) ([]*Commit, error) { // avoid: ambiguous argument 'refs/a...refs/b': unknown revision or path not in the working tree. Use '--': 'git [...] -- [...]' logs, _, err := gitcmd.NewCommand("log").AddArguments(prettyLogFormat). diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 5f4487ce7e24b..4a441429f4193 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -11,7 +11,6 @@ import ( "strconv" "strings" - "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/setting" ) @@ -216,16 +215,6 @@ func (repo *Repository) FileChangedBetweenCommits(filename, id1, id2 string) (bo 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 @@ -433,25 +422,6 @@ func (repo *Repository) CommitsBetweenIDs(last, before string) ([]*Commit, error 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 := gitcmd.NewCommand("log", prettyLogFormat) @@ -564,23 +534,6 @@ func (repo *Repository) IsCommitInBranch(commitID, branch string) (r bool, err e 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 -} - // GetCommitBranchStart returns the commit where the branch diverged func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID string) (string, error) { cmd := gitcmd.NewCommand("log", prettyLogFormat) diff --git a/modules/gitrepo/cat_file.go b/modules/gitrepo/cat_file.go new file mode 100644 index 0000000000000..c6ac74756f184 --- /dev/null +++ b/modules/gitrepo/cat_file.go @@ -0,0 +1,14 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + + "code.gitea.io/gitea/modules/git" +) + +func NewBatch(ctx context.Context, repo Repository) (*git.Batch, error) { + return git.NewBatch(ctx, repoPath(repo)) +} diff --git a/modules/gitrepo/commit.go b/modules/gitrepo/commit.go new file mode 100644 index 0000000000000..e0a87ac10b4cc --- /dev/null +++ b/modules/gitrepo/commit.go @@ -0,0 +1,96 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/gitcmd" +) + +// CommitsCountOptions the options when counting commits +type CommitsCountOptions struct { + Not string + Revision []string + RelPath []string + Since string + Until string +} + +// CommitsCount returns number of total commits of until given revision. +func CommitsCount(ctx context.Context, repo Repository, opts CommitsCountOptions) (int64, error) { + cmd := gitcmd.NewCommand("rev-list", "--count") + + cmd.AddDynamicArguments(opts.Revision...) + + if opts.Not != "" { + cmd.AddOptionValues("--not", opts.Not) + } + + if len(opts.RelPath) > 0 { + cmd.AddDashesAndList(opts.RelPath...) + } + + stdout, _, err := cmd.WithDir(repoPath(repo)).RunStdString(ctx) + if err != nil { + return 0, err + } + + return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64) +} + +// CommitsCountBetween return numbers of commits between two commits +func CommitsCountBetween(ctx context.Context, repo Repository, start, end string) (int64, error) { + count, err := CommitsCount(ctx, repo, CommitsCountOptions{ + 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(ctx, repo, CommitsCountOptions{ + Revision: []string{start, end}, + }) + } + + return count, err +} + +// FileCommitsCount return the number of files at a revision +func FileCommitsCount(ctx context.Context, repo Repository, revision, file string) (int64, error) { + return CommitsCount(ctx, repo, + CommitsCountOptions{ + Revision: []string{revision}, + RelPath: []string{file}, + }) +} + +// CommitsCountOfCommit returns number of total commits of until current revision. +func CommitsCountOfCommit(ctx context.Context, repo Repository, commitID string) (int64, error) { + return CommitsCount(ctx, repo, CommitsCountOptions{ + Revision: []string{commitID}, + }) +} + +// AllCommitsCount returns count of all commits in repository +func AllCommitsCount(ctx context.Context, repo Repository, hidePRRefs bool, files ...string) (int64, error) { + cmd := gitcmd.NewCommand("rev-list") + if hidePRRefs { + cmd.AddArguments("--exclude=" + git.PullPrefix + "*") + } + cmd.AddArguments("--all", "--count") + if len(files) > 0 { + cmd.AddDashesAndList(files...) + } + + stdout, err := RunCmdString(ctx, repo, cmd) + if err != nil { + return 0, err + } + + return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64) +} diff --git a/modules/gitrepo/commit_file.go b/modules/gitrepo/commit_file.go new file mode 100644 index 0000000000000..cd4bb340d0e2e --- /dev/null +++ b/modules/gitrepo/commit_file.go @@ -0,0 +1,93 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "bufio" + "bytes" + "context" + "io" + + "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/log" +) + +// CommitFileStatus represents status of files in a commit. +type CommitFileStatus struct { + Added []string + Removed []string + Modified []string +} + +// NewCommitFileStatus creates a CommitFileStatus +func NewCommitFileStatus() *CommitFileStatus { + return &CommitFileStatus{ + []string{}, []string{}, []string{}, + } +} + +func parseCommitFileStatus(fileStatus *CommitFileStatus, stdout io.Reader) { + rd := bufio.NewReader(stdout) + peek, err := rd.Peek(1) + if err != nil { + if err != io.EOF { + log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) + } + return + } + if peek[0] == '\n' || peek[0] == '\x00' { + _, _ = rd.Discard(1) + } + for { + modifier, err := rd.ReadString('\x00') + if err != nil { + if err != io.EOF { + log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) + } + return + } + file, err := rd.ReadString('\x00') + if err != nil { + if err != io.EOF { + log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) + } + return + } + file = file[:len(file)-1] + switch modifier[0] { + case 'A': + fileStatus.Added = append(fileStatus.Added, file) + case 'D': + fileStatus.Removed = append(fileStatus.Removed, file) + case 'M': + fileStatus.Modified = append(fileStatus.Modified, file) + } + } +} + +// GetCommitFileStatus returns file status of commit in given repository. +func GetCommitFileStatus(ctx context.Context, repo Repository, commitID string) (*CommitFileStatus, error) { + stdout, w := io.Pipe() + done := make(chan struct{}) + fileStatus := NewCommitFileStatus() + go func() { + parseCommitFileStatus(fileStatus, stdout) + close(done) + }() + + stderr := new(bytes.Buffer) + err := gitcmd.NewCommand("log", "--name-status", "-m", "--pretty=format:", "--first-parent", "--no-renames", "-z", "-1"). + AddDynamicArguments(commitID). + WithDir(repoPath(repo)). + WithStdout(w). + WithStderr(stderr). + Run(ctx) + w.Close() // Close writer to exit parsing goroutine + if err != nil { + return nil, gitcmd.ConcatenateError(err, stderr.String()) + } + + <-done + return fileStatus, nil +} diff --git a/modules/gitrepo/commit_file_test.go b/modules/gitrepo/commit_file_test.go new file mode 100644 index 0000000000000..ec1018eebaef2 --- /dev/null +++ b/modules/gitrepo/commit_file_test.go @@ -0,0 +1,175 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseCommitFileStatus(t *testing.T) { + type testcase struct { + output string + added []string + removed []string + modified []string + } + + kases := []testcase{ + { + // Merge commit + output: "MM\x00options/locale/locale_en-US.ini\x00", + modified: []string{ + "options/locale/locale_en-US.ini", + }, + added: []string{}, + removed: []string{}, + }, + { + // Spaces commit + output: "D\x00b\x00D\x00b b/b\x00A\x00b b/b b/b b/b\x00A\x00b b/b b/b b/b b/b\x00", + removed: []string{ + "b", + "b b/b", + }, + modified: []string{}, + added: []string{ + "b b/b b/b b/b", + "b b/b b/b b/b b/b", + }, + }, + { + // larger commit + output: "M\x00go.mod\x00M\x00go.sum\x00M\x00modules/ssh/ssh.go\x00M\x00vendor/github.com/gliderlabs/ssh/circle.yml\x00M\x00vendor/github.com/gliderlabs/ssh/context.go\x00A\x00vendor/github.com/gliderlabs/ssh/go.mod\x00A\x00vendor/github.com/gliderlabs/ssh/go.sum\x00M\x00vendor/github.com/gliderlabs/ssh/server.go\x00M\x00vendor/github.com/gliderlabs/ssh/session.go\x00M\x00vendor/github.com/gliderlabs/ssh/ssh.go\x00M\x00vendor/golang.org/x/sys/unix/mkerrors.sh\x00M\x00vendor/golang.org/x/sys/unix/syscall_darwin.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_darwin_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_darwin_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_386.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_arm.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_linux.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_darwin_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_darwin_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_dragonfly_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_386.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_arm.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_386.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_arm.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_arm64.go\x00M\x00vendor/modules.txt\x00", + modified: []string{ + "go.mod", + "go.sum", + "modules/ssh/ssh.go", + "vendor/github.com/gliderlabs/ssh/circle.yml", + "vendor/github.com/gliderlabs/ssh/context.go", + "vendor/github.com/gliderlabs/ssh/server.go", + "vendor/github.com/gliderlabs/ssh/session.go", + "vendor/github.com/gliderlabs/ssh/ssh.go", + "vendor/golang.org/x/sys/unix/mkerrors.sh", + "vendor/golang.org/x/sys/unix/syscall_darwin.go", + "vendor/golang.org/x/sys/unix/zerrors_darwin_amd64.go", + "vendor/golang.org/x/sys/unix/zerrors_darwin_arm64.go", + "vendor/golang.org/x/sys/unix/zerrors_freebsd_386.go", + "vendor/golang.org/x/sys/unix/zerrors_freebsd_amd64.go", + "vendor/golang.org/x/sys/unix/zerrors_freebsd_arm.go", + "vendor/golang.org/x/sys/unix/zerrors_freebsd_arm64.go", + "vendor/golang.org/x/sys/unix/zerrors_linux.go", + "vendor/golang.org/x/sys/unix/ztypes_darwin_amd64.go", + "vendor/golang.org/x/sys/unix/ztypes_darwin_arm64.go", + "vendor/golang.org/x/sys/unix/ztypes_dragonfly_amd64.go", + "vendor/golang.org/x/sys/unix/ztypes_freebsd_386.go", + "vendor/golang.org/x/sys/unix/ztypes_freebsd_amd64.go", + "vendor/golang.org/x/sys/unix/ztypes_freebsd_arm.go", + "vendor/golang.org/x/sys/unix/ztypes_freebsd_arm64.go", + "vendor/golang.org/x/sys/unix/ztypes_netbsd_386.go", + "vendor/golang.org/x/sys/unix/ztypes_netbsd_amd64.go", + "vendor/golang.org/x/sys/unix/ztypes_netbsd_arm.go", + "vendor/golang.org/x/sys/unix/ztypes_netbsd_arm64.go", + "vendor/modules.txt", + }, + added: []string{ + "vendor/github.com/gliderlabs/ssh/go.mod", + "vendor/github.com/gliderlabs/ssh/go.sum", + }, + removed: []string{}, + }, + { + // git 1.7.2 adds an unnecessary \x00 on merge commit + output: "\x00MM\x00options/locale/locale_en-US.ini\x00", + modified: []string{ + "options/locale/locale_en-US.ini", + }, + added: []string{}, + removed: []string{}, + }, + { + // git 1.7.2 adds an unnecessary \n on normal commit + output: "\nD\x00b\x00D\x00b b/b\x00A\x00b b/b b/b b/b\x00A\x00b b/b b/b b/b b/b\x00", + removed: []string{ + "b", + "b b/b", + }, + modified: []string{}, + added: []string{ + "b b/b b/b b/b", + "b b/b b/b b/b b/b", + }, + }, + } + + for _, kase := range kases { + fileStatus := NewCommitFileStatus() + parseCommitFileStatus(fileStatus, strings.NewReader(kase.output)) + + assert.Equal(t, kase.added, fileStatus.Added) + assert.Equal(t, kase.removed, fileStatus.Removed) + assert.Equal(t, kase.modified, fileStatus.Modified) + } +} + +func TestGetCommitFileStatusMerges(t *testing.T) { + bareRepo6 := &mockRepository{path: "repo6_merge"} + + commitFileStatus, err := GetCommitFileStatus(t.Context(), bareRepo6, "022f4ce6214973e018f02bf363bf8a2e3691f699") + assert.NoError(t, err) + + expected := CommitFileStatus{ + []string{ + "add_file.txt", + }, + []string{ + "to_remove.txt", + }, + []string{ + "to_modify.txt", + }, + } + + assert.Equal(t, expected.Added, commitFileStatus.Added) + assert.Equal(t, expected.Removed, commitFileStatus.Removed) + assert.Equal(t, expected.Modified, commitFileStatus.Modified) +} + +func TestGetCommitFileStatusMergesSha256(t *testing.T) { + bareRepo6Sha256 := &mockRepository{path: "repo6_merge_sha256"} + + commitFileStatus, err := GetCommitFileStatus(t.Context(), bareRepo6Sha256, "d2e5609f630dd8db500f5298d05d16def282412e3e66ed68cc7d0833b29129a1") + assert.NoError(t, err) + + expected := CommitFileStatus{ + []string{ + "add_file.txt", + }, + []string{}, + []string{ + "to_modify.txt", + }, + } + + assert.Equal(t, expected.Added, commitFileStatus.Added) + assert.Equal(t, expected.Removed, commitFileStatus.Removed) + assert.Equal(t, expected.Modified, commitFileStatus.Modified) + + expected = CommitFileStatus{ + []string{}, + []string{ + "to_remove.txt", + }, + []string{}, + } + + commitFileStatus, err = GetCommitFileStatus(t.Context(), bareRepo6Sha256, "da1ded40dc8e5b7c564171f4bf2fc8370487decfb1cb6a99ef28f3ed73d09172") + assert.NoError(t, err) + + assert.Equal(t, expected.Added, commitFileStatus.Added) + assert.Equal(t, expected.Removed, commitFileStatus.Removed) + assert.Equal(t, expected.Modified, commitFileStatus.Modified) +} diff --git a/modules/gitrepo/commit_test.go b/modules/gitrepo/commit_test.go new file mode 100644 index 0000000000000..93483f3e0de98 --- /dev/null +++ b/modules/gitrepo/commit_test.go @@ -0,0 +1,35 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCommitsCount(t *testing.T) { + bareRepo1 := &mockRepository{path: "repo1_bare"} + + commitsCount, err := CommitsCount(t.Context(), bareRepo1, + CommitsCountOptions{ + Revision: []string{"8006ff9adbf0cb94da7dad9e537e53817f9fa5c0"}, + }) + + assert.NoError(t, err) + assert.Equal(t, int64(3), commitsCount) +} + +func TestCommitsCountWithoutBase(t *testing.T) { + bareRepo1 := &mockRepository{path: "repo1_bare"} + + commitsCount, err := CommitsCount(t.Context(), bareRepo1, + CommitsCountOptions{ + Not: "master", + Revision: []string{"branch1"}, + }) + + assert.NoError(t, err) + assert.Equal(t, int64(2), commitsCount) +} diff --git a/modules/indexer/code/bleve/bleve.go b/modules/indexer/code/bleve/bleve.go index 0e2d0f879a5ea..bdb477ce6e7fb 100644 --- a/modules/indexer/code/bleve/bleve.go +++ b/modules/indexer/code/bleve/bleve.go @@ -218,7 +218,7 @@ func (b *Indexer) addDelete(filename string, repo *repo_model.Repository, batch func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *internal.RepoChanges) error { batch := inner_bleve.NewFlushingBatch(b.inner.Indexer, maxBatchSize) if len(changes.Updates) > 0 { - gitBatch, err := git.NewBatch(ctx, repo.RepoPath()) + gitBatch, err := gitrepo.NewBatch(ctx, repo) if err != nil { return err } diff --git a/modules/indexer/code/elasticsearch/elasticsearch.go b/modules/indexer/code/elasticsearch/elasticsearch.go index 012c57da291ab..b2eb301a5dc8b 100644 --- a/modules/indexer/code/elasticsearch/elasticsearch.go +++ b/modules/indexer/code/elasticsearch/elasticsearch.go @@ -210,7 +210,7 @@ func (b *Indexer) addDelete(filename string, repo *repo_model.Repository) elasti func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *internal.RepoChanges) error { reqs := make([]elastic.BulkableRequest, 0) if len(changes.Updates) > 0 { - batch, err := git.NewBatch(ctx, repo.RepoPath()) + batch, err := gitrepo.NewBatch(ctx, repo) if err != nil { return err } diff --git a/modules/repository/commits.go b/modules/repository/commits.go index 878fdc16039bb..a3e253e998e42 100644 --- a/modules/repository/commits.go +++ b/modules/repository/commits.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/cachegroup" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" @@ -72,7 +73,7 @@ func ToAPIPayloadCommit(ctx context.Context, emailUsers map[string]*user_model.U committerUsername = committer.Name } - fileStatus, err := git.GetCommitFileStatus(ctx, repo.RepoPath(), commit.Sha1) + fileStatus, err := gitrepo.GetCommitFileStatus(ctx, repo, commit.Sha1) if err != nil { return nil, fmt.Errorf("FileStatus [commit_sha1: %s]: %w", commit.Sha1, err) } diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go index 6a93be624f615..2a7efa0ea6f1b 100644 --- a/routers/api/v1/repo/commits.go +++ b/routers/api/v1/repo/commits.go @@ -13,6 +13,7 @@ import ( issues_model "code.gitea.io/gitea/models/issues" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/routers/api/v1/utils" @@ -222,8 +223,7 @@ func GetAllCommits(ctx *context.APIContext) { } // Total commit count - commitsCountTotal, err = git.CommitsCount(ctx.Repo.GitRepo.Ctx, git.CommitsCountOptions{ - RepoPath: ctx.Repo.GitRepo.Path, + commitsCountTotal, err = gitrepo.CommitsCount(ctx, ctx.Repo.Repository, gitrepo.CommitsCountOptions{ Not: not, Revision: []string{baseCommit.ID.String()}, Since: since, @@ -245,9 +245,8 @@ func GetAllCommits(ctx *context.APIContext) { sha = ctx.Repo.Repository.DefaultBranch } - commitsCountTotal, err = git.CommitsCount(ctx, - git.CommitsCountOptions{ - RepoPath: ctx.Repo.GitRepo.Path, + commitsCountTotal, err = gitrepo.CommitsCount(ctx, ctx.Repo.Repository, + gitrepo.CommitsCountOptions{ Not: not, Revision: []string{sha}, RelPath: []string{path}, diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 22851ad2c5207..a929c3e2dd6a2 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -1191,7 +1191,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(baseRefToGuess) headRef := headGitRepo.UnstableGuessRefByShortName(headRefToGuess) - log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.GitRepo.Path, baseRefToGuess, baseRef, headRefToGuess, headRef) + log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.Repository.RelativePath(), baseRefToGuess, baseRef, headRefToGuess, headRef) baseRefValid := baseRef.IsBranch() || baseRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName), baseRef.ShortName()) headRefValid := headRef.IsBranch() || headRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(headRepo.ObjectFormatName), headRef.ShortName()) diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go index 8e24ffa465c14..baf5e0189fe03 100644 --- a/routers/api/v1/repo/wiki.go +++ b/routers/api/v1/repo/wiki.go @@ -193,7 +193,7 @@ func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.Wi } // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) + commitsCount, _ := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository.WikiStorageRepo(), ctx.Repo.Repository.DefaultWikiBranch, pageFilename) // Get last change information. lastCommit, err := wikiRepo.GetCommitByPath(pageFilename) @@ -429,7 +429,7 @@ func ListPageRevisions(ctx *context.APIContext) { } // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) + commitsCount, _ := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository.WikiStorageRepo(), ctx.Repo.Repository.DefaultWikiBranch, pageFilename) page := max(ctx.FormInt("page"), 1) diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 1a86a62fae172..a8e3ea502f94a 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -222,7 +222,7 @@ func FileHistory(ctx *context.Context) { return } - commitsCount, err := ctx.Repo.GitRepo.FileCommitsCount(ctx.Repo.RefFullName.ShortName(), ctx.Repo.TreePath) + commitsCount, err := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository, ctx.Repo.RefFullName.ShortName(), ctx.Repo.TreePath) if err != nil { ctx.ServerError("FileCommitsCount", err) return diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index 36ea20c23e6f7..4ae021bc3fe84 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -5,6 +5,7 @@ package repo import ( + stdCtx "context" "errors" "fmt" "net/http" @@ -40,7 +41,7 @@ const ( ) // calReleaseNumCommitsBehind calculates given release has how many commits behind release target. -func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *repo_model.Release, countCache map[string]int64) error { +func calReleaseNumCommitsBehind(ctx stdCtx.Context, repoCtx *context.Repository, release *repo_model.Release, countCache map[string]int64) error { target := release.Target if target == "" { target = repoCtx.Repository.DefaultBranch @@ -60,7 +61,7 @@ func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *repo_model return fmt.Errorf("GetBranchCommit(DefaultBranch): %w", err) } } - countCache[target], err = commit.CommitsCount() + countCache[target], err = gitrepo.CommitsCountOfCommit(ctx, repoCtx.Repository, commit.ID.String()) if err != nil { return fmt.Errorf("CommitsCount: %w", err) } @@ -123,7 +124,7 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions) } if !r.IsDraft { - if err := calReleaseNumCommitsBehind(ctx.Repo, r, countCache); err != nil { + if err := calReleaseNumCommitsBehind(ctx, ctx.Repo, r, countCache); err != nil { return nil, err } } diff --git a/routers/web/repo/release_test.go b/routers/web/repo/release_test.go index 9f49fc750070c..7ba91afb29705 100644 --- a/routers/web/repo/release_test.go +++ b/routers/web/repo/release_test.go @@ -158,7 +158,7 @@ func TestCalReleaseNumCommitsBehind(t *testing.T) { countCache := make(map[string]int64) for _, release := range releases { - err := calReleaseNumCommitsBehind(ctx.Repo, release, countCache) + err := calReleaseNumCommitsBehind(ctx, ctx.Repo, release, countCache) assert.NoError(t, err) } diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 07435a550dc81..3a0976ffa06a4 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -70,7 +70,7 @@ func CommitInfoCache(ctx *context.Context) { ctx.ServerError("GetBranchCommit", err) return } - ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount() + ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount(ctx) if err != nil { ctx.ServerError("GetCommitsCount", err) return diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 289db11a4f830..e97ffc34b38cd 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -310,7 +310,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { } // get commit count - wiki revisions - commitsCount, _ := wikiGitRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) + commitsCount, _ := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository.WikiStorageRepo(), ctx.Repo.Repository.DefaultWikiBranch, pageFilename) ctx.Data["CommitCount"] = commitsCount return wikiGitRepo, entry @@ -350,7 +350,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) } // get commit count - wiki revisions - commitsCount, _ := wikiGitRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) + commitsCount, _ := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository.WikiStorageRepo(), ctx.Repo.Repository.DefaultWikiBranch, pageFilename) ctx.Data["CommitCount"] = commitsCount // get page diff --git a/services/context/repo.go b/services/context/repo.go index 0ff1c7ea0337c..5967492c5ffd3 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -202,14 +202,14 @@ func (r *Repository) CanCreateIssueDependencies(ctx context.Context, user *user_ } // GetCommitsCount returns cached commit count for current view -func (r *Repository) GetCommitsCount() (int64, error) { +func (r *Repository) GetCommitsCount(ctx context.Context) (int64, error) { if r.Commit == nil { return 0, nil } contextName := r.RefFullName.ShortName() isRef := r.RefFullName.IsBranch() || r.RefFullName.IsTag() return cache.GetInt64(r.Repository.GetCommitsCountCacheKey(contextName, isRef), func() (int64, error) { - return r.Commit.CommitsCount() + return gitrepo.CommitsCountOfCommit(ctx, r.Repository, r.Commit.ID.String()) }) } @@ -219,11 +219,10 @@ func (r *Repository) GetCommitGraphsCount(ctx context.Context, hidePRRefs bool, return cache.GetInt64(cacheKey, func() (int64, error) { if len(branches) == 0 { - return git.AllCommitsCount(ctx, r.Repository.RepoPath(), hidePRRefs, files...) + return gitrepo.AllCommitsCount(ctx, r.Repository, hidePRRefs, files...) } - return git.CommitsCount(ctx, - git.CommitsCountOptions{ - RepoPath: r.Repository.RepoPath(), + return gitrepo.CommitsCount(ctx, r.Repository, + gitrepo.CommitsCountOptions{ Revision: branches, RelPath: files, }) @@ -820,7 +819,7 @@ func RepoRefByDefaultBranch() func(*Context) { ctx.Repo.RefFullName = git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch) ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch ctx.Repo.Commit, _ = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName) - ctx.Repo.CommitsCount, _ = ctx.Repo.GetCommitsCount() + ctx.Repo.CommitsCount, _ = ctx.Repo.GetCommitsCount(ctx) ctx.Data["RefFullName"] = ctx.Repo.RefFullName ctx.Data["BranchName"] = ctx.Repo.BranchName ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount @@ -857,7 +856,7 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) { if err == nil && len(brs) != 0 { refShortName = brs[0] } else if len(brs) == 0 { - log.Error("No branches in non-empty repository %s", ctx.Repo.GitRepo.Path) + log.Error("No branches in non-empty repository %s", ctx.Repo.Repository.RelativePath()) } else { log.Error("GetBranches error: %v", err) } @@ -969,7 +968,7 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) { ctx.Data["CanCreateBranch"] = ctx.Repo.CanCreateBranch() // only used by the branch selector dropdown: AllowCreateNewRef - ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount() + ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount(ctx) if err != nil { ctx.ServerError("GetCommitsCount", err) return diff --git a/services/convert/git_commit.go b/services/convert/git_commit.go index d0228c45fb214..bf17024d2d0a0 100644 --- a/services/convert/git_commit.go +++ b/services/convert/git_commit.go @@ -11,6 +11,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -190,7 +191,7 @@ func ToCommit(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Rep // Retrieve files affected by the commit if opts.Files { - fileStatus, err := git.GetCommitFileStatus(gitRepo.Ctx, repo.RepoPath(), commit.ID.String()) + fileStatus, err := gitrepo.GetCommitFileStatus(ctx, repo, commit.ID.String()) if err != nil { return nil, err } diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index 4d23060661d9a..5c2d86550b2b2 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -303,7 +303,7 @@ func (g *GiteaLocalUploader) CreateReleases(ctx context.Context, releases ...*ba return fmt.Errorf("GetTagCommit[%v]: %w", rel.TagName, err) } rel.Sha1 = commit.ID.String() - rel.NumCommits, err = commit.CommitsCount() + rel.NumCommits, err = gitrepo.CommitsCountOfCommit(ctx, g.repo, commit.ID.String()) if err != nil { return fmt.Errorf("CommitsCount: %w", err) } diff --git a/services/release/release.go b/services/release/release.go index 28061ae8b180c..e09e224201c64 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -148,7 +148,7 @@ func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Rel } rel.Sha1 = commit.ID.String() - rel.NumCommits, err = commit.CommitsCount() + rel.NumCommits, err = gitrepo.CommitsCountOfCommit(ctx, rel.Repo, commit.ID.String()) if err != nil { return false, fmt.Errorf("CommitsCount: %w", err) } diff --git a/services/repository/cache.go b/services/repository/cache.go index b0811a99fc03b..4fc8d3ddf785e 100644 --- a/services/repository/cache.go +++ b/services/repository/cache.go @@ -9,6 +9,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" ) // CacheRef cachhe last commit information of the branch or the tag @@ -19,7 +20,9 @@ func CacheRef(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Rep } if gitRepo.LastCommitCache == nil { - commitsCount, err := cache.GetInt64(repo.GetCommitsCountCacheKey(fullRefName.ShortName(), true), commit.CommitsCount) + commitsCount, err := cache.GetInt64(repo.GetCommitsCountCacheKey(fullRefName.ShortName(), true), func() (int64, error) { + return gitrepo.CommitsCountOfCommit(ctx, repo, commit.ID.String()) + }) if err != nil { return err } diff --git a/services/repository/files/content.go b/services/repository/files/content.go index 2c1e88bb59ecb..d32d3041c291c 100644 --- a/services/repository/files/content.go +++ b/services/repository/files/content.go @@ -11,7 +11,9 @@ import ( "strings" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" @@ -125,7 +127,20 @@ func GetFileContents(ctx context.Context, repo *repo_model.Repository, gitRepo * return getFileContentsByEntryInternal(ctx, repo, gitRepo, refCommit, entry, opts) } -func getFileContentsByEntryInternal(_ context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, entry *git.TreeEntry, opts GetContentsOrListOptions) (*api.ContentsResponse, error) { +func addLastCommitCache(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, cacheKey, fullName, sha string) error { + if gitRepo.LastCommitCache == nil { + commitsCount, err := cache.GetInt64(cacheKey, func() (int64, error) { + return gitrepo.CommitsCountOfCommit(ctx, repo, sha) + }) + if err != nil { + return err + } + gitRepo.LastCommitCache = git.NewLastCommitCache(commitsCount, fullName, gitRepo, cache.GetCache()) + } + return nil +} + +func getFileContentsByEntryInternal(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, entry *git.TreeEntry, opts GetContentsOrListOptions) (*api.ContentsResponse, error) { refType := refCommit.RefName.RefType() commit := refCommit.Commit selfURL, err := url.Parse(repo.APIURL() + "/contents/" + util.PathEscapeSegments(opts.TreePath) + "?ref=" + url.QueryEscape(refCommit.InputRef)) @@ -147,7 +162,7 @@ func getFileContentsByEntryInternal(_ context.Context, repo *repo_model.Reposito } if opts.IncludeCommitMetadata || opts.IncludeCommitMessage { - err = gitRepo.AddLastCommitCache(repo.GetCommitsCountCacheKey(refCommit.InputRef, refType != git.RefTypeCommit), repo.FullName(), refCommit.CommitID) + err = addLastCommitCache(ctx, repo, gitRepo, repo.GetCommitsCountCacheKey(refCommit.InputRef, refType != git.RefTypeCommit), repo.FullName(), refCommit.CommitID) if err != nil { return nil, err }