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 }