mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-27 01:18:27 +00:00 
			
		
		
		
	Fix submodule parsing (#32571)
Fix #32568, parse `.gitmodules` correctly --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -9,7 +9,6 @@ import ( | |||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"context" | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" |  | ||||||
| 	"io" | 	"io" | ||||||
| 	"os/exec" | 	"os/exec" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| @@ -29,7 +28,7 @@ type Commit struct { | |||||||
| 	Signature     *CommitSignature | 	Signature     *CommitSignature | ||||||
|  |  | ||||||
| 	Parents        []ObjectID // ID strings | 	Parents        []ObjectID // ID strings | ||||||
| 	submoduleCache *ObjectCache | 	submoduleCache *ObjectCache[*SubModule] | ||||||
| } | } | ||||||
|  |  | ||||||
| // CommitSignature represents a git commit signature part. | // 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 | 	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') | // GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only') | ||||||
| func (c *Commit) GetBranchName() (string, error) { | func (c *Commit) GetBranchName() (string, error) { | ||||||
| 	cmd := NewCommand(c.repo.Ctx, "name-rev") | 	cmd := NewCommand(c.repo.Ctx, "name-rev") | ||||||
|   | |||||||
| @@ -7,5 +7,5 @@ package git | |||||||
| type CommitInfo struct { | type CommitInfo struct { | ||||||
| 	Entry         *TreeEntry | 	Entry         *TreeEntry | ||||||
| 	Commit        *Commit | 	Commit        *Commit | ||||||
| 	SubModuleFile *SubModuleFile | 	SubModuleFile *CommitSubModuleFile | ||||||
| } | } | ||||||
|   | |||||||
| @@ -71,7 +71,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath | |||||||
| 			commitsInfo[i].Commit = entryCommit | 			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() { | 		if entry.IsSubModule() { | ||||||
| 			subModuleURL := "" | 			subModuleURL := "" | ||||||
| 			var fullPath string | 			var fullPath string | ||||||
| @@ -85,7 +85,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath | |||||||
| 			} else if subModule != nil { | 			} else if subModule != nil { | ||||||
| 				subModuleURL = subModule.URL | 				subModuleURL = subModule.URL | ||||||
| 			} | 			} | ||||||
| 			subModuleFile := NewSubModuleFile(commitsInfo[i].Commit, subModuleURL, entry.ID.String()) | 			subModuleFile := NewCommitSubModuleFile(subModuleURL, entry.ID.String()) | ||||||
| 			commitsInfo[i].SubModuleFile = subModuleFile | 			commitsInfo[i].SubModuleFile = subModuleFile | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -79,7 +79,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath | |||||||
| 			} else if subModule != nil { | 			} else if subModule != nil { | ||||||
| 				subModuleURL = subModule.URL | 				subModuleURL = subModule.URL | ||||||
| 			} | 			} | ||||||
| 			subModuleFile := NewSubModuleFile(commitsInfo[i].Commit, subModuleURL, entry.ID.String()) | 			subModuleFile := NewCommitSubModuleFile(subModuleURL, entry.ID.String()) | ||||||
| 			commitsInfo[i].SubModuleFile = subModuleFile | 			commitsInfo[i].SubModuleFile = subModuleFile | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
							
								
								
									
										47
									
								
								modules/git/commit_submodule.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								modules/git/commit_submodule.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
|  | } | ||||||
| @@ -15,24 +15,15 @@ import ( | |||||||
| 
 | 
 | ||||||
| var scpSyntax = regexp.MustCompile(`^([a-zA-Z0-9_]+@)?([a-zA-Z0-9._-]+):(.*)$`) | var scpSyntax = regexp.MustCompile(`^([a-zA-Z0-9_]+@)?([a-zA-Z0-9._-]+):(.*)$`) | ||||||
| 
 | 
 | ||||||
| // SubModule submodule is a reference on git repository | // CommitSubModuleFile represents a file with submodule type. | ||||||
| type SubModule struct { | type CommitSubModuleFile struct { | ||||||
| 	Name string |  | ||||||
| 	URL  string |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // SubModuleFile represents a file with submodule type. |  | ||||||
| type SubModuleFile struct { |  | ||||||
| 	*Commit |  | ||||||
| 
 |  | ||||||
| 	refURL string | 	refURL string | ||||||
| 	refID  string | 	refID  string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewSubModuleFile create a new submodule file | // NewCommitSubModuleFile create a new submodule file | ||||||
| func NewSubModuleFile(c *Commit, refURL, refID string) *SubModuleFile { | func NewCommitSubModuleFile(refURL, refID string) *CommitSubModuleFile { | ||||||
| 	return &SubModuleFile{ | 	return &CommitSubModuleFile{ | ||||||
| 		Commit: c, |  | ||||||
| 		refURL: refURL, | 		refURL: refURL, | ||||||
| 		refID:  refID, | 		refID:  refID, | ||||||
| 	} | 	} | ||||||
| @@ -109,11 +100,12 @@ func getRefURL(refURL, urlPrefix, repoFullName, sshDomain string) string { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // RefURL guesses and returns reference URL. | // 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) | 	return getRefURL(sf.refURL, urlPrefix, repoFullName, sshDomain) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // RefID returns reference ID. | // RefID returns reference ID. | ||||||
| func (sf *SubModuleFile) RefID() string { | func (sf *CommitSubModuleFile) RefID() string { | ||||||
| 	return sf.refID | 	return sf.refID | ||||||
| } | } | ||||||
| @@ -9,7 +9,7 @@ import ( | |||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestGetRefURL(t *testing.T) { | func TestCommitSubModuleFileGetRefURL(t *testing.T) { | ||||||
| 	kases := []struct { | 	kases := []struct { | ||||||
| 		refURL     string | 		refURL     string | ||||||
| 		prefixURL  string | 		prefixURL  string | ||||||
| @@ -135,7 +135,7 @@ author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100 | |||||||
| committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100 | committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100 | ||||||
| encoding ISO-8859-1 | encoding ISO-8859-1 | ||||||
| gpgsig -----BEGIN PGP SIGNATURE----- | gpgsig -----BEGIN PGP SIGNATURE----- | ||||||
|   | <SPACE> | ||||||
|  iQGzBAABCgAdFiEE9HRrbqvYxPT8PXbefPSEkrowAa8FAmYGg7IACgkQfPSEkrow |  iQGzBAABCgAdFiEE9HRrbqvYxPT8PXbefPSEkrowAa8FAmYGg7IACgkQfPSEkrow | ||||||
|  Aa9olwv+P0HhtCM6CRvlUmPaqswRsDPNR4i66xyXGiSxdI9V5oJL7HLiQIM7KrFR |  Aa9olwv+P0HhtCM6CRvlUmPaqswRsDPNR4i66xyXGiSxdI9V5oJL7HLiQIM7KrFR | ||||||
|  gizKa2COiGtugv8fE+TKqXKaJx6uJUJEjaBd8E9Af9PrAzjWj+A84lU6/PgPS8hq |  gizKa2COiGtugv8fE+TKqXKaJx6uJUJEjaBd8E9Af9PrAzjWj+A84lU6/PgPS8hq | ||||||
| @@ -150,7 +150,7 @@ gpgsig -----BEGIN PGP SIGNATURE----- | |||||||
|  -----END PGP SIGNATURE----- |  -----END PGP SIGNATURE----- | ||||||
|  |  | ||||||
| ISO-8859-1` | 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} | 	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")) | 	gitRepo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare")) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
|   | |||||||
							
								
								
									
										187
									
								
								modules/git/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								modules/git/config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||||
|  | } | ||||||
							
								
								
									
										75
									
								
								modules/git/config_submodule.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								modules/git/config_submodule.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||||
|  | } | ||||||
							
								
								
									
										49
									
								
								modules/git/config_submodule_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								modules/git/config_submodule_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||||
|  | } | ||||||
							
								
								
									
										66
									
								
								modules/git/config_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								modules/git/config_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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")) | ||||||
|  | } | ||||||
							
								
								
									
										14
									
								
								modules/git/fsck.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								modules/git/fsck.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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}) | ||||||
|  | } | ||||||
| @@ -11,7 +11,6 @@ import ( | |||||||
| 	"os" | 	"os" | ||||||
| 	"os/exec" | 	"os/exec" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"regexp" |  | ||||||
| 	"runtime" | 	"runtime" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| @@ -95,17 +94,18 @@ func parseGitVersionLine(s string) (*version.Version, error) { | |||||||
| 	return version.NewVersion(versionString) | 	return version.NewVersion(versionString) | ||||||
| } | } | ||||||
|  |  | ||||||
| // SetExecutablePath changes the path of git executable and checks the file permission and version. | func checkGitVersionCompatibility(gitVer *version.Version) error { | ||||||
| func SetExecutablePath(path string) error { | 	badVersions := []struct { | ||||||
| 	// If path is empty, we use the default value of GitExecutable "git" to search for the location of git. | 		Version *version.Version | ||||||
| 	if path != "" { | 		Reason  string | ||||||
| 		GitExecutable = path | 	}{ | ||||||
|  | 		{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) | ||||||
| 		} | 		} | ||||||
| 	absPath, err := exec.LookPath(GitExecutable) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("git not found: %w", err) |  | ||||||
| 	} | 	} | ||||||
| 	GitExecutable = absPath |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -128,6 +128,20 @@ func ensureGitVersion() error { | |||||||
| 	return nil | 	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 | // HomeDir is the home dir for git to store the global config file used by Gitea internally | ||||||
| func HomeDir() string { | func HomeDir() string { | ||||||
| 	if setting.Git.HomePath == "" { | 	if setting.Git.HomePath == "" { | ||||||
| @@ -204,196 +218,3 @@ func InitFull(ctx context.Context) (err error) { | |||||||
|  |  | ||||||
| 	return syncGitConfig() | 	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}) |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -7,7 +7,6 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| 	"strings" |  | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"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) { | func TestParseGitVersion(t *testing.T) { | ||||||
| 	v, err := parseGitVersionLine("git version 2.29.3") | 	v, err := parseGitVersionLine("git version 2.29.3") | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ const isGogit = true | |||||||
| type Repository struct { | type Repository struct { | ||||||
| 	Path string | 	Path string | ||||||
|  |  | ||||||
| 	tagCache *ObjectCache | 	tagCache *ObjectCache[*Tag] | ||||||
|  |  | ||||||
| 	gogitRepo    *gogit.Repository | 	gogitRepo    *gogit.Repository | ||||||
| 	gogitStorage *filesystem.Storage | 	gogitStorage *filesystem.Storage | ||||||
| @@ -79,7 +79,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { | |||||||
| 		Path:         repoPath, | 		Path:         repoPath, | ||||||
| 		gogitRepo:    gogitRepo, | 		gogitRepo:    gogitRepo, | ||||||
| 		gogitStorage: storage, | 		gogitStorage: storage, | ||||||
| 		tagCache:     newObjectCache(), | 		tagCache:     newObjectCache[*Tag](), | ||||||
| 		Ctx:          ctx, | 		Ctx:          ctx, | ||||||
| 		objectFormat: ParseGogitHash(plumbing.ZeroHash).Type(), | 		objectFormat: ParseGogitHash(plumbing.ZeroHash).Type(), | ||||||
| 	}, nil | 	}, nil | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ const isGogit = false | |||||||
| type Repository struct { | type Repository struct { | ||||||
| 	Path string | 	Path string | ||||||
|  |  | ||||||
| 	tagCache *ObjectCache | 	tagCache *ObjectCache[*Tag] | ||||||
|  |  | ||||||
| 	gpgSettings *GPGSettings | 	gpgSettings *GPGSettings | ||||||
|  |  | ||||||
| @@ -53,7 +53,7 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { | |||||||
|  |  | ||||||
| 	return &Repository{ | 	return &Repository{ | ||||||
| 		Path:     repoPath, | 		Path:     repoPath, | ||||||
| 		tagCache: newObjectCache(), | 		tagCache: newObjectCache[*Tag](), | ||||||
| 		Ctx:      ctx, | 		Ctx:      ctx, | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -72,7 +72,7 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) { | |||||||
| 	t, ok := repo.tagCache.Get(tagID.String()) | 	t, ok := repo.tagCache.Get(tagID.String()) | ||||||
| 	if ok { | 	if ok { | ||||||
| 		log.Debug("Hit cache: %s", tagID) | 		log.Debug("Hit cache: %s", tagID) | ||||||
| 		tagClone := *t.(*Tag) | 		tagClone := *t | ||||||
| 		tagClone.Name = name // This is necessary because lightweight tags may have same id | 		tagClone.Name = name // This is necessary because lightweight tags may have same id | ||||||
| 		return &tagClone, nil | 		return &tagClone, nil | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -51,7 +51,7 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) { | |||||||
| 	t, ok := repo.tagCache.Get(tagID.String()) | 	t, ok := repo.tagCache.Get(tagID.String()) | ||||||
| 	if ok { | 	if ok { | ||||||
| 		log.Debug("Hit cache: %s", tagID) | 		log.Debug("Hit cache: %s", tagID) | ||||||
| 		tagClone := *t.(*Tag) | 		tagClone := *t | ||||||
| 		tagClone.Name = name // This is necessary because lightweight tags may have same id | 		tagClone.Name = name // This is necessary because lightweight tags may have same id | ||||||
| 		return &tagClone, nil | 		return &tagClone, nil | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -15,27 +15,25 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| // ObjectCache provides thread-safe cache operations. | // ObjectCache provides thread-safe cache operations. | ||||||
| type ObjectCache struct { | type ObjectCache[T any] struct { | ||||||
| 	lock  sync.RWMutex | 	lock  sync.RWMutex | ||||||
| 	cache map[string]any | 	cache map[string]T | ||||||
| } | } | ||||||
|  |  | ||||||
| func newObjectCache() *ObjectCache { | func newObjectCache[T any]() *ObjectCache[T] { | ||||||
| 	return &ObjectCache{ | 	return &ObjectCache[T]{cache: make(map[string]T, 10)} | ||||||
| 		cache: make(map[string]any, 10), |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // Set add obj to cache | // Set adds obj to cache | ||||||
| func (oc *ObjectCache) Set(id string, obj any) { | func (oc *ObjectCache[T]) Set(id string, obj T) { | ||||||
| 	oc.lock.Lock() | 	oc.lock.Lock() | ||||||
| 	defer oc.lock.Unlock() | 	defer oc.lock.Unlock() | ||||||
|  |  | ||||||
| 	oc.cache[id] = obj | 	oc.cache[id] = obj | ||||||
| } | } | ||||||
|  |  | ||||||
| // Get get cached obj by id | // Get gets cached obj by id | ||||||
| func (oc *ObjectCache) Get(id string) (any, bool) { | func (oc *ObjectCache[T]) Get(id string) (T, bool) { | ||||||
| 	oc.lock.RLock() | 	oc.lock.RLock() | ||||||
| 	defer oc.lock.RUnlock() | 	defer oc.lock.RUnlock() | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user