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