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 22:27:42 -08:00
parent 6f5c1a9a22
commit 83b4dd21de
4 changed files with 305 additions and 11 deletions

View File

@ -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_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

@ -115,6 +115,8 @@ var flAskPassURL = pflag.String("askpass-url", envString("GIT_ASKPASS_URL", ""),
var flGitCmd = pflag.String("git", envString("GIT_SYNC_GIT", "git"),
"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", ""),
"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.
cancel()
@ -941,12 +951,15 @@ func cmdForLog(command string, args ...string) string {
if strings.ContainsAny(command, " \t\n") {
command = fmt.Sprintf("%q", command)
}
// Don't modify the passed-in args.
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) {
@ -1039,8 +1052,7 @@ func (git *repoSync) SetupCookieFile(ctx context.Context) error {
return fmt.Errorf("can't access git cookiefile: %w", err)
}
if _, err = runCommand(ctx, "",
git.cmd, "config", "--global", "http.cookiefile", pathToCookieFile); err != nil {
if _, err = runCommand(ctx, "", git.cmd, "config", "--global", "http.cookiefile", pathToCookieFile); err != nil {
return fmt.Errorf("can't configure git cookiefile: %w", err)
}
@ -1102,6 +1114,167 @@ func (git *repoSync) CallAskPassURL(ctx context.Context) 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)
}
// This string is formatted for 80 columns. Please keep it that way.
// DO NOT USE TABS.
var manual = `
@ -1164,17 +1337,20 @@ OPTIONS
Create a shallow clone with history truncated to the specified
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
The git command to run (subject to PATH search, mostly for testing).
(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
Print help text and exit.
@ -1190,6 +1366,13 @@ OPTIONS
Enable the pprof debug endpoints on git-sync's HTTP endpoint (see
--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
Print this manual and exit.

View File

@ -18,6 +18,7 @@ package main
import (
"os"
"reflect"
"strings"
"testing"
"time"
@ -150,3 +151,91 @@ func TestManualHasNoTabs(t *testing.T) {
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)
}
})
}
}

View File

@ -1233,6 +1233,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" \
--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 "all tests passed: cleaning up $DIR"
rm -rf "$DIR"