aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLunny Xiao <xiaolunwen@gmail.com>2024-11-20 11:26:12 -0800
committerGitHub <noreply@github.com>2024-11-20 19:26:12 +0000
commit33850a83fe4ebd23a762a7aac81614c42e303bfa (patch)
treedbb95edd9c1c2604d262441b1e236cf107df1327
parent407b6e6dfc7ee9ebb8a16c7f1a786e4c24d0516e (diff)
downloadgitea-33850a83fe4ebd23a762a7aac81614c42e303bfa.tar.gz
gitea-33850a83fe4ebd23a762a7aac81614c42e303bfa.zip
Fix submodule parsing (#32571)
Fix #32568, parse `.gitmodules` correctly --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
-rw-r--r--modules/git/commit.go66
-rw-r--r--modules/git/commit_info.go2
-rw-r--r--modules/git/commit_info_gogit.go4
-rw-r--r--modules/git/commit_info_nogogit.go2
-rw-r--r--modules/git/commit_submodule.go47
-rw-r--r--modules/git/commit_submodule_file.go (renamed from modules/git/submodule.go)24
-rw-r--r--modules/git/commit_submodule_file_test.go (renamed from modules/git/submodule_test.go)2
-rw-r--r--modules/git/commit_test.go4
-rw-r--r--modules/git/config.go187
-rw-r--r--modules/git/config_submodule.go75
-rw-r--r--modules/git/config_submodule_test.go49
-rw-r--r--modules/git/config_test.go66
-rw-r--r--modules/git/fsck.go14
-rw-r--r--modules/git/git.go227
-rw-r--r--modules/git/git_test.go53
-rw-r--r--modules/git/repo_base_gogit.go4
-rw-r--r--modules/git/repo_base_nogogit.go4
-rw-r--r--modules/git/repo_tag_gogit.go2
-rw-r--r--modules/git/repo_tag_nogogit.go2
-rw-r--r--modules/git/utils.go18
20 files changed, 492 insertions, 360 deletions
diff --git a/modules/git/commit.go b/modules/git/commit.go
index 86adaa79a6..010b56948e 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 c740a4e13e..545081275b 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 31ffc9aec1..11b44f7c35 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 cfde64a033..20d586f0ff 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 0000000000..6603061da2
--- /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
index b99c81582b..bdec35f682 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
index e05f2510c4..473b996b82 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 0ddeb182ef..bf381a5350 100644
--- a/modules/git/commit_test.go
+++ b/modules/git/commit_test.go
@@ -135,7 +135,7 @@ author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
encoding ISO-8859-1
gpgsig -----BEGIN PGP SIGNATURE-----
-
+<SPACE>
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, "<SPACE>", " ")
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 0000000000..9c36cf1654
--- /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 0000000000..fe33208850
--- /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 0000000000..f0846c7bfb
--- /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 0000000000..59f70c99e2
--- /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 0000000000..cec27f165b
--- /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 a19dd7771b..e3e5b83274 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 fc92bebe04..5472842b76 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 a1127f4e6c..0ca1ea79c2 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 3eb2e2ee6b..477e3b8742 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 4a7a06e9bd..3e1b4e89ad 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 8b06a6a1c3..e0a3104249 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 53211c6451..56cba9087a 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()