code.gitea.io/gitea@v1.19.3/modules/git/git.go (about)

     1  // Copyright 2015 The Gogs Authors. All rights reserved.
     2  // Copyright 2017 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package git
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"os"
    12  	"os/exec"
    13  	"path/filepath"
    14  	"regexp"
    15  	"runtime"
    16  	"strings"
    17  	"time"
    18  
    19  	"code.gitea.io/gitea/modules/log"
    20  	"code.gitea.io/gitea/modules/setting"
    21  
    22  	"github.com/hashicorp/go-version"
    23  )
    24  
    25  // RequiredVersion is the minimum Git version required
    26  const RequiredVersion = "2.0.0"
    27  
    28  var (
    29  	// GitExecutable is the command name of git
    30  	// Could be updated to an absolute path while initialization
    31  	GitExecutable = "git"
    32  
    33  	// DefaultContext is the default context to run git commands in, must be initialized by git.InitXxx
    34  	DefaultContext context.Context
    35  
    36  	// SupportProcReceive version >= 2.29.0
    37  	SupportProcReceive bool
    38  
    39  	gitVersion *version.Version
    40  )
    41  
    42  // loadGitVersion returns current Git version from shell. Internal usage only.
    43  func loadGitVersion() (*version.Version, error) {
    44  	// doesn't need RWMutex because it's executed by Init()
    45  	if gitVersion != nil {
    46  		return gitVersion, nil
    47  	}
    48  
    49  	stdout, _, runErr := NewCommand(DefaultContext, "version").RunStdString(nil)
    50  	if runErr != nil {
    51  		return nil, runErr
    52  	}
    53  
    54  	fields := strings.Fields(stdout)
    55  	if len(fields) < 3 {
    56  		return nil, fmt.Errorf("invalid git version output: %s", stdout)
    57  	}
    58  
    59  	var versionString string
    60  
    61  	// Handle special case on Windows.
    62  	i := strings.Index(fields[2], "windows")
    63  	if i >= 1 {
    64  		versionString = fields[2][:i-1]
    65  	} else {
    66  		versionString = fields[2]
    67  	}
    68  
    69  	var err error
    70  	gitVersion, err = version.NewVersion(versionString)
    71  	return gitVersion, err
    72  }
    73  
    74  // SetExecutablePath changes the path of git executable and checks the file permission and version.
    75  func SetExecutablePath(path string) error {
    76  	// If path is empty, we use the default value of GitExecutable "git" to search for the location of git.
    77  	if path != "" {
    78  		GitExecutable = path
    79  	}
    80  	absPath, err := exec.LookPath(GitExecutable)
    81  	if err != nil {
    82  		return fmt.Errorf("git not found: %w", err)
    83  	}
    84  	GitExecutable = absPath
    85  
    86  	_, err = loadGitVersion()
    87  	if err != nil {
    88  		return fmt.Errorf("unable to load git version: %w", err)
    89  	}
    90  
    91  	versionRequired, err := version.NewVersion(RequiredVersion)
    92  	if err != nil {
    93  		return err
    94  	}
    95  
    96  	if gitVersion.LessThan(versionRequired) {
    97  		moreHint := "get git: https://git-scm.com/download/"
    98  		if runtime.GOOS == "linux" {
    99  			// there are a lot of CentOS/RHEL users using old git, so we add a special hint for them
   100  			if _, err = os.Stat("/etc/redhat-release"); err == nil {
   101  				// ius.io is the recommended official(git-scm.com) method to install git
   102  				moreHint = "get git: https://git-scm.com/download/linux and https://ius.io"
   103  			}
   104  		}
   105  		return fmt.Errorf("installed git version %q is not supported, Gitea requires git version >= %q, %s", gitVersion.Original(), RequiredVersion, moreHint)
   106  	}
   107  
   108  	return nil
   109  }
   110  
   111  // VersionInfo returns git version information
   112  func VersionInfo() string {
   113  	if gitVersion == nil {
   114  		return "(git not found)"
   115  	}
   116  	format := "%s"
   117  	args := []interface{}{gitVersion.Original()}
   118  	// Since git wire protocol has been released from git v2.18
   119  	if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil {
   120  		format += ", Wire Protocol %s Enabled"
   121  		args = append(args, "Version 2") // for focus color
   122  	}
   123  
   124  	return fmt.Sprintf(format, args...)
   125  }
   126  
   127  func checkInit() error {
   128  	if setting.Git.HomePath == "" {
   129  		return errors.New("unable to init Git's HomeDir, incorrect initialization of the setting and git modules")
   130  	}
   131  	if DefaultContext != nil {
   132  		log.Warn("git module has been initialized already, duplicate init may work but it's better to fix it")
   133  	}
   134  	return nil
   135  }
   136  
   137  // HomeDir is the home dir for git to store the global config file used by Gitea internally
   138  func HomeDir() string {
   139  	if setting.Git.HomePath == "" {
   140  		// strict check, make sure the git module is initialized correctly.
   141  		// attention: when the git module is called in gitea sub-command (serv/hook), the log module might not obviously show messages to users/developers.
   142  		// 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.
   143  		log.Fatal("Unable to init Git's HomeDir, incorrect initialization of the setting and git modules")
   144  		return ""
   145  	}
   146  	return setting.Git.HomePath
   147  }
   148  
   149  // InitSimple initializes git module with a very simple step, no config changes, no global command arguments.
   150  // This method doesn't change anything to filesystem. At the moment, it is only used by some Gitea sub-commands.
   151  func InitSimple(ctx context.Context) error {
   152  	if err := checkInit(); err != nil {
   153  		return err
   154  	}
   155  
   156  	DefaultContext = ctx
   157  	globalCommandArgs = nil
   158  
   159  	if setting.Git.Timeout.Default > 0 {
   160  		defaultCommandExecutionTimeout = time.Duration(setting.Git.Timeout.Default) * time.Second
   161  	}
   162  
   163  	return SetExecutablePath(setting.Git.Path)
   164  }
   165  
   166  // InitFull initializes git module with version check and change global variables, sync gitconfig.
   167  // It should only be called once at the beginning of the program initialization (TestMain/GlobalInitInstalled) as this code makes unsynchronized changes to variables.
   168  func InitFull(ctx context.Context) (err error) {
   169  	if err = checkInit(); err != nil {
   170  		return err
   171  	}
   172  
   173  	if err = InitSimple(ctx); err != nil {
   174  		return
   175  	}
   176  
   177  	// when git works with gnupg (commit signing), there should be a stable home for gnupg commands
   178  	if _, ok := os.LookupEnv("GNUPGHOME"); !ok {
   179  		_ = os.Setenv("GNUPGHOME", filepath.Join(HomeDir(), ".gnupg"))
   180  	}
   181  
   182  	// Since git wire protocol has been released from git v2.18
   183  	if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil {
   184  		globalCommandArgs = append(globalCommandArgs, "-c", "protocol.version=2")
   185  	}
   186  
   187  	// Explicitly disable credential helper, otherwise Git credentials might leak
   188  	if CheckGitVersionAtLeast("2.9") == nil {
   189  		globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=")
   190  	}
   191  
   192  	SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil
   193  
   194  	if setting.LFS.StartServer {
   195  		if CheckGitVersionAtLeast("2.1.2") != nil {
   196  			return errors.New("LFS server support requires Git >= 2.1.2")
   197  		}
   198  		globalCommandArgs = append(globalCommandArgs, "-c", "filter.lfs.required=", "-c", "filter.lfs.smudge=", "-c", "filter.lfs.clean=")
   199  	}
   200  
   201  	return syncGitConfig()
   202  }
   203  
   204  func enableReflogs() error {
   205  	if err := configSet("core.logAllRefUpdates", "true"); err != nil {
   206  		return err
   207  	}
   208  	err := configSet("gc.reflogExpire", fmt.Sprintf("%d", setting.Git.Reflog.Expiration))
   209  	return err
   210  }
   211  
   212  func disableReflogs() error {
   213  	if err := configUnsetAll("core.logAllRefUpdates", "true"); err != nil {
   214  		return err
   215  	} else if err := configUnsetAll("gc.reflogExpire", ""); err != nil {
   216  		return err
   217  	}
   218  	return nil
   219  }
   220  
   221  // syncGitConfig only modifies gitconfig, won't change global variables (otherwise there will be data-race problem)
   222  func syncGitConfig() (err error) {
   223  	if err = os.MkdirAll(HomeDir(), os.ModePerm); err != nil {
   224  		return fmt.Errorf("unable to prepare git home directory %s, err: %w", HomeDir(), err)
   225  	}
   226  
   227  	// Git requires setting user.name and user.email in order to commit changes - old comment: "if they're not set just add some defaults"
   228  	// 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.
   229  	// If these values are not really used, then they can be set (overwritten) directly without considering about existence.
   230  	for configKey, defaultValue := range map[string]string{
   231  		"user.name":  "Gitea",
   232  		"user.email": "gitea@fake.local",
   233  	} {
   234  		if err := configSetNonExist(configKey, defaultValue); err != nil {
   235  			return err
   236  		}
   237  	}
   238  
   239  	// Set git some configurations - these must be set to these values for gitea to work correctly
   240  	if err := configSet("core.quotePath", "false"); err != nil {
   241  		return err
   242  	}
   243  
   244  	if setting.Git.Reflog.Enabled {
   245  		if err := enableReflogs(); err != nil {
   246  			return err
   247  		}
   248  	} else {
   249  		if err := disableReflogs(); err != nil {
   250  			return err
   251  		}
   252  	}
   253  
   254  	if CheckGitVersionAtLeast("2.10") == nil {
   255  		if err := configSet("receive.advertisePushOptions", "true"); err != nil {
   256  			return err
   257  		}
   258  	}
   259  
   260  	if CheckGitVersionAtLeast("2.18") == nil {
   261  		if err := configSet("core.commitGraph", "true"); err != nil {
   262  			return err
   263  		}
   264  		if err := configSet("gc.writeCommitGraph", "true"); err != nil {
   265  			return err
   266  		}
   267  		if err := configSet("fetch.writeCommitGraph", "true"); err != nil {
   268  			return err
   269  		}
   270  	}
   271  
   272  	if SupportProcReceive {
   273  		// set support for AGit flow
   274  		if err := configAddNonExist("receive.procReceiveRefs", "refs/for"); err != nil {
   275  			return err
   276  		}
   277  	} else {
   278  		if err := configUnsetAll("receive.procReceiveRefs", "refs/for"); err != nil {
   279  			return err
   280  		}
   281  	}
   282  
   283  	// Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user
   284  	// 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.)
   285  	// see issue: https://github.com/go-gitea/gitea/issues/19455
   286  	// Fundamentally the problem lies with the uid-gid-mapping mechanism for filesystems in docker on windows (and to a lesser extent samba).
   287  	// 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
   288  	// Thus the owner uid/gid for files on these filesystems will be marked as root.
   289  	// As Gitea now always use its internal git config file, and access to the git repositories is managed through Gitea,
   290  	// it is now safe to set "safe.directory=*" for internal usage only.
   291  	// 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
   292  	// 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
   293  	if err := configAddNonExist("safe.directory", "*"); err != nil {
   294  		return err
   295  	}
   296  	if runtime.GOOS == "windows" {
   297  		if err := configSet("core.longpaths", "true"); err != nil {
   298  			return err
   299  		}
   300  		if setting.Git.DisableCoreProtectNTFS {
   301  			err = configSet("core.protectNTFS", "false")
   302  		} else {
   303  			err = configUnsetAll("core.protectNTFS", "false")
   304  		}
   305  		if err != nil {
   306  			return err
   307  		}
   308  	}
   309  
   310  	// By default partial clones are disabled, enable them from git v2.22
   311  	if !setting.Git.DisablePartialClone && CheckGitVersionAtLeast("2.22") == nil {
   312  		if err = configSet("uploadpack.allowfilter", "true"); err != nil {
   313  			return err
   314  		}
   315  		err = configSet("uploadpack.allowAnySHA1InWant", "true")
   316  	} else {
   317  		if err = configUnsetAll("uploadpack.allowfilter", "true"); err != nil {
   318  			return err
   319  		}
   320  		err = configUnsetAll("uploadpack.allowAnySHA1InWant", "true")
   321  	}
   322  
   323  	return err
   324  }
   325  
   326  // CheckGitVersionAtLeast check git version is at least the constraint version
   327  func CheckGitVersionAtLeast(atLeast string) error {
   328  	if _, err := loadGitVersion(); err != nil {
   329  		return err
   330  	}
   331  	atLeastVersion, err := version.NewVersion(atLeast)
   332  	if err != nil {
   333  		return err
   334  	}
   335  	if gitVersion.Compare(atLeastVersion) < 0 {
   336  		return fmt.Errorf("installed git binary version %s is not at least %s", gitVersion.Original(), atLeast)
   337  	}
   338  	return nil
   339  }
   340  
   341  func configSet(key, value string) error {
   342  	stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
   343  	if err != nil && !err.IsExitCode(1) {
   344  		return fmt.Errorf("failed to get git config %s, err: %w", key, err)
   345  	}
   346  
   347  	currValue := strings.TrimSpace(stdout)
   348  	if currValue == value {
   349  		return nil
   350  	}
   351  
   352  	_, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil)
   353  	if err != nil {
   354  		return fmt.Errorf("failed to set git global config %s, err: %w", key, err)
   355  	}
   356  
   357  	return nil
   358  }
   359  
   360  func configSetNonExist(key, value string) error {
   361  	_, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
   362  	if err == nil {
   363  		// already exist
   364  		return nil
   365  	}
   366  	if err.IsExitCode(1) {
   367  		// not exist, set new config
   368  		_, _, err = NewCommand(DefaultContext, "config", "--global").AddDynamicArguments(key, value).RunStdString(nil)
   369  		if err != nil {
   370  			return fmt.Errorf("failed to set git global config %s, err: %w", key, err)
   371  		}
   372  		return nil
   373  	}
   374  
   375  	return fmt.Errorf("failed to get git config %s, err: %w", key, err)
   376  }
   377  
   378  func configAddNonExist(key, value string) error {
   379  	_, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(nil)
   380  	if err == nil {
   381  		// already exist
   382  		return nil
   383  	}
   384  	if err.IsExitCode(1) {
   385  		// not exist, add new config
   386  		_, _, err = NewCommand(DefaultContext, "config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(nil)
   387  		if err != nil {
   388  			return fmt.Errorf("failed to add git global config %s, err: %w", key, err)
   389  		}
   390  		return nil
   391  	}
   392  	return fmt.Errorf("failed to get git config %s, err: %w", key, err)
   393  }
   394  
   395  func configUnsetAll(key, value string) error {
   396  	_, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil)
   397  	if err == nil {
   398  		// exist, need to remove
   399  		_, _, err = NewCommand(DefaultContext, "config", "--global", "--unset-all").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(nil)
   400  		if err != nil {
   401  			return fmt.Errorf("failed to unset git global config %s, err: %w", key, err)
   402  		}
   403  		return nil
   404  	}
   405  	if err.IsExitCode(1) {
   406  		// not exist
   407  		return nil
   408  	}
   409  	return fmt.Errorf("failed to get git config %s, err: %w", key, err)
   410  }
   411  
   412  // Fsck verifies the connectivity and validity of the objects in the database
   413  func Fsck(ctx context.Context, repoPath string, timeout time.Duration, args TrustedCmdArgs) error {
   414  	return NewCommand(ctx, "fsck").AddArguments(args...).Run(&RunOpts{Timeout: timeout, Dir: repoPath})
   415  }