diff --git a/models/migrations/base/tests.go b/models/migrations/base/tests.go index 85cafc1ab915e..5834c4492ee61 100644 --- a/models/migrations/base/tests.go +++ b/models/migrations/base/tests.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/testlogger" @@ -155,6 +156,10 @@ func MainTest(m *testing.M) { fmt.Printf("Unable to InitFull: %v\n", err) os.Exit(1) } + if err = gitrepo.Init(context.Background()); err != nil { + fmt.Printf("Unable to InitFull: %v\n", err) + os.Exit(1) + } setting.LoadDBSetting() setting.InitLoggersForTest() diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 53c9dbdd77254..6eb4ceb9f9dff 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting/config" @@ -174,6 +175,9 @@ func MainTest(m *testing.M, testOpts ...*TestOptions) { if err = git.InitFull(context.Background()); err != nil { fatalTestError("git.Init: %v\n", err) } + if err = gitrepo.Init(context.Background()); err != nil { + fatalTestError("gitrepo.Init: %v\n", err) + } ownerDirs, err := os.ReadDir(setting.RepoRootPath) if err != nil { fatalTestError("unable to read the new repo root: %v\n", err) diff --git a/modules/gitrepo/branch.go b/modules/gitrepo/branch.go index e13a4c82e1c25..ff41faf4e2d09 100644 --- a/modules/gitrepo/branch.go +++ b/modules/gitrepo/branch.go @@ -5,45 +5,55 @@ package gitrepo import ( "context" + "errors" + "strings" "code.gitea.io/gitea/modules/git" ) -// GetBranchesByPath returns a branch by its path +// GetBranches returns branch names by repository // if limit = 0 it will not limit -func GetBranchesByPath(ctx context.Context, repo Repository, skip, limit int) ([]*git.Branch, int, error) { - gitRepo, err := OpenRepository(ctx, repo) - if err != nil { - return nil, 0, err - } - defer gitRepo.Close() - - return gitRepo.GetBranches(skip, limit) +func GetBranches(ctx context.Context, repo Repository, skip, limit int) ([]string, int, error) { + branchNames := make([]string, 0, limit) + countAll, err := curService.WalkReferences(ctx, repo, git.ObjectBranch, skip, limit, func(_, branchName string) error { + branchName = strings.TrimPrefix(branchName, git.BranchPrefix) + branchNames = append(branchNames, branchName) + return nil + }) + return branchNames, countAll, err } func GetBranchCommitID(ctx context.Context, repo Repository, branch string) (string, error) { - gitRepo, err := OpenRepository(ctx, repo) + gitRepo, err := curService.OpenRepository(ctx, repo) if err != nil { return "", err } defer gitRepo.Close() - - return gitRepo.GetBranchCommitID(branch) + return gitRepo.GetRefCommitID(git.BranchPrefix + branch) } // SetDefaultBranch sets default branch of repository. func SetDefaultBranch(ctx context.Context, repo Repository, name string) error { - _, _, err := git.NewCommand(ctx, "symbolic-ref", "HEAD"). - AddDynamicArguments(git.BranchPrefix + name). - RunStdString(&git.RunOpts{Dir: repoPath(repo)}) + cmd := git.NewCommand(ctx, "symbolic-ref", "HEAD"). + AddDynamicArguments(git.BranchPrefix + name) + _, _, err := RunGitCmdStdString(ctx, repo, cmd, &git.RunOpts{}) return err } // GetDefaultBranch gets default branch of repository. func GetDefaultBranch(ctx context.Context, repo Repository) (string, error) { - return git.GetDefaultBranch(ctx, repoPath(repo)) + cmd := git.NewCommand(ctx, "symbolic-ref", "HEAD") + stdout, _, err := RunGitCmdStdString(ctx, repo, cmd, &git.RunOpts{}) + if err != nil { + return "", err + } + stdout = strings.TrimSpace(stdout) + if !strings.HasPrefix(stdout, git.BranchPrefix) { + return "", errors.New("the HEAD is not a branch: " + stdout) + } + return strings.TrimPrefix(stdout, git.BranchPrefix), nil } func GetWikiDefaultBranch(ctx context.Context, repo Repository) (string, error) { - return git.GetDefaultBranch(ctx, wikiPath(repo)) + return GetDefaultBranch(ctx, wikiRepo(repo)) } diff --git a/modules/gitrepo/command.go b/modules/gitrepo/command.go new file mode 100644 index 0000000000000..b770934c2e0f0 --- /dev/null +++ b/modules/gitrepo/command.go @@ -0,0 +1,88 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "bytes" + "context" + "fmt" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/util" +) + +// RunGitCmd runs the command with the RunOpts +func RunGitCmd(ctx context.Context, repo Repository, c *git.Command, opts *git.RunOpts) error { + if opts.Dir != "" { + return fmt.Errorf("dir field must be empty") + } + opts.Dir = repoRelativePath(repo) + return curService.Run(ctx, c, opts) +} + +type runStdError struct { + err error + stderr string + errMsg string +} + +func (r *runStdError) Error() string { + // the stderr must be in the returned error text, some code only checks `strings.Contains(err.Error(), "git error")` + if r.errMsg == "" { + r.errMsg = git.ConcatenateError(r.err, r.stderr).Error() + } + return r.errMsg +} + +func (r *runStdError) Unwrap() error { + return r.err +} + +func (r *runStdError) Stderr() string { + return r.stderr +} + +// RunStdBytes runs the command with options and returns stdout/stderr as bytes. and store stderr to returned error (err combined with stderr). +func RunGitCmdStdBytes(ctx context.Context, repo Repository, c *git.Command, opts *git.RunOpts) (stdout, stderr []byte, runErr git.RunStdError) { + if opts == nil { + opts = &git.RunOpts{} + } + if opts.Stdout != nil || opts.Stderr != nil { + // we must panic here, otherwise there would be bugs if developers set Stdin/Stderr by mistake, and it would be very difficult to debug + panic("stdout and stderr field must be nil when using RunStdBytes") + } + stdoutBuf := &bytes.Buffer{} + stderrBuf := &bytes.Buffer{} + + // We must not change the provided options as it could break future calls - therefore make a copy. + newOpts := &git.RunOpts{ + Env: opts.Env, + Timeout: opts.Timeout, + UseContextTimeout: opts.UseContextTimeout, + Stdout: stdoutBuf, + Stderr: stderrBuf, + Stdin: opts.Stdin, + PipelineFunc: opts.PipelineFunc, + } + + err := RunGitCmd(ctx, repo, c, newOpts) + stderr = stderrBuf.Bytes() + if err != nil { + return nil, stderr, &runStdError{err: err, stderr: util.UnsafeBytesToString(stderr)} + } + // even if there is no err, there could still be some stderr output + return stdoutBuf.Bytes(), stderr, nil +} + +// RunStdString runs the command with options and returns stdout/stderr as string. and store stderr to returned error (err combined with stderr). +func RunGitCmdStdString(ctx context.Context, repo Repository, c *git.Command, opts *git.RunOpts) (stdout, stderr string, runErr git.RunStdError) { + stdoutBytes, stderrBytes, err := RunGitCmdStdBytes(ctx, repo, c, opts) + stdout = util.UnsafeBytesToString(stdoutBytes) + stderr = util.UnsafeBytesToString(stderrBytes) + if err != nil { + return stdout, stderr, &runStdError{err: err, stderr: stderr} + } + // even if there is no err, there could still be some stderr output, so we just return stdout/stderr as they are + return stdout, stderr, nil +} diff --git a/modules/gitrepo/gitrepo.go b/modules/gitrepo/gitrepo.go index d89f8f9c0c88c..1ac7a123dee13 100644 --- a/modules/gitrepo/gitrepo.go +++ b/modules/gitrepo/gitrepo.go @@ -10,7 +10,6 @@ import ( "strings" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/setting" ) type Repository interface { @@ -18,21 +17,20 @@ type Repository interface { GetOwnerName() string } -func repoPath(repo Repository) string { - return filepath.Join(setting.RepoRootPath, strings.ToLower(repo.GetOwnerName()), strings.ToLower(repo.GetName())+".git") +type wikiRepository struct { + Repository } -func wikiPath(repo Repository) string { - return filepath.Join(setting.RepoRootPath, strings.ToLower(repo.GetOwnerName()), strings.ToLower(repo.GetName())+".wiki.git") +func (w wikiRepository) GetName() string { + return w.Repository.GetName() + ".wiki" } -// OpenRepository opens the repository at the given relative path with the provided context. -func OpenRepository(ctx context.Context, repo Repository) (*git.Repository, error) { - return git.OpenRepository(ctx, repoPath(repo)) +func wikiRepo(repo Repository) Repository { + return wikiRepository{repo} } -func OpenWikiRepository(ctx context.Context, repo Repository) (*git.Repository, error) { - return git.OpenRepository(ctx, wikiPath(repo)) +func repoRelativePath(repo Repository) string { + return strings.ToLower(repo.GetOwnerName()) + "/" + strings.ToLower(repo.GetName()) + ".git" } // contextKey is a value for use with context.WithValue. @@ -51,7 +49,8 @@ func repositoryFromContext(ctx context.Context, repo Repository) *git.Repository } if gitRepo, ok := value.(*git.Repository); ok && gitRepo != nil { - if gitRepo.Path == repoPath(repo) { + relativePath := filepath.Join(strings.ToLower(repo.GetOwnerName()), strings.ToLower(repo.GetName())+".git") + if strings.HasSuffix(gitRepo.Path, relativePath) { return gitRepo } } diff --git a/modules/gitrepo/init.go b/modules/gitrepo/init.go new file mode 100644 index 0000000000000..4ce1ac7daff3f --- /dev/null +++ b/modules/gitrepo/init.go @@ -0,0 +1,19 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + + "code.gitea.io/gitea/modules/setting" +) + +var curService Service + +func Init(ctx context.Context) error { + curService = &localServiceImpl{ + repoRootDir: setting.RepoRootPath, + } + return nil +} diff --git a/modules/gitrepo/repository.go b/modules/gitrepo/repository.go new file mode 100644 index 0000000000000..9f22627c95f39 --- /dev/null +++ b/modules/gitrepo/repository.go @@ -0,0 +1,19 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + + "code.gitea.io/gitea/modules/git" +) + +// OpenRepository opens the repository at the given relative path with the provided context. +func OpenRepository(ctx context.Context, repo Repository) (*git.Repository, error) { + return curService.OpenRepository(ctx, repo) +} + +func OpenWikiRepository(ctx context.Context, repo Repository) (*git.Repository, error) { + return curService.OpenRepository(ctx, wikiRepo(repo)) +} diff --git a/modules/gitrepo/service.go b/modules/gitrepo/service.go new file mode 100644 index 0000000000000..19e1405a6a678 --- /dev/null +++ b/modules/gitrepo/service.go @@ -0,0 +1,57 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + "path/filepath" + + "code.gitea.io/gitea/modules/git" +) + +type Service interface { + OpenRepository(ctx context.Context, repo Repository) (*git.Repository, error) + Run(ctx context.Context, c *git.Command, opts *git.RunOpts) error + RepoGitURL(repo Repository) string + WalkReferences(ctx context.Context, repo Repository, refType git.ObjectType, skip, limit int, walkfn func(sha1, refname string) error) (int, error) +} + +var _ Service = &localServiceImpl{} + +type localServiceImpl struct { + repoRootDir string +} + +func (s *localServiceImpl) Run(ctx context.Context, c *git.Command, opts *git.RunOpts) error { + opts.Dir = s.absPath(opts.Dir) + return c.Run(opts) +} + +func (s *localServiceImpl) absPath(relativePaths ...string) string { + for _, p := range relativePaths { + if filepath.IsAbs(p) { + // we must panic here, otherwise there would be bugs if developers set Dir by mistake, and it would be very difficult to debug + panic("dir field must be relative path") + } + } + path := append([]string{s.repoRootDir}, relativePaths...) + return filepath.Join(path...) +} + +func (s *localServiceImpl) OpenRepository(ctx context.Context, repo Repository) (*git.Repository, error) { + return git.OpenRepository(ctx, s.absPath(repoRelativePath(repo))) +} + +func (s *localServiceImpl) RepoGitURL(repo Repository) string { + return s.absPath(repoRelativePath(repo)) +} + +func (s *localServiceImpl) WalkReferences(ctx context.Context, repo Repository, refType git.ObjectType, skip, limit int, walkfn func(sha1, refname string) error) (int, error) { + gitRepo, err := s.OpenRepository(ctx, repo) + if err != nil { + return 0, err + } + defer gitRepo.Close() + return gitRepo.WalkReferences(refType, skip, limit, walkfn) +} diff --git a/modules/gitrepo/url.go b/modules/gitrepo/url.go index b355d0fa93aac..004c3aca27ec2 100644 --- a/modules/gitrepo/url.go +++ b/modules/gitrepo/url.go @@ -4,5 +4,5 @@ package gitrepo func RepoGitURL(repo Repository) string { - return repoPath(repo) + return curService.RepoGitURL(repo) } diff --git a/modules/gitrepo/walk_nogogit.go b/modules/gitrepo/walk.go similarity index 52% rename from modules/gitrepo/walk_nogogit.go rename to modules/gitrepo/walk.go index ff9555996dff5..45394b03d0615 100644 --- a/modules/gitrepo/walk_nogogit.go +++ b/modules/gitrepo/walk.go @@ -1,8 +1,6 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//go:build !gogit - package gitrepo import ( @@ -12,6 +10,6 @@ import ( ) // WalkReferences walks all the references from the repository -func WalkReferences(ctx context.Context, repo Repository, walkfn func(sha1, refname string) error) (int, error) { - return git.WalkShowRef(ctx, repoPath(repo), nil, 0, 0, walkfn) +func WalkReferences(ctx context.Context, repo Repository, refType git.ObjectType, walkfn func(sha1, refname string) error) (int, error) { + return curService.WalkReferences(ctx, repo, refType, 0, 0, walkfn) } diff --git a/modules/gitrepo/walk_gogit.go b/modules/gitrepo/walk_gogit.go deleted file mode 100644 index 6370faf08e7df..0000000000000 --- a/modules/gitrepo/walk_gogit.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build gogit - -package gitrepo - -import ( - "context" - - "github.com/go-git/go-git/v5/plumbing" -) - -// WalkReferences walks all the references from the repository -// refname is empty, ObjectTag or ObjectBranch. All other values should be treated as equivalent to empty. -func WalkReferences(ctx context.Context, repo Repository, walkfn func(sha1, refname string) error) (int, error) { - gitRepo := repositoryFromContext(ctx, repo) - if gitRepo == nil { - var err error - gitRepo, err = OpenRepository(ctx, repo) - if err != nil { - return 0, err - } - defer gitRepo.Close() - } - - i := 0 - iter, err := gitRepo.GoGitRepo().References() - if err != nil { - return i, err - } - defer iter.Close() - - err = iter.ForEach(func(ref *plumbing.Reference) error { - err := walkfn(ref.Hash().String(), string(ref.Name())) - i++ - return err - }) - return i, err -} diff --git a/routers/init.go b/routers/init.go index e21f763c1e527..d693098d55672 100644 --- a/routers/init.go +++ b/routers/init.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/eventsource" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" @@ -113,6 +114,7 @@ func InitWebInstallPage(ctx context.Context) { // InitWebInstalled is for global installed configuration. func InitWebInstalled(ctx context.Context) { mustInitCtx(ctx, git.InitFull) + mustInitCtx(ctx, gitrepo.Init) log.Info("Git version: %s (home: %s)", git.DefaultFeatures().VersionInfo(), git.HomeDir()) if !git.DefaultFeatures().SupportHashSha256 { log.Warn("sha256 hash support is disabled - requires Git >= 2.42." + util.Iif(git.DefaultFeatures().UsingGogit, " Gogit is currently unsupported.", "")) diff --git a/services/doctor/doctor.go b/services/doctor/doctor.go index a4eb5e16b91c5..1c35a81ec1979 100644 --- a/services/doctor/doctor.go +++ b/services/doctor/doctor.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" @@ -39,6 +40,9 @@ func initDBSkipLogger(ctx context.Context) error { if err := git.InitFull(ctx); err != nil { return fmt.Errorf("git.InitFull: %w", err) } + if err := gitrepo.Init(ctx); err != nil { + return fmt.Errorf("gitrepo.Init: %w", err) + } return nil } diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index 9f7ffb29c9f36..93199a4314bfe 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -405,14 +405,14 @@ func runSync(ctx context.Context, m *repo_model.Mirror) ([]*mirrorSyncResult, bo } log.Trace("SyncMirrors [repo: %-v]: invalidating mirror branch caches...", m.Repo) - branches, _, err := gitrepo.GetBranchesByPath(ctx, m.Repo, 0, 0) + branches, _, err := gitrepo.GetBranches(ctx, m.Repo, 0, 0) if err != nil { log.Error("SyncMirrors [repo: %-v]: failed to GetBranches: %v", m.Repo, err) return nil, false } for _, branch := range branches { - cache.Remove(m.Repo.GetCommitsCountCacheKey(branch.Name, true)) + cache.Remove(m.Repo.GetCommitsCountCacheKey(branch, true)) } m.UpdatedUnix = timeutil.TimeStampNow() diff --git a/services/pull/pull.go b/services/pull/pull.go index 154ff6c5c6851..73349c7dff698 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -674,14 +674,14 @@ func CloseBranchPulls(ctx context.Context, doer *user_model.User, repoID int64, // CloseRepoBranchesPulls close all pull requests which head branches are in the given repository, but only whose base repo is not in the given repository func CloseRepoBranchesPulls(ctx context.Context, doer *user_model.User, repo *repo_model.Repository) error { - branches, _, err := gitrepo.GetBranchesByPath(ctx, repo, 0, 0) + branches, _, err := gitrepo.GetBranches(ctx, repo, 0, 0) if err != nil { return err } var errs errlist for _, branch := range branches { - prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repo.ID, branch.Name) + prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, repo.ID, branch) if err != nil { return err } diff --git a/services/repository/branch.go b/services/repository/branch.go index f5cdb72a7bb08..4834b448d769e 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -254,7 +254,7 @@ func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *g // checkBranchName validates branch name with existing repository branches func checkBranchName(ctx context.Context, repo *repo_model.Repository, name string) error { - _, err := gitrepo.WalkReferences(ctx, repo, func(_, refName string) error { + _, err := gitrepo.WalkReferences(ctx, repo, git.ObjectBranch, func(_, refName string) error { branchRefName := strings.TrimPrefix(refName, git.BranchPrefix) switch { case branchRefName == name: diff --git a/tests/integration/migration-test/migration_test.go b/tests/integration/migration-test/migration_test.go index 40fcf95705887..b4641bd6488de 100644 --- a/tests/integration/migration-test/migration_test.go +++ b/tests/integration/migration-test/migration_test.go @@ -24,6 +24,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/testlogger" @@ -87,6 +88,7 @@ func initMigrationTest(t *testing.T) func() { } assert.NoError(t, git.InitFull(context.Background())) + assert.NoError(t, gitrepo.Init(context.Background())) setting.LoadDBSetting() setting.InitLoggersForTest() return deferFn diff --git a/tests/test_utils.go b/tests/test_utils.go index 6f9592b204112..c68df3bc33782 100644 --- a/tests/test_utils.go +++ b/tests/test_utils.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" @@ -83,6 +84,9 @@ func InitTest(requireGitea bool) { if err := git.InitFull(context.Background()); err != nil { log.Fatal("git.InitOnceWithSync: %v", err) } + if err := gitrepo.Init(context.Background()); err != nil { + log.Fatal("gitrepo.Init: %v", err) + } setting.LoadDBSetting() if err := storage.Init(); err != nil {