diff --git a/models/activities/repo_activity.go b/models/activities/repo_activity.go index aeaa452c9e905..6151e3b70d2ce 100644 --- a/models/activities/repo_activity.go +++ b/models/activities/repo_activity.go @@ -13,7 +13,6 @@ import ( issues_model "code.gitea.io/gitea/models/issues" 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" "xorm.io/builder" @@ -43,12 +42,12 @@ type ActivityStats struct { UnresolvedIssues issues_model.IssueList PublishedReleases []*repo_model.Release PublishedReleaseAuthorCount int64 - Code *git.CodeActivityStats + Code *gitrepo.CodeActivityStats } // GetActivityStats return stats for repository at given time range func GetActivityStats(ctx context.Context, repo *repo_model.Repository, timeFrom time.Time, releases, issues, prs, code bool) (*ActivityStats, error) { - stats := &ActivityStats{Code: &git.CodeActivityStats{}} + stats := &ActivityStats{Code: &gitrepo.CodeActivityStats{}} if releases { if err := stats.FillReleases(ctx, repo.ID, timeFrom); err != nil { return nil, fmt.Errorf("FillReleases: %w", err) @@ -68,13 +67,7 @@ func GetActivityStats(ctx context.Context, repo *repo_model.Repository, timeFrom return nil, fmt.Errorf("FillUnresolvedIssues: %w", err) } if code { - gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo) - if err != nil { - return nil, fmt.Errorf("OpenRepository: %w", err) - } - defer closer.Close() - - code, err := gitRepo.GetCodeActivityStats(timeFrom, repo.DefaultBranch) + code, err := gitrepo.GetCodeActivityStats(ctx, repo, timeFrom, repo.DefaultBranch) if err != nil { return nil, fmt.Errorf("FillFromGit: %w", err) } @@ -85,13 +78,7 @@ func GetActivityStats(ctx context.Context, repo *repo_model.Repository, timeFrom // GetActivityStatsTopAuthors returns top author stats for git commits for all branches func GetActivityStatsTopAuthors(ctx context.Context, repo *repo_model.Repository, timeFrom time.Time, count int) ([]*ActivityAuthorData, error) { - gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo) - if err != nil { - return nil, fmt.Errorf("OpenRepository: %w", err) - } - defer closer.Close() - - code, err := gitRepo.GetCodeActivityStats(timeFrom, "") + code, err := gitrepo.GetCodeActivityStats(ctx, repo, timeFrom, "") if err != nil { return nil, fmt.Errorf("FillFromGit: %w", err) } diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 26a6ebc37009c..83cd7074697c8 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -5,10 +5,13 @@ package actions import ( "bytes" + stdCtx "context" "slices" "strings" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/glob" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" @@ -99,6 +102,8 @@ func GetEventsFromContent(content []byte) ([]*jobparser.Event, error) { } func DetectWorkflows( + ctx stdCtx.Context, + repo *repo_model.Repository, gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, @@ -135,7 +140,7 @@ func DetectWorkflows( } schedules = append(schedules, dwf) } - } else if detectMatched(gitRepo, commit, triggedEvent, payload, evt) { + } else if detectMatched(ctx, repo, gitRepo, commit, triggedEvent, payload, evt) { dwf := &DetectedWorkflow{ EntryName: entry.Name(), TriggerEvent: evt, @@ -184,7 +189,7 @@ func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*D return wfs, nil } -func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool { +func detectMatched(ctx stdCtx.Context, repo *repo_model.Repository, gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool { if !canGithubEventMatch(evt.Name, triggedEvent) { return false } @@ -204,7 +209,7 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web case // push webhook_module.HookEventPush: - return matchPushEvent(commit, payload.(*api.PushPayload), evt) + return matchPushEvent(ctx, repo, commit, payload.(*api.PushPayload), evt) case // issues webhook_module.HookEventIssues, @@ -227,7 +232,7 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web webhook_module.HookEventPullRequestLabel, webhook_module.HookEventPullRequestReviewRequest, webhook_module.HookEventPullRequestMilestone: - return matchPullRequestEvent(gitRepo, commit, payload.(*api.PullRequestPayload), evt) + return matchPullRequestEvent(ctx, repo, gitRepo, commit, payload.(*api.PullRequestPayload), evt) case // pull_request_review webhook_module.HookEventPullRequestReviewApproved, @@ -256,7 +261,7 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web } } -func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobparser.Event) bool { +func matchPushEvent(ctx stdCtx.Context, repo *repo_model.Repository, commit *git.Commit, pushPayload *api.PushPayload, evt *jobparser.Event) bool { // with no special filter parameters if len(evt.Acts()) == 0 { return true @@ -322,7 +327,7 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa matchTimes++ break } - filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before) + filesChanged, err := gitrepo.GetFilesChangedBetween(ctx, repo, pushPayload.Before, commit.ID.String()) if err != nil { log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) } else { @@ -339,7 +344,7 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa matchTimes++ break } - filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before) + filesChanged, err := gitrepo.GetFilesChangedBetween(ctx, repo, pushPayload.Before, commit.ID.String()) if err != nil { log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) } else { @@ -410,7 +415,7 @@ func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool return matchTimes == len(evt.Acts()) } -func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) bool { +func matchPullRequestEvent(ctx stdCtx.Context, repo *repo_model.Repository, gitRepo *git.Repository, commit *git.Commit, prPayload *api.PullRequestPayload, evt *jobparser.Event) bool { acts := evt.Acts() activityTypeMatched := false matchTimes := 0 @@ -486,7 +491,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa matchTimes++ } case "paths": - filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase) + filesChanged, err := gitrepo.GetFilesChangedBetween(ctx, repo, prPayload.PullRequest.MergeBase, headCommit.ID.String()) if err != nil { log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err) } else { @@ -499,7 +504,7 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa } } case "paths-ignore": - filesChanged, err := headCommit.GetFilesChangedSinceCommit(prPayload.PullRequest.MergeBase) + filesChanged, err := gitrepo.GetFilesChangedBetween(ctx, repo, prPayload.PullRequest.MergeBase, headCommit.ID.String()) if err != nil { log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", headCommit.ID.String(), err) } else { diff --git a/modules/actions/workflows_test.go b/modules/actions/workflows_test.go index 89620fb698861..29c83e9d54152 100644 --- a/modules/actions/workflows_test.go +++ b/modules/actions/workflows_test.go @@ -150,7 +150,7 @@ func TestDetectMatched(t *testing.T) { evts, err := GetEventsFromContent([]byte(tc.yamlOn)) assert.NoError(t, err) assert.Len(t, evts, 1) - assert.Equal(t, tc.expected, detectMatched(nil, tc.commit, tc.triggedEvent, tc.payload, evts[0])) + assert.Equal(t, tc.expected, detectMatched(t.Context(), nil, nil, tc.commit, tc.triggedEvent, tc.payload, evts[0])) }) } } diff --git a/modules/git/commit.go b/modules/git/commit.go index e66a33ef98ae1..9cae56ff8114c 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -6,9 +6,7 @@ package git import ( "context" - "errors" "io" - "os/exec" "strings" "code.gitea.io/gitea/modules/git/gitcmd" @@ -136,49 +134,6 @@ func (c *Commit) CommitsBefore() ([]*Commit, error) { return c.repo.getCommitsBefore(c.ID) } -// HasPreviousCommit returns true if a given commitHash is contained in commit's parents -func (c *Commit) HasPreviousCommit(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(c.repo.Path). - RunStdString(c.repo.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(oldCommitID string) (bool, error) { - objectFormat, err := c.repo.GetObjectFormat() - if err != nil { - return false, err - } - if oldCommitID == objectFormat.EmptyObjectID().String() { - return false, nil - } - - oldCommit, err := c.repo.GetCommit(oldCommitID) - if err != nil { - return false, err - } - hasPreviousCommit, err := c.HasPreviousCommit(oldCommit.ID) - return !hasPreviousCommit, err -} - // CommitsBeforeLimit returns num commits before current revision func (c *Commit) CommitsBeforeLimit(num int) ([]*Commit, error) { return c.repo.getCommitsBeforeLimit(c.ID, num) @@ -237,11 +192,6 @@ func (c *Commit) SearchCommits(opts SearchCommitsOptions) ([]*Commit, error) { return c.repo.searchCommits(c.ID, opts) } -// GetFilesChangedSinceCommit get all changed file names between pastCommit to current revision -func (c *Commit) GetFilesChangedSinceCommit(pastCommit string) ([]string, error) { - return c.repo.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(filename, pastCommit string) (bool, error) { @@ -287,27 +237,6 @@ func (c *Commit) GetFileContent(filename string, limit int) (string, error) { return string(bytes), nil } -// GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only') -func (c *Commit) GetBranchName() (string, error) { - cmd := gitcmd.NewCommand("name-rev") - if DefaultFeatures().CheckVersionAtLeast("2.13.0") { - cmd.AddArguments("--exclude", "refs/tags/*") - } - cmd.AddArguments("--name-only", "--no-undefined").AddDynamicArguments(c.ID.String()) - data, _, err := cmd.WithDir(c.repo.Path).RunStdString(c.repo.Ctx) - if err != nil { - // handle special case where git can not describe commit - if strings.Contains(err.Error(), "cannot describe") { - return "", nil - } - - return "", err - } - - // name-rev commitID output will be "master" or "master~12" - return strings.SplitN(strings.TrimSpace(data), "~", 2)[0], 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 0aefb30c95ea5..d8923e52b275f 100644 --- a/modules/git/commit_sha256_test.go +++ b/modules/git/commit_sha256_test.go @@ -99,34 +99,3 @@ signed commit`, commitFromReader.Signature.Payload) commitFromReader.Signature.Payload += "\n\n" assert.Equal(t, commitFromReader, commitFromReader2) } - -func TestHasPreviousCommitSha256(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256") - - repo, err := OpenRepository(t.Context(), bareRepo1Path) - assert.NoError(t, err) - defer repo.Close() - - commit, err := repo.GetCommit("f004f41359117d319dedd0eaab8c5259ee2263da839dcba33637997458627fdc") - assert.NoError(t, err) - - objectFormat, err := repo.GetObjectFormat() - assert.NoError(t, err) - - parentSHA := MustIDFromString("b0ec7af4547047f12d5093e37ef8f1b3b5415ed8ee17894d43a34d7d34212e9c") - notParentSHA := MustIDFromString("42e334efd04cd36eea6da0599913333c26116e1a537ca76e5b6e4af4dda00236") - assert.Equal(t, objectFormat, parentSHA.Type()) - assert.Equal(t, "sha256", objectFormat.Name()) - - haz, err := commit.HasPreviousCommit(parentSHA) - assert.NoError(t, err) - assert.True(t, haz) - - hazNot, err := commit.HasPreviousCommit(notParentSHA) - assert.NoError(t, err) - assert.False(t, hazNot) - - selfNot, err := commit.HasPreviousCommit(commit.ID) - assert.NoError(t, err) - assert.False(t, selfNot) -} diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go index de7b7455ebb07..2965b2fa4a55d 100644 --- a/modules/git/commit_test.go +++ b/modules/git/commit_test.go @@ -159,32 +159,6 @@ ISO-8859-1`, commitFromReader.Signature.Payload) assert.Equal(t, commitFromReader, commitFromReader2) } -func TestHasPreviousCommit(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - - repo, err := OpenRepository(t.Context(), bareRepo1Path) - assert.NoError(t, err) - defer repo.Close() - - commit, err := repo.GetCommit("8006ff9adbf0cb94da7dad9e537e53817f9fa5c0") - assert.NoError(t, err) - - parentSHA := MustIDFromString("8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2") - notParentSHA := MustIDFromString("2839944139e0de9737a044f78b0e4b40d989a9e3") - - haz, err := commit.HasPreviousCommit(parentSHA) - assert.NoError(t, err) - assert.True(t, haz) - - hazNot, err := commit.HasPreviousCommit(notParentSHA) - assert.NoError(t, err) - assert.False(t, hazNot) - - selfNot, err := commit.HasPreviousCommit(commit.ID) - assert.NoError(t, err) - assert.False(t, selfNot) -} - func Test_GetCommitBranchStart(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") repo, err := OpenRepository(t.Context(), bareRepo1Path) diff --git a/modules/git/repo_archive.go b/modules/git/repo_archive.go deleted file mode 100644 index 2bbe3f0bb312b..0000000000000 --- a/modules/git/repo_archive.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package git - -import ( - "context" - "fmt" - "io" - "path/filepath" - "strings" - - "code.gitea.io/gitea/modules/git/gitcmd" -) - -// ArchiveType archive types -type ArchiveType int - -const ( - ArchiveUnknown ArchiveType = iota - ArchiveZip // 1 - ArchiveTarGz // 2 - ArchiveBundle // 3 -) - -// String converts an ArchiveType to string: the extension of the archive file without prefix dot -func (a ArchiveType) String() string { - switch a { - case ArchiveZip: - return "zip" - case ArchiveTarGz: - return "tar.gz" - case ArchiveBundle: - return "bundle" - } - return "unknown" -} - -func SplitArchiveNameType(s string) (string, ArchiveType) { - switch { - case strings.HasSuffix(s, ".zip"): - return strings.TrimSuffix(s, ".zip"), ArchiveZip - case strings.HasSuffix(s, ".tar.gz"): - return strings.TrimSuffix(s, ".tar.gz"), ArchiveTarGz - case strings.HasSuffix(s, ".bundle"): - return strings.TrimSuffix(s, ".bundle"), ArchiveBundle - } - return s, ArchiveUnknown -} - -// CreateArchive create archive content to the target path -func (repo *Repository) CreateArchive(ctx context.Context, format ArchiveType, target io.Writer, usePrefix bool, commitID string) error { - if format.String() == "unknown" { - return fmt.Errorf("unknown format: %v", format) - } - - cmd := gitcmd.NewCommand("archive") - if usePrefix { - cmd.AddOptionFormat("--prefix=%s", filepath.Base(strings.TrimSuffix(repo.Path, ".git"))+"/") - } - cmd.AddOptionFormat("--format=%s", format.String()) - cmd.AddDynamicArguments(commitID) - - return cmd.WithDir(repo.Path). - WithStdout(target). - RunWithStderr(ctx) -} diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index bf3f7238d2d2c..51e7694ecc845 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -282,26 +282,6 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) } } -// FilesCountBetween return the number of files changed between two commits -func (repo *Repository) FilesCountBetween(startCommitID, endCommitID string) (int, error) { - stdout, _, err := gitcmd.NewCommand("diff", "--name-only"). - AddDynamicArguments(startCommitID + "..." + endCommitID). - WithDir(repo.Path). - RunStdString(repo.Ctx) - 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 = gitcmd.NewCommand("diff", "--name-only"). - AddDynamicArguments(startCommitID, endCommitID). - WithDir(repo.Path). - RunStdString(repo.Ctx) - } - 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) { @@ -516,18 +496,6 @@ func (repo *Repository) GetCommitsFromIDs(commitIDs []string) []*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 := gitcmd.NewCommand("branch", "--contains"). - AddDynamicArguments(commitID, branch). - WithDir(repo.Path). - RunStdString(repo.Ctx) - if err != nil { - return false, err - } - return len(stdout) > 0, err -} - // 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/git/repo_commit_test.go b/modules/git/repo_commit_test.go index 3f7883ab14d6a..84f170e1dad46 100644 --- a/modules/git/repo_commit_test.go +++ b/modules/git/repo_commit_test.go @@ -69,21 +69,6 @@ func TestGetCommitWithBadCommitID(t *testing.T) { assert.True(t, IsErrNotExist(err)) } -func TestIsCommitInBranch(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - bareRepo1, err := OpenRepository(t.Context(), bareRepo1Path) - assert.NoError(t, err) - defer bareRepo1.Close() - - result, err := bareRepo1.IsCommitInBranch("2839944139e0de9737a044f78b0e4b40d989a9e3", "branch1") - assert.NoError(t, err) - assert.True(t, result) - - result, err = bareRepo1.IsCommitInBranch("2839944139e0de9737a044f78b0e4b40d989a9e3", "branch2") - assert.NoError(t, err) - assert.False(t, result) -} - func TestRepository_CommitsBetweenIDs(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo4_commitsbetween") bareRepo1, err := OpenRepository(t.Context(), bareRepo1Path) diff --git a/modules/git/repo_compare.go b/modules/git/repo_compare.go index f78d569b83438..178f1d2f1f2aa 100644 --- a/modules/git/repo_compare.go +++ b/modules/git/repo_compare.go @@ -6,75 +6,18 @@ package git import ( "bufio" - "bytes" "errors" "fmt" "io" "os" "path/filepath" "regexp" - "strings" "code.gitea.io/gitea/modules/git/gitcmd" ) -type lineCountWriter struct { - numLines int -} - -// Write counts the number of newlines in the provided bytestream -func (l *lineCountWriter) Write(p []byte) (n int, err error) { - n = len(p) - l.numLines += bytes.Count(p, []byte{'\000'}) - return n, err -} - -// GetDiffNumChangedFiles counts the number of changed files -// This is substantially quicker than shortstat but... -func (repo *Repository) GetDiffNumChangedFiles(base, head string, directComparison bool) (int, error) { - // Now there is git diff --shortstat but this appears to be slower than simply iterating with --nameonly - w := &lineCountWriter{} - - separator := "..." - if directComparison { - separator = ".." - } - - // avoid: ambiguous argument 'refs/a...refs/b': unknown revision or path not in the working tree. Use '--': 'git [...] -- [...]' - if err := gitcmd.NewCommand("diff", "-z", "--name-only"). - AddDynamicArguments(base + separator + head). - AddArguments("--"). - WithDir(repo.Path). - WithStdout(w). - RunWithStderr(repo.Ctx); err != nil { - if strings.Contains(err.Stderr(), "no merge base") { - // git >= 2.28 now returns an error if base and head have become unrelated. - // previously it would return the results of git diff -z --name-only base head so let's try that... - w = &lineCountWriter{} - if err = gitcmd.NewCommand("diff", "-z", "--name-only"). - AddDynamicArguments(base, head). - AddArguments("--"). - WithDir(repo.Path). - WithStdout(w). - RunWithStderr(repo.Ctx); err == nil { - return w.numLines, nil - } - } - return 0, err - } - return w.numLines, nil -} - var patchCommits = regexp.MustCompile(`^From\s(\w+)\s`) -// GetDiff generates and returns patch data between given revisions, optimized for human readability -func (repo *Repository) GetDiff(compareArg string, w io.Writer) error { - return gitcmd.NewCommand("diff", "-p").AddDynamicArguments(compareArg). - WithDir(repo.Path). - WithStdout(w). - Run(repo.Ctx) -} - // GetDiffBinary generates and returns patch data between given revisions, including binary diffs. func (repo *Repository) GetDiffBinary(compareArg string, w io.Writer) error { return gitcmd.NewCommand("diff", "-p", "--binary", "--histogram"). @@ -84,42 +27,6 @@ func (repo *Repository) GetDiffBinary(compareArg string, w io.Writer) error { Run(repo.Ctx) } -// GetPatch generates and returns format-patch data between given revisions, able to be used with `git apply` -func (repo *Repository) GetPatch(compareArg string, w io.Writer) error { - return gitcmd.NewCommand("format-patch", "--binary", "--stdout").AddDynamicArguments(compareArg). - WithDir(repo.Path). - WithStdout(w). - Run(repo.Ctx) -} - -// GetFilesChangedBetween returns a list of all files that have been changed between the given commits -// If base is undefined empty SHA (zeros), it only returns the files changed in the head commit -// If base is the SHA of an empty tree (EmptyTreeSHA), it returns the files changes from the initial commit to the head commit -func (repo *Repository) GetFilesChangedBetween(base, head string) ([]string, error) { - objectFormat, err := repo.GetObjectFormat() - if err != nil { - return nil, err - } - cmd := gitcmd.NewCommand("diff-tree", "--name-only", "--root", "--no-commit-id", "-r", "-z") - if base == objectFormat.EmptyObjectID().String() { - cmd.AddDynamicArguments(head) - } else { - cmd.AddDynamicArguments(base, head) - } - stdout, _, err := cmd.WithDir(repo.Path).RunStdString(repo.Ctx) - if err != nil { - return nil, err - } - split := strings.Split(stdout, "\000") - - // Because Git will always emit filenames with a terminal NUL ignore the last entry in the split - which will always be empty. - if len(split) > 0 { - split = split[:len(split)-1] - } - - return split, err -} - // ReadPatchCommit will check if a diff patch exists and return stats func (repo *Repository) ReadPatchCommit(prID int64) (commitSHA string, err error) { // Migrated repositories download patches to "pulls" location diff --git a/modules/git/repo_compare_test.go b/modules/git/repo_compare_test.go index bf16b7cfceb61..7ab114407e038 100644 --- a/modules/git/repo_compare_test.go +++ b/modules/git/repo_compare_test.go @@ -4,8 +4,6 @@ package git import ( - "bytes" - "io" "path/filepath" "testing" @@ -14,39 +12,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestGetFormatPatch(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - clonedPath, err := cloneRepo(t, bareRepo1Path) - if err != nil { - assert.NoError(t, err) - return - } - - repo, err := OpenRepository(t.Context(), clonedPath) - if err != nil { - assert.NoError(t, err) - return - } - defer repo.Close() - - rd := &bytes.Buffer{} - err = repo.GetPatch("8d92fc95^...8d92fc95", rd) - if err != nil { - assert.NoError(t, err) - return - } - - patchb, err := io.ReadAll(rd) - if err != nil { - assert.NoError(t, err) - return - } - - patch := string(patchb) - assert.Regexp(t, "^From 8d92fc95", patch) - assert.Contains(t, patch, "Subject: [PATCH] Add file2.txt") -} - func TestReadPatch(t *testing.T) { // Ensure we can read the patch files bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") @@ -127,45 +92,3 @@ func TestReadWritePullHead(t *testing.T) { RunStdString(t.Context()) assert.NoError(t, err) } - -func TestGetCommitFilesChanged(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - repo, err := OpenRepository(t.Context(), bareRepo1Path) - assert.NoError(t, err) - defer repo.Close() - - objectFormat, err := repo.GetObjectFormat() - assert.NoError(t, err) - - testCases := []struct { - base, head string - files []string - }{ - { - objectFormat.EmptyObjectID().String(), - "95bb4d39648ee7e325106df01a621c530863a653", - []string{"file1.txt"}, - }, - { - objectFormat.EmptyObjectID().String(), - "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", - []string{"file2.txt"}, - }, - { - "95bb4d39648ee7e325106df01a621c530863a653", - "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", - []string{"file2.txt"}, - }, - { - objectFormat.EmptyTree().String(), - "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", - []string{"file1.txt", "file2.txt"}, - }, - } - - for _, tc := range testCases { - changedFiles, err := repo.GetFilesChangedBetween(tc.base, tc.head) - assert.NoError(t, err) - assert.ElementsMatch(t, tc.files, changedFiles) - } -} diff --git a/modules/git/repo_tree.go b/modules/git/repo_tree.go index 9b119e8426480..e33073ef6228e 100644 --- a/modules/git/repo_tree.go +++ b/modules/git/repo_tree.go @@ -6,6 +6,7 @@ package git import ( "bytes" + "context" "os" "strings" "time" @@ -23,7 +24,7 @@ type CommitTreeOpts struct { } // CommitTree creates a commit from a given tree id for the user with provided message -func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opts CommitTreeOpts) (ObjectID, error) { +func CommitTree(ctx context.Context, repoPath string, author, committer *Signature, tree *Tree, opts CommitTreeOpts) (ObjectID, error) { commitTimeStr := time.Now().Format(time.RFC3339) // Because this may call hooks we should pass in the environment @@ -59,9 +60,9 @@ func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opt } stdout, _, err := cmd.WithEnv(env). - WithDir(repo.Path). + WithDir(repoPath). WithStdin(messageBytes). - RunStdString(repo.Ctx) + RunStdString(ctx) if err != nil { return nil, err } diff --git a/modules/gitrepo/commit.go b/modules/gitrepo/commit.go index 0ab17862feec9..0fc191723f9c0 100644 --- a/modules/gitrepo/commit.go +++ b/modules/gitrepo/commit.go @@ -5,6 +5,8 @@ package gitrepo import ( "context" + "errors" + "os/exec" "strconv" "strings" "time" @@ -110,3 +112,64 @@ func GetLatestCommitTime(ctx context.Context, repo Repository) (time.Time, error commitTime := strings.TrimSpace(stdout) return time.Parse("Mon Jan _2 15:04:05 2006 -0700", commitTime) } + +// IsForcePush returns true if a push from oldCommitHash to this is a force push +func IsCommitForcePush(ctx context.Context, repo Repository, newCommitID, oldCommitID string) (bool, error) { + if oldCommitID == git.Sha1ObjectFormat.EmptyObjectID().String() || oldCommitID == git.Sha256ObjectFormat.EmptyObjectID().String() { + return false, nil + } + + hasPreviousCommit, err := HasPreviousCommit(ctx, repo, newCommitID, oldCommitID) + return !hasPreviousCommit, err +} + +// HasPreviousCommit returns true if a given commitHash is contained in commit's parents +func HasPreviousCommit(ctx context.Context, repo Repository, newCommitID, oldCommitID string) (bool, error) { + if newCommitID == oldCommitID { + return false, nil + } + + _, _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("merge-base", "--is-ancestor"). + AddDynamicArguments(oldCommitID, newCommitID)) + 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 +} + +// GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only') +func GetCommitBranchName(ctx context.Context, repo Repository, commitID string) (string, error) { + cmd := gitcmd.NewCommand("name-rev") + if git.DefaultFeatures().CheckVersionAtLeast("2.13.0") { + cmd.AddArguments("--exclude", "refs/tags/*") + } + cmd.AddArguments("--name-only", "--no-undefined").AddDynamicArguments(commitID) + data, _, err := RunCmdString(ctx, repo, cmd) + if err != nil { + // handle special case where git can not describe commit + if strings.Contains(err.Error(), "cannot describe") { + return "", nil + } + + return "", err + } + + // name-rev commitID output will be "master" or "master~12" + return strings.SplitN(strings.TrimSpace(data), "~", 2)[0], nil +} + +// IsCommitInBranch check if the commit is on the branch +func IsCommitInBranch(ctx context.Context, repo Repository, commitID, branch string) (r bool, err error) { + stdout, _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("branch", "--contains"). + AddDynamicArguments(commitID, branch)) + if err != nil { + return false, err + } + return len(stdout) > 0, err +} diff --git a/modules/gitrepo/commit_test.go b/modules/gitrepo/commit_test.go index 05cedc39efd14..dc897eeb0b3b0 100644 --- a/modules/gitrepo/commit_test.go +++ b/modules/gitrepo/commit_test.go @@ -43,3 +43,56 @@ func TestGetLatestCommitTime(t *testing.T) { // ce064814f4a0d337b333e646ece456cd39fab612 (refs/heads/master) assert.EqualValues(t, 1668354014, lct.Unix()) } + +func TestHasPreviousCommitSha256(t *testing.T) { + repo := &mockRepository{path: "repo1_bare_sha256"} + newCommitID := "f004f41359117d319dedd0eaab8c5259ee2263da839dcba33637997458627fdc" + + parentSHA := "b0ec7af4547047f12d5093e37ef8f1b3b5415ed8ee17894d43a34d7d34212e9c" + notParentSHA := "42e334efd04cd36eea6da0599913333c26116e1a537ca76e5b6e4af4dda00236" + + haz, err := HasPreviousCommit(t.Context(), repo, newCommitID, parentSHA) + assert.NoError(t, err) + assert.True(t, haz) + + hazNot, err := HasPreviousCommit(t.Context(), repo, newCommitID, notParentSHA) + assert.NoError(t, err) + assert.False(t, hazNot) + + selfNot, err := HasPreviousCommit(t.Context(), repo, newCommitID, newCommitID) + assert.NoError(t, err) + assert.False(t, selfNot) +} + +func TestHasPreviousCommit(t *testing.T) { + repo := &mockRepository{path: "repo1_bare"} + + newCommitID := "8006ff9adbf0cb94da7dad9e537e53817f9fa5c0" + + parentSHA := "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2" + notParentSHA := "2839944139e0de9737a044f78b0e4b40d989a9e3" + + haz, err := HasPreviousCommit(t.Context(), repo, newCommitID, parentSHA) + assert.NoError(t, err) + assert.True(t, haz) + + hazNot, err := HasPreviousCommit(t.Context(), repo, newCommitID, notParentSHA) + assert.NoError(t, err) + assert.False(t, hazNot) + + selfNot, err := HasPreviousCommit(t.Context(), repo, newCommitID, newCommitID) + assert.NoError(t, err) + assert.False(t, selfNot) +} + +func TestIsCommitInBranch(t *testing.T) { + repo := &mockRepository{path: "repo1_bare"} + + result, err := IsCommitInBranch(t.Context(), repo, "2839944139e0de9737a044f78b0e4b40d989a9e3", "branch1") + assert.NoError(t, err) + assert.True(t, result) + + result, err = IsCommitInBranch(t.Context(), repo, "2839944139e0de9737a044f78b0e4b40d989a9e3", "branch2") + assert.NoError(t, err) + assert.False(t, result) +} diff --git a/modules/gitrepo/compare.go b/modules/gitrepo/compare.go index 06cf880d993af..302fb2642b56d 100644 --- a/modules/gitrepo/compare.go +++ b/modules/gitrepo/compare.go @@ -4,11 +4,13 @@ package gitrepo import ( + "bytes" "context" "fmt" "strconv" "strings" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/gitcmd" ) @@ -42,3 +44,70 @@ func GetDivergingCommits(ctx context.Context, repo Repository, baseBranch, targe } return &DivergeObject{Ahead: ahead, Behind: behind}, nil } + +type lineCountWriter struct { + numLines int +} + +// Write counts the number of newlines in the provided bytestream +func (l *lineCountWriter) Write(p []byte) (n int, err error) { + n = len(p) + l.numLines += bytes.Count(p, []byte{'\000'}) + return n, err +} + +// GetDiffNumChangedFiles counts the number of changed files +// This is substantially quicker than shortstat but... +func GetDiffNumChangedFiles(ctx context.Context, repo Repository, base, head string, directComparison bool) (int, error) { + // Now there is git diff --shortstat but this appears to be slower than simply iterating with --nameonly + w := &lineCountWriter{} + + separator := "..." + if directComparison { + separator = ".." + } + + // avoid: ambiguous argument 'refs/a...refs/b': unknown revision or path not in the working tree. Use '--': 'git [...] -- [...]' + if err := RunCmdWithStderr(ctx, repo, gitcmd.NewCommand("diff", "-z", "--name-only"). + AddDynamicArguments(base+separator+head). + AddArguments("--"). + WithStdout(w)); err != nil { + if strings.Contains(err.Stderr(), "no merge base") { + // git >= 2.28 now returns an error if base and head have become unrelated. + // previously it would return the results of git diff -z --name-only base head so let's try that... + w = &lineCountWriter{} + if err = RunCmdWithStderr(ctx, repo, gitcmd.NewCommand("diff", "-z", "--name-only"). + AddDynamicArguments(base, head). + AddArguments("--"). + WithStdout(w)); err == nil { + return w.numLines, nil + } + } + return 0, err + } + return w.numLines, nil +} + +// GetFilesChangedBetween returns a list of all files that have been changed between the given commits +// If base is undefined empty SHA (zeros), it only returns the files changed in the head commit +// If base is the SHA of an empty tree (EmptyTreeSHA), it returns the files changes from the initial commit to the head commit +func GetFilesChangedBetween(ctx context.Context, repo Repository, base, head string) ([]string, error) { + cmd := gitcmd.NewCommand("diff-tree", "--name-only", "--root", "--no-commit-id", "-r", "-z") + if base == git.Sha1ObjectFormat.EmptyObjectID().String() || base == git.Sha256ObjectFormat.EmptyObjectID().String() { + cmd.AddDynamicArguments(head) + } else { + cmd.AddDynamicArguments(base, head) + } + stdout, _, err := RunCmdString(ctx, repo, cmd) + if err != nil { + return nil, err + } + split := strings.Split(stdout, "\000") + + // Because Git will always emit filenames with a terminal NUL ignore the last entry in the split - which will always be empty. + if len(split) > 0 { + split = split[:len(split)-1] + } + + return split, err +} diff --git a/modules/gitrepo/compare_test.go b/modules/gitrepo/compare_test.go index f8661d9412102..9a45a4a76d2df 100644 --- a/modules/gitrepo/compare_test.go +++ b/modules/gitrepo/compare_test.go @@ -6,6 +6,8 @@ package gitrepo import ( "testing" + "code.gitea.io/gitea/modules/git" + "github.com/stretchr/testify/assert" ) @@ -40,3 +42,39 @@ func TestRepoGetDivergingCommits(t *testing.T) { Behind: 2, }, do) } + +func TestGetCommitFilesChanged(t *testing.T) { + repo := &mockRepository{path: "repo1_bare"} + + testCases := []struct { + base, head string + files []string + }{ + { + git.Sha1ObjectFormat.EmptyObjectID().String(), + "95bb4d39648ee7e325106df01a621c530863a653", + []string{"file1.txt"}, + }, + { + git.Sha1ObjectFormat.EmptyObjectID().String(), + "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", + []string{"file2.txt"}, + }, + { + "95bb4d39648ee7e325106df01a621c530863a653", + "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", + []string{"file2.txt"}, + }, + { + git.Sha1ObjectFormat.EmptyTree().String(), + "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", + []string{"file1.txt", "file2.txt"}, + }, + } + + for _, tc := range testCases { + changedFiles, err := GetFilesChangedBetween(t.Context(), repo, tc.base, tc.head) + assert.NoError(t, err) + assert.ElementsMatch(t, tc.files, changedFiles) + } +} diff --git a/modules/git/grep.go b/modules/gitrepo/grep.go similarity index 95% rename from modules/git/grep.go rename to modules/gitrepo/grep.go index edd46f431492b..cdcb4d44b832c 100644 --- a/modules/git/grep.go +++ b/modules/gitrepo/grep.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package git +package gitrepo import ( "bufio" @@ -40,7 +40,7 @@ type GrepOptions struct { PathspecList []string } -func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) { +func GrepSearch(ctx context.Context, repo Repository, search string, opts GrepOptions) ([]*GrepResult, error) { /* The output is like this ( "^@" means \x00): @@ -75,7 +75,7 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50) var stdoutReader io.ReadCloser - err := cmd.WithDir(repo.Path). + err := RunCmdWithStderr(ctx, repo, cmd. WithStdoutReader(&stdoutReader). WithPipelineFunc(func(ctx context.Context, cancel context.CancelFunc) error { defer stdoutReader.Close() @@ -121,8 +121,7 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO } } return nil - }). - RunWithStderr(ctx) + })) // git grep exits by cancel (killed), usually it is caused by the limit of results if gitcmd.IsErrorExitCode(err, -1) && err.Stderr() == "" { return results, nil diff --git a/modules/git/grep_test.go b/modules/gitrepo/grep_test.go similarity index 87% rename from modules/git/grep_test.go rename to modules/gitrepo/grep_test.go index b87ac4bea73a8..e1323c5cebf37 100644 --- a/modules/git/grep_test.go +++ b/modules/gitrepo/grep_test.go @@ -1,19 +1,16 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package git +package gitrepo import ( - "path/filepath" "testing" "github.com/stretchr/testify/assert" ) func TestGrepSearch(t *testing.T) { - repo, err := OpenRepository(t.Context(), filepath.Join(testReposDir, "language_stats_repo")) - assert.NoError(t, err) - defer repo.Close() + repo := &mockRepository{path: "language_stats_repo"} res, err := GrepSearch(t.Context(), repo, "void", GrepOptions{}) assert.NoError(t, err) @@ -74,7 +71,7 @@ func TestGrepSearch(t *testing.T) { assert.NoError(t, err) assert.Empty(t, res) - res, err = GrepSearch(t.Context(), &Repository{Path: "no-such-git-repo"}, "no-such-content", GrepOptions{}) + res, err = GrepSearch(t.Context(), &mockRepository{path: "no-such-git-repo"}, "no-such-content", GrepOptions{}) assert.Error(t, err) assert.Empty(t, res) } diff --git a/modules/gitrepo/patch.go b/modules/gitrepo/patch.go new file mode 100644 index 0000000000000..5ffe016dc1c03 --- /dev/null +++ b/modules/gitrepo/patch.go @@ -0,0 +1,30 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + "io" + + "code.gitea.io/gitea/modules/git/gitcmd" +) + +// GetDiff generates and returns patch data between given revisions, optimized for human readability +func GetDiff(ctx context.Context, repo Repository, compareArg string, w io.Writer) error { + return RunCmdWithStderr(ctx, repo, gitcmd.NewCommand("diff", "-p").AddDynamicArguments(compareArg). + WithStdout(w)) +} + +// GetDiffBinary generates and returns patch data between given revisions, including binary diffs. +func GetDiffBinary(ctx context.Context, repo Repository, compareArg string, w io.Writer) error { + return RunCmd(ctx, repo, gitcmd.NewCommand("diff", "-p", "--binary", "--histogram"). + AddDynamicArguments(compareArg). + WithStdout(w)) +} + +// GetPatch generates and returns format-patch data between given revisions, able to be used with `git apply` +func GetPatch(ctx context.Context, repo Repository, compareArg string, w io.Writer) error { + return RunCmdWithStderr(ctx, repo, gitcmd.NewCommand("format-patch", "--binary", "--stdout").AddDynamicArguments(compareArg). + WithStdout(w)) +} diff --git a/modules/gitrepo/patch_test.go b/modules/gitrepo/patch_test.go new file mode 100644 index 0000000000000..adde8f169205e --- /dev/null +++ b/modules/gitrepo/patch_test.go @@ -0,0 +1,33 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "bytes" + "io" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetFormatPatch(t *testing.T) { + repo := &mockRepository{path: "repo1_bare"} + + rd := &bytes.Buffer{} + err := GetPatch(t.Context(), repo, "8d92fc95^...8d92fc95", rd) + if err != nil { + assert.NoError(t, err) + return + } + + patchb, err := io.ReadAll(rd) + if err != nil { + assert.NoError(t, err) + return + } + + patch := string(patchb) + assert.Regexp(t, "^From 8d92fc95", patch) + assert.Contains(t, patch, "Subject: [PATCH] Add file2.txt") +} diff --git a/modules/git/repo_stats.go b/modules/gitrepo/stats.go similarity index 90% rename from modules/git/repo_stats.go rename to modules/gitrepo/stats.go index 175f5592c201f..ed21a86f8d5d6 100644 --- a/modules/git/repo_stats.go +++ b/modules/gitrepo/stats.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package git +package gitrepo import ( "bufio" @@ -36,15 +36,13 @@ type CodeActivityAuthor struct { } // GetCodeActivityStats returns code statistics for activity page -func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) (*CodeActivityStats, error) { +func GetCodeActivityStats(ctx context.Context, repo Repository, fromTime time.Time, branch string) (*CodeActivityStats, error) { stats := &CodeActivityStats{} since := fromTime.Format(time.RFC3339) - stdout, _, runErr := gitcmd.NewCommand("rev-list", "--count", "--no-merges", "--branches=*", "--date=iso"). - AddOptionFormat("--since=%s", since). - WithDir(repo.Path). - RunStdString(repo.Ctx) + stdout, _, runErr := RunCmdString(ctx, repo, gitcmd.NewCommand("rev-list", "--count", "--no-merges", "--branches=*", "--date=iso"). + AddOptionFormat("--since=%s", since)) if runErr != nil { return nil, runErr } @@ -72,8 +70,7 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) gitCmd.AddArguments("--first-parent").AddDynamicArguments(branch) } - err = gitCmd. - WithDir(repo.Path). + err = RunCmdWithStderr(ctx, repo, gitCmd. WithStdout(stdoutWriter). WithPipelineFunc(func(ctx context.Context, cancel context.CancelFunc) error { _ = stdoutWriter.Close() @@ -143,8 +140,7 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) stats.Authors = a _ = stdoutReader.Close() return nil - }). - RunWithStderr(repo.Ctx) + })) if err != nil { return nil, fmt.Errorf("GetCodeActivityStats: %w", err) } diff --git a/modules/git/repo_stats_test.go b/modules/gitrepo/stats_test.go similarity index 75% rename from modules/git/repo_stats_test.go rename to modules/gitrepo/stats_test.go index 538283111bdc1..b731ee5bfe23b 100644 --- a/modules/git/repo_stats_test.go +++ b/modules/gitrepo/stats_test.go @@ -1,10 +1,9 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package git +package gitrepo import ( - "path/filepath" "testing" "time" @@ -12,15 +11,12 @@ import ( ) func TestRepository_GetCodeActivityStats(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - bareRepo1, err := OpenRepository(t.Context(), bareRepo1Path) - assert.NoError(t, err) - defer bareRepo1.Close() + repo := &mockRepository{path: "repo1_bare"} timeFrom, err := time.Parse(time.RFC3339, "2016-01-01T00:00:00+00:00") assert.NoError(t, err) - code, err := bareRepo1.GetCodeActivityStats(timeFrom, "") + code, err := GetCodeActivityStats(t.Context(), repo, timeFrom, "") assert.NoError(t, err) assert.NotNil(t, code) diff --git a/modules/indexer/code/gitgrep/gitgrep.go b/modules/indexer/code/gitgrep/gitgrep.go index 6f6e0b47b9e2e..2e76511d792d0 100644 --- a/modules/indexer/code/gitgrep/gitgrep.go +++ b/modules/indexer/code/gitgrep/gitgrep.go @@ -8,7 +8,9 @@ import ( "fmt" "strings" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/indexer" code_indexer "code.gitea.io/gitea/modules/indexer/code" "code.gitea.io/gitea/modules/setting" @@ -24,15 +26,15 @@ func indexSettingToGitGrepPathspecList() (list []string) { return list } -func PerformSearch(ctx context.Context, page int, repoID int64, gitRepo *git.Repository, ref git.RefName, keyword string, searchMode indexer.SearchModeType) (searchResults []*code_indexer.Result, total int, err error) { - grepMode := git.GrepModeWords +func PerformSearch(ctx context.Context, page int, repo *repo_model.Repository, gitRepo *git.Repository, ref git.RefName, keyword string, searchMode indexer.SearchModeType) (searchResults []*code_indexer.Result, total int, err error) { + grepMode := gitrepo.GrepModeWords switch searchMode { case indexer.SearchModeExact: - grepMode = git.GrepModeExact + grepMode = gitrepo.GrepModeExact case indexer.SearchModeRegexp: - grepMode = git.GrepModeRegexp + grepMode = gitrepo.GrepModeRegexp } - res, err := git.GrepSearch(ctx, gitRepo, keyword, git.GrepOptions{ + res, err := gitrepo.GrepSearch(ctx, repo, keyword, gitrepo.GrepOptions{ ContextLineNumber: 1, GrepMode: grepMode, RefName: ref.String(), @@ -53,7 +55,7 @@ func PerformSearch(ctx context.Context, page int, repoID int64, gitRepo *git.Rep res = res[pageStart:pageEnd] for _, r := range res { searchResults = append(searchResults, &code_indexer.Result{ - RepoID: repoID, + RepoID: repo.ID, Filename: r.Filename, CommitID: commitID, // UpdatedUnix: not supported yet diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index cff501ad71d92..37748b51a3cf5 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -810,7 +810,7 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) { var reviewState *pull_model.ReviewState var numViewedFiles int if ctx.IsSigned && isShowAllCommits { - reviewState, err = gitdiff.SyncUserSpecificDiff(ctx, ctx.Doer.ID, pull, gitRepo, diff, diffOptions) + reviewState, err = gitdiff.SyncUserSpecificDiff(ctx, ctx.Doer.ID, pull, diff, diffOptions) if err != nil { ctx.ServerError("SyncUserSpecificDiff", err) return diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go index 12216fc620109..6b3d442bd66db 100644 --- a/routers/web/repo/search.go +++ b/routers/web/repo/search.go @@ -60,7 +60,7 @@ func Search(ctx *context.Context) { var err error // ref should be default branch or the first existing branch searchRef := git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch) - searchResults, total, err = gitgrep.PerformSearch(ctx, page, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, searchRef, prepareSearch.Keyword, prepareSearch.SearchMode) + searchResults, total, err = gitgrep.PerformSearch(ctx, page, ctx.Repo.Repository, ctx.Repo.GitRepo, searchRef, prepareSearch.Keyword, prepareSearch.SearchMode) if err != nil { ctx.ServerError("gitgrep.PerformSearch", err) return diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 10b36a5a52a86..2a7f38fedf58f 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -183,7 +183,7 @@ func notify(ctx context.Context, input *notifyInput) error { var detectedWorkflows []*actions_module.DetectedWorkflow actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig() - workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit, + workflows, schedules, err := actions_module.DetectWorkflows(ctx, input.Repo, gitRepo, commit, input.Event, input.Payload, shouldDetectSchedules, @@ -218,7 +218,7 @@ func notify(ctx context.Context, input *notifyInput) error { if err != nil { return fmt.Errorf("gitRepo.GetCommit: %w", err) } - baseWorkflows, _, err := actions_module.DetectWorkflows(gitRepo, baseCommit, input.Event, input.Payload, false) + baseWorkflows, _, err := actions_module.DetectWorkflows(ctx, input.Repo, gitRepo, baseCommit, input.Event, input.Payload, false) if err != nil { return fmt.Errorf("DetectWorkflows: %w", err) } diff --git a/services/convert/convert.go b/services/convert/convert.go index c081aec771bf6..c7dfe19315f67 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -29,6 +29,7 @@ import ( "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/container" "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" @@ -394,7 +395,7 @@ func getActionWorkflowEntry(ctx context.Context, repo *repo_model.Repository, co cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions) cfg := cfgUnit.ActionsConfig() - defaultBranch, _ := commit.GetBranchName() + defaultBranch, _ := gitrepo.GetCommitBranchName(ctx, repo, commit.ID.String()) workflowURL := fmt.Sprintf("%s/actions/workflows/%s", repo.APIURL(), util.PathEscapeSegments(entry.Name())) workflowRepoURL := fmt.Sprintf("%s/src/branch/%s/%s/%s", repo.HTMLURL(ctx), util.PathEscapeSegments(defaultBranch), util.PathEscapeSegments(folder), util.PathEscapeSegments(entry.Name())) diff --git a/services/git/compare.go b/services/git/compare.go index 6c49fff26acef..b83c99ac3a15d 100644 --- a/services/git/compare.go +++ b/services/git/compare.go @@ -95,7 +95,7 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito // Count number of changed files. // This probably should be removed as we need to use shortstat elsewhere // Now there is git diff --shortstat but this appears to be slower than simply iterating with --nameonly - compareInfo.NumFiles, err = headGitRepo.GetDiffNumChangedFiles(compareInfo.BaseCommitID, compareInfo.HeadCommitID, directComparison) + compareInfo.NumFiles, err = gitrepo.GetDiffNumChangedFiles(ctx, headRepo, compareInfo.BaseCommitID, compareInfo.HeadCommitID, directComparison) if err != nil { return nil, err } diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index dbb1898f21e29..614184f15ee75 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -1403,7 +1403,7 @@ func GetDiffShortStat(ctx context.Context, repoStorage gitrepo.Repository, gitRe // SyncUserSpecificDiff inserts user-specific data such as which files the user has already viewed on the given diff // Additionally, the database is updated asynchronously if files have changed since the last review -func SyncUserSpecificDiff(ctx context.Context, userID int64, pull *issues_model.PullRequest, gitRepo *git.Repository, diff *Diff, opts *DiffOptions) (*pull_model.ReviewState, error) { +func SyncUserSpecificDiff(ctx context.Context, userID int64, pull *issues_model.PullRequest, diff *Diff, opts *DiffOptions) (*pull_model.ReviewState, error) { review, err := pull_model.GetNewestReviewState(ctx, userID, pull.ID) if err != nil { return nil, err @@ -1417,14 +1417,18 @@ func SyncUserSpecificDiff(ctx context.Context, userID int64, pull *issues_model. latestCommit = pull.HeadBranch // opts.AfterCommitID is preferred because it handles PRs from forks correctly and the branch name doesn't } - changedFiles, errIgnored := gitRepo.GetFilesChangedBetween(review.CommitSHA, latestCommit) + if err := pull.LoadBaseRepo(ctx); err != nil { + return nil, err + } + + changedFiles, errIgnored := gitrepo.GetFilesChangedBetween(ctx, pull.BaseRepo, review.CommitSHA, latestCommit) // There are way too many possible errors. // Examples are various git errors such as the commit the review was based on was gc'ed and hence doesn't exist anymore as well as unrecoverable errors where we should serve a 500 response // Due to the current architecture and physical limitation of needing to compare explicit error messages, we can only choose one approach without the code getting ugly // For SOME of the errors such as the gc'ed commit, it would be best to mark all files as changed // But as that does not work for all potential errors, we simply mark all files as unchanged and drop the error which always works, even if not as good as possible if errIgnored != nil { - log.Error("Could not get changed files between %s and %s for pull request %d in repo with path %s. Assuming no changes. Error: %w", review.CommitSHA, latestCommit, pull.Index, gitRepo.Path, err) + log.Error("Could not get changed files between %s and %s for pull request %d in repo with path %s. Assuming no changes. Error: %w", review.CommitSHA, latestCommit, pull.Index, pull.BaseRepo.RelativePath(), err) } filesChangedSinceLastDiff := make(map[string]pull_model.ViewedState) diff --git a/services/issue/pull.go b/services/issue/pull.go index 2fcf3860d031d..4c37ad0129538 100644 --- a/services/issue/pull.go +++ b/services/issue/pull.go @@ -86,7 +86,7 @@ func PullRequestCodeOwnersReview(ctx context.Context, pr *issues_model.PullReque } // https://github.com/go-gitea/gitea/issues/29763, we need to get the files changed // between the merge base and the head commit but not the base branch and the head commit - changedFiles, err := repo.GetFilesChangedBetween(mergeBase, pr.GetGitHeadRefName()) + changedFiles, err := gitrepo.GetFilesChangedBetween(ctx, pr.BaseRepo, mergeBase, pr.GetGitHeadRefName()) if err != nil { return nil, err } diff --git a/services/pull/merge.go b/services/pull/merge.go index 6dab23aa21fb9..a53cde6e7750f 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -27,6 +27,7 @@ import ( "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/globallock" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" @@ -647,7 +648,7 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use } commitID = commit.ID.String() - ok, err := baseGitRepo.IsCommitInBranch(commitID, pr.BaseBranch) + ok, err := gitrepo.IsCommitInBranch(ctx, pr.BaseRepo, commitID, pr.BaseBranch) if err != nil { return err } diff --git a/services/pull/patch.go b/services/pull/patch.go index ce3afcc331dc9..2d0cb515ab12a 100644 --- a/services/pull/patch.go +++ b/services/pull/patch.go @@ -35,20 +35,15 @@ func DownloadDiffOrPatch(ctx context.Context, pr *issues_model.PullRequest, w io return err } - gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo) - if err != nil { - return fmt.Errorf("OpenRepository: %w", err) - } - defer closer.Close() - + var err error compareArg := pr.MergeBase + "..." + pr.GetGitHeadRefName() switch { case patch: - err = gitRepo.GetPatch(compareArg, w) + err = gitrepo.GetPatch(ctx, pr.BaseRepo, compareArg, w) case binary: - err = gitRepo.GetDiffBinary(compareArg, w) + err = gitrepo.GetDiffBinary(ctx, pr.BaseRepo, compareArg, w) default: - err = gitRepo.GetDiff(compareArg, w) + err = gitrepo.GetDiff(ctx, pr.BaseRepo, compareArg, w) } if err != nil { diff --git a/services/pull/pull.go b/services/pull/pull.go index baad89aa55c22..2dfa4acb9f1f2 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -1042,7 +1042,7 @@ func IsHeadEqualWithBranch(ctx context.Context, pr *issues_model.PullRequest, br return false, err } } - return baseCommit.HasPreviousCommit(headCommit.ID) + return gitrepo.HasPreviousCommit(ctx, pr.BaseRepo, baseCommit.ID.String(), headCommit.ID.String()) } type CommitInfo struct { diff --git a/services/repository/branch.go b/services/repository/branch.go index 142073eabe7d9..d31f66ebfa5ea 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -517,7 +517,7 @@ func UpdateBranch(ctx context.Context, repo *repo_model.Repository, gitRepo *git return nil } - isForcePush, err := newCommit.IsForcePush(branch.CommitID) + isForcePush, err := gitrepo.IsCommitForcePush(ctx, repo, newCommit.ID.String(), branch.CommitID) if err != nil { return err } @@ -788,7 +788,7 @@ func GetBranchDivergingInfo(ctx reqctx.RequestContext, baseRepo *repo_model.Repo if err != nil { return nil, err } - hasPreviousCommit, _ := headCommit.HasPreviousCommit(baseCommitID) + hasPreviousCommit, _ := gitrepo.HasPreviousCommit(ctx, headRepo, headCommit.ID.String(), baseCommitID.String()) info.BaseHasNewCommits = !hasPreviousCommit return info, nil } diff --git a/services/repository/push.go b/services/repository/push.go index 7c68a7f176308..d19b52f185287 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -296,7 +296,7 @@ func pushNewBranch(ctx context.Context, repo *repo_model.Repository, pusher *use return l, nil } -func pushUpdateBranch(_ context.Context, repo *repo_model.Repository, pusher *user_model.User, opts *repo_module.PushUpdateOptions, newCommit *git.Commit) ([]*git.Commit, error) { +func pushUpdateBranch(ctx context.Context, repo *repo_model.Repository, pusher *user_model.User, opts *repo_module.PushUpdateOptions, newCommit *git.Commit) ([]*git.Commit, error) { l, err := newCommit.CommitsBeforeUntil(opts.OldCommitID) if err != nil { return nil, fmt.Errorf("newCommit.CommitsBeforeUntil: %w", err) @@ -304,7 +304,7 @@ func pushUpdateBranch(_ context.Context, repo *repo_model.Repository, pusher *us branch := opts.RefFullName.BranchName() - isForcePush, err := newCommit.IsForcePush(opts.OldCommitID) + isForcePush, err := gitrepo.IsCommitForcePush(ctx, repo, newCommit.ID.String(), opts.OldCommitID) if err != nil { log.Error("IsForcePush %s:%s failed: %v", repo.FullName(), branch, err) } diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index f4115038cbd2c..c264466758d63 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -212,7 +212,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model commitTreeOpts.Parents = []string{"HEAD"} } - commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts) + commitHash, err := git.CommitTree(ctx, basePath, doer.NewGitSig(), committer, tree, commitTreeOpts) if err != nil { log.Error("CommitTree failed: %v", err) return err @@ -337,12 +337,12 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model commitTreeOpts.NoGPGSign = true } - commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts) + commitHash, err := git.CommitTree(ctx, basePath, doer.NewGitSig(), committer, tree, commitTreeOpts) if err != nil { return err } - if err := gitrepo.PushFromLocal(gitRepo.Ctx, basePath, repo.WikiStorageRepo(), git.PushOptions{ + if err := gitrepo.PushFromLocal(ctx, basePath, repo.WikiStorageRepo(), git.PushOptions{ Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.DefaultWikiBranch), Env: repo_module.FullPushingEnvironment( doer,