Refactor git module, make Gitea use internal git config (#19732)

* Refactor git module, make Gitea use internal git config, add safe.directory config

* introduce git.InitSimple and git.InitWithConfigSync, make serv cmd use gitconfig

* use HOME instead of GIT_CONFIG_GLOBAL, because git always needs a correct HOME

* fix cmd env in cmd/serv.go

* fine tune error message

* Fix a incorrect test case

* fix configAddNonExist

* fix configAddNonExist logic, add `--fixed-value` flag, add tests

* add configSetNonExist function in case it's needed.

* use configSetNonExist for `user.name` and `user.email`

* add some comments

* Update cmd/serv.go

Co-authored-by: zeripath <art27@cantab.net>

* Update cmd/serv.go

Co-authored-by: zeripath <art27@cantab.net>

* Update modules/git/git.go

Co-authored-by: zeripath <art27@cantab.net>

* Update modules/setting/setting.go

Co-authored-by: zeripath <art27@cantab.net>

* Update modules/git/repo_attribute.go

Co-authored-by: zeripath <art27@cantab.net>

* fix spaces in messages

* use `configSet("core.protectNTFS", ...)` instead of `globalCommandArgs`

* remove GIT_CONFIG_NOSYSTEM, continue to use system's git config

* Update cmd/serv.go

Co-authored-by: zeripath <art27@cantab.net>

* fix merge

* remove code for safe.directory

* separate git.CommonEnvs to CommonGitCmdEnvs and CommonCmdServEnvs

* avoid Golang's data race error

Co-authored-by: zeripath <art27@cantab.net>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
wxiaoguang 2022-06-10 09:57:49 +08:00 committed by GitHub
parent e26f84a9b7
commit a0051634b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 286 additions and 193 deletions

View file

@ -6,6 +6,7 @@
package cmd package cmd
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
@ -65,6 +66,21 @@ func setup(logPath string, debug bool) {
if debug { if debug {
setting.RunMode = "dev" setting.RunMode = "dev"
} }
// Check if setting.RepoRootPath exists. It could be the case that it doesn't exist, this can happen when
// `[repository]` `ROOT` is a relative path and $GITEA_WORK_DIR isn't passed to the SSH connection.
if _, err := os.Stat(setting.RepoRootPath); err != nil {
if os.IsNotExist(err) {
_ = fail("Incorrect configuration, no repository directory.", "Directory `[repository].ROOT` %q was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository].ROOT` an absolute value.", setting.RepoRootPath)
} else {
_ = fail("Incorrect configuration, repository directory is inaccessible", "Directory `[repository].ROOT` %q is inaccessible. err: %v", setting.RepoRootPath, err)
}
return
}
if err := git.InitSimple(context.Background()); err != nil {
_ = fail("Failed to init git", "Failed to init git, err: %v", err)
}
} }
var ( var (
@ -80,12 +96,12 @@ var (
func fail(userMessage, logMessage string, args ...interface{}) error { func fail(userMessage, logMessage string, args ...interface{}) error {
// There appears to be a chance to cause a zombie process and failure to read the Exit status // There appears to be a chance to cause a zombie process and failure to read the Exit status
// if nothing is outputted on stdout. // if nothing is outputted on stdout.
fmt.Fprintln(os.Stdout, "") _, _ = fmt.Fprintln(os.Stdout, "")
fmt.Fprintln(os.Stderr, "Gitea:", userMessage) _, _ = fmt.Fprintln(os.Stderr, "Gitea:", userMessage)
if len(logMessage) > 0 { if len(logMessage) > 0 {
if !setting.IsProd { if !setting.IsProd {
fmt.Fprintf(os.Stderr, logMessage+"\n", args...) _, _ = fmt.Fprintf(os.Stderr, logMessage+"\n", args...)
} }
} }
ctx, cancel := installSignals() ctx, cancel := installSignals()
@ -237,17 +253,6 @@ func runServ(c *cli.Context) error {
} }
return fail("Internal Server Error", "%s", err.Error()) return fail("Internal Server Error", "%s", err.Error())
} }
os.Setenv(repo_module.EnvRepoIsWiki, strconv.FormatBool(results.IsWiki))
os.Setenv(repo_module.EnvRepoName, results.RepoName)
os.Setenv(repo_module.EnvRepoUsername, results.OwnerName)
os.Setenv(repo_module.EnvPusherName, results.UserName)
os.Setenv(repo_module.EnvPusherEmail, results.UserEmail)
os.Setenv(repo_module.EnvPusherID, strconv.FormatInt(results.UserID, 10))
os.Setenv(repo_module.EnvRepoID, strconv.FormatInt(results.RepoID, 10))
os.Setenv(repo_module.EnvPRID, fmt.Sprintf("%d", 0))
os.Setenv(repo_module.EnvDeployKeyID, fmt.Sprintf("%d", results.DeployKeyID))
os.Setenv(repo_module.EnvKeyID, fmt.Sprintf("%d", results.KeyID))
os.Setenv(repo_module.EnvAppURL, setting.AppURL)
// LFS token authentication // LFS token authentication
if verb == lfsAuthenticateVerb { if verb == lfsAuthenticateVerb {
@ -298,20 +303,29 @@ func runServ(c *cli.Context) error {
gitcmd = exec.CommandContext(ctx, verb, repoPath) gitcmd = exec.CommandContext(ctx, verb, repoPath)
} }
// Check if setting.RepoRootPath exists. It could be the case that it doesn't exist, this can happen when
// `[repository]` `ROOT` is a relative path and $GITEA_WORK_DIR isn't passed to the SSH connection.
if _, err := os.Stat(setting.RepoRootPath); err != nil {
if os.IsNotExist(err) {
return fail("Incorrect configuration.",
"Directory `[repository]` `ROOT` %s was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository]` `ROOT` an absolute value.", setting.RepoRootPath)
}
}
process.SetSysProcAttribute(gitcmd) process.SetSysProcAttribute(gitcmd)
gitcmd.Dir = setting.RepoRootPath gitcmd.Dir = setting.RepoRootPath
gitcmd.Stdout = os.Stdout gitcmd.Stdout = os.Stdout
gitcmd.Stdin = os.Stdin gitcmd.Stdin = os.Stdin
gitcmd.Stderr = os.Stderr gitcmd.Stderr = os.Stderr
gitcmd.Env = append(gitcmd.Env, os.Environ()...)
gitcmd.Env = append(gitcmd.Env,
repo_module.EnvRepoIsWiki+"="+strconv.FormatBool(results.IsWiki),
repo_module.EnvRepoName+"="+results.RepoName,
repo_module.EnvRepoUsername+"="+results.OwnerName,
repo_module.EnvPusherName+"="+results.UserName,
repo_module.EnvPusherEmail+"="+results.UserEmail,
repo_module.EnvPusherID+"="+strconv.FormatInt(results.UserID, 10),
repo_module.EnvRepoID+"="+strconv.FormatInt(results.RepoID, 10),
repo_module.EnvPRID+"="+fmt.Sprintf("%d", 0),
repo_module.EnvDeployKeyID+"="+fmt.Sprintf("%d", results.DeployKeyID),
repo_module.EnvKeyID+"="+fmt.Sprintf("%d", results.KeyID),
repo_module.EnvAppURL+"="+setting.AppURL,
)
// to avoid breaking, here only use the minimal environment variables for the "gitea serv" command.
// it could be re-considered whether to use the same git.CommonGitCmdEnvs() as "git" command later.
gitcmd.Env = append(gitcmd.Env, git.CommonCmdServEnvs()...)
if err = gitcmd.Run(); err != nil { if err = gitcmd.Run(); err != nil {
return fail("Internal error", "Failed to execute git command: %v", err) return fail("Internal error", "Failed to execute git command: %v", err)
} }

View file

@ -274,8 +274,8 @@ func prepareTestEnv(t testing.TB, skip ...int) func() {
deferFn := PrintCurrentTest(t, ourSkip) deferFn := PrintCurrentTest(t, ourSkip)
assert.NoError(t, unittest.LoadFixtures()) assert.NoError(t, unittest.LoadFixtures())
assert.NoError(t, util.RemoveAll(setting.RepoRootPath)) assert.NoError(t, util.RemoveAll(setting.RepoRootPath))
assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), setting.RepoRootPath)) assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), setting.RepoRootPath))
assert.NoError(t, git.InitWithConfigSync(context.Background()))
ownerDirs, err := os.ReadDir(setting.RepoRootPath) ownerDirs, err := os.ReadDir(setting.RepoRootPath)
if err != nil { if err != nil {
assert.NoError(t, err, "unable to read the new repo root: %v\n", err) assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
@ -576,6 +576,7 @@ func resetFixtures(t *testing.T) {
assert.NoError(t, unittest.LoadFixtures()) assert.NoError(t, unittest.LoadFixtures())
assert.NoError(t, util.RemoveAll(setting.RepoRootPath)) assert.NoError(t, util.RemoveAll(setting.RepoRootPath))
assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), setting.RepoRootPath)) assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), setting.RepoRootPath))
assert.NoError(t, git.InitWithConfigSync(context.Background()))
ownerDirs, err := os.ReadDir(setting.RepoRootPath) ownerDirs, err := os.ReadDir(setting.RepoRootPath)
if err != nil { if err != nil {
assert.NoError(t, err, "unable to read the new repo root: %v\n", err) assert.NoError(t, err, "unable to read the new repo root: %v\n", err)

View file

@ -62,6 +62,7 @@ func initMigrationTest(t *testing.T) func() {
assert.True(t, len(setting.RepoRootPath) != 0) assert.True(t, len(setting.RepoRootPath) != 0)
assert.NoError(t, util.RemoveAll(setting.RepoRootPath)) assert.NoError(t, util.RemoveAll(setting.RepoRootPath))
assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), setting.RepoRootPath)) assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), setting.RepoRootPath))
assert.NoError(t, git.InitWithConfigSync(context.Background()))
ownerDirs, err := os.ReadDir(setting.RepoRootPath) ownerDirs, err := os.ReadDir(setting.RepoRootPath)
if err != nil { if err != nil {
assert.NoError(t, err, "unable to read the new repo root: %v\n", err) assert.NoError(t, err, "unable to read the new repo root: %v\n", err)

View file

@ -202,9 +202,8 @@ func prepareTestEnv(t *testing.T, skip int, syncModels ...interface{}) (*xorm.En
ourSkip += skip ourSkip += skip
deferFn := PrintCurrentTest(t, ourSkip) deferFn := PrintCurrentTest(t, ourSkip)
assert.NoError(t, os.RemoveAll(setting.RepoRootPath)) assert.NoError(t, os.RemoveAll(setting.RepoRootPath))
assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), setting.RepoRootPath))
assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), assert.NoError(t, git.InitWithConfigSync(context.Background()))
setting.RepoRootPath))
ownerDirs, err := os.ReadDir(setting.RepoRootPath) ownerDirs, err := os.ReadDir(setting.RepoRootPath)
if err != nil { if err != nil {
assert.NoError(t, err, "unable to read the new repo root: %v\n", err) assert.NoError(t, err, "unable to read the new repo root: %v\n", err)

View file

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -116,6 +117,9 @@ func MainTest(m *testing.M, testOpts *TestOptions) {
if err = CopyDir(filepath.Join(testOpts.GiteaRootPath, "integrations", "gitea-repositories-meta"), setting.RepoRootPath); err != nil { if err = CopyDir(filepath.Join(testOpts.GiteaRootPath, "integrations", "gitea-repositories-meta"), setting.RepoRootPath); err != nil {
fatalTestError("util.CopyDir: %v\n", err) fatalTestError("util.CopyDir: %v\n", err)
} }
if err = git.InitWithConfigSync(context.Background()); err != nil {
fatalTestError("git.Init: %v\n", err)
}
ownerDirs, err := os.ReadDir(setting.RepoRootPath) ownerDirs, err := os.ReadDir(setting.RepoRootPath)
if err != nil { if err != nil {
@ -198,6 +202,7 @@ func PrepareTestEnv(t testing.TB) {
assert.NoError(t, util.RemoveAll(setting.RepoRootPath)) assert.NoError(t, util.RemoveAll(setting.RepoRootPath))
metaPath := filepath.Join(giteaRoot, "integrations", "gitea-repositories-meta") metaPath := filepath.Join(giteaRoot, "integrations", "gitea-repositories-meta")
assert.NoError(t, CopyDir(metaPath, setting.RepoRootPath)) assert.NoError(t, CopyDir(metaPath, setting.RepoRootPath))
assert.NoError(t, git.InitWithConfigSync(context.Background()))
ownerDirs, err := os.ReadDir(setting.RepoRootPath) ownerDirs, err := os.ReadDir(setting.RepoRootPath)
assert.NoError(t, err) assert.NoError(t, err)

View file

@ -8,6 +8,7 @@ package git
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -104,6 +105,25 @@ type RunOpts struct {
PipelineFunc func(context.Context, context.CancelFunc) error PipelineFunc func(context.Context, context.CancelFunc) error
} }
// CommonGitCmdEnvs returns the common environment variables for a "git" command.
func CommonGitCmdEnvs() []string {
// at the moment, do not set "GIT_CONFIG_NOSYSTEM", users may have put some configs like "receive.certNonceSeed" in it
return []string{
fmt.Sprintf("LC_ALL=%s", DefaultLocale),
"GIT_TERMINAL_PROMPT=0", // avoid prompting for credentials interactively, supported since git v2.3
"GIT_NO_REPLACE_OBJECTS=1", // ignore replace references (https://git-scm.com/docs/git-replace)
"HOME=" + HomeDir(), // make Gitea use internal git config only, to prevent conflicts with user's git config
}
}
// CommonCmdServEnvs is like CommonGitCmdEnvs but it only returns minimal required environment variables for the "gitea serv" command
func CommonCmdServEnvs() []string {
return []string{
"GIT_NO_REPLACE_OBJECTS=1", // ignore replace references (https://git-scm.com/docs/git-replace)
"HOME=" + HomeDir(), // make Gitea use internal git config only, to prevent conflicts with user's git config
}
}
// Run runs the command with the RunOpts // Run runs the command with the RunOpts
func (c *Command) Run(opts *RunOpts) error { func (c *Command) Run(opts *RunOpts) error {
if opts == nil { if opts == nil {
@ -148,16 +168,8 @@ func (c *Command) Run(opts *RunOpts) error {
cmd.Env = opts.Env cmd.Env = opts.Env
} }
cmd.Env = append(
cmd.Env,
fmt.Sprintf("LC_ALL=%s", DefaultLocale),
// avoid prompting for credentials interactively, supported since git v2.3
"GIT_TERMINAL_PROMPT=0",
// ignore replace references (https://git-scm.com/docs/git-replace)
"GIT_NO_REPLACE_OBJECTS=1",
)
process.SetSysProcAttribute(cmd) process.SetSysProcAttribute(cmd)
cmd.Env = append(cmd.Env, CommonGitCmdEnvs()...)
cmd.Dir = opts.Dir cmd.Dir = opts.Dir
cmd.Stdout = opts.Stdout cmd.Stdout = opts.Stdout
cmd.Stderr = opts.Stderr cmd.Stderr = opts.Stderr
@ -184,7 +196,9 @@ func (c *Command) Run(opts *RunOpts) error {
type RunStdError interface { type RunStdError interface {
error error
Unwrap() error
Stderr() string Stderr() string
IsExitCode(code int) bool
} }
type runStdError struct { type runStdError struct {
@ -209,6 +223,14 @@ func (r *runStdError) Stderr() string {
return r.stderr return r.stderr
} }
func (r *runStdError) IsExitCode(code int) bool {
var exitError *exec.ExitError
if errors.As(r.err, &exitError) {
return exitError.ExitCode() == code
}
return false
}
func bytesToString(b []byte) string { func bytesToString(b []byte) string {
return *(*string)(unsafe.Pointer(&b)) // that's what Golang's strings.Builder.String() does (go/src/strings/builder.go) return *(*string)(unsafe.Pointer(&b)) // that's what Golang's strings.Builder.String() does (go/src/strings/builder.go)
} }

View file

@ -398,11 +398,6 @@ func (c *Commit) GetSubModule(entryname string) (*SubModule, error) {
// GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only') // GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only')
func (c *Commit) GetBranchName() (string, error) { func (c *Commit) GetBranchName() (string, error) {
err := LoadGitVersion()
if err != nil {
return "", fmt.Errorf("Git version missing: %v", err)
}
args := []string{ args := []string{
"name-rev", "name-rev",
} }

View file

@ -10,11 +10,13 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"runtime" "runtime"
"strings" "strings"
"sync"
"time" "time"
"code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"github.com/hashicorp/go-version" "github.com/hashicorp/go-version"
@ -32,38 +34,33 @@ var (
GitExecutable = "git" GitExecutable = "git"
// DefaultContext is the default context to run git commands in // DefaultContext is the default context to run git commands in
// will be overwritten by Init with HammerContext // will be overwritten by InitWithConfigSync with HammerContext
DefaultContext = context.Background() DefaultContext = context.Background()
gitVersion *version.Version
// SupportProcReceive version >= 2.29.0 // SupportProcReceive version >= 2.29.0
SupportProcReceive bool SupportProcReceive bool
// initMutex is used to avoid Golang's data race error. see the comments below.
initMutex sync.Mutex
gitVersion *version.Version
) )
// LocalVersion returns current Git version from shell. // loadGitVersion returns current Git version from shell. Internal usage only.
func LocalVersion() (*version.Version, error) { func loadGitVersion() (*version.Version, error) {
if err := LoadGitVersion(); err != nil {
return nil, err
}
return gitVersion, nil
}
// LoadGitVersion returns current Git version from shell.
func LoadGitVersion() error {
// doesn't need RWMutex because its exec by Init() // doesn't need RWMutex because its exec by Init()
if gitVersion != nil { if gitVersion != nil {
return nil return gitVersion, nil
} }
stdout, _, runErr := NewCommand(context.Background(), "version").RunStdString(nil) stdout, _, runErr := NewCommand(DefaultContext, "version").RunStdString(nil)
if runErr != nil { if runErr != nil {
return runErr return nil, runErr
} }
fields := strings.Fields(stdout) fields := strings.Fields(stdout)
if len(fields) < 3 { if len(fields) < 3 {
return fmt.Errorf("not enough output: %s", stdout) return nil, fmt.Errorf("invalid git version output: %s", stdout)
} }
var versionString string var versionString string
@ -78,7 +75,7 @@ func LoadGitVersion() error {
var err error var err error
gitVersion, err = version.NewVersion(versionString) gitVersion, err = version.NewVersion(versionString)
return err return gitVersion, err
} }
// SetExecutablePath changes the path of git executable and checks the file permission and version. // SetExecutablePath changes the path of git executable and checks the file permission and version.
@ -93,7 +90,7 @@ func SetExecutablePath(path string) error {
} }
GitExecutable = absPath GitExecutable = absPath
err = LoadGitVersion() _, err = loadGitVersion()
if err != nil { if err != nil {
return fmt.Errorf("unable to load git version: %w", err) return fmt.Errorf("unable to load git version: %w", err)
} }
@ -120,7 +117,10 @@ func SetExecutablePath(path string) error {
// VersionInfo returns git version information // VersionInfo returns git version information
func VersionInfo() string { func VersionInfo() string {
format := "Git Version: %s" if gitVersion == nil {
return "(git not found)"
}
format := "%s"
args := []interface{}{gitVersion.Original()} args := []interface{}{gitVersion.Original()}
// Since git wire protocol has been released from git v2.18 // Since git wire protocol has been released from git v2.18
if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil { if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil {
@ -131,8 +131,33 @@ func VersionInfo() string {
return fmt.Sprintf(format, args...) return fmt.Sprintf(format, args...)
} }
// Init initializes git module // InitSimple initializes git module with a very simple step, no config changes, no global command arguments.
func Init(ctx context.Context) error { // This method doesn't change anything to filesystem
func InitSimple(ctx context.Context) error {
initMutex.Lock()
defer initMutex.Unlock()
return initSimpleInternal(ctx)
}
// HomeDir is the home dir for git to store the global config file used by Gitea internally
func HomeDir() string {
if setting.RepoRootPath == "" {
// TODO: now, some unit test code call the git module directly without initialization, which is incorrect.
// at the moment, we just use a temp HomeDir to prevent from conflicting with user's git config
// in the future, the git module should be initialized first before use.
tmpHomeDir := filepath.Join(os.TempDir(), "gitea-temp-home")
log.Error("Git's HomeDir is empty (RepoRootPath is empty), the git module is not initialized correctly, using a temp HomeDir (%s) temporarily", tmpHomeDir)
return tmpHomeDir
}
return setting.RepoRootPath
}
func initSimpleInternal(ctx context.Context) error {
// at the moment, when running integration tests, the git.InitXxx would be called twice.
// one is called by the GlobalInitInstalled, one is called by TestMain.
// so the init functions should be protected by a mutex to avoid Golang's data race error.
DefaultContext = ctx DefaultContext = ctx
if setting.Git.Timeout.Default > 0 { if setting.Git.Timeout.Default > 0 {
@ -146,6 +171,23 @@ func Init(ctx context.Context) error {
// force cleanup args // force cleanup args
globalCommandArgs = []string{} globalCommandArgs = []string{}
return nil
}
// InitWithConfigSync initializes git module. This method may create directories or write files into filesystem
func InitWithConfigSync(ctx context.Context) error {
initMutex.Lock()
defer initMutex.Unlock()
err := initSimpleInternal(ctx)
if err != nil {
return err
}
if err = os.MkdirAll(setting.RepoRootPath, os.ModePerm); err != nil {
return fmt.Errorf("unable to create directory %s, err: %w", setting.RepoRootPath, err)
}
if CheckGitVersionAtLeast("2.9") == nil { if CheckGitVersionAtLeast("2.9") == nil {
// Explicitly disable credential helper, otherwise Git credentials might leak // Explicitly disable credential helper, otherwise Git credentials might leak
globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=") globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=")
@ -161,68 +203,71 @@ func Init(ctx context.Context) error {
globalCommandArgs = append(globalCommandArgs, "-c", "uploadpack.allowfilter=true", "-c", "uploadpack.allowAnySHA1InWant=true") globalCommandArgs = append(globalCommandArgs, "-c", "uploadpack.allowfilter=true", "-c", "uploadpack.allowAnySHA1InWant=true")
} }
// Save current git version on init to gitVersion otherwise it would require an RWMutex // Git requires setting user.name and user.email in order to commit changes - old comment: "if they're not set just add some defaults"
if err := LoadGitVersion(); err != nil { // TODO: need to confirm whether users really need to change these values manually. It seems that these values are dummy only and not really used.
return err // If these values are not really used, then they can be set (overwritten) directly without considering about existence.
} for configKey, defaultValue := range map[string]string{
"user.name": "Gitea",
// Git requires setting user.name and user.email in order to commit changes - if they're not set just add some defaults "user.email": "gitea@fake.local",
for configKey, defaultValue := range map[string]string{"user.name": "Gitea", "user.email": "gitea@fake.local"} { } {
if err := checkAndSetConfig(configKey, defaultValue, false); err != nil { if err := configSetNonExist(configKey, defaultValue); err != nil {
return err return err
} }
} }
// Set git some configurations - these must be set to these values for gitea to work correctly // Set git some configurations - these must be set to these values for gitea to work correctly
if err := checkAndSetConfig("core.quotePath", "false", true); err != nil { if err := configSet("core.quotePath", "false"); err != nil {
return err return err
} }
if CheckGitVersionAtLeast("2.10") == nil { if CheckGitVersionAtLeast("2.10") == nil {
if err := checkAndSetConfig("receive.advertisePushOptions", "true", true); err != nil { if err := configSet("receive.advertisePushOptions", "true"); err != nil {
return err return err
} }
} }
if CheckGitVersionAtLeast("2.18") == nil { if CheckGitVersionAtLeast("2.18") == nil {
if err := checkAndSetConfig("core.commitGraph", "true", true); err != nil { if err := configSet("core.commitGraph", "true"); err != nil {
return err return err
} }
if err := checkAndSetConfig("gc.writeCommitGraph", "true", true); err != nil { if err := configSet("gc.writeCommitGraph", "true"); err != nil {
return err return err
} }
} }
if CheckGitVersionAtLeast("2.29") == nil { if CheckGitVersionAtLeast("2.29") == nil {
// set support for AGit flow // set support for AGit flow
if err := checkAndAddConfig("receive.procReceiveRefs", "refs/for"); err != nil { if err := configAddNonExist("receive.procReceiveRefs", "refs/for"); err != nil {
return err return err
} }
SupportProcReceive = true SupportProcReceive = true
} else { } else {
if err := checkAndRemoveConfig("receive.procReceiveRefs", "refs/for"); err != nil { if err := configUnsetAll("receive.procReceiveRefs", "refs/for"); err != nil {
return err return err
} }
SupportProcReceive = false SupportProcReceive = false
} }
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
if err := checkAndSetConfig("core.longpaths", "true", true); err != nil { if err := configSet("core.longpaths", "true"); err != nil {
return err
}
if setting.Git.DisableCoreProtectNTFS {
err = configSet("core.protectNTFS", "false")
} else {
err = configUnsetAll("core.protectNTFS", "false")
}
if err != nil {
return err return err
} }
} }
if setting.Git.DisableCoreProtectNTFS {
if err := checkAndSetConfig("core.protectntfs", "false", true); err != nil {
return err
}
globalCommandArgs = append(globalCommandArgs, "-c", "core.protectntfs=false")
}
return nil return nil
} }
// CheckGitVersionAtLeast check git version is at least the constraint version // CheckGitVersionAtLeast check git version is at least the constraint version
func CheckGitVersionAtLeast(atLeast string) error { func CheckGitVersionAtLeast(atLeast string) error {
if err := LoadGitVersion(); err != nil { if _, err := loadGitVersion(); err != nil {
return err return err
} }
atLeastVersion, err := version.NewVersion(atLeast) atLeastVersion, err := version.NewVersion(atLeast)
@ -235,75 +280,75 @@ func CheckGitVersionAtLeast(atLeast string) error {
return nil return nil
} }
func checkAndSetConfig(key, defaultValue string, forceToDefault bool) error { func configSet(key, value string) error {
stdout, stderr, err := process.GetManager().Exec("git.Init(get setting)", GitExecutable, "config", "--get", key) stdout, _, err := NewCommand(DefaultContext, "config", "--get", key).RunStdString(nil)
if err != nil { if err != nil && !err.IsExitCode(1) {
perr, ok := err.(*process.Error) return fmt.Errorf("failed to get git config %s, err: %w", key, err)
if !ok {
return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr)
}
eerr, ok := perr.Err.(*exec.ExitError)
if !ok || eerr.ExitCode() != 1 {
return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr)
}
} }
currValue := strings.TrimSpace(stdout) currValue := strings.TrimSpace(stdout)
if currValue == value {
if currValue == defaultValue || (!forceToDefault && len(currValue) > 0) {
return nil return nil
} }
if _, stderr, err = process.GetManager().Exec(fmt.Sprintf("git.Init(set %s)", key), "git", "config", "--global", key, defaultValue); err != nil { _, _, err = NewCommand(DefaultContext, "config", "--global", key, value).RunStdString(nil)
return fmt.Errorf("Failed to set git %s(%s): %s", key, err, stderr) if err != nil {
return fmt.Errorf("failed to set git global config %s, err: %w", key, err)
} }
return nil return nil
} }
func checkAndAddConfig(key, value string) error { func configSetNonExist(key, value string) error {
_, stderr, err := process.GetManager().Exec("git.Init(get setting)", GitExecutable, "config", "--get", key, value) _, _, err := NewCommand(DefaultContext, "config", "--get", key).RunStdString(nil)
if err != nil { if err == nil {
perr, ok := err.(*process.Error) // already exist
if !ok { return nil
return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr) }
} if err.IsExitCode(1) {
eerr, ok := perr.Err.(*exec.ExitError) // not exist, set new config
if !ok || eerr.ExitCode() != 1 { _, _, err = NewCommand(DefaultContext, "config", "--global", key, value).RunStdString(nil)
return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr) if err != nil {
} return fmt.Errorf("failed to set git global config %s, err: %w", key, err)
if eerr.ExitCode() == 1 {
if _, stderr, err = process.GetManager().Exec(fmt.Sprintf("git.Init(set %s)", key), "git", "config", "--global", "--add", key, value); err != nil {
return fmt.Errorf("Failed to set git %s(%s): %s", key, err, stderr)
}
return nil
} }
return nil
} }
return nil return fmt.Errorf("failed to get git config %s, err: %w", key, err)
} }
func checkAndRemoveConfig(key, value string) error { func configAddNonExist(key, value string) error {
_, stderr, err := process.GetManager().Exec("git.Init(get setting)", GitExecutable, "config", "--get", key, value) _, _, err := NewCommand(DefaultContext, "config", "--fixed-value", "--get", key, value).RunStdString(nil)
if err != nil { if err == nil {
perr, ok := err.(*process.Error) // already exist
if !ok { return nil
return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr)
}
eerr, ok := perr.Err.(*exec.ExitError)
if !ok || eerr.ExitCode() != 1 {
return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr)
}
if eerr.ExitCode() == 1 {
return nil
}
} }
if err.IsExitCode(1) {
if _, stderr, err = process.GetManager().Exec(fmt.Sprintf("git.Init(set %s)", key), "git", "config", "--global", "--unset-all", key, value); err != nil { // not exist, add new config
return fmt.Errorf("Failed to set git %s(%s): %s", key, err, stderr) _, _, err = NewCommand(DefaultContext, "config", "--global", "--add", key, value).RunStdString(nil)
if err != nil {
return fmt.Errorf("failed to add git global config %s, err: %w", key, err)
}
return nil
} }
return fmt.Errorf("failed to get git config %s, err: %w", key, err)
}
return nil func configUnsetAll(key, value string) error {
_, _, err := NewCommand(DefaultContext, "config", "--get", key).RunStdString(nil)
if err == nil {
// exist, need to remove
_, _, err = NewCommand(DefaultContext, "config", "--global", "--fixed-value", "--unset-all", key, value).RunStdString(nil)
if err != nil {
return fmt.Errorf("failed to unset git global config %s, err: %w", key, err)
}
return nil
}
if err.IsExitCode(1) {
// not exist
return nil
}
return fmt.Errorf("failed to get git config %s, err: %w", key, err)
} }
// Fsck verifies the connectivity and validity of the objects in the database // Fsck verifies the connectivity and validity of the objects in the database

View file

@ -8,23 +8,74 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"strings"
"testing" "testing"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
) )
func fatalTestError(fmtStr string, args ...interface{}) { func testRun(m *testing.M) error {
fmt.Fprintf(os.Stderr, fmtStr, args...) _ = log.NewLogger(1000, "console", "console", `{"level":"trace","stacktracelevel":"NONE","stderr":true}`)
os.Exit(1)
repoRootPath, err := os.MkdirTemp(os.TempDir(), "repos")
if err != nil {
return fmt.Errorf("unable to create temp dir: %w", err)
}
defer util.RemoveAll(repoRootPath)
setting.RepoRootPath = repoRootPath
if err = InitWithConfigSync(context.Background()); err != nil {
return fmt.Errorf("failed to call Init: %w", err)
}
exitCode := m.Run()
if exitCode != 0 {
return fmt.Errorf("run test failed, ExitCode=%d", exitCode)
}
return nil
} }
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
_ = log.NewLogger(1000, "console", "console", `{"level":"trace","stacktracelevel":"NONE","stderr":true}`) if err := testRun(m); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Test failed: %v", err)
os.Exit(1)
}
}
if err := Init(context.Background()); err != nil { func TestGitConfig(t *testing.T) {
fatalTestError("Init failed: %v", err) gitConfigContains := func(sub string) bool {
if b, err := os.ReadFile(HomeDir() + "/.gitconfig"); err == nil {
return strings.Contains(string(b), sub)
}
return false
} }
exitStatus := m.Run() assert.False(t, gitConfigContains("key-a"))
os.Exit(exitStatus)
assert.NoError(t, configSetNonExist("test.key-a", "val-a"))
assert.True(t, gitConfigContains("key-a = val-a"))
assert.NoError(t, configSetNonExist("test.key-a", "val-a-changed"))
assert.False(t, gitConfigContains("key-a = val-a-changed"))
assert.NoError(t, configSet("test.key-a", "val-a-changed"))
assert.True(t, gitConfigContains("key-a = val-a-changed"))
assert.NoError(t, configAddNonExist("test.key-b", "val-b"))
assert.True(t, gitConfigContains("key-b = val-b"))
assert.NoError(t, configAddNonExist("test.key-b", "val-2b"))
assert.True(t, gitConfigContains("key-b = val-b"))
assert.True(t, gitConfigContains("key-b = val-2b"))
assert.NoError(t, configUnsetAll("test.key-b", "val-b"))
assert.False(t, gitConfigContains("key-b = val-b"))
assert.True(t, gitConfigContains("key-b = val-2b"))
assert.NoError(t, configUnsetAll("test.key-b", "val-2b"))
assert.False(t, gitConfigContains("key-b = val-2b"))
} }

View file

@ -18,12 +18,6 @@ func CheckLFSVersion() {
if setting.LFS.StartServer { if setting.LFS.StartServer {
// Disable LFS client hooks if installed for the current OS user // Disable LFS client hooks if installed for the current OS user
// Needs at least git v2.1.2 // Needs at least git v2.1.2
err := LoadGitVersion()
if err != nil {
logger.Fatal("Error retrieving git version: %v", err)
}
if CheckGitVersionAtLeast("2.1.2") != nil { if CheckGitVersionAtLeast("2.1.2") != nil {
setting.LFS.StartServer = false setting.LFS.StartServer = false
logger.Error("LFS server support needs at least Git v2.1.2") logger.Error("LFS server support needs at least Git v2.1.2")

View file

@ -11,10 +11,6 @@ import (
// GetRemoteAddress returns the url of a specific remote of the repository. // GetRemoteAddress returns the url of a specific remote of the repository.
func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (*url.URL, error) { func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (*url.URL, error) {
err := LoadGitVersion()
if err != nil {
return nil, err
}
var cmd *Command var cmd *Command
if CheckGitVersionAtLeast("2.7") == nil { if CheckGitVersionAtLeast("2.7") == nil {
cmd = NewCommand(ctx, "remote", "get-url", remoteName) cmd = NewCommand(ctx, "remote", "get-url", remoteName)

View file

@ -28,11 +28,6 @@ type CheckAttributeOpts struct {
// CheckAttribute return the Blame object of file // CheckAttribute return the Blame object of file
func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[string]string, error) { func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[string]string, error) {
err := LoadGitVersion()
if err != nil {
return nil, fmt.Errorf("git version missing: %v", err)
}
env := []string{} env := []string{}
if len(opts.IndexFile) > 0 && CheckGitVersionAtLeast("1.7.8") == nil { if len(opts.IndexFile) > 0 && CheckGitVersionAtLeast("1.7.8") == nil {
@ -126,7 +121,7 @@ type CheckAttributeReader struct {
cancel context.CancelFunc cancel context.CancelFunc
} }
// Init initializes the cmd // Init initializes the CheckAttributeReader
func (c *CheckAttributeReader) Init(ctx context.Context) error { func (c *CheckAttributeReader) Init(ctx context.Context) error {
cmdArgs := []string{"check-attr", "--stdin", "-z"} cmdArgs := []string{"check-attr", "--stdin", "-z"}

View file

@ -24,11 +24,6 @@ type CommitTreeOpts struct {
// CommitTree creates a commit from a given tree id for the user with provided message // 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) (SHA1, error) { func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opts CommitTreeOpts) (SHA1, error) {
err := LoadGitVersion()
if err != nil {
return SHA1{}, err
}
commitTimeStr := time.Now().Format(time.RFC3339) commitTimeStr := time.Now().Format(time.RFC3339)
// Because this may call hooks we should pass in the environment // Because this may call hooks we should pass in the environment
@ -60,14 +55,13 @@ func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opt
stdout := new(bytes.Buffer) stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer) stderr := new(bytes.Buffer)
err = cmd.Run(&RunOpts{ err := cmd.Run(&RunOpts{
Env: env, Env: env,
Dir: repo.Path, Dir: repo.Path,
Stdin: messageBytes, Stdin: messageBytes,
Stdout: stdout, Stdout: stdout,
Stderr: stderr, Stderr: stderr,
}) })
if err != nil { if err != nil {
return SHA1{}, ConcatenateError(err, stderr.String()) return SHA1{}, ConcatenateError(err, stderr.String())
} }

View file

@ -30,7 +30,7 @@ func TestMain(m *testing.M) {
} }
func TestRepoStatsIndex(t *testing.T) { func TestRepoStatsIndex(t *testing.T) {
if err := git.Init(context.Background()); !assert.NoError(t, err) { if err := git.InitWithConfigSync(context.Background()); !assert.NoError(t, err) {
return return
} }

View file

@ -317,11 +317,6 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi
return fmt.Errorf("git add --all: %v", err) return fmt.Errorf("git add --all: %v", err)
} }
err = git.LoadGitVersion()
if err != nil {
return fmt.Errorf("Unable to get git version: %v", err)
}
args := []string{ args := []string{
"commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email),
"-m", "Initial commit", "-m", "Initial commit",

View file

@ -182,6 +182,7 @@ func FileURLToPath(u *url.URL) (string, error) {
// it returns error when the variable does not exist. // it returns error when the variable does not exist.
func HomeDir() (home string, err error) { func HomeDir() (home string, err error) {
// TODO: some users run Gitea with mismatched uid and "HOME=xxx" (they set HOME=xxx by environment manually) // TODO: some users run Gitea with mismatched uid and "HOME=xxx" (they set HOME=xxx by environment manually)
// TODO: when running gitea as a sub command inside git, the HOME directory is not the user's home directory
// so at the moment we can not use `user.Current().HomeDir` // so at the moment we can not use `user.Current().HomeDir`
if isOSWindows() { if isOSWindows() {
home = os.Getenv("USERPROFILE") home = os.Getenv("USERPROFILE")

View file

@ -102,8 +102,8 @@ func GlobalInitInstalled(ctx context.Context) {
log.Fatal("Gitea is not installed") log.Fatal("Gitea is not installed")
} }
mustInitCtx(ctx, git.Init) mustInitCtx(ctx, git.InitWithConfigSync)
log.Info(git.VersionInfo()) log.Info("Git Version: %s (home: %s)", git.VersionInfo(), git.HomeDir())
git.CheckLFSVersion() git.CheckLFSVersion()
log.Info("AppPath: %s", setting.AppPath) log.Info("AppPath: %s", setting.AppPath)

View file

@ -247,9 +247,8 @@ func Config(ctx *context.Context) {
ctx.Data["DisableRouterLog"] = setting.DisableRouterLog ctx.Data["DisableRouterLog"] = setting.DisableRouterLog
ctx.Data["RunUser"] = setting.RunUser ctx.Data["RunUser"] = setting.RunUser
ctx.Data["RunMode"] = util.ToTitleCase(setting.RunMode) ctx.Data["RunMode"] = util.ToTitleCase(setting.RunMode)
if version, err := git.LocalVersion(); err == nil { ctx.Data["GitVersion"] = git.VersionInfo()
ctx.Data["GitVersion"] = version.Original()
}
ctx.Data["RepoRootPath"] = setting.RepoRootPath ctx.Data["RepoRootPath"] = setting.RepoRootPath
ctx.Data["CustomRootPath"] = setting.CustomPath ctx.Data["CustomRootPath"] = setting.CustomPath
ctx.Data["StaticRootPath"] = setting.StaticRootPath ctx.Data["StaticRootPath"] = setting.StaticRootPath

View file

@ -421,12 +421,9 @@ func LFSPointerFiles(ctx *context.Context) {
return return
} }
ctx.Data["PageIsSettingsLFS"] = true ctx.Data["PageIsSettingsLFS"] = true
err := git.LoadGitVersion()
if err != nil {
log.Fatal("Error retrieving git version: %v", err)
}
ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs" ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
var err error
err = func() error { err = func() error {
pointerChan := make(chan lfs.PointerBlob) pointerChan := make(chan lfs.PointerBlob)
errChan := make(chan error, 1) errChan := make(chan error, 1)

View file

@ -223,12 +223,6 @@ func Merge(ctx context.Context, pr *models.PullRequest, doer *user_model.User, b
// rawMerge perform the merge operation without changing any pull information in database // rawMerge perform the merge operation without changing any pull information in database
func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) (string, error) { func rawMerge(ctx context.Context, pr *models.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string) (string, error) {
err := git.LoadGitVersion()
if err != nil {
log.Error("git.LoadGitVersion: %v", err)
return "", fmt.Errorf("Unable to get git version: %v", err)
}
// Clone base repo. // Clone base repo.
tmpBasePath, err := createTemporaryRepo(ctx, pr) tmpBasePath, err := createTemporaryRepo(ctx, pr)
if err != nil { if err != nil {

View file

@ -229,11 +229,6 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(parent string, author, co
authorSig := author.NewGitSig() authorSig := author.NewGitSig()
committerSig := committer.NewGitSig() committerSig := committer.NewGitSig()
err := git.LoadGitVersion()
if err != nil {
return "", fmt.Errorf("Unable to get git version: %v", err)
}
// Because this may call hooks we should pass in the environment // Because this may call hooks we should pass in the environment
env := append(os.Environ(), env := append(os.Environ(),
"GIT_AUTHOR_NAME="+authorSig.Name, "GIT_AUTHOR_NAME="+authorSig.Name,