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