Skip to content

Introduce git service interface and refactor gitrepo module #31971

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions models/migrations/base/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()

Expand Down
4 changes: 4 additions & 0 deletions models/unittest/testdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
44 changes: 27 additions & 17 deletions modules/gitrepo/branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
88 changes: 88 additions & 0 deletions modules/gitrepo/command.go
Original file line number Diff line number Diff line change
@@ -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
}
21 changes: 10 additions & 11 deletions modules/gitrepo/gitrepo.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,27 @@ import (
"strings"

"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
)

type Repository interface {
GetName() string
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.
Expand All @@ -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
}
}
Expand Down
19 changes: 19 additions & 0 deletions modules/gitrepo/init.go
Original file line number Diff line number Diff line change
@@ -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
}
19 changes: 19 additions & 0 deletions modules/gitrepo/repository.go
Original file line number Diff line number Diff line change
@@ -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))
}
57 changes: 57 additions & 0 deletions modules/gitrepo/service.go
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 1 addition & 1 deletion modules/gitrepo/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
package gitrepo

func RepoGitURL(repo Repository) string {
return repoPath(repo)
return curService.RepoGitURL(repo)
}
6 changes: 2 additions & 4 deletions modules/gitrepo/walk_nogogit.go → modules/gitrepo/walk.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

//go:build !gogit

package gitrepo

import (
Expand All @@ -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)
}
Loading
Loading