Skip to content

Add --git-config flag #341

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)]()
180 changes: 176 additions & 4 deletions cmd/git-sync/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
}
89 changes: 89 additions & 0 deletions cmd/git-sync/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package main

import (
"os"
"reflect"
"testing"
)

Expand Down Expand Up @@ -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 changes: 21 additions & 0 deletions test_e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"