Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ retract v0.6.1 // init -s writes to stderr instead of stdout, breaking eval

require (
github.com/caarlos0/go-version v0.2.2
github.com/fatih/color v1.19.0
github.com/google/go-cmp v0.7.0
github.com/mattn/go-isatty v0.0.20
github.com/mroth/porcelain v0.1.1
github.com/rogpeppe/go-internal v1.14.1
github.com/spf13/cobra v1.10.2
)

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/spf13/pflag v1.0.9 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/tools v0.26.0 // indirect
)
11 changes: 9 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
github.com/caarlos0/go-version v0.2.2 h1:5r+nlrg4H2wOVwWjqRqRRIRbZ7ytRmjC9xoMIP0a5kQ=
github.com/caarlos0/go-version v0.2.2/go.mod h1:X+rI5VAtJDpcjCjeEIXpxGa5+rTcgur1FK66wS0/944=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mroth/porcelain v0.1.1 h1:9gj2GZtxsO/d23liwm2cIG45kTFP2lkI8hXqpxeE31E=
github.com/mroth/porcelain v0.1.1/go.mod h1:j/qPthvngCR1yxkK890O4cP4mlTWbxnGb4s0MR+m6EE=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
Expand All @@ -15,8 +21,9 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
44 changes: 21 additions & 23 deletions internal/cmd/status/color.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
package status

import "github.com/mroth/scmpuff/internal/gitstatus"

// Color represents an ANSI color code
type Color string
import (
"github.com/fatih/color"
"github.com/mroth/scmpuff/internal/gitstatus"
)

// Base color constants with full ANSI escape sequences
const (
RedColor Color = "\033[0;31m"
GreenColor Color = "\033[0;32m"
YellowColor Color = "\033[0;33m"
BlueColor Color = "\033[0;34m"
MagentaColor Color = "\033[0;35m"
CyanColor Color = "\033[0;36m"
BoldColor Color = "\033[1m"
DimWhiteColor Color = "\033[2;37m"
DimForegroundColor Color = "\033[2;39m"
ResetColor Color = "\033[0m"
// Color definitions using fatih/color for cross-platform terminal support.
var (
RedColor = color.New(color.FgRed)
GreenColor = color.New(color.FgGreen)
YellowColor = color.New(color.FgYellow)
BlueColor = color.New(color.FgBlue)
MagentaColor = color.New(color.FgMagenta)
CyanColor = color.New(color.FgCyan)
BoldColor = color.New(color.Bold)
DimForegroundColor = color.New(color.Faint)
)

// Semantic color mappings for different change states
var stateColors = map[gitstatus.ChangeState]Color{
var stateColors = map[gitstatus.ChangeState]*color.Color{
gitstatus.NewState: YellowColor,
gitstatus.ModifiedState: GreenColor,
gitstatus.DeletedState: RedColor,
Expand All @@ -31,17 +29,17 @@ var stateColors = map[gitstatus.ChangeState]Color{
}

// Group color mappings for status groups
var groupColors = map[gitstatus.StatusGroup]Color{
var groupColors = map[gitstatus.StatusGroup]*color.Color{
gitstatus.Staged: YellowColor,
gitstatus.Unmerged: RedColor,
gitstatus.Unstaged: GreenColor,
gitstatus.Untracked: CyanColor,
}

// Bold group colors for headers (arrows)
var groupBoldColors = map[gitstatus.StatusGroup]Color{
gitstatus.Staged: "\033[1;33m", // bold yellow
gitstatus.Unmerged: "\033[1;31m", // bold red
gitstatus.Unstaged: "\033[1;32m", // bold green
gitstatus.Untracked: "\033[1;36m", // bold cyan
var groupBoldColors = map[gitstatus.StatusGroup]*color.Color{
gitstatus.Staged: color.New(color.FgYellow, color.Bold),
gitstatus.Unmerged: color.New(color.FgRed, color.Bold),
gitstatus.Unstaged: color.New(color.FgGreen, color.Bold),
gitstatus.Untracked: color.New(color.FgCyan, color.Bold),
}
48 changes: 22 additions & 26 deletions internal/cmd/status/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"fmt"
"io"
"strconv"
"strings"

"github.com/mroth/scmpuff/internal/gitstatus"
Expand Down Expand Up @@ -178,17 +179,16 @@ func formatBranchBannerPrelude(b gitstatus.BranchInfo) string {
var diffFormatted string
if diffStr != "" {
diffFormatted = fmt.Sprintf(
" %s| %s%s%s",
DimForegroundColor, YellowColor, diffStr, ResetColor,
" %s %s",
DimForegroundColor.Sprint("|"), YellowColor.Sprint(diffStr),
)
}

return fmt.Sprintf(
"%s#%s On branch: %s%s%s %s| ",
DimForegroundColor, ResetColor, BoldColor,
b.Name, diffFormatted,
DimForegroundColor,
)
hash := DimForegroundColor.Sprint("#")
branch := BoldColor.Sprint(b.Name)
separator := DimForegroundColor.Sprint("| ")

return fmt.Sprintf("%s On branch: %s%s %s", hash, branch, diffFormatted, separator)
}

// formatUpstreamDiffIndicator formats the +1/-2 ahead/behind diff indicator for a branch relative to upstream
Expand All @@ -207,17 +207,14 @@ func formatUpstreamDiffIndicator(b gitstatus.BranchInfo) string {

func bannerChangeHeader() string {
return fmt.Sprintf(
"[%s*%s]%s => $e*\n%s#%s",
ResetColor, DimForegroundColor, ResetColor, DimForegroundColor, ResetColor,
"%s*%s => $e*\n%s",
DimForegroundColor.Sprint("["), DimForegroundColor.Sprint("]"), DimForegroundColor.Sprint("#"),
)
}

// bannerNoChanges returns the no changes message when working directory is clean
func bannerNoChanges() string {
return fmt.Sprintf(
"%sNo changes (working directory clean)%s",
GreenColor, ResetColor,
)
return GreenColor.Sprint("No changes (working directory clean)")
}

// formatHeaderForGroup returns the display header string for a file group.
Expand All @@ -230,15 +227,14 @@ func formatHeaderForGroup(group gitstatus.StatusGroup) string {
groupColor := groupColors[group]
groupBoldColor := groupBoldColors[group]
return fmt.Sprintf(
"%s➤%s %s\n%s#%s\n",
groupBoldColor, ResetColor, group.Description(), groupColor, ResetColor,
"%s %s\n%s\n",
groupBoldColor.Sprint("➤"), group.Description(), groupColor.Sprint("#"),
)
}

// formatFooterForGroup prints a final "#" for vertical padding
func formatFooterForGroup(group gitstatus.StatusGroup) string {
groupColor := groupColors[group]
return fmt.Sprintf("%s#%s\n", groupColor, ResetColor)
return groupColors[group].Sprint("#") + "\n"
}

// formatStatusItemDisplay returns print string for an individual status item for a group.
Expand All @@ -247,9 +243,8 @@ func formatFooterForGroup(group gitstatus.StatusGroup) string {
//
// # modified: [1] commands/status/constants.go
func (r *Renderer) formatStatusItemDisplay(item gitstatus.StatusItem, displayNum int) string {
// Get configured colors for the item display based on status group and state.
groupColor := string(groupColors[item.StatusGroup()])
stateColor := string(stateColors[item.State()])
groupColor := groupColors[item.StatusGroup()]
stateColor := stateColors[item.State()]

// For reasons lost to time, I originally decided to use a fixed width of 2
// to pad the display number, so that entries 1-99 would align nicely.
Expand All @@ -274,9 +269,10 @@ func (r *Renderer) formatStatusItemDisplay(item gitstatus.StatusItem, displayNum
paddedMsg = fmt.Sprintf("%10s", baseMsg)
}

return fmt.Sprintf(
"%s#%s %s%s:%s%s [%s%d%s] %s%s%s\n",
groupColor, ResetColor, stateColor, paddedMsg, padding, DimForegroundColor,
ResetColor, displayNum, DimForegroundColor, groupColor, itemDisplayPath, ResetColor,
)
hash := groupColor.Sprint("#")
state := stateColor.Sprintf("%s:", paddedMsg)
num := DimForegroundColor.Sprint("[") + strconv.Itoa(displayNum) + DimForegroundColor.Sprint("]")
path := groupColor.Sprint(itemDisplayPath)

return fmt.Sprintf("%s %s%s %s %s\n", hash, state, padding, num, path)
}
8 changes: 7 additions & 1 deletion internal/cmd/status/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"path/filepath"
"testing"

"github.com/fatih/color"
"github.com/mroth/scmpuff/internal/gitstatus"
)

Expand Down Expand Up @@ -223,13 +224,18 @@ func TestRenderer_Display(t *testing.T) {
name string
includeParseData bool
includeStatusOutput bool
forceColor bool
}{
{name: "parsedata.txt", includeParseData: true, includeStatusOutput: false},
{name: "display.ansi", includeParseData: false, includeStatusOutput: true},
{name: "display.ansi", includeParseData: false, includeStatusOutput: true, forceColor: true},
{name: "display.plain", includeParseData: false, includeStatusOutput: true, forceColor: false},
}

for _, oc := range optionCases {
t.Run(oc.name, func(t *testing.T) {
// Control color output for this test case.
color.NoColor = !oc.forceColor

renderer, err := NewRenderer(&tc.info, tc.root, tc.cwd)
if err != nil {
t.Fatalf("NewRenderer() error: %v", err)
Expand Down
20 changes: 19 additions & 1 deletion internal/cmd/status/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"os/exec"
"path/filepath"

"github.com/fatih/color"
"github.com/mattn/go-isatty"
"github.com/mroth/scmpuff/internal/gitstatus/porcelainv2"
"github.com/spf13/cobra"
)
Expand All @@ -34,6 +36,22 @@ see 'scmpuff init'.)
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true // silence usage-on-error after args processed

// Determine color output based on the user's terminal, not our stdout.
//
// stdout is always a pipe when invoked via the scmpuff_status() shell
// wrapper (which captures output in a subshell), so we cannot rely on
// fatih/color's default TTY detection against stdout. Instead, check
// stderr (which remains connected to the user's terminal) and honor
// the NO_COLOR convention (https://no-color.org/).
switch {
case os.Getenv("NO_COLOR") != "":
color.NoColor = true
case isatty.IsTerminal(os.Stderr.Fd()) || isatty.IsCygwinTerminal(os.Stderr.Fd()):
color.NoColor = false
default:
color.NoColor = true
}

// Obtain the current working directory (needed to determine git root and relative paths)
wd, err := os.Getwd()
if err != nil {
Expand All @@ -49,7 +67,7 @@ see 'scmpuff init'.)
var exitErr *exec.ExitError
if errors.As(err, &exitErr) && exitErr.ExitCode() == 128 {
msg := "Not a git repository (or any of the parent directories)"
fmt.Fprintf(os.Stderr, "%s%s%s\n", RedColor, msg, ResetColor)
fmt.Fprintln(os.Stderr, RedColor.Sprint(msg))
os.Exit(128)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# On branch: merge-conflict | [*] => $e*
#
➤ Unmerged paths
#
#  both added:  [1] both_added
#  both modified:  [2] both_modified
# deleted by them:  [3] deleted_by_them
#  deleted by us:  [4] deleted_by_us
#  both deleted:  [5] renamed_file
#  added by them:  [6] renamed_file_on_branch
#  added by us:  [7] renamed_file_on_master
#
# On branch: merge-conflict | [*] => $e*
#
➤ Unmerged paths
#
#  both added: [1] both_added
#  both modified: [2] both_modified
# deleted by them: [3] deleted_by_them
#  deleted by us: [4] deleted_by_us
#  both deleted: [5] renamed_file
#  added by them: [6] renamed_file_on_branch
#  added by us: [7] renamed_file_on_master
#
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# On branch: merge-conflict | [*] => $e*
#
➤ Unmerged paths
#
# both added: [1] both_added
# both modified: [2] both_modified
# deleted by them: [3] deleted_by_them
# deleted by us: [4] deleted_by_us
# both deleted: [5] renamed_file
# added by them: [6] renamed_file_on_branch
# added by us: [7] renamed_file_on_master
#
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# On branch: feature | +2/-1 | [*] => $e*
#
➤ Changes to be committed
#
#  new file:  [1] new.go
#  new file:  [2] new_b.go
#
➤ Changes not staged for commit
#
#  modified:  [3] modified.go
#
➤ Untracked files
#
#  untracked:  [4] untracked.go
#
# On branch: feature | +2/-1 | [*] => $e*
#
➤ Changes to be committed
#
#  new file: [1] new.go
#  new file: [2] new_b.go
#
➤ Changes not staged for commit
#
#  modified: [3] modified.go
#
➤ Untracked files
#
#  untracked: [4] untracked.go
#
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# On branch: feature | +2/-1 | [*] => $e*
#
➤ Changes to be committed
#
# new file: [1] new.go
# new file: [2] new_b.go
#
➤ Changes not staged for commit
#
# modified: [3] modified.go
#
➤ Untracked files
#
# untracked: [4] untracked.go
#
Original file line number Diff line number Diff line change
@@ -1 +1 @@
# On branch: main | No changes (working directory clean)
# On branch: main | No changes (working directory clean)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# On branch: main | No changes (working directory clean)
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# On branch: main | [*] => $e*
#
➤ Changes not staged for commit
#
#  new file:  [1] intent_to_add.txt
#  new file:  [2] another_new.txt
#  modified:  [3] modified.txt
#
# On branch: main | [*] => $e*
#
➤ Changes not staged for commit
#
#  new file: [1] intent_to_add.txt
#  new file: [2] another_new.txt
#  modified: [3] modified.txt
#
Loading
Loading