1package git 2 3import ( 4 "fmt" 5 "io" 6 "regexp" 7 "strings" 8 9 "gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb" 10) 11 12var ( 13 configKeyOptionRegex = regexp.MustCompile(`^[[:alnum:]]+[-[:alnum:]]*\.(.+\.)*[[:alnum:]]+[-[:alnum:]]*$`) 14 // configKeyGlobalRegex is intended to verify config keys when used as 15 // global arguments. We're playing it safe here by disallowing lots of 16 // keys which git would parse just fine, but we only have a limited 17 // number of config entries anyway. Most importantly, we cannot allow 18 // `=` as part of the key as that would break parsing of `git -c`. 19 configKeyGlobalRegex = regexp.MustCompile(`^[[:alnum:]]+(\.[-/_a-zA-Z0-9]+)+$`) 20 21 flagRegex = regexp.MustCompile(`^(-|--)[[:alnum:]]`) 22) 23 24// GlobalOption is an interface for all options which can be globally applied 25// to git commands. This is the command-inspecific part before the actual 26// command that's being run, e.g. the `-c` part in `git -c foo.bar=value 27// command`. 28type GlobalOption interface { 29 GlobalArgs() ([]string, error) 30} 31 32// Option is a git command line flag with validation logic 33type Option interface { 34 OptionArgs() ([]string, error) 35} 36 37// ConfigPair is a sub-command option for use with commands like "git config" 38type ConfigPair struct { 39 Key string 40 Value string 41 // Origin shows the origin type: file, standard input, blob, command line. 42 // https://git-scm.com/docs/git-config#Documentation/git-config.txt---show-origin 43 Origin string 44 // Scope shows the scope of this config value: local, global, system, command. 45 // https://git-scm.com/docs/git-config#Documentation/git-config.txt---show-scope 46 Scope string 47} 48 49// OptionArgs validates the config pair args 50func (cp ConfigPair) OptionArgs() ([]string, error) { 51 if !configKeyOptionRegex.MatchString(cp.Key) { 52 return nil, fmt.Errorf("config key %q failed regexp validation: %w", cp.Key, ErrInvalidArg) 53 } 54 return []string{cp.Key, cp.Value}, nil 55} 56 57// GlobalArgs generates a git `-c <key>=<value>` flag. The key must pass 58// validation by containing only alphanumeric sections separated by dots. 59// No other characters are allowed for now as `git -c` may not correctly parse 60// them, most importantly when they contain equals signs. 61func (cp ConfigPair) GlobalArgs() ([]string, error) { 62 if !configKeyGlobalRegex.MatchString(cp.Key) { 63 return nil, fmt.Errorf("config key %q failed regexp validation: %w", cp.Key, ErrInvalidArg) 64 } 65 return []string{"-c", fmt.Sprintf("%s=%s", cp.Key, cp.Value)}, nil 66} 67 68// Flag is a single token optional command line argument that enables or 69// disables functionality (e.g. "-L") 70type Flag struct { 71 Name string 72} 73 74// GlobalArgs returns the arguments for the given flag, which should typically 75// only be the flag itself. It returns an error if the flag is not sanitary. 76func (f Flag) GlobalArgs() ([]string, error) { 77 return f.OptionArgs() 78} 79 80// OptionArgs returns an error if the flag is not sanitary 81func (f Flag) OptionArgs() ([]string, error) { 82 if !flagRegex.MatchString(f.Name) { 83 return nil, fmt.Errorf("flag %q failed regex validation: %w", f.Name, ErrInvalidArg) 84 } 85 return []string{f.Name}, nil 86} 87 88// ValueFlag is an optional command line argument that is comprised of pair of 89// tokens (e.g. "-n 50") 90type ValueFlag struct { 91 Name string 92 Value string 93} 94 95// GlobalArgs returns the arguments for the given value flag, which should 96// typically be two arguments: the flag and its value. It returns an error if the value flag is not sanitary. 97func (vf ValueFlag) GlobalArgs() ([]string, error) { 98 return vf.OptionArgs() 99} 100 101// OptionArgs returns an error if the flag is not sanitary 102func (vf ValueFlag) OptionArgs() ([]string, error) { 103 if !flagRegex.MatchString(vf.Name) { 104 return nil, fmt.Errorf("value flag %q failed regex validation: %w", vf.Name, ErrInvalidArg) 105 } 106 return []string{vf.Name, vf.Value}, nil 107} 108 109// ConvertGlobalOptions converts a protobuf message to a CmdOpt. 110func ConvertGlobalOptions(options *gitalypb.GlobalOptions) []CmdOpt { 111 if options != nil && options.GetLiteralPathspecs() { 112 return []CmdOpt{ 113 WithEnv("GIT_LITERAL_PATHSPECS=1"), 114 } 115 } 116 117 return nil 118} 119 120// ConvertConfigOptions converts `<key>=<value>` config entries into `ConfigPairs`. 121func ConvertConfigOptions(options []string) ([]ConfigPair, error) { 122 configPairs := make([]ConfigPair, len(options)) 123 124 for i, option := range options { 125 configPair := strings.SplitN(option, "=", 2) 126 if len(configPair) != 2 { 127 return nil, fmt.Errorf("cannot convert invalid config key: %q", option) 128 } 129 130 configPairs[i] = ConfigPair{Key: configPair[0], Value: configPair[1]} 131 } 132 133 return configPairs, nil 134} 135 136type cmdCfg struct { 137 env []string 138 globals []GlobalOption 139 stdin io.Reader 140 stdout io.Writer 141 stderr io.Writer 142 hooksConfigured bool 143} 144 145// CmdOpt is an option for running a command 146type CmdOpt func(*cmdCfg) error 147 148// WithStdin sets the command's stdin. Pass `command.SetupStdin` to make the 149// command suitable for `Write()`ing to. 150func WithStdin(r io.Reader) CmdOpt { 151 return func(c *cmdCfg) error { 152 c.stdin = r 153 return nil 154 } 155} 156 157// WithStdout sets the command's stdout. 158func WithStdout(w io.Writer) CmdOpt { 159 return func(c *cmdCfg) error { 160 c.stdout = w 161 return nil 162 } 163} 164 165// WithStderr sets the command's stderr. 166func WithStderr(w io.Writer) CmdOpt { 167 return func(c *cmdCfg) error { 168 c.stderr = w 169 return nil 170 } 171} 172 173// WithEnv adds environment variables to the command. 174func WithEnv(envs ...string) CmdOpt { 175 return func(c *cmdCfg) error { 176 c.env = append(c.env, envs...) 177 return nil 178 } 179} 180 181// WithConfig adds git configuration entries to the command. 182func WithConfig(configPairs ...ConfigPair) CmdOpt { 183 return func(c *cmdCfg) error { 184 for _, configPair := range configPairs { 185 c.globals = append(c.globals, configPair) 186 } 187 return nil 188 } 189} 190 191// WithConfigEnv adds git configuration entries to the command's environment. This should be used 192// in place of `WithConfig()` in case config entries may contain secrets which shouldn't leak e.g. 193// via the process's command line. 194func WithConfigEnv(configPairs ...ConfigPair) CmdOpt { 195 return func(c *cmdCfg) error { 196 env := make([]string, 0, len(configPairs)*2+1) 197 198 for i, configPair := range configPairs { 199 env = append(env, 200 fmt.Sprintf("GIT_CONFIG_KEY_%d=%s", i, configPair.Key), 201 fmt.Sprintf("GIT_CONFIG_VALUE_%d=%s", i, configPair.Value), 202 ) 203 } 204 env = append(env, fmt.Sprintf("GIT_CONFIG_COUNT=%d", len(configPairs))) 205 206 c.env = append(c.env, env...) 207 return nil 208 } 209} 210 211// WithGlobalOption adds the global options to the command. These are universal options which work 212// across all git commands. 213func WithGlobalOption(opts ...GlobalOption) CmdOpt { 214 return func(c *cmdCfg) error { 215 c.globals = append(c.globals, opts...) 216 return nil 217 } 218} 219