1package git 2 3import ( 4 "fmt" 5 "log" 6 "strings" 7) 8 9const ( 10 // scNoRefUpdates denotes a command which will never update refs 11 scNoRefUpdates = 1 << iota 12 // scNoEndOfOptions denotes a command which doesn't know --end-of-options 13 scNoEndOfOptions 14 // scGeneratesPackfiles denotes a command which may generate packfiles 15 scGeneratesPackfiles 16) 17 18type commandDescription struct { 19 flags uint 20 opts []GlobalOption 21 validatePositionalArgs func([]string) error 22} 23 24// commandDescriptions is a curated list of Git command descriptions for special 25// git.ExecCommandFactory validation logic 26var commandDescriptions = map[string]commandDescription{ 27 "am": {}, 28 "apply": { 29 flags: scNoRefUpdates, 30 }, 31 "archive": { 32 // git-archive(1) does not support disambiguating options from paths from revisions. 33 flags: scNoRefUpdates | scNoEndOfOptions, 34 }, 35 "blame": { 36 // git-blame(1) does not support disambiguating options from paths from revisions. 37 flags: scNoRefUpdates | scNoEndOfOptions, 38 }, 39 "bundle": { 40 flags: scNoRefUpdates | scGeneratesPackfiles, 41 }, 42 "cat-file": { 43 flags: scNoRefUpdates, 44 }, 45 "check-ref-format": { 46 // git-check-ref-format(1) uses a hand-rolled option parser which doesn't support 47 // `--end-of-options`. 48 flags: scNoRefUpdates | scNoEndOfOptions, 49 }, 50 "checkout": { 51 // git-checkout(1) does not support disambiguating options from paths from 52 // revisions. 53 flags: scNoEndOfOptions, 54 }, 55 "clone": { 56 flags: scGeneratesPackfiles, 57 }, 58 "commit": { 59 flags: 0, 60 }, 61 "commit-graph": { 62 flags: scNoRefUpdates, 63 }, 64 "commit-tree": { 65 flags: scNoRefUpdates, 66 }, 67 "config": { 68 flags: scNoRefUpdates, 69 }, 70 "count-objects": { 71 flags: scNoRefUpdates, 72 }, 73 "diff": { 74 flags: scNoRefUpdates, 75 }, 76 "diff-tree": { 77 flags: scNoRefUpdates, 78 }, 79 "fetch": { 80 flags: 0, 81 82 opts: []GlobalOption{ 83 // When fetching objects from an untrusted source, we want to always assert 84 // that all objects are valid. Please refer to the receive-pack 85 // description with regards to why we ignore some checks. 86 ConfigPair{Key: "fetch.fsckObjects", Value: "true"}, 87 ConfigPair{Key: "fetch.fsck.badTimezone", Value: "ignore"}, 88 ConfigPair{Key: "fetch.fsck.missingSpaceBeforeDate", Value: "ignore"}, 89 // While git-fetch(1) by default won't write commit graphs, both CNG and 90 // Omnibus set this value to true. This has caused performance issues when 91 // doing internal fetches, and furthermore it's not encouraged to run such 92 // maintenance tasks on "normal" Git operations. Instead, writing commit 93 // graphs should be done in our housekeeping RPCs, which already know to do 94 // so. So let's disable writing commit graphs on fetches -- if it really is 95 // required, we can enable it on a case-by-case basis. 96 ConfigPair{Key: "fetch.writeCommitGraph", Value: "false"}, 97 }, 98 }, 99 "for-each-ref": { 100 flags: scNoRefUpdates, 101 }, 102 "format-patch": { 103 flags: scNoRefUpdates, 104 }, 105 "fsck": { 106 flags: scNoRefUpdates, 107 }, 108 "gc": { 109 flags: scNoRefUpdates | scGeneratesPackfiles, 110 }, 111 "grep": { 112 // git-grep(1) does not support disambiguating options from paths from 113 // revisions. 114 flags: scNoRefUpdates | scNoEndOfOptions, 115 }, 116 "hash-object": { 117 flags: scNoRefUpdates, 118 }, 119 "init": { 120 flags: scNoRefUpdates, 121 opts: []GlobalOption{ 122 // We're not prepared for a world where the user has configured the default 123 // branch to be something different from "master" in Gitaly's git 124 // configuration. There explicitly override it on git-init. 125 ConfigPair{Key: "init.defaultBranch", Value: DefaultBranch}, 126 }, 127 }, 128 "linguist": { 129 // linguist is not a native Git command, so we cannot use --end-of-options. 130 flags: scNoEndOfOptions, 131 }, 132 "log": { 133 flags: scNoRefUpdates, 134 }, 135 "ls-remote": { 136 flags: scNoRefUpdates, 137 }, 138 "ls-tree": { 139 flags: scNoRefUpdates, 140 }, 141 "merge-base": { 142 flags: scNoRefUpdates, 143 }, 144 "merge-file": { 145 flags: scNoRefUpdates, 146 }, 147 "mktag": { 148 flags: scNoRefUpdates, 149 }, 150 "multi-pack-index": { 151 flags: scNoRefUpdates, 152 }, 153 "pack-refs": { 154 flags: scNoRefUpdates, 155 }, 156 "pack-objects": { 157 flags: scNoRefUpdates | scGeneratesPackfiles, 158 }, 159 "push": { 160 flags: scNoRefUpdates, 161 }, 162 "receive-pack": { 163 flags: 0, 164 opts: append([]GlobalOption{ 165 // In case the repository belongs to an object pool, we want to prevent 166 // Git from including the pool's refs in the ref advertisement. We do 167 // this by rigging core.alternateRefsCommand to produce no output. 168 // Because Git itself will append the pool repository directory, the 169 // command ends with a "#". The end result is that Git runs `/bin/sh -c 'exit 0 # /path/to/pool.git`. 170 ConfigPair{Key: "core.alternateRefsCommand", Value: "exit 0 #"}, 171 172 // When receiving objects from an untrusted source, we want to always assert 173 // that all objects are valid. 174 ConfigPair{Key: "receive.fsckObjects", Value: "true"}, 175 176 // In the past, there was a bug in git that caused users to 177 // create commits with invalid timezones. As a result, some 178 // histories contain commits that do not match the spec. As we 179 // fsck received packfiles by default, any push containing such 180 // a commit will be rejected. As this is a mostly harmless 181 // issue, we add the following flag to ignore this check. 182 ConfigPair{Key: "receive.fsck.badTimezone", Value: "ignore"}, 183 184 // git-fsck(1) complains in case a signature does not have a space 185 // between mail and date. The most common case where this can be hit 186 // is in case the date is missing completely. This error is harmless 187 // enough and we cope just fine parsing such signatures, so we can 188 // ignore this error. 189 ConfigPair{Key: "receive.fsck.missingSpaceBeforeDate", Value: "ignore"}, 190 191 // Make git-receive-pack(1) advertise the push options 192 // capability to clients. 193 ConfigPair{Key: "receive.advertisePushOptions", Value: "true"}, 194 }, hiddenReceivePackRefPrefixes()...), 195 }, 196 "remote": { 197 // While git-remote(1)'s `add` subcommand does support `--end-of-options`, 198 // `remove` doesn't. 199 flags: scNoEndOfOptions, 200 }, 201 "repack": { 202 flags: scNoRefUpdates | scGeneratesPackfiles, 203 opts: []GlobalOption{ 204 // Write bitmap indices when packing objects, which 205 // speeds up packfile creation for fetches. 206 ConfigPair{Key: "repack.writeBitmaps", Value: "true"}, 207 }, 208 }, 209 "rev-list": { 210 // We cannot use --end-of-options here because pseudo revisions like `--all` 211 // and `--not` count as options. 212 flags: scNoRefUpdates | scNoEndOfOptions, 213 validatePositionalArgs: func(args []string) error { 214 for _, arg := range args { 215 // git-rev-list(1) supports pseudo-revision arguments which can be 216 // intermingled with normal positional arguments. Given that these 217 // pseudo-revisions have leading dashes, normal validation would 218 // refuse them as positional arguments. We thus override validation 219 // for two of these which we are using in our codebase. There are 220 // more, but we can add them at a later point if they're ever 221 // required. 222 if arg == "--all" || arg == "--not" { 223 continue 224 } 225 if err := validatePositionalArg(arg); err != nil { 226 return fmt.Errorf("rev-list: %w", err) 227 } 228 } 229 return nil 230 }, 231 }, 232 "rev-parse": { 233 // --end-of-options is echoed by git-rev-parse(1) if used without 234 // `--verify`. 235 flags: scNoRefUpdates | scNoEndOfOptions, 236 }, 237 "show": { 238 flags: scNoRefUpdates, 239 }, 240 "show-ref": { 241 flags: scNoRefUpdates, 242 }, 243 "symbolic-ref": { 244 flags: 0, 245 }, 246 "tag": { 247 flags: 0, 248 }, 249 "update-ref": { 250 flags: 0, 251 }, 252 "upload-archive": { 253 // git-upload-archive(1) has a handrolled parser which always interprets the 254 // first argument as directory, so we cannot use `--end-of-options`. 255 flags: scNoRefUpdates | scNoEndOfOptions, 256 }, 257 "upload-pack": { 258 flags: scNoRefUpdates | scGeneratesPackfiles, 259 opts: []GlobalOption{ 260 ConfigPair{Key: "uploadpack.allowFilter", Value: "true"}, 261 // Enables the capability to request individual SHA1's from the 262 // remote repo. 263 ConfigPair{Key: "uploadpack.allowAnySHA1InWant", Value: "true"}, 264 }, 265 }, 266 "version": { 267 flags: scNoRefUpdates, 268 }, 269 "worktree": { 270 flags: 0, 271 }, 272} 273 274func init() { 275 // This is the poor-mans static assert that all internal ref prefixes are properly hidden 276 // from git-receive-pack(1) such that they cannot be written to when the user pushes. 277 receivePackDesc, ok := commandDescriptions["receive-pack"] 278 if !ok { 279 log.Fatal("could not find command description of git-receive-pack(1)") 280 } 281 282 hiddenRefs := map[string]bool{} 283 for _, opt := range receivePackDesc.opts { 284 configPair, ok := opt.(ConfigPair) 285 if !ok { 286 continue 287 } 288 if configPair.Key != "receive.hideRefs" { 289 continue 290 } 291 292 hiddenRefs[configPair.Value] = true 293 } 294 295 for _, internalRef := range InternalRefPrefixes { 296 if !hiddenRefs[internalRef] { 297 log.Fatalf("command description of receive-pack is missing hidden ref %q", internalRef) 298 } 299 } 300} 301 302// mayUpdateRef indicates if a command is known to update references. 303// This is useful to determine if a command requires reference hook 304// configuration. A non-exhaustive list of commands is consulted to determine if 305// refs are updated. When unknown, true is returned to err on the side of 306// caution. 307func (c commandDescription) mayUpdateRef() bool { 308 return c.flags&scNoRefUpdates == 0 309} 310 311// mayGeneratePackfiles indicates if a command is known to generate 312// packfiles. This is used in order to inject packfile configuration. 313func (c commandDescription) mayGeneratePackfiles() bool { 314 return c.flags&scGeneratesPackfiles != 0 315} 316 317// supportsEndOfOptions indicates whether a command can handle the 318// `--end-of-options` option. 319func (c commandDescription) supportsEndOfOptions() bool { 320 return c.flags&scNoEndOfOptions == 0 321} 322 323// args validates the given flags and arguments and, if valid, returns the complete command line. 324func (c commandDescription) args(flags []Option, args []string, postSepArgs []string) ([]string, error) { 325 var commandArgs []string 326 327 for _, o := range flags { 328 args, err := o.OptionArgs() 329 if err != nil { 330 return nil, err 331 } 332 commandArgs = append(commandArgs, args...) 333 } 334 335 if c.supportsEndOfOptions() { 336 commandArgs = append(commandArgs, "--end-of-options") 337 } 338 339 if c.validatePositionalArgs != nil { 340 if err := c.validatePositionalArgs(args); err != nil { 341 return nil, err 342 } 343 } else { 344 for _, a := range args { 345 if err := validatePositionalArg(a); err != nil { 346 return nil, err 347 } 348 } 349 } 350 commandArgs = append(commandArgs, args...) 351 352 if len(postSepArgs) > 0 { 353 commandArgs = append(commandArgs, "--") 354 } 355 356 // post separator args do not need any validation 357 commandArgs = append(commandArgs, postSepArgs...) 358 359 return commandArgs, nil 360} 361 362func validatePositionalArg(arg string) error { 363 if strings.HasPrefix(arg, "-") { 364 return fmt.Errorf("positional arg %q cannot start with dash '-': %w", arg, ErrInvalidArg) 365 } 366 return nil 367} 368 369func hiddenReceivePackRefPrefixes() []GlobalOption { 370 var cps []GlobalOption 371 372 for _, ns := range InternalRefPrefixes { 373 cps = append(cps, ConfigPair{Key: "receive.hideRefs", Value: ns}) 374 } 375 376 return cps 377} 378