From 33850a83fe4ebd23a762a7aac81614c42e303bfa Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 20 Nov 2024 11:26:12 -0800 Subject: [PATCH] Fix submodule parsing (#32571) Fix #32568, parse `.gitmodules` correctly --------- Co-authored-by: wxiaoguang --- modules/git/commit.go | 66 +---- modules/git/commit_info.go | 2 +- modules/git/commit_info_gogit.go | 4 +- modules/git/commit_info_nogogit.go | 2 +- modules/git/commit_submodule.go | 47 ++++ ...{submodule.go => commit_submodule_file.go} | 24 +- ..._test.go => commit_submodule_file_test.go} | 2 +- modules/git/commit_test.go | 4 +- modules/git/config.go | 187 +++++++++++++++ modules/git/config_submodule.go | 75 ++++++ modules/git/config_submodule_test.go | 49 ++++ modules/git/config_test.go | 66 +++++ modules/git/fsck.go | 14 ++ modules/git/git.go | 227 ++---------------- modules/git/git_test.go | 53 ---- modules/git/repo_base_gogit.go | 4 +- modules/git/repo_base_nogogit.go | 4 +- modules/git/repo_tag_gogit.go | 2 +- modules/git/repo_tag_nogogit.go | 2 +- modules/git/utils.go | 18 +- 20 files changed, 492 insertions(+), 360 deletions(-) create mode 100644 modules/git/commit_submodule.go rename modules/git/{submodule.go => commit_submodule_file.go} (83%) rename modules/git/{submodule_test.go => commit_submodule_file_test.go} (97%) create mode 100644 modules/git/config.go create mode 100644 modules/git/config_submodule.go create mode 100644 modules/git/config_submodule_test.go create mode 100644 modules/git/config_test.go create mode 100644 modules/git/fsck.go diff --git a/modules/git/commit.go b/modules/git/commit.go index 86adaa79a66..010b56948ef 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -9,7 +9,6 @@ import ( "bytes" "context" "errors" - "fmt" "io" "os/exec" "strconv" @@ -29,7 +28,7 @@ type Commit struct { Signature *CommitSignature Parents []ObjectID // ID strings - submoduleCache *ObjectCache + submoduleCache *ObjectCache[*SubModule] } // CommitSignature represents a git commit signature part. @@ -357,69 +356,6 @@ func (c *Commit) GetFileContent(filename string, limit int) (string, error) { return string(bytes), nil } -// GetSubModules get all the sub modules of current revision git tree -func (c *Commit) GetSubModules() (*ObjectCache, error) { - if c.submoduleCache != nil { - return c.submoduleCache, nil - } - - entry, err := c.GetTreeEntryByPath(".gitmodules") - if err != nil { - if _, ok := err.(ErrNotExist); ok { - return nil, nil - } - return nil, err - } - - rd, err := entry.Blob().DataAsync() - if err != nil { - return nil, err - } - - defer rd.Close() - scanner := bufio.NewScanner(rd) - c.submoduleCache = newObjectCache() - var ismodule bool - var path string - for scanner.Scan() { - if strings.HasPrefix(scanner.Text(), "[submodule") { - ismodule = true - continue - } - if ismodule { - fields := strings.Split(scanner.Text(), "=") - k := strings.TrimSpace(fields[0]) - if k == "path" { - path = strings.TrimSpace(fields[1]) - } else if k == "url" { - c.submoduleCache.Set(path, &SubModule{path, strings.TrimSpace(fields[1])}) - ismodule = false - } - } - } - if err = scanner.Err(); err != nil { - return nil, fmt.Errorf("GetSubModules scan: %w", err) - } - - return c.submoduleCache, nil -} - -// GetSubModule get the sub module according entryname -func (c *Commit) GetSubModule(entryname string) (*SubModule, error) { - modules, err := c.GetSubModules() - if err != nil { - return nil, err - } - - if modules != nil { - module, has := modules.Get(entryname) - if has { - return module.(*SubModule), nil - } - } - return nil, nil -} - // GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only') func (c *Commit) GetBranchName() (string, error) { cmd := NewCommand(c.repo.Ctx, "name-rev") diff --git a/modules/git/commit_info.go b/modules/git/commit_info.go index c740a4e13eb..545081275b7 100644 --- a/modules/git/commit_info.go +++ b/modules/git/commit_info.go @@ -7,5 +7,5 @@ package git type CommitInfo struct { Entry *TreeEntry Commit *Commit - SubModuleFile *SubModuleFile + SubModuleFile *CommitSubModuleFile } diff --git a/modules/git/commit_info_gogit.go b/modules/git/commit_info_gogit.go index 31ffc9aec1c..11b44f7c356 100644 --- a/modules/git/commit_info_gogit.go +++ b/modules/git/commit_info_gogit.go @@ -71,7 +71,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath commitsInfo[i].Commit = entryCommit } - // If the entry if a submodule add a submodule file for this + // If the entry is a submodule add a submodule file for this if entry.IsSubModule() { subModuleURL := "" var fullPath string @@ -85,7 +85,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath } else if subModule != nil { subModuleURL = subModule.URL } - subModuleFile := NewSubModuleFile(commitsInfo[i].Commit, subModuleURL, entry.ID.String()) + subModuleFile := NewCommitSubModuleFile(subModuleURL, entry.ID.String()) commitsInfo[i].SubModuleFile = subModuleFile } } diff --git a/modules/git/commit_info_nogogit.go b/modules/git/commit_info_nogogit.go index cfde64a0339..20d586f0ff5 100644 --- a/modules/git/commit_info_nogogit.go +++ b/modules/git/commit_info_nogogit.go @@ -79,7 +79,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath } else if subModule != nil { subModuleURL = subModule.URL } - subModuleFile := NewSubModuleFile(commitsInfo[i].Commit, subModuleURL, entry.ID.String()) + subModuleFile := NewCommitSubModuleFile(subModuleURL, entry.ID.String()) commitsInfo[i].SubModuleFile = subModuleFile } } diff --git a/modules/git/commit_submodule.go b/modules/git/commit_submodule.go new file mode 100644 index 00000000000..6603061da29 --- /dev/null +++ b/modules/git/commit_submodule.go @@ -0,0 +1,47 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +// GetSubModules get all the submodules of current revision git tree +func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) { + if c.submoduleCache != nil { + return c.submoduleCache, nil + } + + entry, err := c.GetTreeEntryByPath(".gitmodules") + if err != nil { + if _, ok := err.(ErrNotExist); ok { + return nil, nil + } + return nil, err + } + + rd, err := entry.Blob().DataAsync() + if err != nil { + return nil, err + } + defer rd.Close() + + // at the moment we do not strictly limit the size of the .gitmodules file because some users would have huge .gitmodules files (>1MB) + c.submoduleCache, err = configParseSubModules(rd) + if err != nil { + return nil, err + } + return c.submoduleCache, nil +} + +// GetSubModule get the submodule according entry name +func (c *Commit) GetSubModule(entryName string) (*SubModule, error) { + modules, err := c.GetSubModules() + if err != nil { + return nil, err + } + + if modules != nil { + if module, has := modules.Get(entryName); has { + return module, nil + } + } + return nil, nil +} diff --git a/modules/git/submodule.go b/modules/git/commit_submodule_file.go similarity index 83% rename from modules/git/submodule.go rename to modules/git/commit_submodule_file.go index b99c81582b7..bdec35f6829 100644 --- a/modules/git/submodule.go +++ b/modules/git/commit_submodule_file.go @@ -15,24 +15,15 @@ import ( var scpSyntax = regexp.MustCompile(`^([a-zA-Z0-9_]+@)?([a-zA-Z0-9._-]+):(.*)$`) -// SubModule submodule is a reference on git repository -type SubModule struct { - Name string - URL string -} - -// SubModuleFile represents a file with submodule type. -type SubModuleFile struct { - *Commit - +// CommitSubModuleFile represents a file with submodule type. +type CommitSubModuleFile struct { refURL string refID string } -// NewSubModuleFile create a new submodule file -func NewSubModuleFile(c *Commit, refURL, refID string) *SubModuleFile { - return &SubModuleFile{ - Commit: c, +// NewCommitSubModuleFile create a new submodule file +func NewCommitSubModuleFile(refURL, refID string) *CommitSubModuleFile { + return &CommitSubModuleFile{ refURL: refURL, refID: refID, } @@ -109,11 +100,12 @@ func getRefURL(refURL, urlPrefix, repoFullName, sshDomain string) string { } // RefURL guesses and returns reference URL. -func (sf *SubModuleFile) RefURL(urlPrefix, repoFullName, sshDomain string) string { +// FIXME: template passes AppURL as urlPrefix, it needs to figure out the correct approach (no hard-coded AppURL anymore) +func (sf *CommitSubModuleFile) RefURL(urlPrefix, repoFullName, sshDomain string) string { return getRefURL(sf.refURL, urlPrefix, repoFullName, sshDomain) } // RefID returns reference ID. -func (sf *SubModuleFile) RefID() string { +func (sf *CommitSubModuleFile) RefID() string { return sf.refID } diff --git a/modules/git/submodule_test.go b/modules/git/commit_submodule_file_test.go similarity index 97% rename from modules/git/submodule_test.go rename to modules/git/commit_submodule_file_test.go index e05f2510c4c..473b996b820 100644 --- a/modules/git/submodule_test.go +++ b/modules/git/commit_submodule_file_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestGetRefURL(t *testing.T) { +func TestCommitSubModuleFileGetRefURL(t *testing.T) { kases := []struct { refURL string prefixURL string diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go index 0ddeb182ef8..bf381a53501 100644 --- a/modules/git/commit_test.go +++ b/modules/git/commit_test.go @@ -135,7 +135,7 @@ author KN4CK3R 1711702962 +0100 committer KN4CK3R 1711702962 +0100 encoding ISO-8859-1 gpgsig -----BEGIN PGP SIGNATURE----- - + iQGzBAABCgAdFiEE9HRrbqvYxPT8PXbefPSEkrowAa8FAmYGg7IACgkQfPSEkrow Aa9olwv+P0HhtCM6CRvlUmPaqswRsDPNR4i66xyXGiSxdI9V5oJL7HLiQIM7KrFR gizKa2COiGtugv8fE+TKqXKaJx6uJUJEjaBd8E9Af9PrAzjWj+A84lU6/PgPS8hq @@ -150,7 +150,7 @@ gpgsig -----BEGIN PGP SIGNATURE----- -----END PGP SIGNATURE----- ISO-8859-1` - + commitString = strings.ReplaceAll(commitString, "", " ") sha := &Sha1Hash{0xfe, 0xaf, 0x4b, 0xa6, 0xbc, 0x63, 0x5f, 0xec, 0x44, 0x2f, 0x46, 0xdd, 0xd4, 0x51, 0x24, 0x16, 0xec, 0x43, 0xc2, 0xc2} gitRepo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare")) assert.NoError(t, err) diff --git a/modules/git/config.go b/modules/git/config.go new file mode 100644 index 00000000000..9c36cf16540 --- /dev/null +++ b/modules/git/config.go @@ -0,0 +1,187 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "fmt" + "os" + "regexp" + "runtime" + "strings" + + "code.gitea.io/gitea/modules/setting" +) + +// syncGitConfig only modifies gitconfig, won't change global variables (otherwise there will be data-race problem) +func syncGitConfig() (err error) { + if err = os.MkdirAll(HomeDir(), os.ModePerm); err != nil { + return fmt.Errorf("unable to prepare git home directory %s, err: %w", HomeDir(), err) + } + + // first, write user's git config options to git config file + // user config options could be overwritten by builtin values later, because if a value is builtin, it must have some special purposes + for k, v := range setting.GitConfig.Options { + if err = configSet(strings.ToLower(k), v); err != nil { + return err + } + } + + // 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", + } { + if err := configSetNonExist(configKey, defaultValue); err != nil { + return err + } + } + + // Set git some configurations - these must be set to these values for gitea to work correctly + if err := configSet("core.quotePath", "false"); err != nil { + return err + } + + if DefaultFeatures().CheckVersionAtLeast("2.10") { + if err := configSet("receive.advertisePushOptions", "true"); err != nil { + return err + } + } + + if DefaultFeatures().CheckVersionAtLeast("2.18") { + if err := configSet("core.commitGraph", "true"); err != nil { + return err + } + if err := configSet("gc.writeCommitGraph", "true"); err != nil { + return err + } + if err := configSet("fetch.writeCommitGraph", "true"); err != nil { + return err + } + } + + if DefaultFeatures().SupportProcReceive { + // set support for AGit flow + if err := configAddNonExist("receive.procReceiveRefs", "refs/for"); err != nil { + return err + } + } else { + if err := configUnsetAll("receive.procReceiveRefs", "refs/for"); err != nil { + return err + } + } + + // Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user. + // However, some docker users and samba users find it difficult to configure their systems correctly, + // so that Gitea's git repositories are owned by the Gitea user. + // (Possibly Windows Service users - but ownership in this case should really be set correctly on the filesystem.) + // See issue: https://github.com/go-gitea/gitea/issues/19455 + // As Gitea now always use its internal git config file, and access to the git repositories is managed through Gitea, + // it is now safe to set "safe.directory=*" for internal usage only. + // Although this setting is only supported by some new git versions, it is also tolerated by earlier versions + if err := configAddNonExist("safe.directory", "*"); err != nil { + return err + } + + if runtime.GOOS == "windows" { + 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 + } + } + + // By default partial clones are disabled, enable them from git v2.22 + if !setting.Git.DisablePartialClone && DefaultFeatures().CheckVersionAtLeast("2.22") { + if err = configSet("uploadpack.allowfilter", "true"); err != nil { + return err + } + err = configSet("uploadpack.allowAnySHA1InWant", "true") + } else { + if err = configUnsetAll("uploadpack.allowfilter", "true"); err != nil { + return err + } + err = configUnsetAll("uploadpack.allowAnySHA1InWant", "true") + } + + return err +} + +func configSet(key, value string) error { + stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) + if err != nil && !IsErrorExitCode(err, 1) { + return fmt.Errorf("failed to get git config %s, err: %w", key, err) + } + + currValue := strings.TrimSpace(stdout) + if currValue == value { + return nil + } + + _, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil) + if err != nil { + return fmt.Errorf("failed to set git global config %s, err: %w", key, err) + } + + return nil +} + +func configSetNonExist(key, value string) error { + _, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) + if err == nil { + // already exist + return nil + } + if IsErrorExitCode(err, 1) { + // not exist, set new config + _, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(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", "--global", "--get").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(nil) + if err == nil { + // already exist + return nil + } + if IsErrorExitCode(err, 1) { + // not exist, add new config + _, _, err = NewCommand(DefaultContext, "config", "--global", "--add").AddDynamicArguments(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) +} + +func configUnsetAll(key, value string) error { + _, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) + if err == nil { + // exist, need to remove + _, _, err = NewCommand(DefaultContext, "config", "--global", "--unset-all").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(nil) + if err != nil { + return fmt.Errorf("failed to unset git global config %s, err: %w", key, err) + } + return nil + } + if IsErrorExitCode(err, 1) { + // not exist + return nil + } + return fmt.Errorf("failed to get git config %s, err: %w", key, err) +} diff --git a/modules/git/config_submodule.go b/modules/git/config_submodule.go new file mode 100644 index 00000000000..fe332088507 --- /dev/null +++ b/modules/git/config_submodule.go @@ -0,0 +1,75 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "bufio" + "fmt" + "io" + "strings" +) + +// SubModule is a reference on git repository +type SubModule struct { + Path string + URL string + Branch string // this field is newly added but not really used +} + +// configParseSubModules this is not a complete parse for gitmodules file, it only +// parses the url and path of submodules. At the moment it only parses well-formed gitmodules files. +// In the future, there should be a complete implementation of https://git-scm.com/docs/git-config#_syntax +func configParseSubModules(r io.Reader) (*ObjectCache[*SubModule], error) { + var subModule *SubModule + subModules := newObjectCache[*SubModule]() + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // Skip empty lines and comments + if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") { + continue + } + + // Section header [section] + if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { + if subModule != nil { + subModules.Set(subModule.Path, subModule) + } + if strings.HasPrefix(line, "[submodule") { + subModule = &SubModule{} + } else { + subModule = nil + } + continue + } + + if subModule == nil { + continue + } + + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + switch key { + case "path": + subModule.Path = value + case "url": + subModule.URL = value + case "branch": + subModule.Branch = value + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error reading file: %w", err) + } + if subModule != nil { + subModules.Set(subModule.Path, subModule) + } + return subModules, nil +} diff --git a/modules/git/config_submodule_test.go b/modules/git/config_submodule_test.go new file mode 100644 index 00000000000..f0846c7bfb7 --- /dev/null +++ b/modules/git/config_submodule_test.go @@ -0,0 +1,49 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConfigSubmodule(t *testing.T) { + input := ` +[core] +path = test + +[submodule "submodule1"] + path = path1 + url = https://gitea.io/foo/foo + #branch = b1 + +[other1] +branch = master + +[submodule "submodule2"] + path = path2 + url = https://gitea.io/bar/bar + branch = b2 + +[other2] +branch = main + +[submodule "submodule3"] + path = path3 + url = https://gitea.io/xxx/xxx +` + + subModules, err := configParseSubModules(strings.NewReader(input)) + assert.NoError(t, err) + assert.Len(t, subModules.cache, 3) + + sm1, _ := subModules.Get("path1") + assert.Equal(t, &SubModule{Path: "path1", URL: "https://gitea.io/foo/foo", Branch: ""}, sm1) + sm2, _ := subModules.Get("path2") + assert.Equal(t, &SubModule{Path: "path2", URL: "https://gitea.io/bar/bar", Branch: "b2"}, sm2) + sm3, _ := subModules.Get("path3") + assert.Equal(t, &SubModule{Path: "path3", URL: "https://gitea.io/xxx/xxx", Branch: ""}, sm3) +} diff --git a/modules/git/config_test.go b/modules/git/config_test.go new file mode 100644 index 00000000000..59f70c99e2f --- /dev/null +++ b/modules/git/config_test.go @@ -0,0 +1,66 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "os" + "strings" + "testing" + + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" +) + +func gitConfigContains(sub string) bool { + if b, err := os.ReadFile(HomeDir() + "/.gitconfig"); err == nil { + return strings.Contains(string(b), sub) + } + return false +} + +func TestGitConfig(t *testing.T) { + assert.False(t, gitConfigContains("key-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")) + + 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")) + + assert.NoError(t, configSet("test.key-x", "*")) + assert.True(t, gitConfigContains("key-x = *")) + assert.NoError(t, configSetNonExist("test.key-x", "*")) + assert.NoError(t, configUnsetAll("test.key-x", "*")) + assert.False(t, gitConfigContains("key-x = *")) +} + +func TestSyncConfig(t *testing.T) { + oldGitConfig := setting.GitConfig + defer func() { + setting.GitConfig = oldGitConfig + }() + + setting.GitConfig.Options["sync-test.cfg-key-a"] = "CfgValA" + assert.NoError(t, syncGitConfig()) + assert.True(t, gitConfigContains("[sync-test]")) + assert.True(t, gitConfigContains("cfg-key-a = CfgValA")) +} diff --git a/modules/git/fsck.go b/modules/git/fsck.go new file mode 100644 index 00000000000..cec27f165b9 --- /dev/null +++ b/modules/git/fsck.go @@ -0,0 +1,14 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "context" + "time" +) + +// Fsck verifies the connectivity and validity of the objects in the database +func Fsck(ctx context.Context, repoPath string, timeout time.Duration, args TrustedCmdArgs) error { + return NewCommand(ctx, "fsck").AddArguments(args...).Run(&RunOpts{Timeout: timeout, Dir: repoPath}) +} diff --git a/modules/git/git.go b/modules/git/git.go index a19dd7771ba..e3e5b832747 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -11,7 +11,6 @@ import ( "os" "os/exec" "path/filepath" - "regexp" "runtime" "strings" "time" @@ -95,17 +94,18 @@ func parseGitVersionLine(s string) (*version.Version, error) { return version.NewVersion(versionString) } -// SetExecutablePath changes the path of git executable and checks the file permission and version. -func SetExecutablePath(path string) error { - // If path is empty, we use the default value of GitExecutable "git" to search for the location of git. - if path != "" { - GitExecutable = path +func checkGitVersionCompatibility(gitVer *version.Version) error { + badVersions := []struct { + Version *version.Version + Reason string + }{ + {version.Must(version.NewVersion("2.43.1")), "regression bug of GIT_FLUSH"}, } - absPath, err := exec.LookPath(GitExecutable) - if err != nil { - return fmt.Errorf("git not found: %w", err) + for _, bad := range badVersions { + if gitVer.Equal(bad.Version) { + return errors.New(bad.Reason) + } } - GitExecutable = absPath return nil } @@ -128,6 +128,20 @@ func ensureGitVersion() error { return nil } +// SetExecutablePath changes the path of git executable and checks the file permission and version. +func SetExecutablePath(path string) error { + // If path is empty, we use the default value of GitExecutable "git" to search for the location of git. + if path != "" { + GitExecutable = path + } + absPath, err := exec.LookPath(GitExecutable) + if err != nil { + return fmt.Errorf("git not found: %w", err) + } + GitExecutable = absPath + return nil +} + // HomeDir is the home dir for git to store the global config file used by Gitea internally func HomeDir() string { if setting.Git.HomePath == "" { @@ -204,196 +218,3 @@ func InitFull(ctx context.Context) (err error) { return syncGitConfig() } - -// syncGitConfig only modifies gitconfig, won't change global variables (otherwise there will be data-race problem) -func syncGitConfig() (err error) { - if err = os.MkdirAll(HomeDir(), os.ModePerm); err != nil { - return fmt.Errorf("unable to prepare git home directory %s, err: %w", HomeDir(), err) - } - - // first, write user's git config options to git config file - // user config options could be overwritten by builtin values later, because if a value is builtin, it must have some special purposes - for k, v := range setting.GitConfig.Options { - if err = configSet(strings.ToLower(k), v); err != nil { - return err - } - } - - // 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", - } { - if err := configSetNonExist(configKey, defaultValue); err != nil { - return err - } - } - - // Set git some configurations - these must be set to these values for gitea to work correctly - if err := configSet("core.quotePath", "false"); err != nil { - return err - } - - if DefaultFeatures().CheckVersionAtLeast("2.10") { - if err := configSet("receive.advertisePushOptions", "true"); err != nil { - return err - } - } - - if DefaultFeatures().CheckVersionAtLeast("2.18") { - if err := configSet("core.commitGraph", "true"); err != nil { - return err - } - if err := configSet("gc.writeCommitGraph", "true"); err != nil { - return err - } - if err := configSet("fetch.writeCommitGraph", "true"); err != nil { - return err - } - } - - if DefaultFeatures().SupportProcReceive { - // set support for AGit flow - if err := configAddNonExist("receive.procReceiveRefs", "refs/for"); err != nil { - return err - } - } else { - if err := configUnsetAll("receive.procReceiveRefs", "refs/for"); err != nil { - return err - } - } - - // Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user. - // However, some docker users and samba users find it difficult to configure their systems correctly, - // so that Gitea's git repositories are owned by the Gitea user. - // (Possibly Windows Service users - but ownership in this case should really be set correctly on the filesystem.) - // See issue: https://github.com/go-gitea/gitea/issues/19455 - // As Gitea now always use its internal git config file, and access to the git repositories is managed through Gitea, - // it is now safe to set "safe.directory=*" for internal usage only. - // Although this setting is only supported by some new git versions, it is also tolerated by earlier versions - if err := configAddNonExist("safe.directory", "*"); err != nil { - return err - } - - if runtime.GOOS == "windows" { - 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 - } - } - - // By default partial clones are disabled, enable them from git v2.22 - if !setting.Git.DisablePartialClone && DefaultFeatures().CheckVersionAtLeast("2.22") { - if err = configSet("uploadpack.allowfilter", "true"); err != nil { - return err - } - err = configSet("uploadpack.allowAnySHA1InWant", "true") - } else { - if err = configUnsetAll("uploadpack.allowfilter", "true"); err != nil { - return err - } - err = configUnsetAll("uploadpack.allowAnySHA1InWant", "true") - } - - return err -} - -func checkGitVersionCompatibility(gitVer *version.Version) error { - badVersions := []struct { - Version *version.Version - Reason string - }{ - {version.Must(version.NewVersion("2.43.1")), "regression bug of GIT_FLUSH"}, - } - for _, bad := range badVersions { - if gitVer.Equal(bad.Version) { - return errors.New(bad.Reason) - } - } - return nil -} - -func configSet(key, value string) error { - stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) - if err != nil && !IsErrorExitCode(err, 1) { - return fmt.Errorf("failed to get git config %s, err: %w", key, err) - } - - currValue := strings.TrimSpace(stdout) - if currValue == value { - return nil - } - - _, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil) - if err != nil { - return fmt.Errorf("failed to set git global config %s, err: %w", key, err) - } - - return nil -} - -func configSetNonExist(key, value string) error { - _, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) - if err == nil { - // already exist - return nil - } - if IsErrorExitCode(err, 1) { - // not exist, set new config - _, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(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", "--global", "--get").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(nil) - if err == nil { - // already exist - return nil - } - if IsErrorExitCode(err, 1) { - // not exist, add new config - _, _, err = NewCommand(DefaultContext, "config", "--global", "--add").AddDynamicArguments(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) -} - -func configUnsetAll(key, value string) error { - _, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) - if err == nil { - // exist, need to remove - _, _, err = NewCommand(DefaultContext, "config", "--global", "--unset-all").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(nil) - if err != nil { - return fmt.Errorf("failed to unset git global config %s, err: %w", key, err) - } - return nil - } - if IsErrorExitCode(err, 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 -func Fsck(ctx context.Context, repoPath string, timeout time.Duration, args TrustedCmdArgs) error { - return NewCommand(ctx, "fsck").AddArguments(args...).Run(&RunOpts{Timeout: timeout, Dir: repoPath}) -} diff --git a/modules/git/git_test.go b/modules/git/git_test.go index fc92bebe047..5472842b76e 100644 --- a/modules/git/git_test.go +++ b/modules/git/git_test.go @@ -7,7 +7,6 @@ import ( "context" "fmt" "os" - "strings" "testing" "code.gitea.io/gitea/modules/setting" @@ -43,58 +42,6 @@ func TestMain(m *testing.M) { } } -func gitConfigContains(sub string) bool { - if b, err := os.ReadFile(HomeDir() + "/.gitconfig"); err == nil { - return strings.Contains(string(b), sub) - } - return false -} - -func TestGitConfig(t *testing.T) { - assert.False(t, gitConfigContains("key-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")) - - 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")) - - assert.NoError(t, configSet("test.key-x", "*")) - assert.True(t, gitConfigContains("key-x = *")) - assert.NoError(t, configSetNonExist("test.key-x", "*")) - assert.NoError(t, configUnsetAll("test.key-x", "*")) - assert.False(t, gitConfigContains("key-x = *")) -} - -func TestSyncConfig(t *testing.T) { - oldGitConfig := setting.GitConfig - defer func() { - setting.GitConfig = oldGitConfig - }() - - setting.GitConfig.Options["sync-test.cfg-key-a"] = "CfgValA" - assert.NoError(t, syncGitConfig()) - assert.True(t, gitConfigContains("[sync-test]")) - assert.True(t, gitConfigContains("cfg-key-a = CfgValA")) -} - func TestParseGitVersion(t *testing.T) { v, err := parseGitVersionLine("git version 2.29.3") assert.NoError(t, err) diff --git a/modules/git/repo_base_gogit.go b/modules/git/repo_base_gogit.go index a1127f4e6c5..0ca1ea79c21 100644 --- a/modules/git/repo_base_gogit.go +++ b/modules/git/repo_base_gogit.go @@ -28,7 +28,7 @@ const isGogit = true type Repository struct { Path string - tagCache *ObjectCache + tagCache *ObjectCache[*Tag] gogitRepo *gogit.Repository gogitStorage *filesystem.Storage @@ -79,7 +79,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { Path: repoPath, gogitRepo: gogitRepo, gogitStorage: storage, - tagCache: newObjectCache(), + tagCache: newObjectCache[*Tag](), Ctx: ctx, objectFormat: ParseGogitHash(plumbing.ZeroHash).Type(), }, nil diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go index 3eb2e2ee6b2..477e3b87429 100644 --- a/modules/git/repo_base_nogogit.go +++ b/modules/git/repo_base_nogogit.go @@ -21,7 +21,7 @@ const isGogit = false type Repository struct { Path string - tagCache *ObjectCache + tagCache *ObjectCache[*Tag] gpgSettings *GPGSettings @@ -53,7 +53,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { return &Repository{ Path: repoPath, - tagCache: newObjectCache(), + tagCache: newObjectCache[*Tag](), Ctx: ctx, }, nil } diff --git a/modules/git/repo_tag_gogit.go b/modules/git/repo_tag_gogit.go index 4a7a06e9bda..3e1b4e89ad6 100644 --- a/modules/git/repo_tag_gogit.go +++ b/modules/git/repo_tag_gogit.go @@ -72,7 +72,7 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) { t, ok := repo.tagCache.Get(tagID.String()) if ok { log.Debug("Hit cache: %s", tagID) - tagClone := *t.(*Tag) + tagClone := *t tagClone.Name = name // This is necessary because lightweight tags may have same id return &tagClone, nil } diff --git a/modules/git/repo_tag_nogogit.go b/modules/git/repo_tag_nogogit.go index 8b06a6a1c3c..e0a3104249e 100644 --- a/modules/git/repo_tag_nogogit.go +++ b/modules/git/repo_tag_nogogit.go @@ -51,7 +51,7 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) { t, ok := repo.tagCache.Get(tagID.String()) if ok { log.Debug("Hit cache: %s", tagID) - tagClone := *t.(*Tag) + tagClone := *t tagClone.Name = name // This is necessary because lightweight tags may have same id return &tagClone, nil } diff --git a/modules/git/utils.go b/modules/git/utils.go index 53211c64518..56cba9087a3 100644 --- a/modules/git/utils.go +++ b/modules/git/utils.go @@ -15,27 +15,25 @@ import ( ) // ObjectCache provides thread-safe cache operations. -type ObjectCache struct { +type ObjectCache[T any] struct { lock sync.RWMutex - cache map[string]any + cache map[string]T } -func newObjectCache() *ObjectCache { - return &ObjectCache{ - cache: make(map[string]any, 10), - } +func newObjectCache[T any]() *ObjectCache[T] { + return &ObjectCache[T]{cache: make(map[string]T, 10)} } -// Set add obj to cache -func (oc *ObjectCache) Set(id string, obj any) { +// Set adds obj to cache +func (oc *ObjectCache[T]) Set(id string, obj T) { oc.lock.Lock() defer oc.lock.Unlock() oc.cache[id] = obj } -// Get get cached obj by id -func (oc *ObjectCache) Get(id string) (any, bool) { +// Get gets cached obj by id +func (oc *ObjectCache[T]) Get(id string) (T, bool) { oc.lock.RLock() defer oc.lock.RUnlock()