Allow quoted keys for --git-config

This allows keys to contain literal ':' which would previously confuse
the parser.
This commit is contained in:
Tim Hockin 2022-11-19 17:28:06 -08:00
parent a05f6c0745
commit 8081a6e1c3
No known key found for this signature in database
2 changed files with 134 additions and 51 deletions

View File

@ -133,7 +133,7 @@ var flAskPassURL = pflag.String("askpass-url", envMultiString([]string{"GIT_SYNC
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", ""), var flGitConfig = pflag.String("git-config", envString("GIT_SYNC_GIT_CONFIG", ""),
"additional git config options in 'key1:val1,key2:val2' format") "additional git config options in 'section.var1:val1,\"section.sub.var2\":\"val2\"' format")
var flGitGC = pflag.String("git-gc", envString("GIT_SYNC_GIT_GC", "auto"), var flGitGC = pflag.String("git-gc", envString("GIT_SYNC_GIT_GC", "auto"),
"git garbage collection behavior: one of 'auto', 'always', 'aggressive', or 'off'") "git garbage collection behavior: one of 'auto', 'always', 'aggressive', or 'off'")
@ -1715,9 +1715,19 @@ func parseGitConfigs(configsFlag string) ([]keyVal, error) {
if r, ok := <-ch; !ok { if r, ok := <-ch; !ok {
break break
} else { } else {
cur.key, err = parseGitConfigKey(r, ch) // This can accumulate things that git doesn't allow, but we'll
if err != nil { // just let git handle it, rather than try to pre-validate to their
return nil, err // spec.
if r == '"' {
cur.key, err = parseGitConfigQKey(ch)
if err != nil {
return nil, err
}
} else {
cur.key, err = parseGitConfigKey(r, ch)
if err != nil {
return nil, err
}
} }
} }
@ -1744,6 +1754,23 @@ func parseGitConfigs(configsFlag string) ([]keyVal, error) {
return result, nil return result, nil
} }
func parseGitConfigQKey(ch <-chan rune) (string, error) {
str, err := parseQString(ch)
if err != nil {
return "", err
}
// The next character must be a colon.
r, ok := <-ch
if !ok {
return "", fmt.Errorf("unexpected end of key: %q", str)
}
if r != ':' {
return "", fmt.Errorf("unexpected character after quoted key: %q%c", str, r)
}
return str, nil
}
func parseGitConfigKey(r rune, ch <-chan rune) (string, error) { func parseGitConfigKey(r rune, ch <-chan rune) (string, error) {
buf := make([]rune, 0, 64) buf := make([]rune, 0, 64)
buf = append(buf, r) buf = append(buf, r)
@ -1753,9 +1780,6 @@ func parseGitConfigKey(r rune, ch <-chan rune) (string, error) {
case r == ':': case r == ':':
return string(buf), nil return string(buf), nil
default: 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) buf = append(buf, r)
} }
} }
@ -1763,30 +1787,17 @@ func parseGitConfigKey(r rune, ch <-chan rune) (string, error) {
} }
func parseGitConfigQVal(ch <-chan rune) (string, error) { func parseGitConfigQVal(ch <-chan rune) (string, error) {
buf := make([]rune, 0, 64) str, err := parseQString(ch)
if err != nil {
for r := range ch { return "", err
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))
// If there is a next character, it must be a comma.
r, ok := <-ch
if ok && r != ',' {
return "", fmt.Errorf("unexpected character after quoted value %q%c", str, r)
}
return str, nil
} }
func parseGitConfigVal(r rune, ch <-chan rune) (string, error) { func parseGitConfigVal(r rune, ch <-chan rune) (string, error) {
@ -1811,6 +1822,26 @@ func parseGitConfigVal(r rune, ch <-chan rune) (string, error) {
return string(buf), nil return string(buf), nil
} }
func parseQString(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 '"':
return string(buf), nil
default:
buf = append(buf, r)
}
}
return "", fmt.Errorf("unexpected end of quoted string: %q", string(buf))
}
// unescape processes most of the documented escapes that git config supports. // unescape processes most of the documented escapes that git config supports.
func unescape(ch <-chan rune) (rune, error) { func unescape(ch <-chan rune) (rune, error) {
r, ok := <-ch r, ok := <-ch
@ -1920,14 +1951,22 @@ OPTIONS
testing). This defaults to "git". testing). This defaults to "git".
--git-config <string>, $GIT_SYNC_GIT_CONFIG --git-config <string>, $GIT_SYNC_GIT_CONFIG
Additional git config options in 'key1:val1,key2:val2' format. The Additional git config options in a comma-separated 'key:val'
key parts are passed to 'git config' and must be valid syntax for format. The parsed keys and values are passed to 'git config' and
that command. The val parts can be either quoted or unquoted must be valid syntax for that command.
values. For all values the following escape sequences are
supported: '\n' => [newline], '\t' => [tab], '\"' => '"', '\,' => Both keys and values can be either quoted or unquoted strings.
',', '\\' => '\'. Within unquoted values, commas MUST be escaped. Within quoted keys and all values (quoted or not), the following
Within quoted values, commas MAY be escaped, but are not required escape sequences are supported:
to be. Any other escape sequence is an error. '\n' => [newline]
'\t' => [tab]
'\"' => '"'
'\,' => ','
'\\' => '\'
To include a colon within a key (e.g. a URL) the key must be
quoted. 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.
--git-gc <string>, $GIT_SYNC_GIT_GC --git-gc <string>, $GIT_SYNC_GIT_GC
The git garbage collection behavior: one of "auto", "always", The git garbage collection behavior: one of "auto", "always",

View File

@ -194,26 +194,66 @@ func TestParseGitConfigs(t *testing.T) {
name: "one-pair", name: "one-pair",
input: `k:v`, input: `k:v`,
expect: []keyVal{keyVal{"k", "v"}}, expect: []keyVal{keyVal{"k", "v"}},
}, {
name: "one-pair-qkey",
input: `"k":v`,
expect: []keyVal{keyVal{"k", "v"}},
}, { }, {
name: "one-pair-qval", name: "one-pair-qval",
input: `k:"v"`, input: `k:"v"`,
expect: []keyVal{keyVal{"k", "v"}}, expect: []keyVal{keyVal{"k", "v"}},
}, {
name: "one-pair-qkey-qval",
input: `"k":"v"`,
expect: []keyVal{keyVal{"k", "v"}},
}, {
name: "multi-pair",
input: `k1:v1,"k2":v2,k3:"v3","k4":"v4"`,
expect: []keyVal{{"k1", "v1"}, {"k2", "v2"}, {"k3", "v3"}, {"k4", "v4"}},
}, { }, {
name: "garbage", name: "garbage",
input: `abc123`, input: `abc123`,
fail: true, fail: true,
}, { }, {
name: "invalid-val", name: "key-section-var",
input: `k:v\xv`, input: `sect.var:v`,
fail: true, expect: []keyVal{keyVal{"sect.var", "v"}},
}, { }, {
name: "invalid-qval", name: "key-section-subsection-var",
input: `k:"v\xv"`, input: `sect.sub.var:v`,
fail: true, expect: []keyVal{keyVal{"sect.sub.var", "v"}},
}, { }, {
name: "two-pair", name: "key-subsection-with-space",
input: `k1:v1,k2:v2`, input: `k.sect.sub section:v`,
expect: []keyVal{{"k1", "v1"}, {"k2", "v2"}}, expect: []keyVal{keyVal{"k.sect.sub section", "v"}},
}, {
name: "key-subsection-with-escape",
input: `k.sect.sub\tsection:v`,
expect: []keyVal{keyVal{"k.sect.sub\\tsection", "v"}},
}, {
name: "key-subsection-with-comma",
input: `k.sect.sub,section:v`,
expect: []keyVal{keyVal{"k.sect.sub,section", "v"}},
}, {
name: "qkey-subsection-with-space",
input: `"k.sect.sub section":v`,
expect: []keyVal{keyVal{"k.sect.sub section", "v"}},
}, {
name: "qkey-subsection-with-escapes",
input: `"k.sect.sub\t\n\\section":v`,
expect: []keyVal{keyVal{"k.sect.sub\t\n\\section", "v"}},
}, {
name: "qkey-subsection-with-comma",
input: `"k.sect.sub,section":v`,
expect: []keyVal{keyVal{"k.sect.sub,section", "v"}},
}, {
name: "qkey-subsection-with-colon",
input: `"k.sect.sub:section":v`,
expect: []keyVal{keyVal{"k.sect.sub:section", "v"}},
}, {
name: "invalid-qkey",
input: `"k\xk":v"`,
fail: true,
}, { }, {
name: "val-spaces", name: "val-spaces",
input: `k1:v 1,k2:v 2`, input: `k1:v 1,k2:v 2`,
@ -235,17 +275,21 @@ func TestParseGitConfigs(t *testing.T) {
input: `k1:"v1",k2:"v2",`, input: `k1:"v1",k2:"v2",`,
expect: []keyVal{{"k1", "v1"}, {"k2", "v2"}}, expect: []keyVal{{"k1", "v1"}, {"k2", "v2"}},
}, { }, {
name: "val-escapes", name: "val-with-escapes",
input: `k1:v\n\t\\\"\,1`, input: `k1:v\n\t\\\"\,1`,
expect: []keyVal{{"k1", "v\n\t\\\",1"}}, expect: []keyVal{{"k1", "v\n\t\\\",1"}},
}, { }, {
name: "qval-escapes", name: "qval-with-escapes",
input: `k1:"v\n\t\\\"\,1"`, input: `k1:"v\n\t\\\"\,1"`,
expect: []keyVal{{"k1", "v\n\t\\\",1"}}, expect: []keyVal{{"k1", "v\n\t\\\",1"}},
}, { }, {
name: "qval-comma", name: "qval-with-comma",
input: `k1:"v,1"`, input: `k1:"v,1"`,
expect: []keyVal{{"k1", "v,1"}}, expect: []keyVal{{"k1", "v,1"}},
}, {
name: "qkey-missing-close",
input: `"k1:v1`,
fail: true,
}, { }, {
name: "qval-missing-close", name: "qval-missing-close",
input: `k1:"v1`, input: `k1:"v1`,
@ -262,7 +306,7 @@ func TestParseGitConfigs(t *testing.T) {
t.Errorf("unexpected success") t.Errorf("unexpected success")
} }
if !reflect.DeepEqual(kvs, tc.expect) { if !reflect.DeepEqual(kvs, tc.expect) {
t.Errorf("bad result: expected %v, got %v", tc.expect, kvs) t.Errorf("bad result:\n\texpected: %#v\n\t got: %#v", tc.expect, kvs)
} }
}) })
} }