diff --git a/cmd/git-sync/main.go b/cmd/git-sync/main.go index 679719a..bd0be47 100644 --- a/cmd/git-sync/main.go +++ b/cmd/git-sync/main.go @@ -133,7 +133,7 @@ var flAskPassURL = pflag.String("askpass-url", envMultiString([]string{"GIT_SYNC 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") + "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"), "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 { break } else { - cur.key, err = parseGitConfigKey(r, ch) - if err != nil { - return nil, err + // 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. + 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 } +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) { buf := make([]rune, 0, 64) buf = append(buf, r) @@ -1753,9 +1780,6 @@ func parseGitConfigKey(r rune, ch <-chan rune) (string, error) { 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) } } @@ -1763,30 +1787,17 @@ func parseGitConfigKey(r rune, ch <-chan rune) (string, error) { } 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) - } + str, err := parseQString(ch) + if err != nil { + return "", err } - 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) { @@ -1811,6 +1822,26 @@ func parseGitConfigVal(r rune, ch <-chan rune) (string, error) { 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. func unescape(ch <-chan rune) (rune, error) { r, ok := <-ch @@ -1920,14 +1951,22 @@ OPTIONS testing). This defaults to "git". --git-config , $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. + Additional git config options in a comma-separated 'key:val' + format. The parsed keys and values are passed to 'git config' and + must be valid syntax for that command. + + Both keys and values can be either quoted or unquoted strings. + Within quoted keys and all values (quoted or not), the following + escape sequences are supported: + '\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 , $GIT_SYNC_GIT_GC The git garbage collection behavior: one of "auto", "always", diff --git a/cmd/git-sync/main_test.go b/cmd/git-sync/main_test.go index 8fe2a50..41ad4f3 100644 --- a/cmd/git-sync/main_test.go +++ b/cmd/git-sync/main_test.go @@ -194,26 +194,66 @@ func TestParseGitConfigs(t *testing.T) { name: "one-pair", input: `k:v`, expect: []keyVal{keyVal{"k", "v"}}, + }, { + name: "one-pair-qkey", + input: `"k":v`, + expect: []keyVal{keyVal{"k", "v"}}, }, { name: "one-pair-qval", input: `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", input: `abc123`, fail: true, }, { - name: "invalid-val", - input: `k:v\xv`, - fail: true, + name: "key-section-var", + input: `sect.var:v`, + expect: []keyVal{keyVal{"sect.var", "v"}}, }, { - name: "invalid-qval", - input: `k:"v\xv"`, - fail: true, + name: "key-section-subsection-var", + input: `sect.sub.var:v`, + expect: []keyVal{keyVal{"sect.sub.var", "v"}}, }, { - name: "two-pair", - input: `k1:v1,k2:v2`, - expect: []keyVal{{"k1", "v1"}, {"k2", "v2"}}, + name: "key-subsection-with-space", + input: `k.sect.sub section:v`, + 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", input: `k1:v 1,k2:v 2`, @@ -235,17 +275,21 @@ func TestParseGitConfigs(t *testing.T) { input: `k1:"v1",k2:"v2",`, expect: []keyVal{{"k1", "v1"}, {"k2", "v2"}}, }, { - name: "val-escapes", + name: "val-with-escapes", input: `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"`, expect: []keyVal{{"k1", "v\n\t\\\",1"}}, }, { - name: "qval-comma", + name: "qval-with-comma", input: `k1:"v,1"`, expect: []keyVal{{"k1", "v,1"}}, + }, { + name: "qkey-missing-close", + input: `"k1:v1`, + fail: true, }, { name: "qval-missing-close", input: `k1:"v1`, @@ -262,7 +306,7 @@ func TestParseGitConfigs(t *testing.T) { t.Errorf("unexpected success") } 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) } }) }