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..3bdcea6 --- /dev/null +++ b/internal/commands/branch/branch.go @@ -0,0 +1,105 @@ +package branch + +import ( + "bufio" + "bytes" + "fmt" + "log" + "os" + "os/exec" + "strings" + + "github.com/spf13/cobra" +) + +const ( + colorBranch = "\033[32m" + colorReset = "\033[0m" +) + +// CommandBranch lists git branches with numbered shortcuts. +// 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 optsBranchlist 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 optsBranchlist { + fmt.Println(strings.Join(list, "\t")) + } + fmt.Print(numbered) + }, + } + + branchCmd.Flags().BoolVarP( + &optsBranchlist, + "branchlist", "f", false, + "include machine-parseable branch list", + ) + + return branchCmd +} + +func gitBranchOutput() []byte { + 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)" + 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%s%s\n", n, colorBranch, starBranch, colorReset)) + result = append(result, starBranch) + n++ + } else { + b.WriteString(colorBranch + starLine + colorReset + "\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..2431941 100644 --- a/internal/commands/inits/data/aliases.sh +++ b/internal/commands/inits/data/aliases.sh @@ -1,4 +1,5 @@ alias gs='scmpuff_status' +alias gb='scmpuff_branch' alias ga='git add' alias gd='git diff' alias gl='git log' diff --git a/internal/commands/inits/data/status_shortcuts.fish b/internal/commands/inits/data/status_shortcuts.fish index 0c9a2cb..10d3459 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 --branchlist $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..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' @@ -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 --branchlist "$@")" + + 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" + (( 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() }