Skip to content

Commit 83b4dd2

Browse files
committed
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.
1 parent 6f5c1a9 commit 83b4dd2

File tree

4 files changed

+305
-11
lines changed

4 files changed

+305
-11
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,5 +115,6 @@ docker run -d \
115115
| GIT_SYNC_HTTP_BIND | `--http-bind` | the bind address (including port) for git-sync's HTTP endpoint | "" |
116116
| GIT_SYNC_HTTP_METRICS | `--http-metrics` | enable metrics on git-sync's HTTP endpoint | true |
117117
| GIT_SYNC_HTTP_PPROF | `--http-pprof` | enable the pprof debug endpoints on git-sync's HTTP endpoint | false |
118+
| GIT_SYNC_GIT_CONFIG | `--git-config` | additional git config options in 'key1:val1,key2:val2' format | "" |
118119

119120
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/git-sync/README.md?pixel)]()

cmd/git-sync/main.go

Lines changed: 194 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ var flAskPassURL = pflag.String("askpass-url", envString("GIT_ASKPASS_URL", ""),
115115

116116
var flGitCmd = pflag.String("git", envString("GIT_SYNC_GIT", "git"),
117117
"the git command to run (subject to PATH search, mostly for testing)")
118+
var flGitConfig = pflag.String("git-config", envString("GIT_SYNC_GIT_CONFIG", ""),
119+
"additional git config options in 'key1:val1,key2:val2' format")
118120

119121
var flHTTPBind = pflag.String("http-bind", envString("GIT_SYNC_HTTP_BIND", ""),
120122
"the bind address (including port) for git-sync's HTTP endpoint")
@@ -485,6 +487,14 @@ func main() {
485487
}
486488
}
487489

490+
// This needs to be after all other git-related config flags.
491+
if *flGitConfig != "" {
492+
if err := setupExtraGitConfigs(ctx, *flGitConfig); err != nil {
493+
log.Error(err, "ERROR: can't set additional git configs")
494+
os.Exit(1)
495+
}
496+
}
497+
488498
// The scope of the initialization context ends here, so we call cancel to release resources associated with it.
489499
cancel()
490500

@@ -941,12 +951,15 @@ func cmdForLog(command string, args ...string) string {
941951
if strings.ContainsAny(command, " \t\n") {
942952
command = fmt.Sprintf("%q", command)
943953
}
954+
// Don't modify the passed-in args.
955+
argsCopy := make([]string, len(args))
956+
copy(argsCopy, args)
944957
for i := range args {
945958
if strings.ContainsAny(args[i], " \t\n") {
946-
args[i] = fmt.Sprintf("%q", args[i])
959+
argsCopy[i] = fmt.Sprintf("%q", args[i])
947960
}
948961
}
949-
return command + " " + strings.Join(args, " ")
962+
return command + " " + strings.Join(argsCopy, " ")
950963
}
951964

952965
func runCommand(ctx context.Context, cwd, command string, args ...string) (string, error) {
@@ -1039,8 +1052,7 @@ func (git *repoSync) SetupCookieFile(ctx context.Context) error {
10391052
return fmt.Errorf("can't access git cookiefile: %w", err)
10401053
}
10411054

1042-
if _, err = runCommand(ctx, "",
1043-
git.cmd, "config", "--global", "http.cookiefile", pathToCookieFile); err != nil {
1055+
if _, err = runCommand(ctx, "", git.cmd, "config", "--global", "http.cookiefile", pathToCookieFile); err != nil {
10441056
return fmt.Errorf("can't configure git cookiefile: %w", err)
10451057
}
10461058

@@ -1102,6 +1114,167 @@ func (git *repoSync) CallAskPassURL(ctx context.Context) error {
11021114
return nil
11031115
}
11041116

1117+
func setupExtraGitConfigs(ctx context.Context, configsFlag string) error {
1118+
log.V(1).Info("setting additional git configs")
1119+
1120+
configs, err := parseGitConfigs(configsFlag)
1121+
if err != nil {
1122+
return fmt.Errorf("can't parse --git-config flag: %v", err)
1123+
}
1124+
for _, kv := range configs {
1125+
if _, err := runCommand(ctx, "", *flGitCmd, "config", "--global", kv.key, kv.val); err != nil {
1126+
return fmt.Errorf("error configuring additional git configs %q %q: %v", kv.key, kv.val, err)
1127+
}
1128+
}
1129+
1130+
return nil
1131+
}
1132+
1133+
type keyVal struct {
1134+
key string
1135+
val string
1136+
}
1137+
1138+
func parseGitConfigs(configsFlag string) ([]keyVal, error) {
1139+
ch := make(chan rune)
1140+
stop := make(chan bool)
1141+
go func() {
1142+
for _, r := range configsFlag {
1143+
select {
1144+
case <-stop:
1145+
break
1146+
default:
1147+
ch <- r
1148+
}
1149+
}
1150+
close(ch)
1151+
return
1152+
}()
1153+
1154+
result := []keyVal{}
1155+
1156+
// This assumes it is at the start of a key.
1157+
for {
1158+
cur := keyVal{}
1159+
var err error
1160+
1161+
// Peek and see if we have a key.
1162+
if r, ok := <-ch; !ok {
1163+
break
1164+
} else {
1165+
cur.key, err = parseGitConfigKey(r, ch)
1166+
if err != nil {
1167+
return nil, err
1168+
}
1169+
}
1170+
1171+
// Peek and see if we have a value.
1172+
if r, ok := <-ch; !ok {
1173+
return nil, fmt.Errorf("key %q: no value", cur.key)
1174+
} else {
1175+
if r == '"' {
1176+
cur.val, err = parseGitConfigQVal(ch)
1177+
if err != nil {
1178+
return nil, fmt.Errorf("key %q: %v", cur.key, err)
1179+
}
1180+
} else {
1181+
cur.val, err = parseGitConfigVal(r, ch)
1182+
if err != nil {
1183+
return nil, fmt.Errorf("key %q: %v", cur.key, err)
1184+
}
1185+
}
1186+
}
1187+
1188+
result = append(result, cur)
1189+
}
1190+
1191+
return result, nil
1192+
}
1193+
1194+
func parseGitConfigKey(r rune, ch <-chan rune) (string, error) {
1195+
buf := make([]rune, 0, 64)
1196+
buf = append(buf, r)
1197+
1198+
for r := range ch {
1199+
switch {
1200+
case r == ':':
1201+
return string(buf), nil
1202+
default:
1203+
// This can accumulate things that git doesn't allow, but we'll
1204+
// just let git handle it, rather than try to pre-validate to their
1205+
// spec.
1206+
buf = append(buf, r)
1207+
}
1208+
}
1209+
return "", fmt.Errorf("unexpected end of key: %q", string(buf))
1210+
}
1211+
1212+
func parseGitConfigQVal(ch <-chan rune) (string, error) {
1213+
buf := make([]rune, 0, 64)
1214+
1215+
for r := range ch {
1216+
switch r {
1217+
case '\\':
1218+
if e, err := unescape(ch); err != nil {
1219+
return "", err
1220+
} else {
1221+
buf = append(buf, e)
1222+
}
1223+
case '"':
1224+
// Once we have a closing quote, the next must be either a comma or
1225+
// end-of-string. This helps reset the state for the next key, if
1226+
// there is one.
1227+
r, ok := <-ch
1228+
if ok && r != ',' {
1229+
return "", fmt.Errorf("unexpected trailing character '%c'", r)
1230+
}
1231+
return string(buf), nil
1232+
default:
1233+
buf = append(buf, r)
1234+
}
1235+
}
1236+
return "", fmt.Errorf("unexpected end of value: %q", string(buf))
1237+
}
1238+
1239+
func parseGitConfigVal(r rune, ch <-chan rune) (string, error) {
1240+
buf := make([]rune, 0, 64)
1241+
buf = append(buf, r)
1242+
1243+
for r := range ch {
1244+
switch r {
1245+
case '\\':
1246+
if r, err := unescape(ch); err != nil {
1247+
return "", err
1248+
} else {
1249+
buf = append(buf, r)
1250+
}
1251+
case ',':
1252+
return string(buf), nil
1253+
default:
1254+
buf = append(buf, r)
1255+
}
1256+
}
1257+
// We ran out of characters, but that's OK.
1258+
return string(buf), nil
1259+
}
1260+
1261+
// unescape processes most of the documented escapes that git config supports.
1262+
func unescape(ch <-chan rune) (rune, error) {
1263+
r, ok := <-ch
1264+
if !ok {
1265+
return 0, fmt.Errorf("unexpected end of escape sequence")
1266+
}
1267+
switch r {
1268+
case 'n':
1269+
return '\n', nil
1270+
case 't':
1271+
return '\t', nil
1272+
case '"', ',', '\\':
1273+
return r, nil
1274+
}
1275+
return 0, fmt.Errorf("unsupported escape character: '%c'", r)
1276+
}
1277+
11051278
// This string is formatted for 80 columns. Please keep it that way.
11061279
// DO NOT USE TABS.
11071280
var manual = `
@@ -1164,17 +1337,20 @@ OPTIONS
11641337
Create a shallow clone with history truncated to the specified
11651338
number of commits.
11661339
1167-
--link <string>, $GIT_SYNC_LINK
1168-
The name of the final symlink (under --root) which will point to the
1169-
current git worktree. This must be a filename, not a path, and may
1170-
not start with a period. The destination of this link (i.e.
1171-
readlink()) is the currently checked out SHA. (default: the leaf
1172-
dir of --repo)
1173-
11741340
--git <string>, $GIT_SYNC_GIT
11751341
The git command to run (subject to PATH search, mostly for testing).
11761342
(default: git)
11771343
1344+
--git-config <string>, $GIT_SYNC_GIT_CONFIG
1345+
Additional git config options in 'key1:val1,key2:val2' format. The
1346+
key parts are passed to 'git config' and must be valid syntax for
1347+
that command. The val parts can be either quoted or unquoted
1348+
values. For all values the following escape sequences are
1349+
supported: '\n' => [newline], '\t' => [tab], '\"' => '"', '\,' =>
1350+
',', '\\' => '\'. Within unquoted values, commas MUST be escaped.
1351+
Within quoted values, commas MAY be escaped, but are not required
1352+
to be. Any other escape sequence is an error. (default: "")
1353+
11781354
-h, --help
11791355
Print help text and exit.
11801356
@@ -1190,6 +1366,13 @@ OPTIONS
11901366
Enable the pprof debug endpoints on git-sync's HTTP endpoint (see
11911367
--http-bind). (default: false)
11921368
1369+
--link <string>, $GIT_SYNC_LINK
1370+
The name of the final symlink (under --root) which will point to the
1371+
current git worktree. This must be a filename, not a path, and may
1372+
not start with a period. The destination of this link (i.e.
1373+
readlink()) is the currently checked out SHA. (default: the leaf
1374+
dir of --repo)
1375+
11931376
--man
11941377
Print this manual and exit.
11951378

cmd/git-sync/main_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package main
1818

1919
import (
2020
"os"
21+
"reflect"
2122
"strings"
2223
"testing"
2324
"time"
@@ -150,3 +151,91 @@ func TestManualHasNoTabs(t *testing.T) {
150151
t.Fatal("the manual text contains a tab")
151152
}
152153
}
154+
155+
func TestParseGitConfigs(t *testing.T) {
156+
cases := []struct {
157+
name string
158+
input string
159+
expect []keyVal
160+
fail bool
161+
}{{
162+
name: "empty",
163+
input: ``,
164+
expect: []keyVal{},
165+
}, {
166+
name: "one-pair",
167+
input: `k:v`,
168+
expect: []keyVal{keyVal{"k", "v"}},
169+
}, {
170+
name: "one-pair-qval",
171+
input: `k:"v"`,
172+
expect: []keyVal{keyVal{"k", "v"}},
173+
}, {
174+
name: "garbage",
175+
input: `abc123`,
176+
fail: true,
177+
}, {
178+
name: "invalid-val",
179+
input: `k:v\xv`,
180+
fail: true,
181+
}, {
182+
name: "invalid-qval",
183+
input: `k:"v\xv"`,
184+
fail: true,
185+
}, {
186+
name: "two-pair",
187+
input: `k1:v1,k2:v2`,
188+
expect: []keyVal{{"k1", "v1"}, {"k2", "v2"}},
189+
}, {
190+
name: "val-spaces",
191+
input: `k1:v 1,k2:v 2`,
192+
expect: []keyVal{{"k1", "v 1"}, {"k2", "v 2"}},
193+
}, {
194+
name: "qval-spaces",
195+
input: `k1:" v 1 ",k2:" v 2 "`,
196+
expect: []keyVal{{"k1", " v 1 "}, {"k2", " v 2 "}},
197+
}, {
198+
name: "mix-val-qval",
199+
input: `k1:v 1,k2:" v 2 "`,
200+
expect: []keyVal{{"k1", "v 1"}, {"k2", " v 2 "}},
201+
}, {
202+
name: "garbage-after-qval",
203+
input: `k1:"v1"x,k2:"v2"`,
204+
fail: true,
205+
}, {
206+
name: "dangling-comma",
207+
input: `k1:"v1",k2:"v2",`,
208+
expect: []keyVal{{"k1", "v1"}, {"k2", "v2"}},
209+
}, {
210+
name: "val-escapes",
211+
input: `k1:v\n\t\\\"\,1`,
212+
expect: []keyVal{{"k1", "v\n\t\\\",1"}},
213+
}, {
214+
name: "qval-escapes",
215+
input: `k1:"v\n\t\\\"\,1"`,
216+
expect: []keyVal{{"k1", "v\n\t\\\",1"}},
217+
}, {
218+
name: "qval-comma",
219+
input: `k1:"v,1"`,
220+
expect: []keyVal{{"k1", "v,1"}},
221+
}, {
222+
name: "qval-missing-close",
223+
input: `k1:"v1`,
224+
fail: true,
225+
}}
226+
227+
for _, tc := range cases {
228+
t.Run(tc.name, func(t *testing.T) {
229+
kvs, err := parseGitConfigs(tc.input)
230+
if err != nil && !tc.fail {
231+
t.Errorf("unexpected error: %v", err)
232+
}
233+
if err == nil && tc.fail {
234+
t.Errorf("unexpected success")
235+
}
236+
if !reflect.DeepEqual(kvs, tc.expect) {
237+
t.Errorf("bad result: expected %v, got %v", tc.expect, kvs)
238+
}
239+
})
240+
}
241+
}

test_e2e.sh

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,6 +1233,27 @@ assert_file_eq "$ROOT"/link/file "$TESTCASE"
12331233
# Wrap up
12341234
pass
12351235

1236+
##############################################
1237+
# Test additional git configs
1238+
##############################################
1239+
testcase "additional-git-configs"
1240+
echo "$TESTCASE" > "$REPO"/file
1241+
git -C "$REPO" commit -qam "$TESTCASE"
1242+
GIT_SYNC \
1243+
--one-time \
1244+
--repo="file://$REPO" \
1245+
--branch=e2e-branch \
1246+
--rev=HEAD \
1247+
--root="$ROOT" \
1248+
--link="link" \
1249+
--git-config='http.postBuffer:10485760,sect.k1:"a val",sect.k2:another val' \
1250+
> "$DIR"/log."$TESTCASE" 2>&1
1251+
assert_link_exists "$ROOT"/link
1252+
assert_file_exists "$ROOT"/link/file
1253+
assert_file_eq "$ROOT"/link/file "$TESTCASE"
1254+
# Wrap up
1255+
pass
1256+
12361257
echo
12371258
echo "all tests passed: cleaning up $DIR"
12381259
rm -rf "$DIR"

0 commit comments

Comments
 (0)