Skip to content

Add --git-config flag #342

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 1 commit into from
Mar 15, 2021
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 @@ -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)]()
205 changes: 194 additions & 11 deletions cmd/git-sync/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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()

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

Expand Down Expand Up @@ -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 = `
Expand Down Expand Up @@ -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.

Expand All @@ -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.

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