diff --git a/doc/src/cli/shorthand.md b/doc/src/cli/shorthand.md index f5ce85f0..723f6a3c 100644 --- a/doc/src/cli/shorthand.md +++ b/doc/src/cli/shorthand.md @@ -82,3 +82,11 @@ For example: {gray}# Replace the "branch restack" shorthand{reset} {green}${reset} git config --global spice.shorthand.br "branch rename" ``` + +If the result of a user-defined shorthand refers to a built-in shorthand, +both will be expanded. + +```freeze language="terminal" +{green}${reset} git config --global spice.shorthand.bb bco +{gray}# bb will expand to bco, which will expand to "branch checkout"{reset} +``` diff --git a/internal/cli/shorthand/expand.go b/internal/cli/shorthand/expand.go index 533ccd7a..aac83717 100644 --- a/internal/cli/shorthand/expand.go +++ b/internal/cli/shorthand/expand.go @@ -16,13 +16,34 @@ type Source interface { } // Sources is a list of shorthand sources composed together. -// These are tried in order until one works. +// These are tried in order repeatedly until there's nothing left to expand. type Sources []Source var _ Source = Sources(nil) // ExpandShorthand expands the given shorthand command. -func (ss Sources) ExpandShorthand(cmd string) ([]string, bool) { +func (ss Sources) ExpandShorthand(orig string) ([]string, bool) { + seen := make(map[string]struct{}) // to prevent infinite loops + result := []string{orig} + for len(result) > 0 { + cmd := result[0] + if _, done := seen[cmd]; done { + break + } + seen[cmd] = struct{}{} + + next, ok := ss.expandOnce(cmd) + if !ok { + break + } + + result = slices.Replace(result, 0, 1, next...) + } + + return result, len(result) > 0 && result[0] != orig +} + +func (ss Sources) expandOnce(cmd string) ([]string, bool) { for _, s := range ss { if args, ok := s.ExpandShorthand(cmd); ok { return args, true diff --git a/internal/cli/shorthand/expand_test.go b/internal/cli/shorthand/expand_test.go index 7103ee7f..bb27a62a 100644 --- a/internal/cli/shorthand/expand_test.go +++ b/internal/cli/shorthand/expand_test.go @@ -61,6 +61,53 @@ func TestExpand(t *testing.T) { args: []string{"foo"}, want: []string{"qux", "baz"}, }, + { + name: "Sources/Cooperative", + src: shorthand.Sources{ + shorthandMap{"can": {"ca", "--no-edit"}}, + shorthandMap{"ca": {"c", "--amend"}}, + shorthandMap{"c": {"commit"}}, + }, + args: []string{"can", "--all"}, + want: []string{"commit", "--amend", "--no-edit", "--all"}, + }, + { + name: "Sources/CooperativeReverse", + src: shorthand.Sources{ + shorthandMap{"c": {"commit"}}, + shorthandMap{"ca": {"c", "--amend"}}, + shorthandMap{"can": {"ca", "--no-edit"}}, + }, + args: []string{"can", "--all"}, + want: []string{"commit", "--amend", "--no-edit", "--all"}, + }, + { + name: "Sources/Delete", + src: shorthand.Sources{ + shorthandMap{"foo": {"bar", "baz"}}, + shorthandMap{"bar": {}}, + }, + args: []string{"foo"}, + want: []string{"baz"}, + }, + { + name: "Sources/NoMatch", + src: shorthand.Sources{ + shorthandMap{"foo": {"bar", "baz"}}, + shorthandMap{"bar": {"qux"}}, + }, + args: []string{"qux"}, + want: []string{"qux"}, + }, + { + name: "Sources/InfiniteLoop", + src: shorthand.Sources{ + shorthandMap{"foo": {"bar"}}, + shorthandMap{"bar": {"foo"}}, + }, + args: []string{"foo"}, + want: []string{"foo"}, + }, } for _, tt := range tests {