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
6f5c1a9a22
commit
83b4dd21de
|
|
@ -115,5 +115,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 | "" |
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,8 @@ var flAskPassURL = pflag.String("askpass-url", envString("GIT_ASKPASS_URL", ""),
|
||||||
|
|
||||||
var flGitCmd = pflag.String("git", envString("GIT_SYNC_GIT", "git"),
|
var flGitCmd = pflag.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 = pflag.String("git-config", envString("GIT_SYNC_GIT_CONFIG", ""),
|
||||||
|
"additional git config options in 'key1:val1,key2:val2' format")
|
||||||
|
|
||||||
var flHTTPBind = pflag.String("http-bind", envString("GIT_SYNC_HTTP_BIND", ""),
|
var flHTTPBind = pflag.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")
|
||||||
|
|
@ -485,6 +487,14 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This needs to be after all other git-related config flags.
|
||||||
|
if *flGitConfig != "" {
|
||||||
|
if err := setupExtraGitConfigs(ctx, *flGitConfig); err != nil {
|
||||||
|
log.Error(err, "ERROR: can't set additional git configs")
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
@ -941,12 +951,15 @@ 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)
|
||||||
}
|
}
|
||||||
|
// Don't modify the passed-in args.
|
||||||
|
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) {
|
||||||
|
|
@ -1039,8 +1052,7 @@ func (git *repoSync) SetupCookieFile(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, "", git.cmd, "config", "--global", "http.cookiefile", pathToCookieFile); err != nil {
|
||||||
git.cmd, "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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1102,6 +1114,167 @@ func (git *repoSync) CallAskPassURL(ctx context.Context) 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)
|
||||||
|
}
|
||||||
|
|
||||||
// This string is formatted for 80 columns. Please keep it that way.
|
// This string is formatted for 80 columns. Please keep it that way.
|
||||||
// DO NOT USE TABS.
|
// DO NOT USE TABS.
|
||||||
var manual = `
|
var manual = `
|
||||||
|
|
@ -1164,17 +1337,20 @@ OPTIONS
|
||||||
Create a shallow clone with history truncated to the specified
|
Create a shallow clone with history truncated to the specified
|
||||||
number of commits.
|
number of commits.
|
||||||
|
|
||||||
--link <string>, $GIT_SYNC_LINK
|
|
||||||
The name of the final symlink (under --root) which will point to the
|
|
||||||
current git worktree. This must be a filename, not a path, and may
|
|
||||||
not start with a period. The destination of this link (i.e.
|
|
||||||
readlink()) is the currently checked out SHA. (default: the leaf
|
|
||||||
dir of --repo)
|
|
||||||
|
|
||||||
--git <string>, $GIT_SYNC_GIT
|
--git <string>, $GIT_SYNC_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).
|
||||||
(default: git)
|
(default: git)
|
||||||
|
|
||||||
|
--git-config <string>, $GIT_SYNC_GIT_CONFIG
|
||||||
|
Additional git config options in 'key1:val1,key2:val2' format. The
|
||||||
|
key parts are passed to 'git config' and must be valid syntax for
|
||||||
|
that command. The val parts can be either quoted or unquoted
|
||||||
|
values. 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. (default: "")
|
||||||
|
|
||||||
-h, --help
|
-h, --help
|
||||||
Print help text and exit.
|
Print help text and exit.
|
||||||
|
|
||||||
|
|
@ -1190,6 +1366,13 @@ OPTIONS
|
||||||
Enable the pprof debug endpoints on git-sync's HTTP endpoint (see
|
Enable the pprof debug endpoints on git-sync's HTTP endpoint (see
|
||||||
--http-bind). (default: false)
|
--http-bind). (default: false)
|
||||||
|
|
||||||
|
--link <string>, $GIT_SYNC_LINK
|
||||||
|
The name of the final symlink (under --root) which will point to the
|
||||||
|
current git worktree. This must be a filename, not a path, and may
|
||||||
|
not start with a period. The destination of this link (i.e.
|
||||||
|
readlink()) is the currently checked out SHA. (default: the leaf
|
||||||
|
dir of --repo)
|
||||||
|
|
||||||
--man
|
--man
|
||||||
Print this manual and exit.
|
Print this manual and exit.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -150,3 +151,91 @@ func TestManualHasNoTabs(t *testing.T) {
|
||||||
t.Fatal("the manual text contains a tab")
|
t.Fatal("the manual text contains a tab")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
|
@ -1233,6 +1233,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" \
|
||||||
|
--link="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 "all tests passed: cleaning up $DIR"
|
echo "all tests passed: cleaning up $DIR"
|
||||||
rm -rf "$DIR"
|
rm -rf "$DIR"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue