From d6193d5660580e142df599bddc5a1eafc632ab05 Mon Sep 17 00:00:00 2001 From: Mike <10135646+mikesmithgh@users.noreply.github.com> Date: Fri, 22 Mar 2024 22:59:38 -0400 Subject: [PATCH] feat: return error exit codes and add windows fixes (#39) --- go.mod | 2 + go.sum | 2 + integration/gps_test.go | 103 +++++----- integration/init_test.go | 10 +- integration/init_unix.go | 15 ++ integration/init_windows.go | 13 ++ main.go | 33 ++-- pkg/git/repo.go | 20 +- pkg/util/util.go | 7 +- pkg/util/util_unix.go | 7 + pkg/util/util_windows.go | 7 + .../buildkite/shellwords/LICENSE.txt | 24 +++ .../github.com/buildkite/shellwords/README.md | 44 +++++ .../github.com/buildkite/shellwords/batch.go | 47 +++++ .../github.com/buildkite/shellwords/parser.go | 176 ++++++++++++++++++ .../github.com/buildkite/shellwords/posix.go | 46 +++++ .../github.com/buildkite/shellwords/quote.go | 13 ++ .../github.com/buildkite/shellwords/split.go | 13 ++ vendor/modules.txt | 3 + 19 files changed, 496 insertions(+), 89 deletions(-) create mode 100644 integration/init_unix.go create mode 100644 integration/init_windows.go create mode 100644 pkg/util/util_unix.go create mode 100644 pkg/util/util_windows.go create mode 100644 vendor/github.com/buildkite/shellwords/LICENSE.txt create mode 100644 vendor/github.com/buildkite/shellwords/README.md create mode 100644 vendor/github.com/buildkite/shellwords/batch.go create mode 100644 vendor/github.com/buildkite/shellwords/parser.go create mode 100644 vendor/github.com/buildkite/shellwords/posix.go create mode 100644 vendor/github.com/buildkite/shellwords/quote.go create mode 100644 vendor/github.com/buildkite/shellwords/split.go diff --git a/go.mod b/go.mod index 38fb4fc..8716ba9 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,5 @@ module github.com/mikesmithgh/git-prompt-string go 1.22.0 require github.com/pelletier/go-toml/v2 v2.1.1 + +require github.com/buildkite/shellwords v0.0.0-20180315110454-59467a9b8e10 diff --git a/go.sum b/go.sum index 532513b..c01fc34 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/buildkite/shellwords v0.0.0-20180315110454-59467a9b8e10 h1:XwHQ5xDtYPdtBbVPyRO6UZoWZe8/mbKUb076f8x7RvI= +github.com/buildkite/shellwords v0.0.0-20180315110454-59467a9b8e10/go.mod h1:gv0DYOzHEsKgo31lTCDGauIg4DTTGn41Bzp+t3wSOlk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/integration/gps_test.go b/integration/gps_test.go index 8fa6478..dbd1fbd 100644 --- a/integration/gps_test.go +++ b/integration/gps_test.go @@ -1,86 +1,81 @@ package integration import ( + "errors" "fmt" "os" "os/exec" "path/filepath" - "runtime" "testing" ) func TestGPS(t *testing.T) { - var notFoundMsg string - if runtime.GOOS == "windows" { - notFoundMsg = "The system cannot find the path specified." - } else { - notFoundMsg = "no such file or directory" - } tests := []struct { dir string input []string expected string environ []string + err error }{ - {"bare", []string{"--config=NONE"}, "\x1b[90m \ue0a0 BARE:main\x1b[0m", nil}, - {"no_upstream", []string{"--config=NONE"}, "\x1b[90m \ue0a0 main\x1b[0m", nil}, - {"no_upstream_remote", []string{"--config=NONE"}, "\x1b[90m \ue0a0 main → mikesmithgh/test/main\x1b[0m", nil}, - {"git_dir", []string{"--config=NONE"}, "\x1b[90m \ue0a0 GIT_DIR!\x1b[0m", nil}, - {"clean", []string{"--config=NONE"}, "\x1b[32m \ue0a0 main\x1b[0m", nil}, - {"tag", []string{"--config=NONE"}, "\x1b[90m \ue0a0 (v1.0.0)\x1b[0m", nil}, - {"commit", []string{"--config=NONE"}, "\x1b[90m \ue0a0 (24afc95)\x1b[0m", nil}, - {"dirty", []string{"--config=NONE"}, "\x1b[31m \ue0a0 main *\x1b[0m", nil}, - {"dirty_staged", []string{"--config=NONE"}, "\x1b[31m \ue0a0 main *\x1b[0m", nil}, - {"conflict_ahead", []string{"--config=NONE"}, "\x1b[33m \ue0a0 main ↑[1]\x1b[0m", nil}, - {"conflict_behind", []string{"--config=NONE"}, "\x1b[33m \ue0a0 main ↓[1]\x1b[0m", nil}, - {"conflict_diverged", []string{"--config=NONE"}, "\x1b[33m \ue0a0 main ↕ ↑[1] ↓[1]\x1b[0m", nil}, - {"untracked", []string{"--config=NONE"}, "\x1b[35m \ue0a0 main *\x1b[0m", nil}, - {"sparse", []string{"--config=NONE"}, "\x1b[32m \ue0a0 main|SPARSE\x1b[0m", nil}, - {"sparse_merge_conflict", []string{"--config=NONE"}, "\x1b[31m \ue0a0 main|SPARSE|MERGING|CONFLICT *↕ ↑[1] ↓[1]\x1b[0m", nil}, + {"bare", []string{"--config=NONE"}, "\x1b[90m \ue0a0 BARE:main\x1b[0m", nil, nil}, + {"no_upstream", []string{"--config=NONE"}, "\x1b[90m \ue0a0 main\x1b[0m", nil, nil}, + {"no_upstream_remote", []string{"--config=NONE"}, "\x1b[90m \ue0a0 main → mikesmithgh/test/main\x1b[0m", nil, nil}, + {"git_dir", []string{"--config=NONE"}, "\x1b[90m \ue0a0 GIT_DIR!\x1b[0m", nil, nil}, + {"clean", []string{"--config=NONE"}, "\x1b[32m \ue0a0 main\x1b[0m", nil, nil}, + {"tag", []string{"--config=NONE"}, "\x1b[90m \ue0a0 (v1.0.0)\x1b[0m", nil, nil}, + {"commit", []string{"--config=NONE"}, "\x1b[90m \ue0a0 (24afc95)\x1b[0m", nil, nil}, + {"dirty", []string{"--config=NONE"}, "\x1b[31m \ue0a0 main *\x1b[0m", nil, nil}, + {"dirty_staged", []string{"--config=NONE"}, "\x1b[31m \ue0a0 main *\x1b[0m", nil, nil}, + {"conflict_ahead", []string{"--config=NONE"}, "\x1b[33m \ue0a0 main ↑[1]\x1b[0m", nil, nil}, + {"conflict_behind", []string{"--config=NONE"}, "\x1b[33m \ue0a0 main ↓[1]\x1b[0m", nil, nil}, + {"conflict_diverged", []string{"--config=NONE"}, "\x1b[33m \ue0a0 main ↕ ↑[1] ↓[1]\x1b[0m", nil, nil}, + {"untracked", []string{"--config=NONE"}, "\x1b[35m \ue0a0 main *\x1b[0m", nil, nil}, + {"sparse", []string{"--config=NONE"}, "\x1b[32m \ue0a0 main|SPARSE\x1b[0m", nil, nil}, + {"sparse_merge_conflict", []string{"--config=NONE"}, "\x1b[31m \ue0a0 main|SPARSE|MERGING|CONFLICT *↕ ↑[1] ↓[1]\x1b[0m", nil, nil}, // rebase merge - {"rebase_i", []string{"--config=NONE"}, "\x1b[34m \ue0a0 main|REBASE-i 1/1\x1b[0m", nil}, - {"rebase_m", []string{"--config=NONE"}, "\x1b[34m \ue0a0 main|REBASE-m 1/1\x1b[0m", nil}, + {"rebase_i", []string{"--config=NONE"}, "\x1b[34m \ue0a0 main|REBASE-i 1/1\x1b[0m", nil, nil}, + {"rebase_m", []string{"--config=NONE"}, "\x1b[34m \ue0a0 main|REBASE-m 1/1\x1b[0m", nil, nil}, // rebase apply - {"am_rebase", []string{"--config=NONE"}, "\x1b[34m \ue0a0 (b69e688)|AM/REBASE 1/1\x1b[0m", nil}, - {"am", []string{"--config=NONE"}, "\x1b[34m \ue0a0 (b69e688)|AM 1/1\x1b[0m", nil}, - {"rebase", []string{"--config=NONE"}, "\x1b[34m \ue0a0 main|REBASE 1/1\x1b[0m", nil}, + {"am_rebase", []string{"--config=NONE"}, "\x1b[34m \ue0a0 (b69e688)|AM/REBASE 1/1\x1b[0m", nil, nil}, + {"am", []string{"--config=NONE"}, "\x1b[34m \ue0a0 (b69e688)|AM 1/1\x1b[0m", nil, nil}, + {"rebase", []string{"--config=NONE"}, "\x1b[34m \ue0a0 main|REBASE 1/1\x1b[0m", nil, nil}, // merge - {"merge_conflict", []string{"--config=NONE"}, "\x1b[31m \ue0a0 main|MERGING|CONFLICT *↕ ↑[1] ↓[1]\x1b[0m", nil}, - {"merge", []string{"--config=NONE"}, "\x1b[35m \ue0a0 main|MERGING *↕ ↑[1] ↓[1]\x1b[0m", nil}, + {"merge_conflict", []string{"--config=NONE"}, "\x1b[31m \ue0a0 main|MERGING|CONFLICT *↕ ↑[1] ↓[1]\x1b[0m", nil, nil}, + {"merge", []string{"--config=NONE"}, "\x1b[35m \ue0a0 main|MERGING *↕ ↑[1] ↓[1]\x1b[0m", nil, nil}, // cherry pick - {"cherry_pick_conflict", []string{"--config=NONE"}, "\x1b[31m \ue0a0 main|CHERRY-PICKING|CONFLICT *↕ ↑[1] ↓[1]\x1b[0m", nil}, - {"cherry_pick", []string{"--config=NONE"}, "\x1b[35m \ue0a0 main|CHERRY-PICKING *↕ ↑[1] ↓[1]\x1b[0m", nil}, + {"cherry_pick_conflict", []string{"--config=NONE"}, "\x1b[31m \ue0a0 main|CHERRY-PICKING|CONFLICT *↕ ↑[1] ↓[1]\x1b[0m", nil, nil}, + {"cherry_pick", []string{"--config=NONE"}, "\x1b[35m \ue0a0 main|CHERRY-PICKING *↕ ↑[1] ↓[1]\x1b[0m", nil, nil}, // revert - {"revert_conflict", []string{"--config=NONE"}, "\x1b[31m \ue0a0 main|REVERTING|CONFLICT *↕ ↑[2] ↓[1]\x1b[0m", nil}, - {"revert", []string{"--config=NONE"}, "\x1b[31m \ue0a0 main|REVERTING *↕ ↑[2] ↓[1]\x1b[0m", nil}, + {"revert_conflict", []string{"--config=NONE"}, "\x1b[31m \ue0a0 main|REVERTING|CONFLICT *↕ ↑[2] ↓[1]\x1b[0m", nil, nil}, + {"revert", []string{"--config=NONE"}, "\x1b[31m \ue0a0 main|REVERTING *↕ ↑[2] ↓[1]\x1b[0m", nil, nil}, // bisect - {"bisect", []string{"--config=NONE"}, "\x1b[34m \ue0a0 main|BISECTING ↓[1]\x1b[0m", nil}, + {"bisect", []string{"--config=NONE"}, "\x1b[34m \ue0a0 main|BISECTING ↓[1]\x1b[0m", nil, nil}, // formatting - {"clean", []string{"--config=NONE", "--color-disabled"}, " \ue0a0 main", nil}, - {"clean", []string{"--config=NONE", "--color-disabled", "--prompt-prefix= start "}, " start main", nil}, - {"clean", []string{"--config=NONE", "--color-disabled", "--prompt-suffix= stop"}, " \ue0a0 main stop", nil}, - {"conflict_ahead", []string{"--config=NONE", "--color-disabled", "--ahead-format=ahead by %d"}, " \ue0a0 main ahead by 1", nil}, - {"conflict_behind", []string{"--config=NONE", "--color-disabled", "--behind-format=behind by %d"}, " \ue0a0 main behind by 1", nil}, - {"conflict_diverged", []string{"--config=NONE", "--color-disabled", "--diverged-format=ahead by %d behind by %d"}, " \ue0a0 main ahead by 1 behind by 1", nil}, - {"no_upstream_remote", []string{"--config=NONE", "--color-disabled", "--no-upstream-remote-format= upstream=[repo: %s branch: %s]"}, " \ue0a0 main upstream=[repo: mikesmithgh/test branch: main]", nil}, + {"clean", []string{"--config=NONE", "--color-disabled"}, " \ue0a0 main", nil, nil}, + {"clean", []string{"--config=NONE", "--color-disabled", "--prompt-prefix= start "}, " start main", nil, nil}, + {"clean", []string{"--config=NONE", "--color-disabled", "--prompt-suffix= stop"}, " \ue0a0 main stop", nil, nil}, + {"conflict_ahead", []string{"--config=NONE", "--color-disabled", "--ahead-format=ahead by %d"}, " \ue0a0 main ahead by 1", nil, nil}, + {"conflict_behind", []string{"--config=NONE", "--color-disabled", "--behind-format=behind by %d"}, " \ue0a0 main behind by 1", nil, nil}, + {"conflict_diverged", []string{"--config=NONE", "--color-disabled", "--diverged-format=ahead by %d behind by %d"}, " \ue0a0 main ahead by 1 behind by 1", nil, nil}, + {"no_upstream_remote", []string{"--config=NONE", "--color-disabled", "--no-upstream-remote-format= upstream=[repo: %s branch: %s]"}, " \ue0a0 main upstream=[repo: mikesmithgh/test branch: main]", nil, nil}, // color overrides - {"clean", []string{"--config=../configs/color_overrides.toml"}, "\x1b[38;2;230;238;4m \ue0a0 main\x1b[0m", nil}, - {"no_upstream", []string{"--config=../configs/color_overrides.toml"}, "\x1b[30m\x1b[47m \ue0a0 main\x1b[0m", nil}, - {"dirty", []string{"--config=../configs/color_overrides.toml"}, "\x1b[48;2;179;5;89m \ue0a0 main *\x1b[0m", nil}, - {"conflict_ahead", []string{"--config=../configs/color_overrides.toml"}, "\x1b[38;2;252;183;40m \ue0a0 main ↑[1]\x1b[0m", nil}, - {"untracked", []string{"--config=../configs/color_overrides.toml"}, "\x1b[38;2;255;0;0m\x1b[48;2;22;242;170m \ue0a0 main *\x1b[0m", nil}, - {"bisect", []string{}, "\x1b[48;2;204;204;255m\x1b[35m \ue0a0 main|BISECTING ↓[1]\x1b[0m", []string{"GIT_PROMPT_STRING_CONFIG=../configs/color_overrides.toml"}}, + {"clean", []string{"--config=../configs/color_overrides.toml"}, "\x1b[38;2;230;238;4m \ue0a0 main\x1b[0m", nil, nil}, + {"no_upstream", []string{"--config=../configs/color_overrides.toml"}, "\x1b[30m\x1b[47m \ue0a0 main\x1b[0m", nil, nil}, + {"dirty", []string{"--config=../configs/color_overrides.toml"}, "\x1b[48;2;179;5;89m \ue0a0 main *\x1b[0m", nil, nil}, + {"conflict_ahead", []string{"--config=../configs/color_overrides.toml"}, "\x1b[38;2;252;183;40m \ue0a0 main ↑[1]\x1b[0m", nil, nil}, + {"untracked", []string{"--config=../configs/color_overrides.toml"}, "\x1b[38;2;255;0;0m\x1b[48;2;22;242;170m \ue0a0 main *\x1b[0m", nil, nil}, + {"bisect", []string{}, "\x1b[48;2;204;204;255m\x1b[35m \ue0a0 main|BISECTING ↓[1]\x1b[0m", []string{"GIT_PROMPT_STRING_CONFIG=../configs/color_overrides.toml"}, nil}, // config errors - {"clean", []string{"--config=/fromparam/does/not/exist"}, fmt.Sprintf("\x1b[31m git-prompt-string error(read config): open /fromparam/does/not/exist: %s\x1b[0m", notFoundMsg), nil}, - {"configs", []string{}, fmt.Sprintf("\x1b[31m git-prompt-string error(read config): open /fromenvvar/does/not/exist: %s\x1b[0m", notFoundMsg), []string{"GIT_PROMPT_STRING_CONFIG=/fromenvvar/does/not/exist"}}, - {"configs", []string{"--config=invalid_syntax.toml"}, "\x1b[31m git-prompt-string error(unmarshal config): toml: expected character =\x1b[0m", nil}, - {"configs", []string{}, "\x1b[31m git-prompt-string error(unmarshal config): toml: expected character =\x1b[0m", []string{"GIT_PROMPT_STRING_CONFIG=invalid_syntax.toml"}}, + {"clean", []string{"--config=/fromparam/does/not/exist"}, fmt.Sprintf("\x1b[31m git-prompt-string error(read config): \"open /fromparam/does/not/exist: %s\"\x1b[0m", notFoundMsg), nil, errors.New("exit status 1")}, + {"configs", []string{}, fmt.Sprintf("\x1b[31m git-prompt-string error(read config): \"open /fromenvvar/does/not/exist: %s\"\x1b[0m", notFoundMsg), []string{"GIT_PROMPT_STRING_CONFIG=/fromenvvar/does/not/exist"}, errors.New("exit status 1")}, + {"configs", []string{"--config=invalid_syntax.toml"}, fmt.Sprintf("\x1b[31m git-prompt-string error(unmarshal config): \"toml: expected character %s\"\x1b[0m", escapedEqualSign), nil, errors.New("exit status 1")}, + {"configs", []string{}, fmt.Sprintf("\x1b[31m git-prompt-string error(unmarshal config): \"toml: expected character %s\"\x1b[0m", escapedEqualSign), []string{"GIT_PROMPT_STRING_CONFIG=invalid_syntax.toml"}, errors.New("exit status 1")}, - {"norepo", []string{"--config=NONE"}, "", nil}, + {"norepo", []string{"--config=NONE"}, "", nil, nil}, } for _, test := range tests { @@ -91,8 +86,10 @@ func TestGPS(t *testing.T) { cmd.Env = append(cmd.Env, test.environ...) } result, err := cmd.CombinedOutput() - if err != nil { + if test.err == nil && err != nil { t.Errorf("Unexpected error: %s", err) + } else if test.err != nil && test.err.Error() != err.Error() { + t.Errorf("Expected error: %s, got: %s", test.err, err) } actual := string(result) if actual != test.expected { diff --git a/integration/init_test.go b/integration/init_test.go index 8d5f5f4..56c1c87 100644 --- a/integration/init_test.go +++ b/integration/init_test.go @@ -5,7 +5,6 @@ import ( "os" "os/exec" "path/filepath" - "runtime" "testing" ) @@ -22,11 +21,7 @@ func TestMain(m *testing.M) { } defer os.RemoveAll(tmpDir) - gps := "git-prompt-string" - if runtime.GOOS == "windows" { - gps += ".exe" - } - builtBinaryPath = filepath.Join(tmpDir, gps) + builtBinaryPath = filepath.Join(tmpDir, git_prompt_string_bin) cmd := exec.Command("go", "build", "-o", builtBinaryPath, "..") output, err := cmd.CombinedOutput() @@ -34,7 +29,8 @@ func TestMain(m *testing.M) { panic(fmt.Sprintf("failed to build gps: %s, %s", output, err)) } - cmd = exec.Command("cp", "-r", "../testdata", tmpDir) + copyCommand, copyArgs := copyTestDataCmd("..", tmpDir) + cmd = exec.Command(copyCommand, copyArgs...) err = cmd.Run() if err != nil { panic(fmt.Sprintf("failed to copy test data: %s", err)) diff --git a/integration/init_unix.go b/integration/init_unix.go new file mode 100644 index 0000000..be3a716 --- /dev/null +++ b/integration/init_unix.go @@ -0,0 +1,15 @@ +//go:build !windows + +package integration + +import "path/filepath" + +var ( + notFoundMsg string = "no such file or directory" + git_prompt_string_bin string = "git-prompt-string" + escapedEqualSign string = "\\=" +) + +func copyTestDataCmd(src string, dest string) (string, []string) { + return "cp", []string{"-r", filepath.Join(src, "testdata"), dest} +} diff --git a/integration/init_windows.go b/integration/init_windows.go new file mode 100644 index 0000000..3c4e80b --- /dev/null +++ b/integration/init_windows.go @@ -0,0 +1,13 @@ +package integration + +import "path/filepath" + +var ( + notFoundMsg string = "The system cannot find the path specified." + git_prompt_string_bin string = "git-prompt-string.exe" + escapedEqualSign string = "^=" +) + +func copyTestDataCmd(src string, dest string) (string, []string) { + return "xcopy", []string{"/S", "/E", "/I", filepath.Join(src, "testdata"), filepath.Join(dest, "testdata")} +} diff --git a/main.go b/main.go index 5fc9e7a..c1cc9ed 100644 --- a/main.go +++ b/main.go @@ -4,8 +4,8 @@ import ( "flag" "fmt" "os" + "os/exec" "path" - "runtime" "strconv" "strings" @@ -38,6 +38,7 @@ var ( ) func main() { + // TODO: check if git is installed? cfg := config.GPSConfig{ PromptPrefix: *promptPrefix, PromptSuffix: *promptSuffix, @@ -68,13 +69,9 @@ func main() { if xdgConfigHome == "" { home, err := os.UserHomeDir() if err != nil { - util.ErrMsg("user home", err, 0) - } - if runtime.GOOS == "windows" { - xdgConfigHome = path.Join(home, "AppData", "Local") - } else { - xdgConfigHome = path.Join(home, ".config") + util.ErrMsg("user home", err) } + xdgConfigHome = path.Join(home, util.XDGConfigPath) } gpsConfig = path.Join(xdgConfigHome, "git-prompt-string", "config.toml") } @@ -82,16 +79,16 @@ func main() { if gpsConfig != "NONE" { gpsConfigRaw, err := os.ReadFile(gpsConfig) if err != nil && !os.IsNotExist(err) { - util.ErrMsg("read config exists", err, 0) + util.ErrMsg("read config exists", err) } if err != nil && (*configPath != "" || gpsConfigEnv != "") { - util.ErrMsg("read config", err, 0) + util.ErrMsg("read config", err) } err = toml.Unmarshal(gpsConfigRaw, &cfg) if err != nil { - util.ErrMsg("unmarshal config", err, 0) + util.ErrMsg("unmarshal config", err) } } @@ -112,7 +109,7 @@ func main() { case "color-disabled": colorDisabled, err := strconv.ParseBool(f.Value.String()) if err != nil { - util.ErrMsg("parse color disabled", err, 0) + util.ErrMsg("parse color disabled", err) } cfg.ColorDisabled = colorDisabled case "color-clean": @@ -147,24 +144,28 @@ func main() { clearColor, err := color.Color("none") if err != nil { - util.ErrMsg("color none", err, 0) + util.ErrMsg("color none", err) } gitRepo, stderr, err := git.RevParse() if err != nil { - if strings.Contains(string(stderr), "not a git repository") { + switch { + case strings.Contains(err.Error(), exec.ErrNotFound.Error()): + util.ErrMsg("rev parse", err) + case strings.Contains(string(stderr), "not a git repository"): os.Exit(0) + default: + // allow other errors to pass through, the git repo may not have upstream } - // allow other errors to pass through, the git repo may not have upstream } branchInfo, err := gitRepo.BranchInfo(cfg) if err != nil { - util.ErrMsg("branch info", err, 0) + util.ErrMsg("branch info", err) } branchStatus, promptColor, err := gitRepo.BranchStatus(cfg) if err != nil { - util.ErrMsg("branch status", err, 0) + util.ErrMsg("branch status", err) } fmt.Printf("%s%s%s%s%s%s", promptColor, cfg.PromptPrefix, branchInfo, branchStatus, cfg.PromptSuffix, clearColor) diff --git a/pkg/git/repo.go b/pkg/git/repo.go index 9dd43e1..0c78cf7 100644 --- a/pkg/git/repo.go +++ b/pkg/git/repo.go @@ -42,7 +42,7 @@ func (g *GitRepo) GitDirFileExists(name string) (bool, error) { func (g *GitRepo) GitDirFileExistsExitOnError(name string) bool { exists, err := g.GitDirFileExists(name) if err != nil { - util.ErrMsg(fmt.Sprintf("dir exists %s", name), err, 0) + util.ErrMsg(fmt.Sprintf("dir exists %s", name), err) } return exists } @@ -66,7 +66,7 @@ func (g *GitRepo) ReadGitDirFile(name string) (string, error) { func (g *GitRepo) ReadGitDirFileExitOnError(name string) string { content, err := g.ReadGitDirFile(name) if err != nil { - util.ErrMsg(fmt.Sprintf("read file %s", name), err, 0) + util.ErrMsg(fmt.Sprintf("read file %s", name), err) } return content } @@ -197,7 +197,7 @@ func (g *GitRepo) BranchStatus(cfg config.GPSConfig) (string, string, error) { if g.IsInBareRepo || g.IsInGitDir { c, err := color.Color(strings.Split(cfg.ColorNoUpstream, " ")...) if err != nil { - util.ErrMsg("color no upstream", err, 0) + util.ErrMsg("color no upstream", err) } return status, c, nil } @@ -222,21 +222,21 @@ func (g *GitRepo) BranchStatus(cfg config.GPSConfig) (string, string, error) { if cleanWorkingTree { statusColor, err = color.Color(strings.Split(cfg.ColorClean, " ")...) if err != nil { - util.ErrMsg("color clean", err, 0) + util.ErrMsg("color clean", err) } } if ahead > 0 { statusColor, err = color.Color(strings.Split(cfg.ColorConflict, " ")...) if err != nil { - util.ErrMsg("color conflict", err, 0) + util.ErrMsg("color conflict", err) } status = fmt.Sprintf(cfg.AheadFormat, ahead) } if behind > 0 { statusColor, err = color.Color(strings.Split(cfg.ColorConflict, " ")...) if err != nil { - util.ErrMsg("color conflict", err, 0) + util.ErrMsg("color conflict", err) } status = fmt.Sprintf(cfg.BehindFormat, behind) } @@ -248,21 +248,21 @@ func (g *GitRepo) BranchStatus(cfg config.GPSConfig) (string, string, error) { if g.ShortSha == "" { statusColor, err = color.Color(strings.Split(cfg.ColorNoUpstream, " ")...) if err != nil { - util.ErrMsg("color no upstream", err, 0) + util.ErrMsg("color no upstream", err) } } if g.PromptMergeStatus != "" { statusColor, err = color.Color(strings.Split(cfg.ColorMerging, " ")...) if err != nil { - util.ErrMsg("color merging", err, 0) + util.ErrMsg("color merging", err) } } if hasUntracked { statusColor, err = color.Color(strings.Split(cfg.ColorUntracked, " ")...) if err != nil { - util.ErrMsg("color untracked", err, 0) + util.ErrMsg("color untracked", err) } status = fmt.Sprintf("*%s", status) } @@ -270,7 +270,7 @@ func (g *GitRepo) BranchStatus(cfg config.GPSConfig) (string, string, error) { if !cleanWorkingTree && !hasUntracked { statusColor, err = color.Color(strings.Split(cfg.ColorDirty, " ")...) if err != nil { - util.ErrMsg("color dirty", err, 0) + util.ErrMsg("color dirty", err) } status = fmt.Sprintf("*%s", status) } diff --git a/pkg/util/util.go b/pkg/util/util.go index c8f1862..55b8b41 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -6,6 +6,7 @@ import ( "os" "strings" + "github.com/buildkite/shellwords" "github.com/mikesmithgh/git-prompt-string/pkg/color" ) @@ -33,7 +34,7 @@ func ReadFileTrimNewline(name string) (string, error) { return strings.TrimRight(string(result), "\r\n"), err } -func ErrMsg(hint string, e error, exitCode int) { +func ErrMsg(hint string, e error) { errorColor, _ := color.Color("red") clearColor, _ := color.Color("none") var error_msg string @@ -42,6 +43,6 @@ func ErrMsg(hint string, e error, exitCode int) { } else { error_msg = strings.ReplaceAll(strings.ReplaceAll(e.Error(), "\n", ""), "\r", "") } - fmt.Printf("%s git-prompt-string error(%s): %s%s", errorColor, hint, error_msg, clearColor) - os.Exit(exitCode) + fmt.Printf("%s git-prompt-string error(%s): %s%s", errorColor, hint, shellwords.Quote(error_msg), clearColor) + os.Exit(1) } diff --git a/pkg/util/util_unix.go b/pkg/util/util_unix.go new file mode 100644 index 0000000..85f70c7 --- /dev/null +++ b/pkg/util/util_unix.go @@ -0,0 +1,7 @@ +//go:build !windows + +package util + +import "path" + +var XDGConfigPath string = path.Join(".config") diff --git a/pkg/util/util_windows.go b/pkg/util/util_windows.go new file mode 100644 index 0000000..27255e5 --- /dev/null +++ b/pkg/util/util_windows.go @@ -0,0 +1,7 @@ +package util + +import "path" + +var ( + XDGConfigPath string = path.Join("AppData", "Local") +) diff --git a/vendor/github.com/buildkite/shellwords/LICENSE.txt b/vendor/github.com/buildkite/shellwords/LICENSE.txt new file mode 100644 index 0000000..951b6ce --- /dev/null +++ b/vendor/github.com/buildkite/shellwords/LICENSE.txt @@ -0,0 +1,24 @@ +# Buildkite Licence + +Copyright (c) 2014-2017 Buildkite Pty Ltd + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/buildkite/shellwords/README.md b/vendor/github.com/buildkite/shellwords/README.md new file mode 100644 index 0000000..3448ded --- /dev/null +++ b/vendor/github.com/buildkite/shellwords/README.md @@ -0,0 +1,44 @@ +Shellwords +=========== + +A golang library for splitting command-line strings into words like a Posix or Windows shell would. + +## Installation + +```bash +go get -u github.com/buildkite/shellwords +``` + +## Usage + +```go +package main + +import ( + "github.com/buildkite/shellwords" + "fmt" +) + +func main() { + words := shellwords.Split(`/usr/bin/bash -e -c "llamas are the \"best\" && echo 'alpacas'"`) + for _, word := range words { + fmt.Println(word) + } +} + +// Outputs: +// /usr/bin/bash +// -e +// -c +// llamas are the "best" && echo 'alpacas' +``` + +## Alternatives + +Previously we were using https://github.com/mattn/go-shellwords, but it lacked support for Windows quoting and escaping conventions, specifically backslashed paths. It also supports things like env expansion and command execution, which made me very nervous, so I decided to write a simpler lexer that just addressed the splitting problem. + +Other alternatives are https://github.com/flynn-archive/go-shlex and https://github.com/kballard/go-shellquote, of which the latter is probably closest in spirit to this library. + +## License + +Licensed under MIT license, in `LICENSE`. diff --git a/vendor/github.com/buildkite/shellwords/batch.go b/vendor/github.com/buildkite/shellwords/batch.go new file mode 100644 index 0000000..147e372 --- /dev/null +++ b/vendor/github.com/buildkite/shellwords/batch.go @@ -0,0 +1,47 @@ +package shellwords + +import ( + "strings" + "unicode" +) + +const ( + batchSpecialChars = "^&;,=%" + batchEscape = '^' +) + +// SplitBatch splits a command string into words like Windows CMD.EXE would +// See https://ss64.com/nt/syntax-esc.html +func SplitBatch(line string) ([]string, error) { + p := parser{ + Input: line, + QuoteChars: []rune{'\'', '"'}, + EscapeChar: batchEscape, + QuoteEscapeChars: []rune{batchEscape, '"'}, + FieldSeperators: []rune{'\n', '\t', ' '}, + } + return p.Parse() +} + +// QuoteBatch returns the string such that a CMD.EXE shell would parse it as a single word +func QuoteBatch(s string) string { + var builder strings.Builder + var needsQuotes bool + + for _, c := range s { + if strings.ContainsRune(batchSpecialChars, c) { + builder.WriteString(string(batchEscape) + string(c)) + } else { + builder.WriteRune(c) + } + if unicode.IsSpace(c) { + needsQuotes = true + } + } + + if needsQuotes { + return `"` + builder.String() + `"` + } + + return builder.String() +} diff --git a/vendor/github.com/buildkite/shellwords/parser.go b/vendor/github.com/buildkite/shellwords/parser.go new file mode 100644 index 0000000..f5725d6 --- /dev/null +++ b/vendor/github.com/buildkite/shellwords/parser.go @@ -0,0 +1,176 @@ +package shellwords + +import ( + "fmt" + "strings" + "unicode/utf8" +) + +// This is a recursive descent parser for our basic shellword grammar. + +const ( + eof = -1 +) + +// Parser takes a string and parses out a tree of structs that represent text and Expansions +type parser struct { + // Input is the string to parse + Input string + + // Characters to use for quoted strings + QuoteChars []rune + + // The character used for escaping + EscapeChar rune + + // The characters used for escaping quotes in quoted strings + QuoteEscapeChars []rune + + // Field seperators are used for splitting words + FieldSeperators []rune + + // The current internal position + pos int +} + +func (p *parser) Parse() ([]string, error) { + var words = []string{} + var word strings.Builder + + for { + // Read until we encounter a delimiter character + scanned := p.scanUntil(func(r rune) bool { + return r == p.EscapeChar || p.isQuote(r) || p.isFieldSeperator(r) + }) + + if len(scanned) > 0 { + word.WriteString(scanned) + } + + // Read the character that caused the scan to stop + r := p.nextRune() + if r == eof { + break + } + + switch { + // Handle quotes + case p.isQuote(r): + quote, err := p.scanQuote(r) + if err != nil { + return nil, err + } + + // Write to the buffer + word.WriteString(quote) + + // Handle escaped characters + case r == p.EscapeChar: + if escaped := p.nextRune(); escaped != eof { + word.WriteRune(escaped) + } + continue + + // Handle field seperators + case p.isFieldSeperator(r): + if word.Len() > 0 { + words = append(words, word.String()) + word.Reset() + } + + default: + return nil, fmt.Errorf("Unhandled character %c at pos %d", r, p.pos) + } + } + + if word.Len() > 0 { + words = append(words, word.String()) + word.Reset() + } + + return words, nil +} + +func (p *parser) scanQuote(delim rune) (string, error) { + var quote strings.Builder + + for { + r := p.nextRune() + if r == eof { + return "", fmt.Errorf( + "Expected closing quote %c at offset %d, got EOF", delim, p.pos-1) + } + // Check for escaped characters + if escapeChar, escaped := p.isQuoteEscape(r); escaped { + // Handle the case where our escape char is our delimiter (e.g "") + if escapeChar != delim || p.peekRune() == delim { + if escaped := p.nextRune(); escaped != eof { + quote.WriteRune(escaped) + } + continue + } + } + if r == delim { + break + } + quote.WriteRune(r) + } + + return quote.String(), nil +} + +func (p *parser) isQuote(r rune) bool { + for _, qr := range p.QuoteChars { + if qr == r { + return true + } + } + return false +} + +func (p *parser) isQuoteEscape(r rune) (rune, bool) { + for _, qr := range p.QuoteEscapeChars { + if qr == r { + return qr, true + } + } + return r, false +} + +func (p *parser) isFieldSeperator(r rune) bool { + for _, qr := range p.FieldSeperators { + if qr == r { + return true + } + } + return false +} + +func (p *parser) scanUntil(f func(rune) bool) string { + start := p.pos + for int(p.pos) < len(p.Input) { + c, size := utf8.DecodeRuneInString(p.Input[p.pos:]) + if c == utf8.RuneError || f(c) { + break + } + p.pos += size + } + return p.Input[start:p.pos] +} + +func (p *parser) nextRune() rune { + if int(p.pos) >= len(p.Input) { + return eof + } + c, size := utf8.DecodeRuneInString(p.Input[p.pos:]) + p.pos += size + return c +} + +func (p *parser) peekRune() rune { + if int(p.pos) >= len(p.Input) { + return eof + } + c, _ := utf8.DecodeRuneInString(p.Input[p.pos:]) + return c +} diff --git a/vendor/github.com/buildkite/shellwords/posix.go b/vendor/github.com/buildkite/shellwords/posix.go new file mode 100644 index 0000000..e0b250d --- /dev/null +++ b/vendor/github.com/buildkite/shellwords/posix.go @@ -0,0 +1,46 @@ +package shellwords + +import ( + "strings" + "unicode" +) + +const ( + posixSpecialChars = "!\"#$&'()*,;<=>?[]\\^`{}|~" + posixEscape = '\\' +) + +// SplitPosix splits a command string into words like a posix shell would +func SplitPosix(line string) ([]string, error) { + p := parser{ + Input: line, + QuoteChars: []rune{'\'', '"'}, + EscapeChar: posixEscape, + QuoteEscapeChars: []rune{posixEscape}, + FieldSeperators: []rune{'\n', '\t', ' '}, + } + return p.Parse() +} + +// QuotePosix returns the string such that a posix shell would parse it as a single word +func QuotePosix(s string) string { + var builder strings.Builder + var needsQuotes bool + + for _, c := range s { + if strings.ContainsRune(posixSpecialChars, c) { + builder.WriteString(string(posixEscape) + string(c)) + } else { + builder.WriteRune(c) + } + if unicode.IsSpace(c) { + needsQuotes = true + } + } + + if needsQuotes { + return `"` + builder.String() + `"` + } + + return builder.String() +} diff --git a/vendor/github.com/buildkite/shellwords/quote.go b/vendor/github.com/buildkite/shellwords/quote.go new file mode 100644 index 0000000..1a5afe1 --- /dev/null +++ b/vendor/github.com/buildkite/shellwords/quote.go @@ -0,0 +1,13 @@ +package shellwords + +import ( + "runtime" +) + +// Quote chooses between QuotePosix and QuoteBatch based on your operating system +func Quote(word string) string { + if runtime.GOOS == `windows` { + return QuoteBatch(word) + } + return QuotePosix(word) +} diff --git a/vendor/github.com/buildkite/shellwords/split.go b/vendor/github.com/buildkite/shellwords/split.go new file mode 100644 index 0000000..f1de853 --- /dev/null +++ b/vendor/github.com/buildkite/shellwords/split.go @@ -0,0 +1,13 @@ +package shellwords + +import ( + "runtime" +) + +// Split chooses between SplitPosix and SplitBatch based on your operating system +func Split(line string) ([]string, error) { + if runtime.GOOS == `windows` { + return SplitBatch(line) + } + return SplitPosix(line) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 11343b5..ad175af 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,3 +1,6 @@ +# github.com/buildkite/shellwords v0.0.0-20180315110454-59467a9b8e10 +## explicit +github.com/buildkite/shellwords # github.com/pelletier/go-toml/v2 v2.1.1 ## explicit; go 1.16 github.com/pelletier/go-toml/v2