Skip to content

Move git config/remote to gitrepo package and add global lock to resolve possible conflict when updating repository git config file #35151

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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
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
10 changes: 0 additions & 10 deletions modules/git/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"net/url"
"strings"

giturl "code.gitea.io/gitea/modules/git/url"
"code.gitea.io/gitea/modules/util"
)

Expand All @@ -33,15 +32,6 @@ func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (string,
return result, nil
}

// GetRemoteURL returns the url of a specific remote of the repository.
func GetRemoteURL(ctx context.Context, repoPath, remoteName string) (*giturl.GitURL, error) {
addr, err := GetRemoteAddress(ctx, repoPath, remoteName)
if err != nil {
return nil, err
}
return giturl.ParseGitURL(addr)
}

// ErrInvalidCloneAddr represents a "InvalidCloneAddr" kind of error.
type ErrInvalidCloneAddr struct {
Host string
Expand Down
11 changes: 11 additions & 0 deletions modules/git/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ func (repo *Repository) GetAllCommitsCount() (int64, error) {
return AllCommitsCount(repo.Ctx, repo.Path, false)
}

func (repo *Repository) ShowPrettyFormatLogToList(ctx context.Context, revisionRange string) ([]*Commit, error) {
// avoid: ambiguous argument 'refs/a...refs/b': unknown revision or path not in the working tree. Use '--': 'git <command> [<revision>...] -- [<file>...]'
logs, _, err := NewCommand("log").AddArguments(prettyLogFormat).
AddDynamicArguments(revisionRange).AddArguments("--").
RunStdBytes(ctx, &RunOpts{Dir: repo.Path})
if err != nil {
return nil, err
}
return repo.parsePrettyFormatLogToList(logs)
}

func (repo *Repository) parsePrettyFormatLogToList(logs []byte) ([]*Commit, error) {
var commits []*Commit
if len(logs) == 0 {
Expand Down
6 changes: 0 additions & 6 deletions modules/git/repo_branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,6 @@ func (repo *Repository) AddRemote(name, url string, fetch bool) error {
return err
}

// RemoveRemote removes a remote from repository.
func (repo *Repository) RemoveRemote(name string) error {
_, _, err := NewCommand("remote", "rm").AddDynamicArguments(name).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path})
return err
}

// RenameBranch rename a branch
func (repo *Repository) RenameBranch(from, to string) error {
_, _, err := NewCommand("branch", "-m").AddDynamicArguments(from, to).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path})
Expand Down
89 changes: 0 additions & 89 deletions modules/git/repo_compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,8 @@ import (
"regexp"
"strconv"
"strings"
"time"

logger "code.gitea.io/gitea/modules/log"
)

// CompareInfo represents needed information for comparing references.
type CompareInfo struct {
MergeBase string
BaseCommitID string
HeadCommitID string
Commits []*Commit
NumFiles int
}

// GetMergeBase checks and returns merge base of two branches and the reference used as base.
func (repo *Repository) GetMergeBase(tmpRemote, base, head string) (string, string, error) {
if tmpRemote == "" {
Expand All @@ -49,83 +37,6 @@ func (repo *Repository) GetMergeBase(tmpRemote, base, head string) (string, stri
return strings.TrimSpace(stdout), base, err
}

// GetCompareInfo generates and returns compare information between base and head branches of repositories.
func (repo *Repository) GetCompareInfo(basePath, baseBranch, headBranch string, directComparison, fileOnly bool) (_ *CompareInfo, err error) {
var (
remoteBranch string
tmpRemote string
)

// We don't need a temporary remote for same repository.
if repo.Path != basePath {
// Add a temporary remote
tmpRemote = strconv.FormatInt(time.Now().UnixNano(), 10)
if err = repo.AddRemote(tmpRemote, basePath, false); err != nil {
return nil, fmt.Errorf("AddRemote: %w", err)
}
defer func() {
if err := repo.RemoveRemote(tmpRemote); err != nil {
logger.Error("GetPullRequestInfo: RemoveRemote: %v", err)
}
}()
}

compareInfo := new(CompareInfo)

compareInfo.HeadCommitID, err = GetFullCommitID(repo.Ctx, repo.Path, headBranch)
if err != nil {
compareInfo.HeadCommitID = headBranch
}

compareInfo.MergeBase, remoteBranch, err = repo.GetMergeBase(tmpRemote, baseBranch, headBranch)
if err == nil {
compareInfo.BaseCommitID, err = GetFullCommitID(repo.Ctx, repo.Path, remoteBranch)
if err != nil {
compareInfo.BaseCommitID = remoteBranch
}
separator := "..."
baseCommitID := compareInfo.MergeBase
if directComparison {
separator = ".."
baseCommitID = compareInfo.BaseCommitID
}

// We have a common base - therefore we know that ... should work
if !fileOnly {
// avoid: ambiguous argument 'refs/a...refs/b': unknown revision or path not in the working tree. Use '--': 'git <command> [<revision>...] -- [<file>...]'
var logs []byte
logs, _, err = NewCommand("log").AddArguments(prettyLogFormat).
AddDynamicArguments(baseCommitID+separator+headBranch).AddArguments("--").
RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path})
if err != nil {
return nil, err
}
compareInfo.Commits, err = repo.parsePrettyFormatLogToList(logs)
if err != nil {
return nil, fmt.Errorf("parsePrettyFormatLogToList: %w", err)
}
} else {
compareInfo.Commits = []*Commit{}
}
} else {
compareInfo.Commits = []*Commit{}
compareInfo.MergeBase, err = GetFullCommitID(repo.Ctx, repo.Path, remoteBranch)
if err != nil {
compareInfo.MergeBase = remoteBranch
}
compareInfo.BaseCommitID = compareInfo.MergeBase
}

// 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 = repo.GetDiffNumChangedFiles(remoteBranch, headBranch, directComparison)
if err != nil {
return nil, err
}
return compareInfo, nil
}

type lineCountWriter struct {
numLines int
}
Expand Down
155 changes: 155 additions & 0 deletions modules/gitrepo/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package gitrepo

import (
"context"
"errors"
"io"
"time"

"code.gitea.io/gitea/modules/git"
giturl "code.gitea.io/gitea/modules/git/url"
"code.gitea.io/gitea/modules/globallock"
)

func GitConfigGet(ctx context.Context, repo Repository, key string) (string, error) {
result, _, err := git.NewCommand("config", "--get").
AddDynamicArguments(key).
RunStdString(ctx, &git.RunOpts{Dir: repoPath(repo)})
if err != nil {
return "", err
}
if len(result) > 0 {
result = result[:len(result)-1] // remove trailing newline
}
return result, nil
}

func getRepoConfigLockKey(repoStoragePath string) string {
return "repo-config:" + repoStoragePath
}

// GitConfigAdd add a git configuration key to a specific value for the given repository.
func GitConfigAdd(ctx context.Context, repo Repository, key, value string) error {
releaser, err := globallock.Lock(ctx, getRepoConfigLockKey(repo.RelativePath()))
if err != nil {
return err
}
defer releaser()

_, _, err = git.NewCommand("config", "--add").
AddDynamicArguments(key, value).
RunStdString(ctx, &git.RunOpts{Dir: repoPath(repo)})
return err
}

// GitConfigSet updates a git configuration key to a specific value for the given repository.
// If the key does not exist, it will be created.
// If the key exists, it will be updated to the new value.
func GitConfigSet(ctx context.Context, repo Repository, key, value string) error {
releaser, err := globallock.Lock(ctx, getRepoConfigLockKey(repo.RelativePath()))
if err != nil {
return err
}
defer releaser()

_, _, err = git.NewCommand("config").
AddDynamicArguments(key, value).
RunStdString(ctx, &git.RunOpts{Dir: repoPath(repo)})
return err
}

type RemoteOption string

const (
RemoteOptionMirrorPush RemoteOption = "--mirror=push"
RemoteOptionMirrorFetch RemoteOption = "--mirror=fetch"
)

func GitRemoteAdd(ctx context.Context, repo Repository, remoteName, remoteURL string, options ...RemoteOption) error {
releaser, err := globallock.Lock(ctx, getRepoConfigLockKey(repo.RelativePath()))
if err != nil {
return err
}
defer releaser()

cmd := git.NewCommand("remote", "add")
if len(options) > 0 {
switch options[0] {
case RemoteOptionMirrorPush:
cmd.AddArguments("--mirror=push")
case RemoteOptionMirrorFetch:
cmd.AddArguments("--mirror=fetch")
default:
return errors.New("unknown remote option: " + string(options[0]))
}
}
_, _, err = cmd.
AddDynamicArguments(remoteName, remoteURL).
RunStdString(ctx, &git.RunOpts{Dir: repoPath(repo)})
return err
}

func GitRemoteRemove(ctx context.Context, repo Repository, remoteName string) error {
releaser, err := globallock.Lock(ctx, getRepoConfigLockKey(repo.RelativePath()))
if err != nil {
return err
}
defer releaser()

cmd := git.NewCommand("remote", "rm").AddDynamicArguments(remoteName)
_, _, err = cmd.RunStdString(ctx, &git.RunOpts{Dir: repoPath(repo)})
return err
}

// GitRemoteGetURL returns the url of a specific remote of the repository.
func GitRemoteGetURL(ctx context.Context, repo Repository, remoteName string) (*giturl.GitURL, error) {
addr, err := git.GetRemoteAddress(ctx, repoPath(repo), remoteName)
if err != nil {
return nil, err
}
if addr == "" {
return nil, nil
}
return giturl.ParseGitURL(addr)
}

func SetRemoteURL(ctx context.Context, repo Repository, remoteName, remoteURL string) error {
releaser, err := globallock.Lock(ctx, getRepoConfigLockKey(repo.RelativePath()))
if err != nil {
return err
}
defer releaser()

cmd := git.NewCommand("remote", "set-url").AddDynamicArguments(remoteName, remoteURL)
_, _, err = cmd.RunStdString(ctx, &git.RunOpts{Dir: repoPath(repo)})
return err
}

// GitRemotePrune prunes the remote branches that no longer exist in the remote repository.
// No lock is needed because the remote remoteName will be checked before invoking this function.
// Then it will not update the remote automatically if the remote does not exist.
func GitRemotePrune(ctx context.Context, repo Repository, remoteName string, timeout time.Duration, stdout, stderr io.Writer) error {
return git.NewCommand("remote", "prune").AddDynamicArguments(remoteName).
Run(ctx, &git.RunOpts{
Timeout: timeout,
Dir: repoPath(repo),
Stdout: stdout,
Stderr: stderr,
})
}

// GitRemoteUpdatePrune updates the remote branches and prunes the ones that no longer exist in the remote repository.
// No lock is needed because the remote remoteName will be checked before invoking this function.
// Then it will not update the remote automatically if the remote does not exist.
func GitRemoteUpdatePrune(ctx context.Context, repo Repository, remoteName string, timeout time.Duration, stdout, stderr io.Writer) error {
return git.NewCommand("remote", "update", "--prune").AddDynamicArguments(remoteName).
Run(ctx, &git.RunOpts{
Timeout: timeout,
Dir: repoPath(repo),
Stdout: stdout,
Stderr: stderr,
})
}
11 changes: 4 additions & 7 deletions modules/templates/util_misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ import (

activities_model "code.gitea.io/gitea/models/activities"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
giturl "code.gitea.io/gitea/modules/git/url"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/repository"
Expand Down Expand Up @@ -145,15 +144,13 @@ type remoteAddress struct {

func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteName string) remoteAddress {
ret := remoteAddress{}
remoteURL, err := git.GetRemoteAddress(ctx, m.RepoPath(), remoteName)
u, err := gitrepo.GitRemoteGetURL(ctx, m, remoteName)
if err != nil {
log.Error("GetRemoteURL %v", err)
return ret
}

u, err := giturl.ParseGitURL(remoteURL)
if err != nil {
log.Error("giturl.Parse %v", err)
if u == nil {
log.Error("GetRemoteURL %s returned nil", remoteName)
return ret
}

Expand Down
16 changes: 8 additions & 8 deletions routers/api/v1/repo/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -1085,7 +1085,7 @@ func MergePullRequest(ctx *context.APIContext) {
type parseCompareInfoResult struct {
headRepo *repo_model.Repository
headGitRepo *git.Repository
compareInfo *git.CompareInfo
compareInfo *pull_service.CompareInfo
baseRef git.RefName
headRef git.RefName
}
Expand Down Expand Up @@ -1201,7 +1201,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
return nil, nil
}

compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseRef.ShortName(), headRef.ShortName(), false, false)
compareInfo, err := pull_service.GetCompareInfo(ctx, baseRepo, headRepo, headGitRepo, baseRef.ShortName(), headRef.ShortName(), false, false)
if err != nil {
ctx.APIErrorInternal(err)
return nil, nil
Expand Down Expand Up @@ -1452,7 +1452,7 @@ func GetPullRequestCommits(ctx *context.APIContext) {
return
}

var prInfo *git.CompareInfo
var prInfo *pull_service.CompareInfo
baseGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pr.BaseRepo)
if err != nil {
ctx.APIErrorInternal(err)
Expand All @@ -1461,9 +1461,9 @@ func GetPullRequestCommits(ctx *context.APIContext) {
defer closer.Close()

if pr.HasMerged {
prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.MergeBase, pr.GetGitHeadRefName(), false, false)
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.MergeBase, pr.GetGitHeadRefName(), false, false)
} else {
prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitHeadRefName(), false, false)
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.BaseBranch, pr.GetGitHeadRefName(), false, false)
}
if err != nil {
ctx.APIErrorInternal(err)
Expand Down Expand Up @@ -1582,11 +1582,11 @@ func GetPullRequestFiles(ctx *context.APIContext) {

baseGitRepo := ctx.Repo.GitRepo

var prInfo *git.CompareInfo
var prInfo *pull_service.CompareInfo
if pr.HasMerged {
prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.MergeBase, pr.GetGitHeadRefName(), true, false)
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.MergeBase, pr.GetGitHeadRefName(), true, false)
} else {
prInfo, err = baseGitRepo.GetCompareInfo(pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitHeadRefName(), true, false)
prInfo, err = pull_service.GetCompareInfo(ctx, pr.BaseRepo, pr.BaseRepo, baseGitRepo, pr.BaseBranch, pr.GetGitHeadRefName(), true, false)
}
if err != nil {
ctx.APIErrorInternal(err)
Expand Down
Loading