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:
parent
05a099a964
commit
45bba183ca
|
|
@ -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_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_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_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 | "" |
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,8 @@ var flAskPassURL = flag.String("askpass-url", envString("GIT_ASKPASS_URL", ""),
|
||||||
|
|
||||||
var flGitCmd = flag.String("git", envString("GIT_SYNC_GIT", "git"),
|
var flGitCmd = flag.String("git", envString("GIT_SYNC_GIT", "git"),
|
||||||
"the git command to run (subject to PATH search, mostly for testing)")
|
"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", ""),
|
var flHTTPBind = flag.String("http-bind", envString("GIT_SYNC_HTTP_BIND", ""),
|
||||||
"the bind address (including port) for git-sync's HTTP endpoint")
|
"the bind address (including port) for git-sync's HTTP endpoint")
|
||||||
|
|
@ -391,6 +393,14 @@ func main() {
|
||||||
askpassCount.WithLabelValues(metricKeySuccess).Inc()
|
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.
|
// The scope of the initialization context ends here, so we call cancel to release resources associated with it.
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
|
|
@ -831,12 +841,14 @@ func cmdForLog(command string, args ...string) string {
|
||||||
if strings.ContainsAny(command, " \t\n") {
|
if strings.ContainsAny(command, " \t\n") {
|
||||||
command = fmt.Sprintf("%q", command)
|
command = fmt.Sprintf("%q", command)
|
||||||
}
|
}
|
||||||
|
argsCopy := make([]string, len(args))
|
||||||
|
copy(argsCopy, args)
|
||||||
for i := range args {
|
for i := range args {
|
||||||
if strings.ContainsAny(args[i], " \t\n") {
|
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) {
|
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)
|
return fmt.Errorf("can't access git cookiefile: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = runCommand(ctx, "",
|
if _, err = runCommand(ctx, "", *flGitCmd, "config", "--global", "http.cookiefile", pathToCookieFile); err != nil {
|
||||||
*flGitCmd, "config", "--global", "http.cookiefile", pathToCookieFile); err != nil {
|
|
||||||
return fmt.Errorf("can't configure git cookiefile: %w", err)
|
return fmt.Errorf("can't configure git cookiefile: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -986,3 +997,164 @@ func callGitAskPassURL(ctx context.Context, url string) error {
|
||||||
|
|
||||||
return nil
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
21
test_e2e.sh
21
test_e2e.sh
|
|
@ -1132,6 +1132,27 @@ assert_file_eq "$ROOT"/link/file "$TESTCASE"
|
||||||
# Wrap up
|
# Wrap up
|
||||||
pass
|
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
|
||||||
echo "cleaning up $DIR"
|
echo "cleaning up $DIR"
|
||||||
rm -rf "$DIR"
|
rm -rf "$DIR"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue