// 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"
"runtime"
"strings"
"time"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/hashicorp/go-version"
)
const RequiredVersion = "2.0.0" // the minimum Git version required
type Features struct {
gitVersion * version . Version
UsingGogit bool
SupportProcReceive bool // >= 2.29
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
SupportedObjectFormats [ ] ObjectFormat // sha1, sha256
}
var (
GitExecutable = "git" // the command name of git, will be updated to an absolute path during initialization
DefaultContext context . Context // the default context to run git commands in, must be initialized by git.InitXxx
defaultFeatures * Features
)
func ( f * Features ) CheckVersionAtLeast ( atLeast string ) bool {
return f . gitVersion . Compare ( version . Must ( version . NewVersion ( atLeast ) ) ) >= 0
}
// VersionInfo returns git version information
func ( f * Features ) VersionInfo ( ) string {
return f . gitVersion . Original ( )
}
func DefaultFeatures ( ) * Features {
if defaultFeatures == nil {
if ! setting . IsProd || setting . IsInTesting {
log . Warn ( "git.DefaultFeatures is called before git.InitXxx, initializing with default values" )
}
if err := InitSimple ( context . Background ( ) ) ; err != nil {
log . Fatal ( "git.InitSimple failed: %v" , err )
}
}
return defaultFeatures
}
func loadGitVersionFeatures ( ) ( * Features , error ) {
stdout , _ , runErr := NewCommand ( DefaultContext , "version" ) . RunStdString ( nil )
if runErr != nil {
return nil , runErr
}
ver , err := parseGitVersionLine ( strings . TrimSpace ( stdout ) )
if err != nil {
return nil , err
}
features := & Features { gitVersion : ver , UsingGogit : isGogit }
features . SupportProcReceive = features . CheckVersionAtLeast ( "2.29" )
features . SupportHashSha256 = features . CheckVersionAtLeast ( "2.42" ) && ! isGogit
features . SupportedObjectFormats = [ ] ObjectFormat { Sha1ObjectFormat }
if features . SupportHashSha256 {
features . SupportedObjectFormats = append ( features . SupportedObjectFormats , Sha256ObjectFormat )
}
return features , nil
}
func parseGitVersionLine ( s string ) ( * version . Version , error ) {
fields := strings . Fields ( s )
if len ( fields ) < 3 {
return nil , fmt . Errorf ( "invalid git version: %q" , s )
}
// version string is like: "git version 2.29.3" or "git version 2.29.3.windows.1"
versionString := fields [ 2 ]
if pos := strings . Index ( versionString , "windows" ) ; pos >= 1 {
versionString = versionString [ : pos - 1 ]
}
return version . NewVersion ( versionString )
}
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 ensureGitVersion ( ) error {
if ! DefaultFeatures ( ) . CheckVersionAtLeast ( RequiredVersion ) {
moreHint := "get git: https://git-scm.com/downloads"
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/downloads/linux and https://ius.io"
}
}
return fmt . Errorf ( "installed git version %q is not supported, Gitea requires git version >= %q, %s" , DefaultFeatures ( ) . gitVersion . Original ( ) , RequiredVersion , moreHint )
}
if err := checkGitVersionCompatibility ( DefaultFeatures ( ) . gitVersion ) ; err != nil {
return fmt . Errorf ( "installed git version %s has a known compatibility issue with Gitea: %w, please upgrade (or downgrade) git" , DefaultFeatures ( ) . gitVersion . String ( ) , err )
}
return nil
}
// SetExecutablePath changes the path of git executable and checks the file permission and version.
func SetExecutablePath ( path string ) error {
// If path is empty, we use the default value of GitExecutable "git" to search for the location of git.
if path != "" {
GitExecutable = path
}
absPath , err := exec . LookPath ( GitExecutable )
if err != nil {
return fmt . Errorf ( "git not found: %w" , err )
}
GitExecutable = absPath
return nil
}
// HomeDir is the home dir for git to store the global config file used by Gitea internally
func HomeDir ( ) string {
if setting . Git . HomePath == "" {
// 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 setting . Git . HomePath == "" {
return errors . New ( "unable to init Git's HomeDir, incorrect initialization of the setting and git modules" )
}
if DefaultContext != nil && ( ! setting . IsProd || setting . IsInTesting ) {
log . Warn ( "git module has been initialized already, duplicate init may work but it's better to fix it" )
}
DefaultContext = ctx
globalCommandArgs = nil
if setting . Git . Timeout . Default > 0 {
defaultCommandExecutionTimeout = time . Duration ( setting . Git . Timeout . Default ) * time . Second
}
if err := SetExecutablePath ( setting . Git . Path ) ; err != nil {
return err
}
var err error
defaultFeatures , err = loadGitVersionFeatures ( )
if err != nil {
return err
}
if err = ensureGitVersion ( ) ; 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" ) )
}
return nil
}
// 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
}
// Since git wire protocol has been released from git v2.18
if setting . Git . EnableAutoGitWireProtocol && DefaultFeatures ( ) . CheckVersionAtLeast ( "2.18" ) {
globalCommandArgs = append ( globalCommandArgs , "-c" , "protocol.version=2" )
}
// Explicitly disable credential helper, otherwise Git credentials might leak
if DefaultFeatures ( ) . CheckVersionAtLeast ( "2.9" ) {
globalCommandArgs = append ( globalCommandArgs , "-c" , "credential.helper=" )
}
if setting . LFS . StartServer {
if ! DefaultFeatures ( ) . CheckVersionAtLeast ( "2.1.2" ) {
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 ( )
}