Add --git-config flag

This allows arbitrary git configs to be passed in.  For example:

`git config --global http.postBuffer 1048576000`

`git config --global http.sslCAInfo /path/to/cert/file`

`git config --global http.sslVerify false`

This flag takes a comma-separated list of `key:val` pairs.  The key part
is passed to `git config` and must be a valid gitconfig section header
and variable name.  The val part can be either a quoted or unquoted
value.  For all values the following escape sequences are supported:
   * `\n` => [newline]
   * `\t` => [tab]
   * `\"` => `"`
   * `\,` => `,`
   * `\\` => `\`

Within unquoted values, commas MUST be escaped. Within quoted values,
commas MAY be escaped, but are not required to be.  Any other escape
sequence is an error.

Example:

`--git-config=foo.one:val1,foo.two:"quoted val",foo.three:12345`

This commit exposed a bug in runCommand() which modified its args when
they had an embedded space.
This commit is contained in:
Tim Hockin 2021-03-10 18:59:47 -08:00
parent 05a099a964
commit 45bba183ca
4 changed files with 287 additions and 4 deletions

View File

@ -113,5 +113,6 @@ docker run -d \
| GIT_SYNC_HTTP_BIND | `--http-bind` | the bind address (including port) for git-sync's HTTP endpoint | "" |
| GIT_SYNC_HTTP_METRICS | `--http-metrics` | enable metrics on git-sync's HTTP endpoint | true |
| GIT_SYNC_HTTP_PPROF | `--http-pprof` | enable the pprof debug endpoints on git-sync's HTTP endpoint | false |
| GIT_SYNC_GIT_CONFIG | `--git-config` | additional git config options in 'key1:val1,key2:val2' format | "" |
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/git-sync/README.md?pixel)]()

View File

@ -109,6 +109,8 @@ var flAskPassURL = flag.String("askpass-url", envString("GIT_ASKPASS_URL", ""),
var flGitCmd = flag.String("git", envString("GIT_SYNC_GIT", "git"),
"the git command to run (subject to PATH search, mostly for testing)")
var flGitConfig = flag.String("git-config", envString("GIT_SYNC_GIT_CONFIG", ""),
"additional git config options in 'key1:val1,key2:val2' format")
var flHTTPBind = flag.String("http-bind", envString("GIT_SYNC_HTTP_BIND", ""),
"the bind address (including port) for git-sync's HTTP endpoint")
@ -391,6 +393,14 @@ func main() {
askpassCount.WithLabelValues(metricKeySuccess).Inc()
}
// This needs to be after all other git-related config flags.
if *flGitConfig != "" {
if err := setupExtraGitConfigs(ctx, *flGitConfig); err != nil {
fmt.Fprintf(os.Stderr, "ERROR: can't set additional git configs: %v\n", err)
os.Exit(1)
}
}
// The scope of the initialization context ends here, so we call cancel to release resources associated with it.
cancel()
@ -831,12 +841,14 @@ func cmdForLog(command string, args ...string) string {
if strings.ContainsAny(command, " \t\n") {
command = fmt.Sprintf("%q", command)
}
argsCopy := make([]string, len(args))
copy(argsCopy, args)
for i := range args {
if strings.ContainsAny(args[i], " \t\n") {
args[i] = fmt.Sprintf("%q", args[i])
argsCopy[i] = fmt.Sprintf("%q", args[i])
}
}
return command + " " + strings.Join(args, " ")
return command + " " + strings.Join(argsCopy, " ")
}
func runCommand(ctx context.Context, cwd, command string, args ...string) (string, error) {
@ -927,8 +939,7 @@ func setupGitCookieFile(ctx context.Context) error {
return fmt.Errorf("can't access git cookiefile: %w", err)
}
if _, err = runCommand(ctx, "",
*flGitCmd, "config", "--global", "http.cookiefile", pathToCookieFile); err != nil {
if _, err = runCommand(ctx, "", *flGitCmd, "config", "--global", "http.cookiefile", pathToCookieFile); err != nil {
return fmt.Errorf("can't configure git cookiefile: %w", err)
}
@ -986,3 +997,164 @@ func callGitAskPassURL(ctx context.Context, url string) error {
return nil
}
func setupExtraGitConfigs(ctx context.Context, configsFlag string) error {
log.V(1).Info("setting additional git configs")
configs, err := parseGitConfigs(configsFlag)
if err != nil {
return fmt.Errorf("can't parse --git-config flag: %v", err)
}
for _, kv := range configs {
if _, err := runCommand(ctx, "", *flGitCmd, "config", "--global", kv.key, kv.val); err != nil {
return fmt.Errorf("error configuring additional git configs %q %q: %v", kv.key, kv.val, err)
}
}
return nil
}
type keyVal struct {
key string
val string
}
func parseGitConfigs(configsFlag string) ([]keyVal, error) {
ch := make(chan rune)
stop := make(chan bool)
go func() {
for _, r := range configsFlag {
select {
case <-stop:
break
default:
ch <- r
}
}
close(ch)
return
}()
result := []keyVal{}
// This assumes it is at the start of a key.
for {
cur := keyVal{}
var err error
// Peek and see if we have a key.
if r, ok := <-ch; !ok {
break
} else {
cur.key, err = parseGitConfigKey(r, ch)
if err != nil {
return nil, err
}
}
// Peek and see if we have a value.
if r, ok := <-ch; !ok {
return nil, fmt.Errorf("key %q: no value", cur.key)
} else {
if r == '"' {
cur.val, err = parseGitConfigQVal(ch)
if err != nil {
return nil, fmt.Errorf("key %q: %v", cur.key, err)
}
} else {
cur.val, err = parseGitConfigVal(r, ch)
if err != nil {
return nil, fmt.Errorf("key %q: %v", cur.key, err)
}
}
}
result = append(result, cur)
}
return result, nil
}
func parseGitConfigKey(r rune, ch <-chan rune) (string, error) {
buf := make([]rune, 0, 64)
buf = append(buf, r)
for r := range ch {
switch {
case r == ':':
return string(buf), nil
default:
// This can accumulate things that git doesn't allow, but we'll
// just let git handle it, rather than try to pre-validate to their
// spec.
buf = append(buf, r)
}
}
return "", fmt.Errorf("unexpected end of key: %q", string(buf))
}
func parseGitConfigQVal(ch <-chan rune) (string, error) {
buf := make([]rune, 0, 64)
for r := range ch {
switch r {
case '\\':
if e, err := unescape(ch); err != nil {
return "", err
} else {
buf = append(buf, e)
}
case '"':
// Once we have a closing quote, the next must be either a comma or
// end-of-string. This helps reset the state for the next key, if
// there is one.
r, ok := <-ch
if ok && r != ',' {
return "", fmt.Errorf("unexpected trailing character '%c'", r)
}
return string(buf), nil
default:
buf = append(buf, r)
}
}
return "", fmt.Errorf("unexpected end of value: %q", string(buf))
}
func parseGitConfigVal(r rune, ch <-chan rune) (string, error) {
buf := make([]rune, 0, 64)
buf = append(buf, r)
for r := range ch {
switch r {
case '\\':
if r, err := unescape(ch); err != nil {
return "", err
} else {
buf = append(buf, r)
}
case ',':
return string(buf), nil
default:
buf = append(buf, r)
}
}
// We ran out of characters, but that's OK.
return string(buf), nil
}
// unescape processes most of the documented escapes that git config supports.
func unescape(ch <-chan rune) (rune, error) {
r, ok := <-ch
if !ok {
return 0, fmt.Errorf("unexpected end of escape sequence")
}
switch r {
case 'n':
return '\n', nil
case 't':
return '\t', nil
case '"', ',', '\\':
return r, nil
}
return 0, fmt.Errorf("unsupported escape character: '%c'", r)
}

View File

@ -18,6 +18,7 @@ package main
import (
"os"
"reflect"
"testing"
)
@ -98,3 +99,91 @@ func TestEnvInt(t *testing.T) {
}
}
}
func TestParseGitConfigs(t *testing.T) {
cases := []struct {
name string
input string
expect []keyVal
fail bool
}{{
name: "empty",
input: ``,
expect: []keyVal{},
}, {
name: "one-pair",
input: `k:v`,
expect: []keyVal{keyVal{"k", "v"}},
}, {
name: "one-pair-qval",
input: `k:"v"`,
expect: []keyVal{keyVal{"k", "v"}},
}, {
name: "garbage",
input: `abc123`,
fail: true,
}, {
name: "invalid-val",
input: `k:v\xv`,
fail: true,
}, {
name: "invalid-qval",
input: `k:"v\xv"`,
fail: true,
}, {
name: "two-pair",
input: `k1:v1,k2:v2`,
expect: []keyVal{{"k1", "v1"}, {"k2", "v2"}},
}, {
name: "val-spaces",
input: `k1:v 1,k2:v 2`,
expect: []keyVal{{"k1", "v 1"}, {"k2", "v 2"}},
}, {
name: "qval-spaces",
input: `k1:" v 1 ",k2:" v 2 "`,
expect: []keyVal{{"k1", " v 1 "}, {"k2", " v 2 "}},
}, {
name: "mix-val-qval",
input: `k1:v 1,k2:" v 2 "`,
expect: []keyVal{{"k1", "v 1"}, {"k2", " v 2 "}},
}, {
name: "garbage-after-qval",
input: `k1:"v1"x,k2:"v2"`,
fail: true,
}, {
name: "dangling-comma",
input: `k1:"v1",k2:"v2",`,
expect: []keyVal{{"k1", "v1"}, {"k2", "v2"}},
}, {
name: "val-escapes",
input: `k1:v\n\t\\\"\,1`,
expect: []keyVal{{"k1", "v\n\t\\\",1"}},
}, {
name: "qval-escapes",
input: `k1:"v\n\t\\\"\,1"`,
expect: []keyVal{{"k1", "v\n\t\\\",1"}},
}, {
name: "qval-comma",
input: `k1:"v,1"`,
expect: []keyVal{{"k1", "v,1"}},
}, {
name: "qval-missing-close",
input: `k1:"v1`,
fail: true,
}}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
kvs, err := parseGitConfigs(tc.input)
if err != nil && !tc.fail {
t.Errorf("unexpected error: %v", err)
}
if err == nil && tc.fail {
t.Errorf("unexpected success")
}
if !reflect.DeepEqual(kvs, tc.expect) {
t.Errorf("bad result: expected %v, got %v", tc.expect, kvs)
}
})
}
}

View File

@ -1132,6 +1132,27 @@ assert_file_eq "$ROOT"/link/file "$TESTCASE"
# Wrap up
pass
##############################################
# Test additional git configs
##############################################
testcase "additional-git-configs"
echo "$TESTCASE" > "$REPO"/file
git -C "$REPO" commit -qam "$TESTCASE"
GIT_SYNC \
--one-time \
--repo="file://$REPO" \
--branch=e2e-branch \
--rev=HEAD \
--root="$ROOT" \
--dest="link" \
--git-config='http.postBuffer:10485760,sect.k1:"a val",sect.k2:another val' \
> "$DIR"/log."$TESTCASE" 2>&1
assert_link_exists "$ROOT"/link
assert_file_exists "$ROOT"/link/file
assert_file_eq "$ROOT"/link/file "$TESTCASE"
# Wrap up
pass
echo
echo "cleaning up $DIR"
rm -rf "$DIR"