From dac2594e113a32641ff5ca3f3805b3acc55033de Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 17 May 2022 14:56:11 +0800
Subject: [PATCH 01/24] Refactor git module, make Gitea use internal git
 config, add safe.directory config

---
 integrations/integration_test.go              |   3 +-
 integrations/migration-test/migration_test.go |   1 +
 models/migrations/migrations_test.go          |   5 +-
 models/unittest/testdb.go                     |   6 +
 modules/git/command.go                        |  28 ++-
 modules/git/commit.go                         |   5 -
 modules/git/git.go                            | 167 +++++++++---------
 modules/git/git_test.go                       |  58 +++++-
 modules/git/lfs.go                            |   6 -
 modules/git/remote.go                         |   4 -
 modules/git/repo_attribute.go                 |   5 -
 modules/git/repo_tree.go                      |   8 +-
 modules/repository/init.go                    |   5 -
 routers/init.go                               |   2 +-
 routers/web/admin/admin.go                    |   5 +-
 routers/web/repo/lfs.go                       |   5 +-
 services/pull/merge.go                        |   6 -
 services/repository/files/temp_repo.go        |   5 -
 18 files changed, 171 insertions(+), 153 deletions(-)

diff --git a/integrations/integration_test.go b/integrations/integration_test.go
index 4df485a6e8e1f..4cb685ec12fa7 100644
--- a/integrations/integration_test.go
+++ b/integrations/integration_test.go
@@ -261,8 +261,8 @@ func prepareTestEnv(t testing.TB, skip ...int) func() {
 	deferFn := PrintCurrentTest(t, ourSkip)
 	assert.NoError(t, unittest.LoadFixtures())
 	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, git.Init(context.Background()))
 	ownerDirs, err := os.ReadDir(setting.RepoRootPath)
 	if err != nil {
 		assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
@@ -563,6 +563,7 @@ func resetFixtures(t *testing.T) {
 	assert.NoError(t, unittest.LoadFixtures())
 	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, git.Init(context.Background()))
 	ownerDirs, err := os.ReadDir(setting.RepoRootPath)
 	if err != nil {
 		assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
diff --git a/integrations/migration-test/migration_test.go b/integrations/migration-test/migration_test.go
index 6e55807c271e3..cc1ab2785c3d4 100644
--- a/integrations/migration-test/migration_test.go
+++ b/integrations/migration-test/migration_test.go
@@ -62,6 +62,7 @@ func initMigrationTest(t *testing.T) func() {
 	assert.True(t, len(setting.RepoRootPath) != 0)
 	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, git.Init(context.Background()))
 	ownerDirs, err := os.ReadDir(setting.RepoRootPath)
 	if err != nil {
 		assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
diff --git a/models/migrations/migrations_test.go b/models/migrations/migrations_test.go
index a1fd49a8b9f20..3f09ecfa2b722 100644
--- a/models/migrations/migrations_test.go
+++ b/models/migrations/migrations_test.go
@@ -202,9 +202,8 @@ func prepareTestEnv(t *testing.T, skip int, syncModels ...interface{}) (*xorm.En
 	ourSkip += skip
 	deferFn := PrintCurrentTest(t, ourSkip)
 	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"), setting.RepoRootPath))
+	assert.NoError(t, git.Init(context.Background()))
 	ownerDirs, err := os.ReadDir(setting.RepoRootPath)
 	if err != nil {
 		assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go
index 117614a7a4f30..c9b9302bdb02f 100644
--- a/models/unittest/testdb.go
+++ b/models/unittest/testdb.go
@@ -14,6 +14,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/storage"
 	"code.gitea.io/gitea/modules/util"
@@ -106,6 +107,10 @@ func MainTest(m *testing.M, testOpts *TestOptions) {
 
 	setting.Packages.Storage.Path = filepath.Join(setting.AppDataPath, "packages")
 
+	if err = git.Init(context.Background()); err != nil {
+		fatalTestError("git.Init: %v\n", err)
+	}
+
 	if err = storage.Init(); err != nil {
 		fatalTestError("storage.Init: %v\n", err)
 	}
@@ -198,6 +203,7 @@ func PrepareTestEnv(t testing.TB) {
 	assert.NoError(t, util.RemoveAll(setting.RepoRootPath))
 	metaPath := filepath.Join(giteaRoot, "integrations", "gitea-repositories-meta")
 	assert.NoError(t, CopyDir(metaPath, setting.RepoRootPath))
+	assert.NoError(t, git.Init(context.Background()))
 
 	ownerDirs, err := os.ReadDir(setting.RepoRootPath)
 	assert.NoError(t, err)
diff --git a/modules/git/command.go b/modules/git/command.go
index 3dd12e421e409..6237518c98a8a 100644
--- a/modules/git/command.go
+++ b/modules/git/command.go
@@ -8,10 +8,12 @@ package git
 import (
 	"bytes"
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"os"
 	"os/exec"
+	"path/filepath"
 	"strings"
 	"time"
 	"unsafe"
@@ -148,13 +150,21 @@ func (c *Command) Run(opts *RunOpts) error {
 		cmd.Env = opts.Env
 	}
 
+	if GlobalConfigFile == "" {
+		// TODO: now, some unit test code call the git module directly without initialization, which is incorrect.
+		// at the moment, we just use a temp gitconfig to prevent from conflicting with user's gitconfig
+		// in future, the git module should be initialized first before use.
+		GlobalConfigFile = filepath.Join(os.TempDir(), "/gitea-temp-gitconfig")
+		log.Warn("GlobalConfigFile is empty, the git module is not initialized correctly, using a temp gitconfig (%s) temporarily", GlobalConfigFile)
+	}
+
 	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",
+		"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)
+		"GIT_CONFIG_NOSYSTEM=1",               // https://git-scm.com/docs/git-config
+		"GIT_CONFIG_GLOBAL="+GlobalConfigFile, // make Gitea use internal git config only, to prevent conflicts with user's git config
 	)
 
 	cmd.Dir = opts.Dir
@@ -183,7 +193,9 @@ func (c *Command) Run(opts *RunOpts) error {
 
 type RunStdError interface {
 	error
+	Unwrap() error
 	Stderr() string
+	IsExitCode(code int) bool
 }
 
 type runStdError struct {
@@ -208,6 +220,14 @@ func (r *runStdError) Stderr() string {
 	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 {
 	return *(*string)(unsafe.Pointer(&b)) // that's what Golang's strings.Builder.String() does (go/src/strings/builder.go)
 }
diff --git a/modules/git/commit.go b/modules/git/commit.go
index 8c194ef502946..99fbbd0cfcb1e 100644
--- a/modules/git/commit.go
+++ b/modules/git/commit.go
@@ -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')
 func (c *Commit) GetBranchName() (string, error) {
-	err := LoadGitVersion()
-	if err != nil {
-		return "", fmt.Errorf("Git version missing: %v", err)
-	}
-
 	args := []string{
 		"name-rev",
 	}
diff --git a/modules/git/git.go b/modules/git/git.go
index 8fad07033006a..78576b936f3aa 100644
--- a/modules/git/git.go
+++ b/modules/git/git.go
@@ -7,6 +7,7 @@ package git
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"os"
 	"os/exec"
@@ -14,7 +15,6 @@ import (
 	"strings"
 	"time"
 
-	"code.gitea.io/gitea/modules/process"
 	"code.gitea.io/gitea/modules/setting"
 
 	"github.com/hashicorp/go-version"
@@ -31,39 +31,34 @@ var (
 	// Could be updated to an absolute path while initialization
 	GitExecutable = "git"
 
+	// GlobalConfigFile is the global config file used by Gitea internally
+	GlobalConfigFile string
+
 	// DefaultContext is the default context to run git commands in
 	// will be overwritten by Init with HammerContext
 	DefaultContext = context.Background()
 
-	gitVersion *version.Version
-
 	// SupportProcReceive version >= 2.29.0
 	SupportProcReceive bool
-)
 
-// LocalVersion returns current Git version from shell.
-func LocalVersion() (*version.Version, error) {
-	if err := LoadGitVersion(); err != nil {
-		return nil, err
-	}
-	return gitVersion, nil
-}
+	gitVersion *version.Version
+)
 
-// LoadGitVersion returns current Git version from shell.
-func LoadGitVersion() error {
+// loadGitVersion returns current Git version from shell. Internal usage only.
+func loadGitVersion() (*version.Version, error) {
 	// doesn't need RWMutex because its exec by Init()
 	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 {
-		return runErr
+		return nil, runErr
 	}
 
 	fields := strings.Fields(stdout)
 	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
@@ -78,7 +73,7 @@ func LoadGitVersion() error {
 
 	var err error
 	gitVersion, err = version.NewVersion(versionString)
-	return err
+	return gitVersion, err
 }
 
 // SetExecutablePath changes the path of git executable and checks the file permission and version.
@@ -93,7 +88,7 @@ func SetExecutablePath(path string) error {
 	}
 	GitExecutable = absPath
 
-	err = LoadGitVersion()
+	_, err = loadGitVersion()
 	if err != nil {
 		return fmt.Errorf("unable to load git version: %w", err)
 	}
@@ -120,7 +115,10 @@ func SetExecutablePath(path string) error {
 
 // VersionInfo returns git version information
 func VersionInfo() string {
-	format := "Git Version: %s"
+	if gitVersion == nil {
+		return "(git not found)"
+	}
+	format := "%s"
 	args := []interface{}{gitVersion.Original()}
 	// Since git wire protocol has been released from git v2.18
 	if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil {
@@ -139,6 +137,15 @@ func Init(ctx context.Context) error {
 		defaultCommandExecutionTimeout = time.Duration(setting.Git.Timeout.Default) * time.Second
 	}
 
+	if setting.RepoRootPath == "" {
+		return errors.New("RepoRootPath is empty, git module needs that setting before initialization")
+	}
+
+	if err := os.MkdirAll(setting.RepoRootPath, os.ModePerm); err != nil {
+		return fmt.Errorf("unable to create directory %s, err:%w", setting.RepoRootPath, err)
+	}
+	GlobalConfigFile = setting.RepoRootPath + "/gitconfig"
+
 	if err := SetExecutablePath(setting.Git.Path); err != nil {
 		return err
 	}
@@ -161,58 +168,64 @@ func Init(ctx context.Context) error {
 		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
-	if err := LoadGitVersion(); err != nil {
-		return err
-	}
-
 	// Git requires setting user.name and user.email in order to commit changes - if they're not set just add some defaults
 	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 := configSet(configKey, defaultValue); err != nil {
 			return err
 		}
 	}
 
 	// 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
 	}
 
 	if CheckGitVersionAtLeast("2.10") == nil {
-		if err := checkAndSetConfig("receive.advertisePushOptions", "true", true); err != nil {
+		if err := configSet("receive.advertisePushOptions", "true"); err != nil {
 			return err
 		}
 	}
 
 	if CheckGitVersionAtLeast("2.18") == nil {
-		if err := checkAndSetConfig("core.commitGraph", "true", true); err != nil {
+		if err := configSet("core.commitGraph", "true"); err != nil {
 			return err
 		}
-		if err := checkAndSetConfig("gc.writeCommitGraph", "true", true); err != nil {
+		if err := configSet("gc.writeCommitGraph", "true"); err != nil {
 			return err
 		}
 	}
 
 	if CheckGitVersionAtLeast("2.29") == nil {
 		// 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
 		}
 		SupportProcReceive = true
 	} else {
-		if err := checkAndRemoveConfig("receive.procReceiveRefs", "refs/for"); err != nil {
+		if err := configUnsetAll("receive.procReceiveRefs", "refs/for"); err != nil {
 			return err
 		}
 		SupportProcReceive = false
 	}
 
+	if CheckGitVersionAtLeast("2.35.2") == nil {
+		// since Git 2.35.2, git adds a protection for CVE-2022-24765, the protection denies the git directories which are not owned by current user
+		// however, some docker users and samba users (maybe more, issue #19455) have difficulty to set their Gitea git repositories to the correct owner.
+		// the reason behind the problem is: docker/samba uses some uid-mapping mechanism, which are unstable/unfixable in some cases.
+		// now Gitea always use its customized git config file, and all the accesses to the git repositories can be managed,
+		// so it's safe to set "safe.directory=*" for internal usage only.
+		if err := configSet("safe.directory", "*"); err != nil {
+			return err
+		}
+	}
+
 	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 {
-		if err := checkAndSetConfig("core.protectntfs", "false", true); err != nil {
+		if err := configSet("core.protectntfs", "false"); err != nil {
 			return err
 		}
 		globalCommandArgs = append(globalCommandArgs, "-c", "core.protectntfs=false")
@@ -222,7 +235,7 @@ func Init(ctx context.Context) error {
 
 // CheckGitVersionAtLeast check git version is at least the constraint version
 func CheckGitVersionAtLeast(atLeast string) error {
-	if err := LoadGitVersion(); err != nil {
+	if _, err := loadGitVersion(); err != nil {
 		return err
 	}
 	atLeastVersion, err := version.NewVersion(atLeast)
@@ -235,75 +248,57 @@ func CheckGitVersionAtLeast(atLeast string) error {
 	return nil
 }
 
-func checkAndSetConfig(key, defaultValue string, forceToDefault bool) error {
-	stdout, stderr, err := process.GetManager().Exec("git.Init(get setting)", GitExecutable, "config", "--get", key)
-	if err != nil {
-		perr, ok := err.(*process.Error)
-		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)
-		}
+func configSet(key, value string) error {
+	stdout, _, err := NewCommand(DefaultContext, "config", "--get", key).RunStdString(nil)
+	if err != nil && !err.IsExitCode(1) {
+		return fmt.Errorf("failed to get git config %s, err:%w", key, err)
 	}
 
 	currValue := strings.TrimSpace(stdout)
-
-	if currValue == defaultValue || (!forceToDefault && len(currValue) > 0) {
+	if currValue == value {
 		return nil
 	}
 
-	if _, stderr, err = process.GetManager().Exec(fmt.Sprintf("git.Init(set %s)", key), "git", "config", "--global", key, defaultValue); err != nil {
-		return fmt.Errorf("Failed to set git %s(%s): %s", key, err, stderr)
+	_, _, err = NewCommand(DefaultContext, "config", "--global", key, value).RunStdString(nil)
+	if err != nil {
+		return fmt.Errorf("failed to set git global config %s, err:%w", key, err)
 	}
 
 	return nil
 }
 
-func checkAndAddConfig(key, value string) error {
-	_, stderr, err := process.GetManager().Exec("git.Init(get setting)", GitExecutable, "config", "--get", key, value)
-	if err != nil {
-		perr, ok := err.(*process.Error)
-		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)
-		}
-		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
+func configAddNonExist(key, value string) error {
+	_, _, err := NewCommand(DefaultContext, "config", "--get", key).RunStdString(nil)
+	if err == nil {
+		// already exist
+		return nil
+	}
+	if err.IsExitCode(1) {
+		// not exist, add new config
+		_, _, err = NewCommand(DefaultContext, "config", "--global", key, value).RunStdString(nil)
+		if err != nil {
+			return fmt.Errorf("failed to set git global config %s, err:%w", key, err)
 		}
+		return nil
 	}
-
-	return nil
+	return fmt.Errorf("failed to get git config %s, err:%w", key, err)
 }
 
-func checkAndRemoveConfig(key, value string) error {
-	_, stderr, err := process.GetManager().Exec("git.Init(get setting)", GitExecutable, "config", "--get", key, value)
-	if err != nil {
-		perr, ok := err.(*process.Error)
-		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)
-		}
-		if eerr.ExitCode() == 1 {
-			return nil
+func configUnsetAll(key, valueRegex string) error {
+	_, _, err := NewCommand(DefaultContext, "config", "--get", key).RunStdString(nil)
+	if err == nil {
+		// exist, need to remove
+		_, _, err = NewCommand(DefaultContext, "config", "--global", "--unset-all", key, valueRegex).RunStdString(nil)
+		if err != nil {
+			return fmt.Errorf("failed to unset git global config %s, err:%w", key, err)
 		}
+		return nil
 	}
-
-	if _, stderr, err = process.GetManager().Exec(fmt.Sprintf("git.Init(set %s)", key), "git", "config", "--global", "--unset-all", key, value); err != nil {
-		return fmt.Errorf("Failed to set git %s(%s): %s", key, err, stderr)
+	if err.IsExitCode(1) {
+		// not exist
+		return nil
 	}
-
-	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
diff --git a/modules/git/git_test.go b/modules/git/git_test.go
index c62a55badc682..61e8a9a463e42 100644
--- a/modules/git/git_test.go
+++ b/modules/git/git_test.go
@@ -8,23 +8,65 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"strings"
 	"testing"
 
 	"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{}) {
-	fmt.Fprintf(os.Stderr, fmtStr, args...)
-	os.Exit(1)
+func testRun(m *testing.M) error {
+	_ = log.NewLogger(1000, "console", "console", `{"level":"trace","stacktracelevel":"NONE","stderr":true}`)
+
+	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 = Init(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) {
-	_ = 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 {
-		fatalTestError("Init failed: %v", err)
+func TestGitConfig(t *testing.T) {
+	gitConfigContains := func(sub string) bool {
+		if b, err := os.ReadFile(GlobalConfigFile); err == nil {
+			return strings.Contains(string(b), sub)
+		}
+		return false
 	}
 
-	exitStatus := m.Run()
-	os.Exit(exitStatus)
+	assert.False(t, gitConfigContains("test.key-a"))
+	assert.NoError(t, configSet("test.key-a", "val-a"))
+	assert.True(t, gitConfigContains("key-a = val-a"))
+
+	assert.NoError(t, configAddNonExist("test.key-a", "val-a-new"))
+	assert.False(t, gitConfigContains("key-a = val-a-new"))
+
+	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, configUnsetAll("test.key-b", "val-b"))
+	assert.False(t, gitConfigContains("key-b = val-b"))
 }
diff --git a/modules/git/lfs.go b/modules/git/lfs.go
index cdd9d15b2e869..c5d8354b6dc8c 100644
--- a/modules/git/lfs.go
+++ b/modules/git/lfs.go
@@ -18,12 +18,6 @@ func CheckLFSVersion() {
 	if setting.LFS.StartServer {
 		// Disable LFS client hooks if installed for the current OS user
 		// 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 {
 			setting.LFS.StartServer = false
 			logger.Error("LFS server support needs at least Git v2.1.2")
diff --git a/modules/git/remote.go b/modules/git/remote.go
index 536b1681cecf9..b2a2e6d7ab41a 100644
--- a/modules/git/remote.go
+++ b/modules/git/remote.go
@@ -11,10 +11,6 @@ import (
 
 // GetRemoteAddress returns the url of a specific remote of the repository.
 func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (*url.URL, error) {
-	err := LoadGitVersion()
-	if err != nil {
-		return nil, err
-	}
 	var cmd *Command
 	if CheckGitVersionAtLeast("2.7") == nil {
 		cmd = NewCommand(ctx, "remote", "get-url", remoteName)
diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go
index a18c80c3f12f3..1ad6e25fdce47 100644
--- a/modules/git/repo_attribute.go
+++ b/modules/git/repo_attribute.go
@@ -28,11 +28,6 @@ type CheckAttributeOpts struct {
 
 // CheckAttribute return the Blame object of file
 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{}
 
 	if len(opts.IndexFile) > 0 && CheckGitVersionAtLeast("1.7.8") == nil {
diff --git a/modules/git/repo_tree.go b/modules/git/repo_tree.go
index 3e7a9c2cfbcea..2e139daddfc72 100644
--- a/modules/git/repo_tree.go
+++ b/modules/git/repo_tree.go
@@ -24,11 +24,6 @@ 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) (SHA1, error) {
-	err := LoadGitVersion()
-	if err != nil {
-		return SHA1{}, err
-	}
-
 	commitTimeStr := time.Now().Format(time.RFC3339)
 
 	// 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)
 	stderr := new(bytes.Buffer)
-	err = cmd.Run(&RunOpts{
+	err := cmd.Run(&RunOpts{
 		Env:    env,
 		Dir:    repo.Path,
 		Stdin:  messageBytes,
 		Stdout: stdout,
 		Stderr: stderr,
 	})
-
 	if err != nil {
 		return SHA1{}, ConcatenateError(err, stderr.String())
 	}
diff --git a/modules/repository/init.go b/modules/repository/init.go
index 845a61ed0a0ce..4248a26c0daa6 100644
--- a/modules/repository/init.go
+++ b/modules/repository/init.go
@@ -317,11 +317,6 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi
 		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{
 		"commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email),
 		"-m", "Initial commit",
diff --git a/routers/init.go b/routers/init.go
index 6036499362944..faa2bb68dc359 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -103,7 +103,7 @@ func GlobalInitInstalled(ctx context.Context) {
 	}
 
 	mustInitCtx(ctx, git.Init)
-	log.Info(git.VersionInfo())
+	log.Info("Git Version: %s (config: %s)", git.VersionInfo(), git.GlobalConfigFile)
 
 	git.CheckLFSVersion()
 	log.Info("AppPath: %s", setting.AppPath)
diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go
index 78347e67c4bd0..24c07b5c1c19d 100644
--- a/routers/web/admin/admin.go
+++ b/routers/web/admin/admin.go
@@ -247,9 +247,8 @@ func Config(ctx *context.Context) {
 	ctx.Data["DisableRouterLog"] = setting.DisableRouterLog
 	ctx.Data["RunUser"] = setting.RunUser
 	ctx.Data["RunMode"] = util.ToTitleCase(setting.RunMode)
-	if version, err := git.LocalVersion(); err == nil {
-		ctx.Data["GitVersion"] = version.Original()
-	}
+	ctx.Data["GitVersion"] = git.VersionInfo()
+
 	ctx.Data["RepoRootPath"] = setting.RepoRootPath
 	ctx.Data["CustomRootPath"] = setting.CustomPath
 	ctx.Data["StaticRootPath"] = setting.StaticRootPath
diff --git a/routers/web/repo/lfs.go b/routers/web/repo/lfs.go
index 7c2ff1cfae124..e2421f1389261 100644
--- a/routers/web/repo/lfs.go
+++ b/routers/web/repo/lfs.go
@@ -421,12 +421,9 @@ func LFSPointerFiles(ctx *context.Context) {
 		return
 	}
 	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"
 
+	var err error
 	err = func() error {
 		pointerChan := make(chan lfs.PointerBlob)
 		errChan := make(chan error, 1)
diff --git a/services/pull/merge.go b/services/pull/merge.go
index fcced65cdf8ca..76798d73edc28 100644
--- a/services/pull/merge.go
+++ b/services/pull/merge.go
@@ -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
 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.
 	tmpBasePath, err := createTemporaryRepo(ctx, pr)
 	if err != nil {
diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go
index 9c7d9aafec71a..97a80a96bd6ab 100644
--- a/services/repository/files/temp_repo.go
+++ b/services/repository/files/temp_repo.go
@@ -229,11 +229,6 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(parent string, author, co
 	authorSig := author.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
 	env := append(os.Environ(),
 		"GIT_AUTHOR_NAME="+authorSig.Name,

From 45a9375c7b453612b996bbe2d3504263227ab0c5 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 17 May 2022 17:15:45 +0800
Subject: [PATCH 02/24] introduce git.InitSimple and git.InitWithConfigSync,
 make serv cmd use gitconfig

---
 cmd/serv.go                                   | 28 +++++++++-------
 integrations/integration_test.go              |  4 +--
 integrations/migration-test/migration_test.go |  2 +-
 models/migrations/migrations_test.go          |  2 +-
 models/unittest/testdb.go                     |  4 +--
 modules/git/command.go                        | 20 ++++++------
 modules/git/git.go                            | 32 ++++++++++++-------
 modules/git/git_test.go                       |  2 +-
 modules/git/repo_attribute.go                 |  2 +-
 modules/indexer/stats/indexer_test.go         |  2 +-
 routers/init.go                               |  2 +-
 11 files changed, 59 insertions(+), 41 deletions(-)

diff --git a/cmd/serv.go b/cmd/serv.go
index 340f591dce0b3..4d9211b7af145 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -6,6 +6,7 @@
 package cmd
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 	"net/url"
@@ -64,6 +65,19 @@ func setup(logPath string, debug bool) {
 	if debug {
 		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.", "Directory `[repository].ROOT` was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository].ROOT` an absolute value.")
+			return
+		}
+	}
+
+	if err := git.InitSimple(context.Background()); err != nil {
+		_ = fail("Failed to init git", "Failed to init git")
+	}
 }
 
 var (
@@ -79,8 +93,8 @@ var (
 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
 	// if nothing is outputted on stdout.
-	fmt.Fprintln(os.Stdout, "")
-	fmt.Fprintln(os.Stderr, "Gitea:", userMessage)
+	_, _ = fmt.Fprintln(os.Stdout, "")
+	_, _ = fmt.Fprintln(os.Stderr, "Gitea:", userMessage)
 
 	if len(logMessage) > 0 {
 		if !setting.IsProd {
@@ -297,19 +311,11 @@ func runServ(c *cli.Context) error {
 		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` was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository]` `ROOT` an absolute value.")
-		}
-	}
-
 	gitcmd.Dir = setting.RepoRootPath
 	gitcmd.Stdout = os.Stdout
 	gitcmd.Stdin = os.Stdin
 	gitcmd.Stderr = os.Stderr
+	gitcmd.Env = append(gitcmd.Env, git.CommonEnvs()...)
 	if err = gitcmd.Run(); err != nil {
 		return fail("Internal error", "Failed to execute git command: %v", err)
 	}
diff --git a/integrations/integration_test.go b/integrations/integration_test.go
index 4cb685ec12fa7..4e481747704c0 100644
--- a/integrations/integration_test.go
+++ b/integrations/integration_test.go
@@ -262,7 +262,7 @@ func prepareTestEnv(t testing.TB, skip ...int) func() {
 	assert.NoError(t, unittest.LoadFixtures())
 	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, git.Init(context.Background()))
+	assert.NoError(t, git.InitWithConfigSync(context.Background()))
 	ownerDirs, err := os.ReadDir(setting.RepoRootPath)
 	if err != nil {
 		assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
@@ -563,7 +563,7 @@ func resetFixtures(t *testing.T) {
 	assert.NoError(t, unittest.LoadFixtures())
 	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, git.Init(context.Background()))
+	assert.NoError(t, git.InitWithConfigSync(context.Background()))
 	ownerDirs, err := os.ReadDir(setting.RepoRootPath)
 	if err != nil {
 		assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
diff --git a/integrations/migration-test/migration_test.go b/integrations/migration-test/migration_test.go
index cc1ab2785c3d4..a8db49dd0167c 100644
--- a/integrations/migration-test/migration_test.go
+++ b/integrations/migration-test/migration_test.go
@@ -62,7 +62,7 @@ func initMigrationTest(t *testing.T) func() {
 	assert.True(t, len(setting.RepoRootPath) != 0)
 	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, git.Init(context.Background()))
+	assert.NoError(t, git.InitWithConfigSync(context.Background()))
 	ownerDirs, err := os.ReadDir(setting.RepoRootPath)
 	if err != nil {
 		assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
diff --git a/models/migrations/migrations_test.go b/models/migrations/migrations_test.go
index 3f09ecfa2b722..2c4d21d974bf6 100644
--- a/models/migrations/migrations_test.go
+++ b/models/migrations/migrations_test.go
@@ -203,7 +203,7 @@ func prepareTestEnv(t *testing.T, skip int, syncModels ...interface{}) (*xorm.En
 	deferFn := PrintCurrentTest(t, ourSkip)
 	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, git.Init(context.Background()))
+	assert.NoError(t, git.InitWithConfigSync(context.Background()))
 	ownerDirs, err := os.ReadDir(setting.RepoRootPath)
 	if err != nil {
 		assert.NoError(t, err, "unable to read the new repo root: %v\n", err)
diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go
index c9b9302bdb02f..f1399c4519514 100644
--- a/models/unittest/testdb.go
+++ b/models/unittest/testdb.go
@@ -107,7 +107,7 @@ func MainTest(m *testing.M, testOpts *TestOptions) {
 
 	setting.Packages.Storage.Path = filepath.Join(setting.AppDataPath, "packages")
 
-	if err = git.Init(context.Background()); err != nil {
+	if err = git.InitWithConfigSync(context.Background()); err != nil {
 		fatalTestError("git.Init: %v\n", err)
 	}
 
@@ -203,7 +203,7 @@ func PrepareTestEnv(t testing.TB) {
 	assert.NoError(t, util.RemoveAll(setting.RepoRootPath))
 	metaPath := filepath.Join(giteaRoot, "integrations", "gitea-repositories-meta")
 	assert.NoError(t, CopyDir(metaPath, setting.RepoRootPath))
-	assert.NoError(t, git.Init(context.Background()))
+	assert.NoError(t, git.InitWithConfigSync(context.Background()))
 
 	ownerDirs, err := os.ReadDir(setting.RepoRootPath)
 	assert.NoError(t, err)
diff --git a/modules/git/command.go b/modules/git/command.go
index 6237518c98a8a..9b79f197f40ae 100644
--- a/modules/git/command.go
+++ b/modules/git/command.go
@@ -106,6 +106,16 @@ type RunOpts struct {
 	PipelineFunc   func(context.Context, context.CancelFunc) error
 }
 
+func CommonEnvs() []string {
+	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)
+		"GIT_CONFIG_NOSYSTEM=1",                 // https://git-scm.com/docs/git-config
+		"GIT_CONFIG_GLOBAL=" + GlobalConfigFile, // make Gitea use internal git config only, to prevent conflicts with user's git config
+	}
+}
+
 // Run runs the command with the RunOpts
 func (c *Command) Run(opts *RunOpts) error {
 	if opts == nil {
@@ -158,15 +168,7 @@ func (c *Command) Run(opts *RunOpts) error {
 		log.Warn("GlobalConfigFile is empty, the git module is not initialized correctly, using a temp gitconfig (%s) temporarily", GlobalConfigFile)
 	}
 
-	cmd.Env = append(
-		cmd.Env,
-		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)
-		"GIT_CONFIG_NOSYSTEM=1",               // https://git-scm.com/docs/git-config
-		"GIT_CONFIG_GLOBAL="+GlobalConfigFile, // make Gitea use internal git config only, to prevent conflicts with user's git config
-	)
-
+	cmd.Env = append(cmd.Env, CommonEnvs()...)
 	cmd.Dir = opts.Dir
 	cmd.Stdout = opts.Stdout
 	cmd.Stderr = opts.Stderr
diff --git a/modules/git/git.go b/modules/git/git.go
index 78576b936f3aa..faaa30fb59a69 100644
--- a/modules/git/git.go
+++ b/modules/git/git.go
@@ -35,7 +35,7 @@ var (
 	GlobalConfigFile string
 
 	// 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()
 
 	// SupportProcReceive version >= 2.29.0
@@ -129,8 +129,9 @@ func VersionInfo() string {
 	return fmt.Sprintf(format, args...)
 }
 
-// Init initializes git module
-func Init(ctx context.Context) error {
+// InitSimple initializes git module with a very simple step, no config changes, no global command arguments.
+// This method doesn't change anything to filesystem
+func InitSimple(ctx context.Context) error {
 	DefaultContext = ctx
 
 	if setting.Git.Timeout.Default > 0 {
@@ -141,9 +142,6 @@ func Init(ctx context.Context) error {
 		return errors.New("RepoRootPath is empty, git module needs that setting before initialization")
 	}
 
-	if err := os.MkdirAll(setting.RepoRootPath, os.ModePerm); err != nil {
-		return fmt.Errorf("unable to create directory %s, err:%w", setting.RepoRootPath, err)
-	}
 	GlobalConfigFile = setting.RepoRootPath + "/gitconfig"
 
 	if err := SetExecutablePath(setting.Git.Path); err != nil {
@@ -153,6 +151,20 @@ func Init(ctx context.Context) error {
 	// force cleanup args
 	globalCommandArgs = []string{}
 
+	return nil
+}
+
+// InitWithConfigSync initializes git module. This method may create directories or write files into filesystem
+func InitWithConfigSync(ctx context.Context) error {
+	err := InitSimple(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 {
 		// Explicitly disable credential helper, otherwise Git credentials might leak
 		globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=")
@@ -223,13 +235,11 @@ func Init(ctx context.Context) error {
 		if err := configSet("core.longpaths", "true"); err != nil {
 			return err
 		}
-	}
-	if setting.Git.DisableCoreProtectNTFS {
-		if err := configSet("core.protectntfs", "false"); err != nil {
-			return err
+		if setting.Git.DisableCoreProtectNTFS {
+			globalCommandArgs = append(globalCommandArgs, "-c", "core.protectntfs=false")
 		}
-		globalCommandArgs = append(globalCommandArgs, "-c", "core.protectntfs=false")
 	}
+
 	return nil
 }
 
diff --git a/modules/git/git_test.go b/modules/git/git_test.go
index 61e8a9a463e42..18c72ef9884f9 100644
--- a/modules/git/git_test.go
+++ b/modules/git/git_test.go
@@ -28,7 +28,7 @@ func testRun(m *testing.M) error {
 	defer util.RemoveAll(repoRootPath)
 	setting.RepoRootPath = repoRootPath
 
-	if err = Init(context.Background()); err != nil {
+	if err = InitWithConfigSync(context.Background()); err != nil {
 		return fmt.Errorf("failed to call Init: %w", err)
 	}
 
diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go
index 1ad6e25fdce47..4919c9b717160 100644
--- a/modules/git/repo_attribute.go
+++ b/modules/git/repo_attribute.go
@@ -121,7 +121,7 @@ type CheckAttributeReader struct {
 	cancel      context.CancelFunc
 }
 
-// Init initializes the cmd
+// InitWithConfigSync initializes the cmd
 func (c *CheckAttributeReader) Init(ctx context.Context) error {
 	cmdArgs := []string{"check-attr", "--stdin", "-z"}
 
diff --git a/modules/indexer/stats/indexer_test.go b/modules/indexer/stats/indexer_test.go
index c8bd8d1783d2f..6d76bea82e949 100644
--- a/modules/indexer/stats/indexer_test.go
+++ b/modules/indexer/stats/indexer_test.go
@@ -29,7 +29,7 @@ func TestMain(m *testing.M) {
 }
 
 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
 	}
 
diff --git a/routers/init.go b/routers/init.go
index faa2bb68dc359..4c977730b1e12 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -102,7 +102,7 @@ func GlobalInitInstalled(ctx context.Context) {
 		log.Fatal("Gitea is not installed")
 	}
 
-	mustInitCtx(ctx, git.Init)
+	mustInitCtx(ctx, git.InitWithConfigSync)
 	log.Info("Git Version: %s (config: %s)", git.VersionInfo(), git.GlobalConfigFile)
 
 	git.CheckLFSVersion()

From 22d62bcf639529258d73f993680c61dbce3c7752 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 17 May 2022 17:50:44 +0800
Subject: [PATCH 03/24] use HOME instead of GIT_CONFIG_GLOBAL, because git
 always needs a correct HOME

---
 models/unittest/testdb.go  |  7 +++----
 modules/git/command.go     | 16 ++++++++--------
 modules/git/git.go         |  6 +++---
 modules/git/git_test.go    |  2 +-
 modules/setting/setting.go |  1 +
 modules/util/path.go       |  1 +
 routers/init.go            |  2 +-
 7 files changed, 18 insertions(+), 17 deletions(-)

diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go
index f1399c4519514..2a366836fe32f 100644
--- a/models/unittest/testdb.go
+++ b/models/unittest/testdb.go
@@ -107,10 +107,6 @@ func MainTest(m *testing.M, testOpts *TestOptions) {
 
 	setting.Packages.Storage.Path = filepath.Join(setting.AppDataPath, "packages")
 
-	if err = git.InitWithConfigSync(context.Background()); err != nil {
-		fatalTestError("git.Init: %v\n", err)
-	}
-
 	if err = storage.Init(); err != nil {
 		fatalTestError("storage.Init: %v\n", err)
 	}
@@ -121,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 {
 		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)
 	if err != nil {
diff --git a/modules/git/command.go b/modules/git/command.go
index 9b79f197f40ae..d1ce64c8898bd 100644
--- a/modules/git/command.go
+++ b/modules/git/command.go
@@ -109,10 +109,10 @@ type RunOpts struct {
 func CommonEnvs() []string {
 	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)
-		"GIT_CONFIG_NOSYSTEM=1",                 // https://git-scm.com/docs/git-config
-		"GIT_CONFIG_GLOBAL=" + GlobalConfigFile, // make Gitea use internal git config only, to prevent conflicts with user's git config
+		"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)
+		"GIT_CONFIG_NOSYSTEM=1",    // https://git-scm.com/docs/git-config, and GIT_CONFIG_GLOBAL, they require git >= 2.32
+		"HOME=" + HomeDir,          // make Gitea use internal git config only, to prevent conflicts with user's git config
 	}
 }
 
@@ -160,12 +160,12 @@ func (c *Command) Run(opts *RunOpts) error {
 		cmd.Env = opts.Env
 	}
 
-	if GlobalConfigFile == "" {
+	if HomeDir == "" {
 		// TODO: now, some unit test code call the git module directly without initialization, which is incorrect.
-		// at the moment, we just use a temp gitconfig to prevent from conflicting with user's gitconfig
+		// at the moment, we just use a temp HomeDir to prevent from conflicting with user's git config
 		// in future, the git module should be initialized first before use.
-		GlobalConfigFile = filepath.Join(os.TempDir(), "/gitea-temp-gitconfig")
-		log.Warn("GlobalConfigFile is empty, the git module is not initialized correctly, using a temp gitconfig (%s) temporarily", GlobalConfigFile)
+		HomeDir = filepath.Join(os.TempDir(), "/gitea-temp-home")
+		log.Warn("Git's HomeDir is empty, the git module is not initialized correctly, using a temp HomeDir (%s) temporarily", HomeDir)
 	}
 
 	cmd.Env = append(cmd.Env, CommonEnvs()...)
diff --git a/modules/git/git.go b/modules/git/git.go
index faaa30fb59a69..3b46d20fa6234 100644
--- a/modules/git/git.go
+++ b/modules/git/git.go
@@ -31,8 +31,8 @@ var (
 	// Could be updated to an absolute path while initialization
 	GitExecutable = "git"
 
-	// GlobalConfigFile is the global config file used by Gitea internally
-	GlobalConfigFile string
+	// HomeDir is the home dir for git to store the global config file used by Gitea internally
+	HomeDir string
 
 	// DefaultContext is the default context to run git commands in
 	// will be overwritten by InitWithConfigSync with HammerContext
@@ -142,7 +142,7 @@ func InitSimple(ctx context.Context) error {
 		return errors.New("RepoRootPath is empty, git module needs that setting before initialization")
 	}
 
-	GlobalConfigFile = setting.RepoRootPath + "/gitconfig"
+	HomeDir = setting.RepoRootPath
 
 	if err := SetExecutablePath(setting.Git.Path); err != nil {
 		return err
diff --git a/modules/git/git_test.go b/modules/git/git_test.go
index 18c72ef9884f9..5fd46ba574bef 100644
--- a/modules/git/git_test.go
+++ b/modules/git/git_test.go
@@ -48,7 +48,7 @@ func TestMain(m *testing.M) {
 
 func TestGitConfig(t *testing.T) {
 	gitConfigContains := func(sub string) bool {
-		if b, err := os.ReadFile(GlobalConfigFile); err == nil {
+		if b, err := os.ReadFile(HomeDir + "/.gitconfig"); err == nil {
 			return strings.Contains(string(b), sub)
 		}
 		return false
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 5e317b39ea289..5c07a73735a02 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -612,6 +612,7 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
 
 	Cfg.NameMapper = ini.SnackCase
 
+	// TODO: when running gitea as a sub command inside git, the HOME directory is not the user's home directory
 	homeDir, err := util.HomeDir()
 	if err != nil {
 		log.Fatal("Failed to get home directory: %v", err)
diff --git a/modules/util/path.go b/modules/util/path.go
index ed7cc62699446..0ccc7a1dc2aca 100644
--- a/modules/util/path.go
+++ b/modules/util/path.go
@@ -182,6 +182,7 @@ func FileURLToPath(u *url.URL) (string, error) {
 // it returns error when the variable does not exist.
 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: 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`
 	if isOSWindows() {
 		home = os.Getenv("USERPROFILE")
diff --git a/routers/init.go b/routers/init.go
index 4c977730b1e12..0ab2382e33003 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -103,7 +103,7 @@ func GlobalInitInstalled(ctx context.Context) {
 	}
 
 	mustInitCtx(ctx, git.InitWithConfigSync)
-	log.Info("Git Version: %s (config: %s)", git.VersionInfo(), git.GlobalConfigFile)
+	log.Info("Git Version: %s (home: %s)", git.VersionInfo(), git.HomeDir)
 
 	git.CheckLFSVersion()
 	log.Info("AppPath: %s", setting.AppPath)

From b7f928cf8e2021d031a14ddb28f1775c9f0e3057 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 17 May 2022 18:42:52 +0800
Subject: [PATCH 04/24] fix cmd env in cmd/serv.go

---
 cmd/serv.go | 26 +++++++++++++++-----------
 1 file changed, 15 insertions(+), 11 deletions(-)

diff --git a/cmd/serv.go b/cmd/serv.go
index 4d9211b7af145..96784ea6ac2cd 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -250,17 +250,6 @@ func runServ(c *cli.Context) 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
 	if verb == lfsAuthenticateVerb {
@@ -315,7 +304,22 @@ func runServ(c *cli.Context) error {
 	gitcmd.Stdout = os.Stdout
 	gitcmd.Stdin = os.Stdin
 	gitcmd.Stderr = os.Stderr
+	gitcmd.Env = append(gitcmd.Env, os.Environ()...) // FIXME: the legacy code passes the whole env. in the future, should only pass the env which are really needed
+	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,
+	)
 	gitcmd.Env = append(gitcmd.Env, git.CommonEnvs()...)
+
 	if err = gitcmd.Run(); err != nil {
 		return fail("Internal error", "Failed to execute git command: %v", err)
 	}

From 0841cc6a5ebf059dbe37361cc54b251422e5f2c9 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 17 May 2022 19:28:20 +0800
Subject: [PATCH 05/24] fine tune error message

---
 cmd/serv.go | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/cmd/serv.go b/cmd/serv.go
index 96784ea6ac2cd..ec1d8426fa3be 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -70,13 +70,15 @@ func setup(logPath string, debug bool) {
 	// `[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.", "Directory `[repository].ROOT` was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository].ROOT` an absolute value.")
-			return
+			_ = fail("Incorrect configuration, no repository directory.", "Directory `[repository].ROOT` was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository].ROOT` an absolute value.")
+		} else {
+			_ = fail("Incorrect configuration, repository directory is inaccessible", "Directory `[repository].ROOT` is inaccessible. err:%v", err)
 		}
+		return
 	}
 
 	if err := git.InitSimple(context.Background()); err != nil {
-		_ = fail("Failed to init git", "Failed to init git")
+		_ = fail("Failed to init git", "Failed to init git, err:%v", err)
 	}
 }
 
@@ -98,7 +100,7 @@ func fail(userMessage, logMessage string, args ...interface{}) error {
 
 	if len(logMessage) > 0 {
 		if !setting.IsProd {
-			fmt.Fprintf(os.Stderr, logMessage+"\n", args...)
+			_, _ = fmt.Fprintf(os.Stderr, logMessage+"\n", args...)
 		}
 	}
 	ctx, cancel := installSignals()

From 6274dc9a8cacc310a35214ebfef0eef2f02f6a77 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Tue, 17 May 2022 23:57:10 +0800
Subject: [PATCH 06/24] Fix a incorrect test case

---
 modules/git/git_test.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/git/git_test.go b/modules/git/git_test.go
index 5fd46ba574bef..2900e43104611 100644
--- a/modules/git/git_test.go
+++ b/modules/git/git_test.go
@@ -54,7 +54,7 @@ func TestGitConfig(t *testing.T) {
 		return false
 	}
 
-	assert.False(t, gitConfigContains("test.key-a"))
+	assert.False(t, gitConfigContains("key-a"))
 	assert.NoError(t, configSet("test.key-a", "val-a"))
 	assert.True(t, gitConfigContains("key-a = val-a"))
 

From b06a4ef175c406b6f049535ddda7cdb4d8b17215 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 18 May 2022 00:11:04 +0800
Subject: [PATCH 07/24] fix configAddNonExist

---
 modules/git/git.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/git/git.go b/modules/git/git.go
index 3b46d20fa6234..1df3ee0865e6c 100644
--- a/modules/git/git.go
+++ b/modules/git/git.go
@@ -278,7 +278,7 @@ func configSet(key, value string) error {
 }
 
 func configAddNonExist(key, value string) error {
-	_, _, err := NewCommand(DefaultContext, "config", "--get", key).RunStdString(nil)
+	_, _, err := NewCommand(DefaultContext, "config", "--get", key, value).RunStdString(nil)
 	if err == nil {
 		// already exist
 		return nil

From fba95dc61e50f06e5e9056a2498f251657d2dfe0 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 18 May 2022 00:22:57 +0800
Subject: [PATCH 08/24] fix configAddNonExist logic, add `--fixed-value` flag,
 add tests

---
 modules/git/git.go      |  8 ++++----
 modules/git/git_test.go | 11 ++++++++---
 2 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/modules/git/git.go b/modules/git/git.go
index 1df3ee0865e6c..d5f47356faa9d 100644
--- a/modules/git/git.go
+++ b/modules/git/git.go
@@ -278,14 +278,14 @@ func configSet(key, value string) error {
 }
 
 func configAddNonExist(key, value string) error {
-	_, _, err := NewCommand(DefaultContext, "config", "--get", key, value).RunStdString(nil)
+	_, _, err := NewCommand(DefaultContext, "config", "--fixed-value", "--get", key, value).RunStdString(nil)
 	if err == nil {
 		// already exist
 		return nil
 	}
 	if err.IsExitCode(1) {
 		// not exist, add new config
-		_, _, err = NewCommand(DefaultContext, "config", "--global", key, value).RunStdString(nil)
+		_, _, err = NewCommand(DefaultContext, "config", "--global", "--add", key, value).RunStdString(nil)
 		if err != nil {
 			return fmt.Errorf("failed to set git global config %s, err:%w", key, err)
 		}
@@ -294,11 +294,11 @@ func configAddNonExist(key, value string) error {
 	return fmt.Errorf("failed to get git config %s, err:%w", key, err)
 }
 
-func configUnsetAll(key, valueRegex string) error {
+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", "--unset-all", key, valueRegex).RunStdString(nil)
+		_, _, 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)
 		}
diff --git a/modules/git/git_test.go b/modules/git/git_test.go
index 2900e43104611..6d5b492adbc7c 100644
--- a/modules/git/git_test.go
+++ b/modules/git/git_test.go
@@ -58,15 +58,20 @@ func TestGitConfig(t *testing.T) {
 	assert.NoError(t, configSet("test.key-a", "val-a"))
 	assert.True(t, gitConfigContains("key-a = val-a"))
 
-	assert.NoError(t, configAddNonExist("test.key-a", "val-a-new"))
-	assert.False(t, gitConfigContains("key-a = val-a-new"))
-
 	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"))
 }

From 241104d5b2b45769c02020defe230f033739ce74 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 18 May 2022 00:34:06 +0800
Subject: [PATCH 09/24] add configSetNonExist function in case it's needed.

---
 modules/git/git.go      | 20 +++++++++++++++++++-
 modules/git/git_test.go |  6 +++++-
 2 files changed, 24 insertions(+), 2 deletions(-)

diff --git a/modules/git/git.go b/modules/git/git.go
index d5f47356faa9d..fa84beed65adf 100644
--- a/modules/git/git.go
+++ b/modules/git/git.go
@@ -277,6 +277,24 @@ func configSet(key, value string) error {
 	return nil
 }
 
+func configSetNonExist(key, value string) error {
+	_, _, err := NewCommand(DefaultContext, "config", "--get", key).RunStdString(nil)
+	if err == nil {
+		// already exist
+		return nil
+	}
+	if err.IsExitCode(1) {
+		// not exist, set new config
+		_, _, err = NewCommand(DefaultContext, "config", "--global", key, value).RunStdString(nil)
+		if err != nil {
+			return fmt.Errorf("failed to set git global config %s, err:%w", key, err)
+		}
+		return nil
+	}
+
+	return fmt.Errorf("failed to get git config %s, err:%w", key, err)
+}
+
 func configAddNonExist(key, value string) error {
 	_, _, err := NewCommand(DefaultContext, "config", "--fixed-value", "--get", key, value).RunStdString(nil)
 	if err == nil {
@@ -287,7 +305,7 @@ func configAddNonExist(key, value string) error {
 		// not exist, add new config
 		_, _, err = NewCommand(DefaultContext, "config", "--global", "--add", key, value).RunStdString(nil)
 		if err != nil {
-			return fmt.Errorf("failed to set git global config %s, err:%w", key, err)
+			return fmt.Errorf("failed to add git global config %s, err:%w", key, err)
 		}
 		return nil
 	}
diff --git a/modules/git/git_test.go b/modules/git/git_test.go
index 6d5b492adbc7c..3f876e9a9320f 100644
--- a/modules/git/git_test.go
+++ b/modules/git/git_test.go
@@ -55,9 +55,13 @@ func TestGitConfig(t *testing.T) {
 	}
 
 	assert.False(t, gitConfigContains("key-a"))
-	assert.NoError(t, configSet("test.key-a", "val-a"))
+
+	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"))
 

From 3b1e6d1deaa12c1415ffd15718a0c00979569acb Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 18 May 2022 00:35:05 +0800
Subject: [PATCH 10/24] use configSetNonExist for `user.name` and `user.email`

---
 modules/git/git.go | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/modules/git/git.go b/modules/git/git.go
index fa84beed65adf..f7fa1726cdc22 100644
--- a/modules/git/git.go
+++ b/modules/git/git.go
@@ -181,8 +181,11 @@ func InitWithConfigSync(ctx context.Context) error {
 	}
 
 	// Git requires setting user.name and user.email in order to commit changes - if they're not set just add some defaults
-	for configKey, defaultValue := range map[string]string{"user.name": "Gitea", "user.email": "gitea@fake.local"} {
-		if err := configSet(configKey, defaultValue); err != nil {
+	for configKey, defaultValue := range map[string]string{
+		"user.name":  "Gitea",
+		"user.email": "gitea@fake.local",
+	} {
+		if err := configSetNonExist(configKey, defaultValue); err != nil {
 			return err
 		}
 	}

From 4e2679f9527c71e022fa66fae27ba71da98fc4a3 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 18 May 2022 00:50:17 +0800
Subject: [PATCH 11/24] add some comments

---
 modules/git/git.go | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/modules/git/git.go b/modules/git/git.go
index f7fa1726cdc22..d8b0a43ae3667 100644
--- a/modules/git/git.go
+++ b/modules/git/git.go
@@ -180,7 +180,9 @@ func InitWithConfigSync(ctx context.Context) error {
 		globalCommandArgs = append(globalCommandArgs, "-c", "uploadpack.allowfilter=true", "-c", "uploadpack.allowAnySHA1InWant=true")
 	}
 
-	// Git requires setting user.name and user.email in order to commit changes - if they're not set just add some defaults
+	// Git requires setting user.name and user.email in order to commit changes - old comment: "if they're not set just add some defaults"
+	// 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.
+	// 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",
 		"user.email": "gitea@fake.local",

From 9866adda2c741a846003fbedf24ac0c3b9b24fd6 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 18 May 2022 11:58:01 +0800
Subject: [PATCH 12/24] Update cmd/serv.go

Co-authored-by: zeripath <art27@cantab.net>
---
 cmd/serv.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/cmd/serv.go b/cmd/serv.go
index ec1d8426fa3be..e8e904d8f4a73 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -72,7 +72,7 @@ func setup(logPath string, debug bool) {
 		if os.IsNotExist(err) {
 			_ = fail("Incorrect configuration, no repository directory.", "Directory `[repository].ROOT` was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository].ROOT` an absolute value.")
 		} else {
-			_ = fail("Incorrect configuration, repository directory is inaccessible", "Directory `[repository].ROOT` is inaccessible. err:%v", err)
+			_ = fail("Incorrect configuration, repository directory is inaccessible", "Directory `[repository].ROOT` is inaccessible. err: %v", err)
 		}
 		return
 	}

From 5d6cd68d529702224900ed5e8f74ee20b216cbd4 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 18 May 2022 11:58:09 +0800
Subject: [PATCH 13/24] Update cmd/serv.go

Co-authored-by: zeripath <art27@cantab.net>
---
 cmd/serv.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/cmd/serv.go b/cmd/serv.go
index e8e904d8f4a73..e84787387b6b2 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -78,7 +78,7 @@ func setup(logPath string, debug bool) {
 	}
 
 	if err := git.InitSimple(context.Background()); err != nil {
-		_ = fail("Failed to init git", "Failed to init git, err:%v", err)
+		_ = fail("Failed to init git", "Failed to init git, err: %v", err)
 	}
 }
 

From 7d96b3902bac6518416cc67404dc81ec116b935a Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 18 May 2022 11:58:58 +0800
Subject: [PATCH 14/24] Update modules/git/git.go

Co-authored-by: zeripath <art27@cantab.net>
---
 modules/git/git.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/git/git.go b/modules/git/git.go
index d8b0a43ae3667..81abf387580b2 100644
--- a/modules/git/git.go
+++ b/modules/git/git.go
@@ -266,7 +266,7 @@ func CheckGitVersionAtLeast(atLeast string) error {
 func configSet(key, value string) error {
 	stdout, _, err := NewCommand(DefaultContext, "config", "--get", key).RunStdString(nil)
 	if err != nil && !err.IsExitCode(1) {
-		return fmt.Errorf("failed to get git config %s, err:%w", key, err)
+		return fmt.Errorf("failed to get git config %s, err: %w", key, err)
 	}
 
 	currValue := strings.TrimSpace(stdout)

From c09f9c9915c69cd1aac768fe00f3b63f28843298 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 18 May 2022 12:09:26 +0800
Subject: [PATCH 15/24] Update modules/setting/setting.go

Co-authored-by: zeripath <art27@cantab.net>
---
 modules/setting/setting.go | 1 -
 1 file changed, 1 deletion(-)

diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 5c07a73735a02..5e317b39ea289 100644
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -612,7 +612,6 @@ func loadFromConf(allowEmpty bool, extraConfig string) {
 
 	Cfg.NameMapper = ini.SnackCase
 
-	// TODO: when running gitea as a sub command inside git, the HOME directory is not the user's home directory
 	homeDir, err := util.HomeDir()
 	if err != nil {
 		log.Fatal("Failed to get home directory: %v", err)

From 57d9b1b14720171a298d53b21eb4c954ca5f185c Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 18 May 2022 12:10:22 +0800
Subject: [PATCH 16/24] Update modules/git/repo_attribute.go

Co-authored-by: zeripath <art27@cantab.net>
---
 modules/git/repo_attribute.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go
index 4919c9b717160..38818788f32a3 100644
--- a/modules/git/repo_attribute.go
+++ b/modules/git/repo_attribute.go
@@ -121,7 +121,7 @@ type CheckAttributeReader struct {
 	cancel      context.CancelFunc
 }
 
-// InitWithConfigSync initializes the cmd
+// Init initializes the CheckAttributeReader
 func (c *CheckAttributeReader) Init(ctx context.Context) error {
 	cmdArgs := []string{"check-attr", "--stdin", "-z"}
 

From ae9d7861f0292a1895d3e5f9f1160f11ac7badab Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 18 May 2022 12:13:41 +0800
Subject: [PATCH 17/24] fix spaces in messages

---
 modules/git/git.go | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/modules/git/git.go b/modules/git/git.go
index 81abf387580b2..046d42c7d16fb 100644
--- a/modules/git/git.go
+++ b/modules/git/git.go
@@ -162,7 +162,7 @@ func InitWithConfigSync(ctx context.Context) error {
 	}
 
 	if err = os.MkdirAll(setting.RepoRootPath, os.ModePerm); err != nil {
-		return fmt.Errorf("unable to create directory %s, err:%w", setting.RepoRootPath, err)
+		return fmt.Errorf("unable to create directory %s, err: %w", setting.RepoRootPath, err)
 	}
 
 	if CheckGitVersionAtLeast("2.9") == nil {
@@ -276,7 +276,7 @@ func configSet(key, value string) error {
 
 	_, _, err = NewCommand(DefaultContext, "config", "--global", key, value).RunStdString(nil)
 	if err != nil {
-		return fmt.Errorf("failed to set git global config %s, err:%w", key, err)
+		return fmt.Errorf("failed to set git global config %s, err: %w", key, err)
 	}
 
 	return nil
@@ -292,12 +292,12 @@ func configSetNonExist(key, value string) error {
 		// not exist, set new config
 		_, _, err = NewCommand(DefaultContext, "config", "--global", key, value).RunStdString(nil)
 		if err != nil {
-			return fmt.Errorf("failed to set git global config %s, err:%w", key, err)
+			return fmt.Errorf("failed to set git global config %s, err: %w", key, err)
 		}
 		return nil
 	}
 
-	return fmt.Errorf("failed to get git config %s, err:%w", key, err)
+	return fmt.Errorf("failed to get git config %s, err: %w", key, err)
 }
 
 func configAddNonExist(key, value string) error {
@@ -310,11 +310,11 @@ func configAddNonExist(key, value string) error {
 		// not exist, add new config
 		_, _, 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 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 fmt.Errorf("failed to get git config %s, err: %w", key, err)
 }
 
 func configUnsetAll(key, value string) error {
@@ -323,7 +323,7 @@ func configUnsetAll(key, value string) error {
 		// 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 fmt.Errorf("failed to unset git global config %s, err: %w", key, err)
 		}
 		return nil
 	}
@@ -331,7 +331,7 @@ func configUnsetAll(key, value string) error {
 		// not exist
 		return nil
 	}
-	return fmt.Errorf("failed to get git config %s, err:%w", key, err)
+	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

From 71eb7f426ea1b97cf4aeff4b7cad2d871b094e95 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Wed, 18 May 2022 12:23:19 +0800
Subject: [PATCH 18/24] use `configSet("core.protectNTFS", ...)` instead of
 `globalCommandArgs`

---
 modules/git/git.go | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/modules/git/git.go b/modules/git/git.go
index 046d42c7d16fb..47c378a76468e 100644
--- a/modules/git/git.go
+++ b/modules/git/git.go
@@ -241,7 +241,12 @@ func InitWithConfigSync(ctx context.Context) error {
 			return err
 		}
 		if setting.Git.DisableCoreProtectNTFS {
-			globalCommandArgs = append(globalCommandArgs, "-c", "core.protectntfs=false")
+			err = configSet("core.protectNTFS", "false")
+		} else {
+			err = configUnsetAll("core.protectNTFS", "false")
+		}
+		if err != nil {
+			return err
 		}
 	}
 

From 1da696d0a9625c53bef6d0c4551c8bcb66546128 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 19 May 2022 12:30:53 +0800
Subject: [PATCH 19/24] remove GIT_CONFIG_NOSYSTEM, continue to use system's
 git config

---
 modules/git/command.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/git/command.go b/modules/git/command.go
index d1ce64c8898bd..07f8cc27855e1 100644
--- a/modules/git/command.go
+++ b/modules/git/command.go
@@ -107,11 +107,11 @@ type RunOpts struct {
 }
 
 func CommonEnvs() []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)
-		"GIT_CONFIG_NOSYSTEM=1",    // https://git-scm.com/docs/git-config, and GIT_CONFIG_GLOBAL, they require git >= 2.32
 		"HOME=" + HomeDir,          // make Gitea use internal git config only, to prevent conflicts with user's git config
 	}
 }

From 733445c55deb4ea6490e4677d64b77a953978fd3 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Fri, 3 Jun 2022 00:00:12 +0800
Subject: [PATCH 20/24] Update cmd/serv.go

Co-authored-by: zeripath <art27@cantab.net>
---
 cmd/serv.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/cmd/serv.go b/cmd/serv.go
index e84787387b6b2..591d209852c9a 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -306,7 +306,7 @@ func runServ(c *cli.Context) error {
 	gitcmd.Stdout = os.Stdout
 	gitcmd.Stdin = os.Stdin
 	gitcmd.Stderr = os.Stderr
-	gitcmd.Env = append(gitcmd.Env, os.Environ()...) // FIXME: the legacy code passes the whole env. in the future, should only pass the env which are really needed
+	gitcmd.Env = append(gitcmd.Env, os.Environ()...)
 	gitcmd.Env = append(gitcmd.Env,
 		repo_module.EnvRepoIsWiki+"="+strconv.FormatBool(results.IsWiki),
 		repo_module.EnvRepoName+"="+results.RepoName,

From 30c49f372850f33a9d04ce5b61df122a790c2062 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 4 Jun 2022 22:26:57 +0800
Subject: [PATCH 21/24] fix merge

---
 cmd/serv.go | 13 ++-----------
 1 file changed, 2 insertions(+), 11 deletions(-)

diff --git a/cmd/serv.go b/cmd/serv.go
index 1430fe5cc17f6..99134adcfa728 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -71,9 +71,9 @@ func setup(logPath string, debug bool) {
 	// `[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` was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository].ROOT` an absolute value.")
+			_ = 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` is inaccessible. err: %v", err)
+			_ = fail("Incorrect configuration, repository directory is inaccessible", "Directory `[repository].ROOT` %q is inaccessible. err: %v", setting.RepoRootPath, err)
 		}
 		return
 	}
@@ -303,15 +303,6 @@ func runServ(c *cli.Context) error {
 		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)
 	gitcmd.Dir = setting.RepoRootPath
 	gitcmd.Stdout = os.Stdout

From c5f1e940b600e029c9c9ecf9d21ddfaf84183f52 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sat, 4 Jun 2022 22:28:41 +0800
Subject: [PATCH 22/24] remove code for safe.directory

---
 modules/git/git.go | 11 -----------
 1 file changed, 11 deletions(-)

diff --git a/modules/git/git.go b/modules/git/git.go
index 47c378a76468e..700d7141cca7c 100644
--- a/modules/git/git.go
+++ b/modules/git/git.go
@@ -225,17 +225,6 @@ func InitWithConfigSync(ctx context.Context) error {
 		SupportProcReceive = false
 	}
 
-	if CheckGitVersionAtLeast("2.35.2") == nil {
-		// since Git 2.35.2, git adds a protection for CVE-2022-24765, the protection denies the git directories which are not owned by current user
-		// however, some docker users and samba users (maybe more, issue #19455) have difficulty to set their Gitea git repositories to the correct owner.
-		// the reason behind the problem is: docker/samba uses some uid-mapping mechanism, which are unstable/unfixable in some cases.
-		// now Gitea always use its customized git config file, and all the accesses to the git repositories can be managed,
-		// so it's safe to set "safe.directory=*" for internal usage only.
-		if err := configSet("safe.directory", "*"); err != nil {
-			return err
-		}
-	}
-
 	if runtime.GOOS == "windows" {
 		if err := configSet("core.longpaths", "true"); err != nil {
 			return err

From b3011979a3f60e668c7d2dba550795ca17e23fd3 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Sun, 5 Jun 2022 10:42:34 +0800
Subject: [PATCH 23/24] separate git.CommonEnvs to CommonGitCmdEnvs and
 CommonCmdServEnvs

---
 cmd/serv.go            |  4 +++-
 modules/git/command.go | 13 +++++++++++--
 2 files changed, 14 insertions(+), 3 deletions(-)

diff --git a/cmd/serv.go b/cmd/serv.go
index 99134adcfa728..6e067a48a570f 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -322,7 +322,9 @@ func runServ(c *cli.Context) error {
 		repo_module.EnvKeyID+"="+fmt.Sprintf("%d", results.KeyID),
 		repo_module.EnvAppURL+"="+setting.AppURL,
 	)
-	gitcmd.Env = append(gitcmd.Env, git.CommonEnvs()...)
+	// 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 {
 		return fail("Internal error", "Failed to execute git command: %v", err)
diff --git a/modules/git/command.go b/modules/git/command.go
index a4b341d7073b5..2be843b6f6673 100644
--- a/modules/git/command.go
+++ b/modules/git/command.go
@@ -106,7 +106,8 @@ type RunOpts struct {
 	PipelineFunc   func(context.Context, context.CancelFunc) error
 }
 
-func CommonEnvs() []string {
+// 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),
@@ -116,6 +117,14 @@ func CommonEnvs() []string {
 	}
 }
 
+// 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
 func (c *Command) Run(opts *RunOpts) error {
 	if opts == nil {
@@ -169,7 +178,7 @@ func (c *Command) Run(opts *RunOpts) error {
 	}
 
 	process.SetSysProcAttribute(cmd)
-	cmd.Env = append(cmd.Env, CommonEnvs()...)
+	cmd.Env = append(cmd.Env, CommonGitCmdEnvs()...)
 	cmd.Dir = opts.Dir
 	cmd.Stdout = opts.Stdout
 	cmd.Stderr = opts.Stderr

From 26fa08779a41905eafe2f4713c4af3ea07363ba6 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 9 Jun 2022 21:22:05 +0800
Subject: [PATCH 24/24] avoid Golang's data race error

---
 models/repo/fork.go      |  1 +
 models/repo/main_test.go |  4 ++--
 modules/git/command.go   | 13 ++----------
 modules/git/git.go       | 45 ++++++++++++++++++++++++++++++----------
 modules/git/git_test.go  |  2 +-
 routers/init.go          |  2 +-
 6 files changed, 41 insertions(+), 26 deletions(-)

diff --git a/models/repo/fork.go b/models/repo/fork.go
index 938bbae17e16b..b54c61c425fd7 100644
--- a/models/repo/fork.go
+++ b/models/repo/fork.go
@@ -9,6 +9,7 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
+
 	"xorm.io/builder"
 )
 
diff --git a/models/repo/main_test.go b/models/repo/main_test.go
index eb04aa8227d8d..f6d704ca65874 100644
--- a/models/repo/main_test.go
+++ b/models/repo/main_test.go
@@ -8,12 +8,12 @@ import (
 	"path/filepath"
 	"testing"
 
+	"code.gitea.io/gitea/models/unittest"
+
 	_ "code.gitea.io/gitea/models"             // register table model
 	_ "code.gitea.io/gitea/models/perm/access" // register table model
 	_ "code.gitea.io/gitea/models/repo"        // register table model
 	_ "code.gitea.io/gitea/models/user"        // register table model
-
-	"code.gitea.io/gitea/models/unittest"
 )
 
 func TestMain(m *testing.M) {
diff --git a/modules/git/command.go b/modules/git/command.go
index 2be843b6f6673..d71497f1d791e 100644
--- a/modules/git/command.go
+++ b/modules/git/command.go
@@ -13,7 +13,6 @@ import (
 	"io"
 	"os"
 	"os/exec"
-	"path/filepath"
 	"strings"
 	"time"
 	"unsafe"
@@ -113,7 +112,7 @@ func CommonGitCmdEnvs() []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
+		"HOME=" + HomeDir(),        // make Gitea use internal git config only, to prevent conflicts with user's git config
 	}
 }
 
@@ -121,7 +120,7 @@ func CommonGitCmdEnvs() []string {
 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
+		"HOME=" + HomeDir(),        // make Gitea use internal git config only, to prevent conflicts with user's git config
 	}
 }
 
@@ -169,14 +168,6 @@ func (c *Command) Run(opts *RunOpts) error {
 		cmd.Env = opts.Env
 	}
 
-	if HomeDir == "" {
-		// 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 future, the git module should be initialized first before use.
-		HomeDir = filepath.Join(os.TempDir(), "/gitea-temp-home")
-		log.Warn("Git's HomeDir is empty, the git module is not initialized correctly, using a temp HomeDir (%s) temporarily", HomeDir)
-	}
-
 	process.SetSysProcAttribute(cmd)
 	cmd.Env = append(cmd.Env, CommonGitCmdEnvs()...)
 	cmd.Dir = opts.Dir
diff --git a/modules/git/git.go b/modules/git/git.go
index 700d7141cca7c..5817bd2c7fe2d 100644
--- a/modules/git/git.go
+++ b/modules/git/git.go
@@ -7,14 +7,16 @@ package git
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"os"
 	"os/exec"
+	"path/filepath"
 	"runtime"
 	"strings"
+	"sync"
 	"time"
 
+	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 
 	"github.com/hashicorp/go-version"
@@ -31,9 +33,6 @@ var (
 	// Could be updated to an absolute path while initialization
 	GitExecutable = "git"
 
-	// HomeDir is the home dir for git to store the global config file used by Gitea internally
-	HomeDir string
-
 	// DefaultContext is the default context to run git commands in
 	// will be overwritten by InitWithConfigSync with HammerContext
 	DefaultContext = context.Background()
@@ -41,6 +40,9 @@ var (
 	// SupportProcReceive version >= 2.29.0
 	SupportProcReceive bool
 
+	// initMutex is used to avoid Golang's data race error. see the comments below.
+	initMutex sync.Mutex
+
 	gitVersion *version.Version
 )
 
@@ -132,18 +134,36 @@ func VersionInfo() string {
 // InitSimple initializes git module with a very simple step, no config changes, no global command arguments.
 // 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
 
 	if setting.Git.Timeout.Default > 0 {
 		defaultCommandExecutionTimeout = time.Duration(setting.Git.Timeout.Default) * time.Second
 	}
 
-	if setting.RepoRootPath == "" {
-		return errors.New("RepoRootPath is empty, git module needs that setting before initialization")
-	}
-
-	HomeDir = setting.RepoRootPath
-
 	if err := SetExecutablePath(setting.Git.Path); err != nil {
 		return err
 	}
@@ -156,7 +176,10 @@ func InitSimple(ctx context.Context) error {
 
 // InitWithConfigSync initializes git module. This method may create directories or write files into filesystem
 func InitWithConfigSync(ctx context.Context) error {
-	err := InitSimple(ctx)
+	initMutex.Lock()
+	defer initMutex.Unlock()
+
+	err := initSimpleInternal(ctx)
 	if err != nil {
 		return err
 	}
diff --git a/modules/git/git_test.go b/modules/git/git_test.go
index 3f876e9a9320f..5b1cd820e85e3 100644
--- a/modules/git/git_test.go
+++ b/modules/git/git_test.go
@@ -48,7 +48,7 @@ func TestMain(m *testing.M) {
 
 func TestGitConfig(t *testing.T) {
 	gitConfigContains := func(sub string) bool {
-		if b, err := os.ReadFile(HomeDir + "/.gitconfig"); err == nil {
+		if b, err := os.ReadFile(HomeDir() + "/.gitconfig"); err == nil {
 			return strings.Contains(string(b), sub)
 		}
 		return false
diff --git a/routers/init.go b/routers/init.go
index 0ab2382e33003..9b6a770f279b6 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -103,7 +103,7 @@ func GlobalInitInstalled(ctx context.Context) {
 	}
 
 	mustInitCtx(ctx, git.InitWithConfigSync)
-	log.Info("Git Version: %s (home: %s)", git.VersionInfo(), git.HomeDir)
+	log.Info("Git Version: %s (home: %s)", git.VersionInfo(), git.HomeDir())
 
 	git.CheckLFSVersion()
 	log.Info("AppPath: %s", setting.AppPath)