From c2739f3589f18df8563305a686ac216fcdda3fb6 Mon Sep 17 00:00:00 2001 From: Hanson Wang Date: Fri, 23 May 2025 20:39:23 -0700 Subject: [PATCH 1/4] feat: add branch listing command --- features/command_branch.feature | 45 ++++++++ internal/commands/branch/branch.go | 100 ++++++++++++++++++ internal/commands/inits/data/aliases.sh | 1 + .../commands/inits/data/status_shortcuts.fish | 22 ++++ .../commands/inits/data/status_shortcuts.sh | 30 ++++++ main.go | 2 + 6 files changed, 200 insertions(+) create mode 100644 features/command_branch.feature create mode 100644 internal/commands/branch/branch.go diff --git a/features/command_branch.feature b/features/command_branch.feature new file mode 100644 index 0000000..a75f0b3 --- /dev/null +++ b/features/command_branch.feature @@ -0,0 +1,45 @@ +Feature: branch command + + Background: + Given a mocked home directory + + @outside-repo + Scenario: Appropriate error status when not in a git repo + When I run `scmpuff branch` + Then the exit status should be 128 + And the output should contain: + """ + Not a git repository (or any of the parent directories) + """ + + Scenario: Numbered output of local branches + Given I am in a git repository + And a file named "base" with: + """ + foo + """ + And I successfully run `git add base` + And I successfully run `git commit -m "init"` + And I successfully run the following commands: + | git branch branch_a | + | git branch branch_b | + When I successfully run `scmpuff branch` + Then the stdout from "scmpuff branch" should contain "* [1] master" + And the stdout from "scmpuff branch" should contain " [2] branch_a" + And the stdout from "scmpuff branch" should contain " [3] branch_b" + + Scenario: Detached HEAD output + Given I am in a git repository + And a file named "foo" with: + """ + bar + """ + And I successfully run `git add foo` + And I successfully run `git commit -m "first"` + And I successfully run `git branch feature` + And I run `git checkout HEAD~0` + When I successfully run `scmpuff branch` + Then the stdout from "scmpuff branch" should match /\* \(HEAD detached/ + And the stdout from "scmpuff branch" should contain "[1] master" + And the stdout from "scmpuff branch" should contain "[2] feature" + diff --git a/internal/commands/branch/branch.go b/internal/commands/branch/branch.go new file mode 100644 index 0000000..ccb3d4f --- /dev/null +++ b/internal/commands/branch/branch.go @@ -0,0 +1,100 @@ +package branch + +import ( + "bufio" + "bytes" + "fmt" + "log" + "os" + "os/exec" + "strings" + + "github.com/spf13/cobra" +) + +// CommandBranch lists git branches with numbered shortcuts. +// The first line of output, when --filelist is provided, will contain +// a TAB separated list of branch names suitable for environment expansion. +func CommandBranch() *cobra.Command { + var optsFilelist bool + + var branchCmd = &cobra.Command{ + Use: "branch", + Short: "Display numbered git branches", + Run: func(cmd *cobra.Command, args []string) { + branches := gitBranchOutput() + numbered, list := process(branches) + + if optsFilelist { + fmt.Println(strings.Join(list, "\t")) + } + fmt.Print(numbered) + }, + } + + branchCmd.Flags().BoolVarP( + &optsFilelist, + "filelist", "f", false, + "include machine-parseable branch list", + ) + + return branchCmd +} + +func gitBranchOutput() []byte { + out, err := exec.Command("git", "branch").Output() + if err != nil { + if err.Error() == "exit status 128" { + msg := "Not a git repository (or any of the parent directories)" + fmt.Fprintf(os.Stderr, "\033[0;31m%s\033[0m\n", msg) + os.Exit(128) + } + log.Fatal(err) + } + return out +} + +// process takes raw `git branch` output and returns numbered output +// along with a slice of branch names in order. +func process(out []byte) (string, []string) { + scanner := bufio.NewScanner(bytes.NewReader(out)) + var starLine string + var starBranch string + var names []string + + for scanner.Scan() { + line := scanner.Text() + if len(line) < 2 { + continue + } + prefix := line[:2] + name := strings.TrimSpace(line[2:]) + if prefix == "* " { + starLine = line + if !strings.HasPrefix(name, "(") { + starBranch = name + } + continue + } + names = append(names, name) + } + + var b strings.Builder + var result []string + n := 1 + if starLine != "" { + if starBranch != "" { + b.WriteString(fmt.Sprintf("* [%d] %s\n", n, starBranch)) + result = append(result, starBranch) + n++ + } else { + b.WriteString(starLine + "\n") + } + } + for _, name := range names { + b.WriteString(fmt.Sprintf(" [%d] %s\n", n, name)) + result = append(result, name) + n++ + } + return b.String(), result +} diff --git a/internal/commands/inits/data/aliases.sh b/internal/commands/inits/data/aliases.sh index c3201bc..2a94f18 100644 --- a/internal/commands/inits/data/aliases.sh +++ b/internal/commands/inits/data/aliases.sh @@ -4,3 +4,4 @@ alias gd='git diff' alias gl='git log' alias gco='git checkout' alias grs='git reset' +alias gb='scmpuff_branch' diff --git a/internal/commands/inits/data/status_shortcuts.fish b/internal/commands/inits/data/status_shortcuts.fish index 0c9a2cb..67694d9 100644 --- a/internal/commands/inits/data/status_shortcuts.fish +++ b/internal/commands/inits/data/status_shortcuts.fish @@ -22,6 +22,28 @@ function scmpuff_status end end +function scmpuff_branch + scmpuff_clear_vars + set -lx scmpuff_env_char "e" + set -l cmd_output (/usr/bin/env scmpuff branch --filelist $argv) + set -l es "$status" + + if test $es -ne 0 + return $es + end + + set -l files (string split \t $cmd_output[1]) + if test (count $files) -gt 0 + for e in (seq (count $files)) + set -gx "$scmpuff_env_char""$e" "$files[$e]" + end + end + + for line in $cmd_output[2..-1] + echo $line + end +end + function scmpuff_clear_vars set -l scmpuff_env_char "e" set -l scmpuff_env_vars (set -x | awk '{print $1}' | grep -E '^'$scmpuff_env_char'[0-9]+') diff --git a/internal/commands/inits/data/status_shortcuts.sh b/internal/commands/inits/data/status_shortcuts.sh index d22f6e9..5f9e4ff 100644 --- a/internal/commands/inits/data/status_shortcuts.sh +++ b/internal/commands/inits/data/status_shortcuts.sh @@ -37,6 +37,36 @@ scmpuff_status() { } +# List git branches with numbered shortcuts +scmpuff_branch() { + local scmpuff_env_char="e" + + if [ -n "$ZSH_VERSION" ]; then setopt shwordsplit; fi; + + local cmd_output + cmd_output="$(/usr/bin/env scmpuff branch --filelist "$@")" + + local es=$? + if [ $es -ne 0 ]; then + return $es + fi + + files="$(echo "$cmd_output" | head -n 1)" + scmpuff_clear_vars + IFS=$'\t' + local e=1 + for file in $files; do + export $scmpuff_env_char$e="$file" + let e++ + done + IFS=$' \t\n' + + echo "$cmd_output" | tail -n +2 + + if [ -n "$ZSH_VERSION" ]; then unsetopt shwordsplit; fi; +} + + # Clear numbered env variables scmpuff_clear_vars() { local scmpuff_env_char="e" diff --git a/main.go b/main.go index ce4a093..9231f63 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" + "github.com/mroth/scmpuff/internal/commands/branch" "github.com/mroth/scmpuff/internal/commands/exec" "github.com/mroth/scmpuff/internal/commands/expand" "github.com/mroth/scmpuff/internal/commands/inits" @@ -44,6 +45,7 @@ func main() { puffCmd.AddCommand(exec.CommandExec()) puffCmd.AddCommand(expand.CommandExpand()) puffCmd.AddCommand(status.CommandStatus()) + puffCmd.AddCommand(branch.CommandBranch()) puffCmd.Execute() } From 37573ad8af26fe8ef07ce8223bc9b952f26eec9f Mon Sep 17 00:00:00 2001 From: Hanson Wang Date: Fri, 23 May 2025 21:28:31 -0700 Subject: [PATCH 2/4] feat: colorize active branch (#3) --- internal/commands/branch/branch.go | 19 ++++++++++++------- .../commands/inits/data/status_shortcuts.fish | 2 +- .../commands/inits/data/status_shortcuts.sh | 2 +- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/internal/commands/branch/branch.go b/internal/commands/branch/branch.go index ccb3d4f..abb5721 100644 --- a/internal/commands/branch/branch.go +++ b/internal/commands/branch/branch.go @@ -12,11 +12,16 @@ import ( "github.com/spf13/cobra" ) +const ( + colorBranch = "\033[1m" + colorReset = "\033[0m" +) + // CommandBranch lists git branches with numbered shortcuts. -// The first line of output, when --filelist is provided, will contain +// The first line of output, when --branchlist is provided, will contain // a TAB separated list of branch names suitable for environment expansion. func CommandBranch() *cobra.Command { - var optsFilelist bool + var optsBranchlist bool var branchCmd = &cobra.Command{ Use: "branch", @@ -25,7 +30,7 @@ func CommandBranch() *cobra.Command { branches := gitBranchOutput() numbered, list := process(branches) - if optsFilelist { + if optsBranchlist { fmt.Println(strings.Join(list, "\t")) } fmt.Print(numbered) @@ -33,8 +38,8 @@ func CommandBranch() *cobra.Command { } branchCmd.Flags().BoolVarP( - &optsFilelist, - "filelist", "f", false, + &optsBranchlist, + "branchlist", "f", false, "include machine-parseable branch list", ) @@ -42,7 +47,7 @@ func CommandBranch() *cobra.Command { } func gitBranchOutput() []byte { - out, err := exec.Command("git", "branch").Output() + out, err := exec.Command("git", "branch", "--color=always").Output() if err != nil { if err.Error() == "exit status 128" { msg := "Not a git repository (or any of the parent directories)" @@ -84,7 +89,7 @@ func process(out []byte) (string, []string) { n := 1 if starLine != "" { if starBranch != "" { - b.WriteString(fmt.Sprintf("* [%d] %s\n", n, starBranch)) + b.WriteString(fmt.Sprintf("* [%d] %s%s%s\n", n, colorBranch, starBranch, colorReset)) result = append(result, starBranch) n++ } else { diff --git a/internal/commands/inits/data/status_shortcuts.fish b/internal/commands/inits/data/status_shortcuts.fish index 67694d9..10d3459 100644 --- a/internal/commands/inits/data/status_shortcuts.fish +++ b/internal/commands/inits/data/status_shortcuts.fish @@ -25,7 +25,7 @@ end function scmpuff_branch scmpuff_clear_vars set -lx scmpuff_env_char "e" - set -l cmd_output (/usr/bin/env scmpuff branch --filelist $argv) + set -l cmd_output (/usr/bin/env scmpuff branch --branchlist $argv) set -l es "$status" if test $es -ne 0 diff --git a/internal/commands/inits/data/status_shortcuts.sh b/internal/commands/inits/data/status_shortcuts.sh index 5f9e4ff..226bb44 100644 --- a/internal/commands/inits/data/status_shortcuts.sh +++ b/internal/commands/inits/data/status_shortcuts.sh @@ -44,7 +44,7 @@ scmpuff_branch() { if [ -n "$ZSH_VERSION" ]; then setopt shwordsplit; fi; local cmd_output - cmd_output="$(/usr/bin/env scmpuff branch --filelist "$@")" + cmd_output="$(/usr/bin/env scmpuff branch --branchlist "$@")" local es=$? if [ $es -ne 0 ]; then From 0596600441c1b42be5c3187d41d98d53e2159a6e Mon Sep 17 00:00:00 2001 From: Hanson Wang Date: Fri, 23 May 2025 22:15:57 -0700 Subject: [PATCH 3/4] Update branch command color handling (#4) --- internal/commands/branch/branch.go | 6 +++--- internal/commands/inits/data/aliases.sh | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/commands/branch/branch.go b/internal/commands/branch/branch.go index abb5721..3bdcea6 100644 --- a/internal/commands/branch/branch.go +++ b/internal/commands/branch/branch.go @@ -13,7 +13,7 @@ import ( ) const ( - colorBranch = "\033[1m" + colorBranch = "\033[32m" colorReset = "\033[0m" ) @@ -47,7 +47,7 @@ func CommandBranch() *cobra.Command { } func gitBranchOutput() []byte { - out, err := exec.Command("git", "branch", "--color=always").Output() + out, err := exec.Command("git", "branch", "--color=never").Output() if err != nil { if err.Error() == "exit status 128" { msg := "Not a git repository (or any of the parent directories)" @@ -93,7 +93,7 @@ func process(out []byte) (string, []string) { result = append(result, starBranch) n++ } else { - b.WriteString(starLine + "\n") + b.WriteString(colorBranch + starLine + colorReset + "\n") } } for _, name := range names { diff --git a/internal/commands/inits/data/aliases.sh b/internal/commands/inits/data/aliases.sh index 2a94f18..2431941 100644 --- a/internal/commands/inits/data/aliases.sh +++ b/internal/commands/inits/data/aliases.sh @@ -1,7 +1,7 @@ alias gs='scmpuff_status' +alias gb='scmpuff_branch' alias ga='git add' alias gd='git diff' alias gl='git log' alias gco='git checkout' alias grs='git reset' -alias gb='scmpuff_branch' From c68537a6a7462f1bf1867937106400e51cef2415 Mon Sep 17 00:00:00 2001 From: Hanson Wang Date: Fri, 23 May 2025 22:30:28 -0700 Subject: [PATCH 4/4] shellcheck --- internal/commands/inits/data/status_shortcuts.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/commands/inits/data/status_shortcuts.sh b/internal/commands/inits/data/status_shortcuts.sh index 226bb44..9962632 100644 --- a/internal/commands/inits/data/status_shortcuts.sh +++ b/internal/commands/inits/data/status_shortcuts.sh @@ -25,7 +25,7 @@ scmpuff_status() { local e=1 for file in $files; do export $scmpuff_env_char$e="$file" - let e++ + (( e++ )) done IFS=$' \t\n' @@ -57,7 +57,7 @@ scmpuff_branch() { local e=1 for file in $files; do export $scmpuff_env_char$e="$file" - let e++ + (( e++ )) done IFS=$' \t\n'