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 }