diff options
Diffstat (limited to 'modules/git/git.go')
-rw-r--r-- | modules/git/git.go | 416 |
1 files changed, 416 insertions, 0 deletions
diff --git a/modules/git/git.go b/modules/git/git.go new file mode 100644 index 00000000..70232c86 --- /dev/null +++ b/modules/git/git.go @@ -0,0 +1,416 @@ +// Copyright 2015 The Gogs Authors. All rights reserved. +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strings" + "time" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + + "github.com/hashicorp/go-version" +) + +// RequiredVersion is the minimum Git version required +const RequiredVersion = "2.0.0" + +var ( + // GitExecutable is the command name of git + // Could be updated to an absolute path while initialization + GitExecutable = "git" + + // DefaultContext is the default context to run git commands in, must be initialized by git.InitXxx + DefaultContext context.Context + + SupportProcReceive bool // >= 2.29 + SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’ + InvertedGitFlushEnv bool // 2.43.1 + SupportCheckAttrOnBare bool // >= 2.40 + + gitVersion *version.Version +) + +// loadGitVersion returns current Git version from shell. Internal usage only. +func loadGitVersion() error { + // doesn't need RWMutex because it's executed by Init() + if gitVersion != nil { + return nil + } + stdout, _, runErr := NewCommand(DefaultContext, "version").RunStdString(nil) + if runErr != nil { + return runErr + } + + fields := strings.Fields(stdout) + if len(fields) < 3 { + return fmt.Errorf("invalid git version output: %s", stdout) + } + + var versionString string + + // Handle special case on Windows. + i := strings.Index(fields[2], "windows") + if i >= 1 { + versionString = fields[2][:i-1] + } else { + versionString = fields[2] + } + + var err error + gitVersion, err = version.NewVersion(versionString) + return err +} + +// 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 + + err = loadGitVersion() + if err != nil { + return fmt.Errorf("unable to load git version: %w", err) + } + + versionRequired, err := version.NewVersion(RequiredVersion) + if err != nil { + return err + } + + if gitVersion.LessThan(versionRequired) { + moreHint := "get git: https://git-scm.com/download/" + if runtime.GOOS == "linux" { + // there are a lot of CentOS/RHEL users using old git, so we add a special hint for them + if _, err = os.Stat("/etc/redhat-release"); err == nil { + // ius.io is the recommended official(git-scm.com) method to install git + moreHint = "get git: https://git-scm.com/download/linux and https://ius.io" + } + } + return fmt.Errorf("installed git version %q is not supported, Gitea requires git version >= %q, %s", gitVersion.Original(), RequiredVersion, moreHint) + } + + return nil +} + +// VersionInfo returns git version information +func VersionInfo() string { + if gitVersion == nil { + return "(git not found)" + } + format := "%s" + args := []any{gitVersion.Original()} + // Since git wire protocol has been released from git v2.18 + if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil { + format += ", Wire Protocol %s Enabled" + args = append(args, "Version 2") // for focus color + } + + return fmt.Sprintf(format, args...) +} + +func checkInit() error { + if setting.Git.HomePath == "" { + return errors.New("unable to init Git's HomeDir, incorrect initialization of the setting and git modules") + } + if DefaultContext != nil { + log.Warn("git module has been initialized already, duplicate init may work but it's better to fix it") + } + 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 == "" { + // strict check, make sure the git module is initialized correctly. + // attention: when the git module is called in gitea sub-command (serv/hook), the log module might not obviously show messages to users/developers. + // for example: if there is gitea git hook code calling git.NewCommand before git.InitXxx, the integration test won't show the real failure reasons. + log.Fatal("Unable to init Git's HomeDir, incorrect initialization of the setting and git modules") + return "" + } + return setting.Git.HomePath +} + +// InitSimple initializes git module with a very simple step, no config changes, no global command arguments. +// This method doesn't change anything to filesystem. At the moment, it is only used by some Gitea sub-commands. +func InitSimple(ctx context.Context) error { + if err := checkInit(); err != nil { + return err + } + + DefaultContext = ctx + globalCommandArgs = nil + + if setting.Git.Timeout.Default > 0 { + defaultCommandExecutionTimeout = time.Duration(setting.Git.Timeout.Default) * time.Second + } + + return SetExecutablePath(setting.Git.Path) +} + +// InitFull initializes git module with version check and change global variables, sync gitconfig. +// It should only be called once at the beginning of the program initialization (TestMain/GlobalInitInstalled) as this code makes unsynchronized changes to variables. +func InitFull(ctx context.Context) (err error) { + if err = InitSimple(ctx); err != nil { + return err + } + + // when git works with gnupg (commit signing), there should be a stable home for gnupg commands + if _, ok := os.LookupEnv("GNUPGHOME"); !ok { + _ = os.Setenv("GNUPGHOME", filepath.Join(HomeDir(), ".gnupg")) + } + + // Since git wire protocol has been released from git v2.18 + if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil { + globalCommandArgs = append(globalCommandArgs, "-c", "protocol.version=2") + } + + // Explicitly disable credential helper, otherwise Git credentials might leak + if CheckGitVersionAtLeast("2.9") == nil { + globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=") + } + SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil + SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil && !isGogit + SupportCheckAttrOnBare = CheckGitVersionAtLeast("2.40") == nil + if SupportHashSha256 { + SupportedObjectFormats = append(SupportedObjectFormats, Sha256ObjectFormat) + } else { + log.Warn("sha256 hash support is disabled - requires Git >= 2.42. Gogit is currently unsupported") + } + + InvertedGitFlushEnv = CheckGitVersionEqual("2.43.1") == nil + + if setting.LFS.StartServer { + if CheckGitVersionAtLeast("2.1.2") != nil { + return errors.New("LFS server support requires Git >= 2.1.2") + } + globalCommandArgs = append(globalCommandArgs, "-c", "filter.lfs.required=", "-c", "filter.lfs.smudge=", "-c", "filter.lfs.clean=") + } + + 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 CheckGitVersionAtLeast("2.10") == nil { + if err := configSet("receive.advertisePushOptions", "true"); err != nil { + return err + } + } + + if CheckGitVersionAtLeast("2.18") == nil { + 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 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 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 + // Fundamentally the problem lies with the uid-gid-mapping mechanism for filesystems in docker on windows (and to a lesser extent samba). + // Docker's configuration mechanism for local filesystems provides no way of setting this mapping and although there is a mechanism for setting this uid through using cifs mounting it is complicated and essentially undocumented + // Thus the owner uid/gid for files on these filesystems will be marked as root. + // 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. + // Please note: the wildcard "*" is only supported by Git 2.30.4/2.31.3/2.32.2/2.33.3/2.34.3/2.35.3/2.36 and later + // Although only supported by Git 2.30.4/2.31.3/2.32.2/2.33.3/2.34.3/2.35.3/2.36 and later - this setting is 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 && CheckGitVersionAtLeast("2.22") == nil { + 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 +} + +// CheckGitVersionAtLeast check git version is at least the constraint version +func CheckGitVersionAtLeast(atLeast string) error { + if err := loadGitVersion(); err != nil { + return err + } + atLeastVersion, err := version.NewVersion(atLeast) + if err != nil { + return err + } + if gitVersion.Compare(atLeastVersion) < 0 { + return fmt.Errorf("installed git binary version %s is not at least %s", gitVersion.Original(), atLeast) + } + return nil +} + +// CheckGitVersionEqual checks if the git version is equal to the constraint version. +func CheckGitVersionEqual(equal string) error { + if err := loadGitVersion(); err != nil { + return err + } + atLeastVersion, err := version.NewVersion(equal) + if err != nil { + return err + } + if !gitVersion.Equal(atLeastVersion) { + return fmt.Errorf("installed git binary version %s is not equal to %s", gitVersion.Original(), equal) + } + 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}) +} |