From b31de718d4a51c62125c31266bd449fa7fc4a440 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 14:29:56 +0200 Subject: [PATCH 01/36] Simplify linters config, fix cyclop issues in main.go --- .golangci.yml | 121 ++++++-------------------------------------------- README.md | 78 ++++++++++++++++---------------- cmd/main.go | 68 ++++++++++++++-------------- 3 files changed, 84 insertions(+), 183 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index cd2979d..25d867c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,115 +1,20 @@ -version: 2 +version: "2" run: - timeout: 5m - go: '1.24' + timeout: 1m -linters-settings: - errcheck: - check-type-assertions: true - check-blank: true - - govet: - enable-all: true - disable: - - shadow - - gocyclo: - min-complexity: 15 - - dupl: - threshold: 100 - - goconst: - min-len: 3 - min-occurrences: 3 - - lll: - line-length: 120 - - unparam: - check-exported: false - - nakedret: - max-func-lines: 30 - - prealloc: - simple: true - range-loops: true - for-loops: false - - gocritic: - enabled-tags: - - diagnostic - - experimental - - opinionated - - performance - - style - - funlen: - lines: 100 - statements: 50 - - godox: - keywords: - - NOTE - - OPTIMIZE - - HACK - - dogsled: - max-blank-identifiers: 2 - - whitespace: - multi-if: false - multi-func: false - -linters: - disable-all: true +formatters: enable: - - bodyclose - - dogsled - - dupl - - errcheck - - funlen - - gochecknoinits - - goconst - - gocritic - - gocyclo - - godox - - goprintffuncname - - gosec - - govet - - ineffassign - - lll - - misspell - - nakedret - - noctx - - nolintlint - - prealloc - - revive - - staticcheck - - unconvert - - unparam - - unused - - whitespace + - gci + - gofmt + - goimports -issues: - exclude-rules: - - path: _test\.go - linters: - - funlen - - goconst - - lll - - - path: pkg/git/test/ - linters: - - goconst +linters: + default: all - exclude-use-default: false - max-issues-per-linter: 0 - max-same-issues: 0 + exclusions: + rules: + - path: _test.go + linters: + - dupl # We don't mind duplicated code in tests. It helps with clarity -output: - format: colored-line-number - print-issued-lines: true - print-linter-name: true \ No newline at end of file diff --git a/README.md b/README.md index 7f34ca0..3ca6462 100644 --- a/README.md +++ b/README.md @@ -296,40 +296,6 @@ git list --fetch git list --out dump > backup-$(date +%Y%m%d).txt ``` -## Testing - -Run the test suite: - -```bash -# Run all tests -go test ./... - -# Run tests with coverage -go test -race -coverprofile=coverage.out ./... -go tool cover -html=coverage.out - -# Run specific package tests -go test -v ./pkg/git -``` - -### Linting - -This project uses comprehensive linting with golangci-lint. The linting configuration includes 25+ linters for code quality, security, and style checking. - -```bash -# Install golangci-lint (if not already installed) -go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest - -# Run linting with the project's configuration -golangci-lint run - -# Run with verbose output -golangci-lint run -v - -# Fix auto-fixable issues -golangci-lint run --fix -``` - ## Troubleshooting ### Common Issues @@ -373,19 +339,51 @@ git get user/repo We welcome contributions! -### Quick Start for Contributors +### Quick Start 1. **Fork the repository** 2. **Create a feature branch**: `git checkout -b feature/amazing-feature` 3. **Install dependencies**: `go mod download` 4. **Make changes and add tests** 5. **Format**: `go fmt ./...` -6. **Run tests**: `go test ./...` -7. **Run linter**: `golangci-lint run` -8. **Commit changes**: `git commit -m 'Add amazing feature'` -9. **Push to branch**: `git push origin feature/amazing-feature` -10. **Open a Pull Request** +6. **Build**: `go build -o git-get ./cmd/` +7. **Run tests**: `go test ./...` +8. **Run linter**: `golangci-lint run` +9. **Commit changes**: `git commit -m 'Add amazing feature'` +10. **Push to branch**: `git push origin feature/amazing-feature` +11. **Open a Pull Request** + +### Testing + +Run the test suite: + +```bash +# Run all tests +go test ./... + +# Run tests with coverage +go test -race -coverprofile=coverage.out ./... +go tool cover -html=coverage.out + +# Run specific package tests +go test -v ./pkg/git +``` +### Linting + +```bash +# Install golangci-lint (if not already installed) +go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + +# Run linting with the project's configuration +golangci-lint run + +# Run with verbose output +golangci-lint run -v + +# Fix auto-fixable issues +golangci-lint run --fix +``` ## License diff --git a/cmd/main.go b/cmd/main.go index 01e5512..84126ba 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -6,56 +6,54 @@ import ( "strings" ) -func main() { - // This program behaves as a git subcommand (see https://git.github.io/htmldocs/howto/new-command.html) - // When added to PATH, git recognizes it as its subcommand and it can be invoked as "git get..." or "git list..." - // It can also be invoked as a regular binary with subcommands: "git-get get..." or "git-get list" - // The following flow detects the invokation method and runs the appropriate command. - - programName := filepath.Base(os.Args[0]) +// This program behaves as a git subcommand (see https://git.github.io/htmldocs/howto/new-command.html) +// When added to PATH, git recognizes it as its subcommand and it can be invoked as "git get..." or "git list..." +// It can also be invoked as a regular binary with subcommands: "git-get get..." or "git-get list" +// The following flow detects the invokation method and runs the appropriate command. - // Remove common executable extensions - programName = strings.TrimSuffix(programName, ".exe") +func main() { + command, args := determineCommand() + executeCommand(command, args) +} - // Determine which command to run based on program name or first argument - var command string - var args []string +func determineCommand() (string, []string) { + programName := strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe") switch programName { case "git-get": - // Check if first argument is a subcommand - if len(os.Args) > 1 && (os.Args[1] == "get" || os.Args[1] == "list") { - // Called as: git-get get or git-get list - command = os.Args[1] - args = os.Args[2:] - } else { - // Called as: git-get (default to get command) - command = "get" - args = os.Args[1:] - } + return handleGitGetInvocation() case "git-list": - // Called as: git-list (symlinked binary) - command = "list" - args = os.Args[1:] + return handleGitListInvocation() default: - // Fallback: use first argument as command - if len(os.Args) > 1 { - command = os.Args[1] - args = os.Args[2:] - } else { - command = "get" - args = []string{} - } + return handleDefaultInvocation() + } +} + +func handleGitGetInvocation() (string, []string) { + if len(os.Args) > 1 && (os.Args[1] == "get" || os.Args[1] == "list") { + return os.Args[1], os.Args[2:] } + return "get", os.Args[1:] +} + +func handleGitListInvocation() (string, []string) { + return "list", os.Args[1:] +} + +func handleDefaultInvocation() (string, []string) { + if len(os.Args) > 1 { + return os.Args[1], os.Args[2:] + } + return "get", []string{} +} - // Execute the appropriate command +func executeCommand(command string, args []string) { switch command { case "get": runGet(args) case "list": runList(args) default: - // Default to get command for unknown commands runGet(os.Args[1:]) } } From 09da7ea05b8f2bf39cf053ec908801a05b9229e2 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 14:35:26 +0200 Subject: [PATCH 02/36] Fix cyclop issues in url.go --- pkg/url.go | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/pkg/url.go b/pkg/url.go index 44dd450..941db48 100644 --- a/pkg/url.go +++ b/pkg/url.go @@ -19,26 +19,41 @@ var scpSyntax = regexp.MustCompile(`^([a-zA-Z0-9_]+)@([a-zA-Z0-9._-]+):(.*)$`) // When the parsed URL has an empty host, use the defaultHost. // When the parsed URL has an empty scheme, use the defaultScheme. func ParseURL(rawURL string, defaultHost string, defaultScheme string) (url *urlpkg.URL, err error) { + url, err = parseRawURL(rawURL) + if err != nil { + return nil, err + } + + if url.Host == "" && url.Path == "" { + return nil, errEmptyURLPath + } + + normalizeURL(url, defaultHost, defaultScheme) + return url, nil +} + +// parseRawURL handles the initial parsing of the raw URL string. +func parseRawURL(rawURL string) (*urlpkg.URL, error) { // If rawURL matches the SCP-like syntax, convert it into a standard ssh Path. // eg, git@github.com:user/repo => ssh://git@github.com/user/repo if m := scpSyntax.FindStringSubmatch(rawURL); m != nil { - url = &urlpkg.URL{ + return &urlpkg.URL{ Scheme: "ssh", User: urlpkg.User(m[1]), Host: m[2], Path: path.Join("/", m[3]), - } - } else { - url, err = urlpkg.Parse(rawURL) - if err != nil { - return nil, fmt.Errorf("failed parsing URL %s: %w", rawURL, err) - } + }, nil } - if url.Host == "" && url.Path == "" { - return nil, errEmptyURLPath + url, err := urlpkg.Parse(rawURL) + if err != nil { + return nil, fmt.Errorf("failed parsing URL %s: %w", rawURL, err) } + return url, nil +} +// normalizeURL applies all the normalization rules to the parsed URL. +func normalizeURL(url *urlpkg.URL, defaultHost string, defaultScheme string) { if url.Scheme == "git+ssh" { url.Scheme = "ssh" } @@ -65,8 +80,6 @@ func ParseURL(rawURL string, defaultHost string, defaultScheme string) (url *url url.Path = path.Join(url.Host, url.Path) url.Host = "" } - - return url, nil } // URLToPath cleans up the URL and converts it into a path string. From d608fe848fa205155c2c175c88c3281df43e03c4 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 14:38:40 +0200 Subject: [PATCH 03/36] Add unit tests for main.go --- cmd/main_test.go | 255 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 cmd/main_test.go diff --git a/cmd/main_test.go b/cmd/main_test.go new file mode 100644 index 0000000..124b54c --- /dev/null +++ b/cmd/main_test.go @@ -0,0 +1,255 @@ +package main + +import ( + "os" + "reflect" + "testing" +) + +func TestDetermineCommand(t *testing.T) { + tests := []struct { + name string + programName string + args []string + wantCmd string + wantArgs []string + }{ + { + name: "git-get with no args", + programName: "git-get", + args: []string{"git-get"}, + wantCmd: "get", + wantArgs: []string{}, + }, + { + name: "git-get with repo arg", + programName: "git-get", + args: []string{"git-get", "user/repo"}, + wantCmd: "get", + wantArgs: []string{"user/repo"}, + }, + { + name: "git-get with get subcommand", + programName: "git-get", + args: []string{"git-get", "get", "user/repo"}, + wantCmd: "get", + wantArgs: []string{"user/repo"}, + }, + { + name: "git-get with list subcommand", + programName: "git-get", + args: []string{"git-get", "list"}, + wantCmd: "list", + wantArgs: []string{}, + }, + { + name: "git-list with no args", + programName: "git-list", + args: []string{"git-list"}, + wantCmd: "list", + wantArgs: []string{}, + }, + { + name: "git-list with args", + programName: "git-list", + args: []string{"git-list", "--fetch"}, + wantCmd: "list", + wantArgs: []string{"--fetch"}, + }, + { + name: "git-get.exe on Windows", + programName: "git-get.exe", + args: []string{"git-get.exe", "user/repo"}, + wantCmd: "get", + wantArgs: []string{"user/repo"}, + }, + { + name: "unknown program name with args", + programName: "some-other-name", + args: []string{"some-other-name", "get", "user/repo"}, + wantCmd: "get", + wantArgs: []string{"user/repo"}, + }, + { + name: "unknown program name with no args", + programName: "some-other-name", + args: []string{"some-other-name"}, + wantCmd: "get", + wantArgs: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save original os.Args + oldArgs := os.Args + defer func() { os.Args = oldArgs }() + + // Set test args + os.Args = tt.args + + gotCmd, gotArgs := determineCommand() + + if gotCmd != tt.wantCmd { + t.Errorf("determineCommand() command = %v, want %v", gotCmd, tt.wantCmd) + } + + if !reflect.DeepEqual(gotArgs, tt.wantArgs) { + t.Errorf("determineCommand() args = %v, want %v", gotArgs, tt.wantArgs) + } + }) + } +} + +func TestHandleGitGetInvocation(t *testing.T) { + tests := []struct { + name string + args []string + wantCmd string + wantArgs []string + }{ + { + name: "no args", + args: []string{"git-get"}, + wantCmd: "get", + wantArgs: []string{}, + }, + { + name: "with repo arg", + args: []string{"git-get", "user/repo"}, + wantCmd: "get", + wantArgs: []string{"user/repo"}, + }, + { + name: "with get subcommand", + args: []string{"git-get", "get", "user/repo"}, + wantCmd: "get", + wantArgs: []string{"user/repo"}, + }, + { + name: "with list subcommand", + args: []string{"git-get", "list", "--fetch"}, + wantCmd: "list", + wantArgs: []string{"--fetch"}, + }, + { + name: "with invalid subcommand", + args: []string{"git-get", "invalid", "user/repo"}, + wantCmd: "get", + wantArgs: []string{"invalid", "user/repo"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save original os.Args + oldArgs := os.Args + defer func() { os.Args = oldArgs }() + + // Set test args + os.Args = tt.args + + gotCmd, gotArgs := handleGitGetInvocation() + + if gotCmd != tt.wantCmd { + t.Errorf("handleGitGetInvocation() command = %v, want %v", gotCmd, tt.wantCmd) + } + + if !reflect.DeepEqual(gotArgs, tt.wantArgs) { + t.Errorf("handleGitGetInvocation() args = %v, want %v", gotArgs, tt.wantArgs) + } + }) + } +} + +func TestHandleGitListInvocation(t *testing.T) { + tests := []struct { + name string + args []string + wantCmd string + wantArgs []string + }{ + { + name: "no args", + args: []string{"git-list"}, + wantCmd: "list", + wantArgs: []string{}, + }, + { + name: "with flags", + args: []string{"git-list", "--fetch", "--out", "flat"}, + wantCmd: "list", + wantArgs: []string{"--fetch", "--out", "flat"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save original os.Args + oldArgs := os.Args + defer func() { os.Args = oldArgs }() + + // Set test args + os.Args = tt.args + + gotCmd, gotArgs := handleGitListInvocation() + + if gotCmd != tt.wantCmd { + t.Errorf("handleGitListInvocation() command = %v, want %v", gotCmd, tt.wantCmd) + } + + if !reflect.DeepEqual(gotArgs, tt.wantArgs) { + t.Errorf("handleGitListInvocation() args = %v, want %v", gotArgs, tt.wantArgs) + } + }) + } +} + +func TestHandleDefaultInvocation(t *testing.T) { + tests := []struct { + name string + args []string + wantCmd string + wantArgs []string + }{ + { + name: "no args", + args: []string{"some-program"}, + wantCmd: "get", + wantArgs: []string{}, + }, + { + name: "with command arg", + args: []string{"some-program", "list"}, + wantCmd: "list", + wantArgs: []string{}, + }, + { + name: "with command and args", + args: []string{"some-program", "get", "user/repo", "--branch", "main"}, + wantCmd: "get", + wantArgs: []string{"user/repo", "--branch", "main"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save original os.Args + oldArgs := os.Args + defer func() { os.Args = oldArgs }() + + // Set test args + os.Args = tt.args + + gotCmd, gotArgs := handleDefaultInvocation() + + if gotCmd != tt.wantCmd { + t.Errorf("handleDefaultInvocation() command = %v, want %v", gotCmd, tt.wantCmd) + } + + if !reflect.DeepEqual(gotArgs, tt.wantArgs) { + t.Errorf("handleDefaultInvocation() args = %v, want %v", gotArgs, tt.wantArgs) + } + }) + } +} \ No newline at end of file From 0fb78ce7e2eb663d6e6d6983e2508e936fbc1b94 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 14:42:46 +0200 Subject: [PATCH 04/36] Disable depguard linter --- .golangci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index 25d867c..bcf1018 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -12,6 +12,9 @@ formatters: linters: default: all + disable: + - depguard # We don't have any packages we need to block + exclusions: rules: - path: _test.go From 59aaaffe35919394ed54c4798d9da462c8613b73 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 14:50:45 +0200 Subject: [PATCH 05/36] Disable paralleltest linter --- .gitignore | 4 ++++ .golangci.yml | 1 + 2 files changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index a17c79c..70875cd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ dist/ __debug_bin .claude/ CLAUDE.md + +# Locally built executables +git-get +git-get.exe diff --git a/.golangci.yml b/.golangci.yml index bcf1018..86de6f4 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -14,6 +14,7 @@ linters: disable: - depguard # We don't have any packages we need to block + - paralleltest # Tests are fast already and paralellizing them adds complexity exclusions: rules: From 0bf276534950770780473c8841ce60cb65624663 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 14:54:02 +0200 Subject: [PATCH 06/36] Fix issues found by wsl linter --- .golangci.yml | 1 + cmd/get.go | 1 + cmd/main.go | 2 ++ cmd/main_test.go | 6 +++++- pkg/cfg/config_test.go | 1 - pkg/dump.go | 8 ++++++-- pkg/get.go | 3 +++ pkg/git/finder.go | 4 ++-- pkg/git/repo.go | 5 +++++ pkg/git/repo_test.go | 13 +++++++------ pkg/git/status.go | 6 ++++++ pkg/git/test/testrepos.go | 3 +++ pkg/list.go | 1 + pkg/print/tree.go | 14 ++++++++++++-- pkg/run/run.go | 4 +++- pkg/url.go | 2 ++ 16 files changed, 59 insertions(+), 15 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 86de6f4..f930ed8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -15,6 +15,7 @@ linters: disable: - depguard # We don't have any packages we need to block - paralleltest # Tests are fast already and paralellizing them adds complexity + - wsl # We use wsl_v5 instead exclusions: rules: diff --git a/cmd/get.go b/cmd/get.go index d44926d..8fba2d5 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -62,6 +62,7 @@ func runGetCommand(cmd *cobra.Command, args []string) error { Root: viper.GetString(cfg.KeyReposRoot), URL: url, } + return pkg.Get(config) } diff --git a/cmd/main.go b/cmd/main.go index 84126ba..22b5bd6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -33,6 +33,7 @@ func handleGitGetInvocation() (string, []string) { if len(os.Args) > 1 && (os.Args[1] == "get" || os.Args[1] == "list") { return os.Args[1], os.Args[2:] } + return "get", os.Args[1:] } @@ -44,6 +45,7 @@ func handleDefaultInvocation() (string, []string) { if len(os.Args) > 1 { return os.Args[1], os.Args[2:] } + return "get", []string{} } diff --git a/cmd/main_test.go b/cmd/main_test.go index 124b54c..2570ec0 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -83,6 +83,7 @@ func TestDetermineCommand(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // Save original os.Args oldArgs := os.Args + defer func() { os.Args = oldArgs }() // Set test args @@ -144,6 +145,7 @@ func TestHandleGitGetInvocation(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // Save original os.Args oldArgs := os.Args + defer func() { os.Args = oldArgs }() // Set test args @@ -187,6 +189,7 @@ func TestHandleGitListInvocation(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // Save original os.Args oldArgs := os.Args + defer func() { os.Args = oldArgs }() // Set test args @@ -236,6 +239,7 @@ func TestHandleDefaultInvocation(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // Save original os.Args oldArgs := os.Args + defer func() { os.Args = oldArgs }() // Set test args @@ -252,4 +256,4 @@ func TestHandleDefaultInvocation(t *testing.T) { } }) } -} \ No newline at end of file +} diff --git a/pkg/cfg/config_test.go b/pkg/cfg/config_test.go index 215e1ab..36d06db 100644 --- a/pkg/cfg/config_test.go +++ b/pkg/cfg/config_test.go @@ -96,7 +96,6 @@ func testConfigOnlyInGitconfig(t *testing.T) { func testConfigOnlyInEnvVar(t *testing.T) { Init(&gitconfigEmpty{}) os.Setenv(envVarName, fromEnv) - } func testConfigInGitconfigAndEnvVar(t *testing.T) { diff --git a/pkg/dump.go b/pkg/dump.go index ecb923f..decc2e0 100644 --- a/pkg/dump.go +++ b/pkg/dump.go @@ -28,10 +28,14 @@ func parseDumpFile(path string) ([]parsedLine, error) { scanner := bufio.NewScanner(file) - var parsedLines []parsedLine - var line int + var ( + parsedLines []parsedLine + line int + ) + for scanner.Scan() { line++ + parsed, err := parseLine(scanner.Text()) if err != nil && !errors.Is(errEmptyLine, err) { return nil, fmt.Errorf("failed parsing dump file line %d: %w", line, err) diff --git a/pkg/get.go b/pkg/get.go index 407fff8..754c73b 100644 --- a/pkg/get.go +++ b/pkg/get.go @@ -30,6 +30,7 @@ func Get(c *GetCfg) error { if c.Dump != "" { return cloneDumpFile(c) } + return nil } @@ -74,10 +75,12 @@ func cloneDumpFile(c *GetCfg) error { } fmt.Printf("Cloning %s...\n", opts.URL.String()) + _, err = git.Clone(opts) if err != nil { return err } } + return nil } diff --git a/pkg/git/finder.go b/pkg/git/finder.go index b9fd39f..5ccd96d 100644 --- a/pkg/git/finder.go +++ b/pkg/git/finder.go @@ -18,7 +18,6 @@ var errDirNotExist = fmt.Errorf("directory doesn't exist") // Exists returns true if a directory exists. If it doesn't or the directory can't be accessed it returns an error. func Exists(path string) (bool, error) { _, err := os.Stat(path) - if err == nil { return true, nil } @@ -61,6 +60,7 @@ func (f *RepoFinder) Find() error { if os.IsPermission(err) { return nil // Skip this path but continue } + return fmt.Errorf("failed to walk %s: %w", path, err) } @@ -73,6 +73,7 @@ func (f *RepoFinder) Find() error { if d.Name() == dotgit { parentPath := filepath.Dir(path) f.addIfOk(parentPath) + return fs.SkipDir // Skip the .git directory contents } @@ -85,7 +86,6 @@ func (f *RepoFinder) Find() error { return nil // Continue walking }) - if err != nil { return fmt.Errorf("failed to walk directory tree: %w", err) } diff --git a/pkg/git/repo.go b/pkg/git/repo.go index 86161e5..31e86f7 100644 --- a/pkg/git/repo.go +++ b/pkg/git/repo.go @@ -61,6 +61,7 @@ func Clone(opts *CloneOpts) (*Repo, error) { } Repo, err := Open(opts.Path) + return Repo, err } @@ -79,6 +80,7 @@ func (r *Repo) Uncommitted() (int, error) { } count := 0 + for _, line := range out { // Don't count lines with untracked files and empty lines. if !strings.HasPrefix(line, untracked) && strings.TrimSpace(line) != "" { @@ -97,6 +99,7 @@ func (r *Repo) Untracked() (int, error) { } count := 0 + for _, line := range out { if strings.HasPrefix(line, untracked) { count++ @@ -122,6 +125,7 @@ func (r *Repo) CurrentBranch() (string, error) { // Fall back to "main" as the modern default return "main", nil } + return "", err } @@ -190,6 +194,7 @@ func (r *Repo) Remote() (string, error) { if strings.Contains(err.Error(), "No remote configured to list refs from") { return "", nil // Return empty string instead of error for missing remote } + return "", err } diff --git a/pkg/git/repo_test.go b/pkg/git/repo_test.go index 0897305..baf2d31 100644 --- a/pkg/git/repo_test.go +++ b/pkg/git/repo_test.go @@ -47,8 +47,8 @@ func TestUncommitted(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { r, _ := Open(test.repoMaker(t).Path()) - got, err := r.Uncommitted() + got, err := r.Uncommitted() if err != nil { t.Errorf("got error %q", err) } @@ -95,8 +95,8 @@ func TestUntracked(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { r, _ := Open(test.repoMaker(t).Path()) - got, err := r.Uncommitted() + got, err := r.Uncommitted() if err != nil { t.Errorf("got error %q", err) } @@ -139,8 +139,8 @@ func TestCurrentBranch(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { r, _ := Open(test.repoMaker(t).Path()) - got, err := r.CurrentBranch() + got, err := r.CurrentBranch() if err != nil { t.Errorf("got error %q", err) } @@ -182,8 +182,8 @@ func TestBranches(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { r, _ := Open(test.repoMaker(t).Path()) - got, err := r.Branches() + got, err := r.Branches() if err != nil { t.Errorf("got error %q", err) } @@ -287,6 +287,7 @@ func TestAheadBehind(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { r, _ := Open(test.repoMaker(t).Path()) + upstream, err := r.Upstream(test.branch) if err != nil { t.Errorf("got error %q", err) @@ -313,7 +314,6 @@ func TestCleanupFailedClone(t *testing.T) { // └── x/ // └── y/ // └── file.txt - tests := []struct { path string // path to cleanup wantGone string // this path should be deleted, if empty - nothing should be deleted @@ -393,6 +393,7 @@ func TestRemote(t *testing.T) { if test.wantErr && err == nil { t.Errorf("expected error but got none") } + if !test.wantErr && err != nil { t.Errorf("unexpected error: %q", err) } @@ -413,8 +414,8 @@ func createTestDirTree(t *testing.T) string { root := test.TempDir(t, "") err := os.MkdirAll(filepath.Join(root, "a", "b", "c"), os.ModePerm) err = os.MkdirAll(filepath.Join(root, "a", "x", "y"), os.ModePerm) - _, err = os.Create(filepath.Join(root, "a", "x", "y", "file.txt")) + _, err = os.Create(filepath.Join(root, "a", "x", "y", "file.txt")) if err != nil { t.Fatal(err) } diff --git a/pkg/git/status.go b/pkg/git/status.go index 6825c46..6f5607c 100644 --- a/pkg/git/status.go +++ b/pkg/git/status.go @@ -32,12 +32,14 @@ func (r *Repo) LoadStatus(fetch bool) *Status { } var err error + status.current, err = r.CurrentBranch() if err != nil { status.errors = append(status.errors, err.Error()) } var errs []error + status.branches, errs = r.loadBranches() for _, err := range errs { status.errors = append(status.errors, err.Error()) @@ -69,6 +71,7 @@ func (r *Repo) loadBranches() (map[string]string, []error) { for _, branch := range branches { status, err := r.loadBranchStatus(branch) statuses[branch] = status + if err != nil { errors = append(errors, err) } @@ -100,6 +103,7 @@ func (r *Repo) loadBranchStatus(branch string) (string, error) { if ahead != 0 { res = append(res, fmt.Sprintf("%d ahead", ahead)) } + if behind != 0 { res = append(res, fmt.Sprintf("%d behind", behind)) } @@ -126,6 +130,7 @@ func (r *Repo) loadWorkTree() (string, error) { if uncommitted != 0 { res = append(res, fmt.Sprintf("%d uncommitted", uncommitted)) } + if untracked != 0 { res = append(res, fmt.Sprintf("%d untracked", untracked)) } @@ -151,6 +156,7 @@ func (s *Status) Branches() []string { branches = append(branches, b) } } + return branches } diff --git a/pkg/git/test/testrepos.go b/pkg/git/test/testrepos.go index 2c65427..407af6f 100644 --- a/pkg/git/test/testrepos.go +++ b/pkg/git/test/testrepos.go @@ -30,6 +30,7 @@ func RepoEmptyInDir(t *testing.T, parent string) *Repo { } r.init() + return r } @@ -97,6 +98,7 @@ func RepoWithBranchWithUpstream(t *testing.T) *Repo { r := origin.clone() r.checkout("feature/branch") + return r } @@ -107,6 +109,7 @@ func RepoWithBranchWithoutUpstream(t *testing.T) *Repo { r := origin.clone() r.branch("feature/branch") r.checkout("feature/branch") + return r } diff --git a/pkg/list.go b/pkg/list.go index 3550026..8b7bd9d 100644 --- a/pkg/list.go +++ b/pkg/list.go @@ -23,6 +23,7 @@ func List(c *ListCfg) error { } statuses := finder.LoadAll(c.Fetch) + printables := make([]print.Printable, len(statuses)) for i := range statuses { printables[i] = statuses[i] diff --git a/pkg/print/tree.go b/pkg/print/tree.go index fc327a3..16fd97b 100644 --- a/pkg/print/tree.go +++ b/pkg/print/tree.go @@ -46,6 +46,7 @@ func Root(val string) *Node { root := &Node{ val: val, } + return root } @@ -61,6 +62,7 @@ func (n *Node) Add(val string) *Node { depth: n.depth + 1, } n.children = append(n.children, child) + return child } @@ -108,9 +110,11 @@ func buildTree(root string, repos []Printable) *Node { continue } + node = child } } + return tree } @@ -180,8 +184,11 @@ func indentation(node *Node) string { var indent strings.Builder - const space = " " - const link = "│ " + const ( + space = " " + link = "│ " + ) + for _, y := range levels { if y { indent.WriteString(space) @@ -203,12 +210,15 @@ func (n *Node) isYoungest() bool { } sisters := n.parent.children + var myIndex int + for i, sis := range sisters { if sis.val == n.val { myIndex = i break } } + return myIndex == len(sisters)-1 } diff --git a/pkg/run/run.go b/pkg/run/run.go index 4b8216b..20e8148 100644 --- a/pkg/run/run.go +++ b/pkg/run/run.go @@ -78,6 +78,7 @@ func (c *Cmd) AndCaptureLine() (string, error) { if err != nil { return "", err } + return lines[0], nil } @@ -90,6 +91,7 @@ func (c *Cmd) AndShow() error { if err != nil { return &GitError{&bytes.Buffer{}, c.args, c.path, err} } + return nil } @@ -104,6 +106,7 @@ func (c *Cmd) AndShutUp() error { if err != nil { return &GitError{errStream, c.args, c.path, err} } + return nil } @@ -123,7 +126,6 @@ func (e GitError) Error() string { } return fmt.Sprintf("git %s failed on %s: %s", e.Args, e.Path, msg) - } func lines(output []byte) []string { diff --git a/pkg/url.go b/pkg/url.go index 941db48..d383bed 100644 --- a/pkg/url.go +++ b/pkg/url.go @@ -29,6 +29,7 @@ func ParseURL(rawURL string, defaultHost string, defaultScheme string) (url *url } normalizeURL(url, defaultHost, defaultScheme) + return url, nil } @@ -49,6 +50,7 @@ func parseRawURL(rawURL string) (*urlpkg.URL, error) { if err != nil { return nil, fmt.Errorf("failed parsing URL %s: %w", rawURL, err) } + return url, nil } From 288a642483239839c76fed628b22a30c2949b398 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 15:00:15 +0200 Subject: [PATCH 07/36] Disable wrapcheck linter --- .golangci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index f930ed8..b2b5a08 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -13,9 +13,10 @@ linters: default: all disable: - - depguard # We don't have any packages we need to block - - paralleltest # Tests are fast already and paralellizing them adds complexity - - wsl # We use wsl_v5 instead + - depguard # We don't have any packages we need to block + - paralleltest # Tests are fast already and paralellizing them adds complexity + - wsl # We use wsl_v5 instead + - wrapcheck # Adds too much bloat, many of the errors are contextual enough and don't need wrapping exclusions: rules: From f60cfd1e40319e9a740d64e6650c88d8adf0c686 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 15:44:22 +0200 Subject: [PATCH 08/36] Fix issues found by varnamelen linter --- .golangci.yml | 7 ++++++- pkg/get.go | 28 ++++++++++++++-------------- pkg/git/finder.go | 18 +++++++++--------- pkg/git/repo.go | 6 +++--- pkg/list.go | 10 +++++----- pkg/print/flat.go | 18 +++++++++--------- pkg/print/tree.go | 32 ++++++++++++++++---------------- 7 files changed, 62 insertions(+), 57 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index b2b5a08..e2a2967 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -22,5 +22,10 @@ linters: rules: - path: _test.go linters: - - dupl # We don't mind duplicated code in tests. It helps with clarity + - dupl # We don't mind duplicated code in tests. It helps with clarity + - varnamelen # We don't mind short var names in tests. + - path: test/ + linters: + - dupl + - varnamelen diff --git a/pkg/get.go b/pkg/get.go index 754c73b..e59f90b 100644 --- a/pkg/get.go +++ b/pkg/get.go @@ -18,32 +18,32 @@ type GetCfg struct { } // Get executes the "git get" command. -func Get(c *GetCfg) error { - if c.URL == "" && c.Dump == "" { +func Get(conf *GetCfg) error { + if conf.URL == "" && conf.Dump == "" { return fmt.Errorf("missing argument or --dump flag") } - if c.URL != "" { - return cloneSingleRepo(c) + if conf.URL != "" { + return cloneSingleRepo(conf) } - if c.Dump != "" { - return cloneDumpFile(c) + if conf.Dump != "" { + return cloneDumpFile(conf) } return nil } -func cloneSingleRepo(c *GetCfg) error { - url, err := ParseURL(c.URL, c.DefHost, c.DefScheme) +func cloneSingleRepo(conf *GetCfg) error { + url, err := ParseURL(conf.URL, conf.DefHost, conf.DefScheme) if err != nil { return err } opts := &git.CloneOpts{ URL: url, - Path: filepath.Join(c.Root, URLToPath(*url, c.SkipHost)), - Branch: c.Branch, + Path: filepath.Join(conf.Root, URLToPath(*url, conf.SkipHost)), + Branch: conf.Branch, } _, err = git.Clone(opts) @@ -51,21 +51,21 @@ func cloneSingleRepo(c *GetCfg) error { return err } -func cloneDumpFile(c *GetCfg) error { - parsedLines, err := parseDumpFile(c.Dump) +func cloneDumpFile(conf *GetCfg) error { + parsedLines, err := parseDumpFile(conf.Dump) if err != nil { return err } for _, line := range parsedLines { - url, err := ParseURL(line.rawurl, c.DefHost, c.DefScheme) + url, err := ParseURL(line.rawurl, conf.DefHost, conf.DefScheme) if err != nil { return err } opts := &git.CloneOpts{ URL: url, - Path: filepath.Join(c.Root, URLToPath(*url, c.SkipHost)), + Path: filepath.Join(conf.Root, URLToPath(*url, conf.SkipHost)), Branch: line.branch, } diff --git a/pkg/git/finder.go b/pkg/git/finder.go index 5ccd96d..374aea0 100644 --- a/pkg/git/finder.go +++ b/pkg/git/finder.go @@ -53,7 +53,7 @@ func (f *RepoFinder) Find() error { return fmt.Errorf("failed to access root path: %w", err) } - err := filepath.WalkDir(f.root, func(path string, d fs.DirEntry, err error) error { + err := filepath.WalkDir(f.root, func(path string, dir fs.DirEntry, err error) error { // Handle walk errors if err != nil { // Skip permission errors but continue walking @@ -65,12 +65,12 @@ func (f *RepoFinder) Find() error { } // Only process directories - if !d.IsDir() { + if !dir.IsDir() { return nil } // Case 1: We're looking at a .git directory itself - if d.Name() == dotgit { + if dir.Name() == dotgit { parentPath := filepath.Dir(path) f.addIfOk(parentPath) @@ -101,7 +101,7 @@ func (f *RepoFinder) Find() error { // If fetch equals true, it first fetches from the remote repo before loading the status. // Each repo is loaded concurrently by a separate worker, with max 100 workers being active at the same time. func (f *RepoFinder) LoadAll(fetch bool) []*Status { - var ss []*Status + var statuses []*Status reposChan := make(chan *Repo, f.maxWorkers) statusChan := make(chan *Status, f.maxWorkers) @@ -118,18 +118,18 @@ func (f *RepoFinder) LoadAll(fetch bool) []*Status { // Read statuses from the statusChan and add then to the result slice. // Close the channel when all repos are loaded. for status := range statusChan { - ss = append(ss, status) - if len(ss) == len(f.repos) { + statuses = append(statuses, status) + if len(statuses) == len(f.repos) { close(statusChan) } } // Sort the status slice by path - sort.Slice(ss, func(i, j int) bool { - return strings.Compare(ss[i].path, ss[j].path) < 0 + sort.Slice(statuses, func(i, j int) bool { + return strings.Compare(statuses[i].path, statuses[j].path) < 0 }) - return ss + return statuses } func loadRepos(repos []*Repo, reposChan chan<- *Repo) { diff --git a/pkg/git/repo.go b/pkg/git/repo.go index 31e86f7..9a11861 100644 --- a/pkg/git/repo.go +++ b/pkg/git/repo.go @@ -170,14 +170,14 @@ func (r *Repo) AheadBehind(branch string, upstream string) (int, int, error) { } // rev-list --left-right --count output is separated by a tab - lr := strings.Split(out, "\t") + count := strings.Split(out, "\t") - ahead, err := strconv.Atoi(lr[0]) + ahead, err := strconv.Atoi(count[0]) if err != nil { return 0, 0, err } - behind, err := strconv.Atoi(lr[1]) + behind, err := strconv.Atoi(count[1]) if err != nil { return 0, 0, err } diff --git a/pkg/list.go b/pkg/list.go index 8b7bd9d..7173b9d 100644 --- a/pkg/list.go +++ b/pkg/list.go @@ -16,24 +16,24 @@ type ListCfg struct { } // List executes the "git list" command. -func List(c *ListCfg) error { - finder := git.NewRepoFinder(c.Root) +func List(conf *ListCfg) error { + finder := git.NewRepoFinder(conf.Root) if err := finder.Find(); err != nil { return err } - statuses := finder.LoadAll(c.Fetch) + statuses := finder.LoadAll(conf.Fetch) printables := make([]print.Printable, len(statuses)) for i := range statuses { printables[i] = statuses[i] } - switch c.Output { + switch conf.Output { case cfg.OutFlat: fmt.Print(print.NewFlatPrinter().Print(printables)) case cfg.OutTree: - fmt.Print(print.NewTreePrinter().Print(c.Root, printables)) + fmt.Print(print.NewTreePrinter().Print(conf.Root, printables)) case cfg.OutDump: fmt.Print(print.NewDumpPrinter().Print(printables)) default: diff --git a/pkg/print/flat.go b/pkg/print/flat.go index 4bafb56..24576bb 100644 --- a/pkg/print/flat.go +++ b/pkg/print/flat.go @@ -18,18 +18,18 @@ func NewFlatPrinter() *FlatPrinter { func (p *FlatPrinter) Print(repos []Printable) string { var str strings.Builder - for _, r := range repos { - str.WriteString(strings.TrimSuffix(r.Path(), string(os.PathSeparator))) + for _, repo := range repos { + str.WriteString(strings.TrimSuffix(repo.Path(), string(os.PathSeparator))) - if len(r.Errors()) > 0 { + if len(repo.Errors()) > 0 { str.WriteString(" " + red("error") + "\n") continue } - str.WriteString(" " + blue(r.Current())) + str.WriteString(" " + blue(repo.Current())) - current := r.BranchStatus(r.Current()) - worktree := r.WorkTreeStatus() + current := repo.BranchStatus(repo.Current()) + worktree := repo.WorkTreeStatus() if worktree != "" { worktree = fmt.Sprintf("[ %s ]", worktree) @@ -41,13 +41,13 @@ func (p *FlatPrinter) Print(repos []Printable) string { str.WriteString(" " + strings.Join([]string{yellow(current), red(worktree)}, " ")) } - for _, branch := range r.Branches() { - status := r.BranchStatus(branch) + for _, branch := range repo.Branches() { + status := repo.BranchStatus(branch) if status == "" { status = green("ok") } - indent := strings.Repeat(" ", len(r.Path())-1) + indent := strings.Repeat(" ", len(repo.Path())-1) str.WriteString(fmt.Sprintf("\n%s %s %s", indent, blue(branch), yellow(status))) } diff --git a/pkg/print/tree.go b/pkg/print/tree.go index 16fd97b..7021673 100644 --- a/pkg/print/tree.go +++ b/pkg/print/tree.go @@ -88,8 +88,8 @@ func (n *Node) GetChild(val string) *Node { func buildTree(root string, repos []Printable) *Node { tree := Root(root) - for _, r := range repos { - path := strings.TrimPrefix(r.Path(), root) + for _, repo := range repos { + path := strings.TrimPrefix(repo.Path(), root) path = strings.Trim(path, string(filepath.Separator)) subs := strings.Split(path, string(filepath.Separator)) @@ -98,14 +98,14 @@ func buildTree(root string, repos []Printable) *Node { // If not, add it to node's children and move to next fragment. // If it does, just move to the next fragment. node := tree - for i, sub := range subs { + for idx, sub := range subs { child := node.GetChild(sub) if child == nil { node = node.Add(sub) // If that's the last fragment, it's a tree leaf and needs a *Repo attached. - if i == len(subs)-1 { - node.repo = r + if idx == len(subs)-1 { + node.repo = repo } continue @@ -120,28 +120,28 @@ func buildTree(root string, repos []Printable) *Node { // printTree renders the repo tree by recursively traversing the tree nodes. // If a node doesn't have any children, it's a leaf node containing the repo status. -func (p *TreePrinter) printTree(node *Node, tp treeprint.Tree) { +func (p *TreePrinter) printTree(node *Node, tree treeprint.Tree) { if node.children == nil { - tp.SetValue(printLeaf(node)) + tree.SetValue(printLeaf(node)) } for _, child := range node.children { - branch := tp.AddBranch(child.val) + branch := tree.AddBranch(child.val) p.printTree(child, branch) } } func printLeaf(node *Node) string { - r := node.repo + repo := node.repo // If any errors happened during status loading, don't print the status but "error" instead. // Actual error messages are printed in bulk below the tree. - if len(r.Errors()) > 0 { + if len(repo.Errors()) > 0 { return fmt.Sprintf("%s %s", node.val, red("error")) } - current := r.BranchStatus(r.Current()) - worktree := r.WorkTreeStatus() + current := repo.BranchStatus(repo.Current()) + worktree := repo.WorkTreeStatus() if worktree != "" { worktree = fmt.Sprintf("[ %s ]", worktree) @@ -150,13 +150,13 @@ func printLeaf(node *Node) string { var str strings.Builder if worktree == "" && current == "" { - str.WriteString(fmt.Sprintf("%s %s %s", node.val, blue(r.Current()), green("ok"))) + str.WriteString(fmt.Sprintf("%s %s %s", node.val, blue(repo.Current()), green("ok"))) } else { - str.WriteString(fmt.Sprintf("%s %s %s", node.val, blue(r.Current()), strings.Join([]string{yellow(current), red(worktree)}, " "))) + str.WriteString(fmt.Sprintf("%s %s %s", node.val, blue(repo.Current()), strings.Join([]string{yellow(current), red(worktree)}, " "))) } - for _, branch := range r.Branches() { - status := r.BranchStatus(branch) + for _, branch := range repo.Branches() { + status := repo.BranchStatus(branch) if status == "" { status = green("ok") } From 46f5aa0c068dc793ece27ef5632d927b80dc846a Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 15:47:57 +0200 Subject: [PATCH 09/36] Disable gochecknoglobals linter --- .golangci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.golangci.yml b/.golangci.yml index e2a2967..fa5268d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -14,6 +14,7 @@ linters: disable: - depguard # We don't have any packages we need to block + - gochecknoglobals # It's too strict and doesn't distinguish between "bad" globals (mutable shared state) and "good" globals (immutable configuration) - paralleltest # Tests are fast already and paralellizing them adds complexity - wsl # We use wsl_v5 instead - wrapcheck # Adds too much bloat, many of the errors are contextual enough and don't need wrapping From 5f60e973f6e85962e75ccf2fd4da026cbb58d4b8 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 15:52:54 +0200 Subject: [PATCH 10/36] Disable exhaustruct linter --- .golangci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.golangci.yml b/.golangci.yml index fa5268d..fffc973 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -14,6 +14,7 @@ linters: disable: - depguard # We don't have any packages we need to block + - exhaustruct # Too pedantic and impractical. Explicitly setting all struct values goes against Go's zero-value philosophy - gochecknoglobals # It's too strict and doesn't distinguish between "bad" globals (mutable shared state) and "good" globals (immutable configuration) - paralleltest # Tests are fast already and paralellizing them adds complexity - wsl # We use wsl_v5 instead From 599643892bd02cfc37556277b9564748999e2926 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 16:09:27 +0200 Subject: [PATCH 11/36] Fix issues found by revive linter --- .golangci.yml | 1 + cmd/get.go | 2 +- cmd/list.go | 2 +- cmd/main.go | 9 ++++----- pkg/git/config.go | 1 + pkg/git/test/helpers.go | 1 + pkg/print/print.go | 1 + pkg/url.go | 1 + 8 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index fffc973..05cdc87 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -26,6 +26,7 @@ linters: linters: - dupl # We don't mind duplicated code in tests. It helps with clarity - varnamelen # We don't mind short var names in tests. + - revive # Complains too much about unused-params, but they help with tests readibility - path: test/ linters: - dupl diff --git a/cmd/get.go b/cmd/get.go index 8fba2d5..c9d4796 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -45,7 +45,7 @@ func newGetCommand() *cobra.Command { return cmd } -func runGetCommand(cmd *cobra.Command, args []string) error { +func runGetCommand(_ *cobra.Command, args []string) error { var url string if len(args) > 0 { url = args[0] diff --git a/cmd/list.go b/cmd/list.go index cf3b244..8a57e84 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -35,7 +35,7 @@ func newListCommand() *cobra.Command { return cmd } -func runListCommand(cmd *cobra.Command, args []string) error { +func runListCommand(_ *cobra.Command, _ []string) error { cfg.Expand(cfg.KeyReposRoot) config := &pkg.ListCfg{ diff --git a/cmd/main.go b/cmd/main.go index 22b5bd6..0222711 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,3 +1,7 @@ +// This program behaves as a git subcommand (see https://git.github.io/htmldocs/howto/new-command.html) +// When added to PATH, git recognizes it as its subcommand and it can be invoked as "git get..." or "git list..." +// It can also be invoked as a regular binary with subcommands: "git-get get..." or "git-get list" +// The following flow detects the invokation method and runs the appropriate command. package main import ( @@ -6,11 +10,6 @@ import ( "strings" ) -// This program behaves as a git subcommand (see https://git.github.io/htmldocs/howto/new-command.html) -// When added to PATH, git recognizes it as its subcommand and it can be invoked as "git get..." or "git list..." -// It can also be invoked as a regular binary with subcommands: "git-get get..." or "git-get list" -// The following flow detects the invokation method and runs the appropriate command. - func main() { command, args := determineCommand() executeCommand(command, args) diff --git a/pkg/git/config.go b/pkg/git/config.go index 113e1f2..7d29556 100644 --- a/pkg/git/config.go +++ b/pkg/git/config.go @@ -1,3 +1,4 @@ +// Package git implements functionalities to read and manipulate git repositories package git import ( diff --git a/pkg/git/test/helpers.go b/pkg/git/test/helpers.go index 20fffdd..b18f2be 100644 --- a/pkg/git/test/helpers.go +++ b/pkg/git/test/helpers.go @@ -1,3 +1,4 @@ +// Package test contains helper utilities and functions creating pre-configured test repositories for testing purposes package test import ( diff --git a/pkg/print/print.go b/pkg/print/print.go index 58d673a..c69e509 100644 --- a/pkg/print/print.go +++ b/pkg/print/print.go @@ -1,3 +1,4 @@ +// Package print implements different outputs for git-list command package print import ( diff --git a/pkg/url.go b/pkg/url.go index d383bed..54f7913 100644 --- a/pkg/url.go +++ b/pkg/url.go @@ -1,3 +1,4 @@ +// Package pkg implements the primary funcionality of the commands: list and get package pkg import ( From 1e1584ba33dea7225f50a2bdc4a985a98713e7e4 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 16:33:15 +0200 Subject: [PATCH 12/36] Fix issues found by goerrcheck linter --- .golangci.yml | 7 +++++++ cmd/get.go | 10 ++++------ cmd/list.go | 6 +++--- pkg/cfg/config.go | 6 +++++- pkg/cfg/config_test.go | 16 +++++++++++----- pkg/git/finder_test.go | 6 +++++- 6 files changed, 35 insertions(+), 16 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 05cdc87..ad90b31 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -21,6 +21,13 @@ linters: - wrapcheck # Adds too much bloat, many of the errors are contextual enough and don't need wrapping exclusions: + # Typical presets to exclude: https://golangci-lint.run/docs/linters/false-positives/#exclusion-presets + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: - path: _test.go linters: diff --git a/cmd/get.go b/cmd/get.go index c9d4796..b54f73e 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "git-get/pkg" "git-get/pkg/cfg" "git-get/pkg/git" @@ -35,12 +36,9 @@ func newGetCommand() *cobra.Command { cmd.PersistentFlags().BoolP("help", "h", false, "Print this help and exit.") cmd.PersistentFlags().BoolP("version", "v", false, "Print version and exit.") - viper.BindPFlag(cfg.KeyBranch, cmd.PersistentFlags().Lookup(cfg.KeyBranch)) - viper.BindPFlag(cfg.KeyDefaultHost, cmd.PersistentFlags().Lookup(cfg.KeyDefaultHost)) - viper.BindPFlag(cfg.KeyDefaultScheme, cmd.PersistentFlags().Lookup(cfg.KeyDefaultScheme)) - viper.BindPFlag(cfg.KeyDump, cmd.PersistentFlags().Lookup(cfg.KeyDump)) - viper.BindPFlag(cfg.KeyReposRoot, cmd.PersistentFlags().Lookup(cfg.KeyReposRoot)) - viper.BindPFlag(cfg.KeySkipHost, cmd.PersistentFlags().Lookup(cfg.KeySkipHost)) + if err := viper.BindPFlags(cmd.PersistentFlags()); err != nil { + panic(fmt.Sprintf("failed to bind flags: %v", err)) + } return cmd } diff --git a/cmd/list.go b/cmd/list.go index 8a57e84..3011cc8 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -28,9 +28,9 @@ func newListCommand() *cobra.Command { cmd.PersistentFlags().BoolP("help", "h", false, "Print this help and exit.") cmd.PersistentFlags().BoolP("version", "v", false, "Print version and exit.") - viper.BindPFlag(cfg.KeyFetch, cmd.PersistentFlags().Lookup(cfg.KeyFetch)) - viper.BindPFlag(cfg.KeyOutput, cmd.PersistentFlags().Lookup(cfg.KeyOutput)) - viper.BindPFlag(cfg.KeyReposRoot, cmd.PersistentFlags().Lookup(cfg.KeyReposRoot)) + if err := viper.BindPFlags(cmd.PersistentFlags()); err != nil { + panic(fmt.Sprintf("failed to bind flags: %v", err)) + } return cmd } diff --git a/pkg/cfg/config.go b/pkg/cfg/config.go index a8f6be1..8e0670a 100644 --- a/pkg/cfg/config.go +++ b/pkg/cfg/config.go @@ -92,7 +92,11 @@ func readGitconfig(cfg Gitconfig) { } viper.SetConfigType("env") - viper.ReadConfig(bytes.NewBuffer([]byte(strings.Join(lines, "\n")))) + + if err := viper.ReadConfig(bytes.NewBuffer([]byte(strings.Join(lines, "\n")))); err != nil { + // Log error but don't fail - configuration is optional + fmt.Fprintf(os.Stderr, "Warning: failed to read git config: %v\n", err) + } // TODO: A hacky way to read boolean flag from gitconfig. Find a cleaner way. if val := cfg.Get(fmt.Sprintf("%s.%s", GitgetPrefix, KeySkipHost)); strings.ToLower(val) == "true" { diff --git a/pkg/cfg/config_test.go b/pkg/cfg/config_test.go index 36d06db..f017152 100644 --- a/pkg/cfg/config_test.go +++ b/pkg/cfg/config_test.go @@ -95,22 +95,28 @@ func testConfigOnlyInGitconfig(t *testing.T) { func testConfigOnlyInEnvVar(t *testing.T) { Init(&gitconfigEmpty{}) - os.Setenv(envVarName, fromEnv) + t.Setenv(envVarName, fromEnv) } func testConfigInGitconfigAndEnvVar(t *testing.T) { Init(&gitconfigValid{}) - os.Setenv(envVarName, fromEnv) + t.Setenv(envVarName, fromEnv) } func testConfigInFlag(t *testing.T) { Init(&gitconfigValid{}) - os.Setenv(envVarName, fromEnv) + t.Setenv(envVarName, fromEnv) cmd := cobra.Command{} cmd.PersistentFlags().String(KeyDefaultHost, Defaults[KeyDefaultHost], "") - viper.BindPFlag(KeyDefaultHost, cmd.PersistentFlags().Lookup(KeyDefaultHost)) + + if err := viper.BindPFlag(KeyDefaultHost, cmd.PersistentFlags().Lookup(KeyDefaultHost)); err != nil { + t.Fatalf("failed to bind flag: %v", err) + } cmd.SetArgs([]string{"--" + KeyDefaultHost, fromFlag}) - cmd.Execute() + + if err := cmd.Execute(); err != nil { + t.Fatalf("failed to execute command: %v", err) + } } diff --git a/pkg/git/finder_test.go b/pkg/git/finder_test.go index 62802e8..aa4d7d0 100644 --- a/pkg/git/finder_test.go +++ b/pkg/git/finder_test.go @@ -39,7 +39,11 @@ func TestFinder(t *testing.T) { root := test.reposMaker(t) finder := NewRepoFinder(root) - finder.Find() + + err := finder.Find() + if err != nil { + t.Fatalf("finder.Find() failed: %v", err) + } assert.Len(t, finder.repos, test.want) }) From f20ab7dea227efd4a3a367e353743a03400d9026 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 16:53:48 +0200 Subject: [PATCH 13/36] Fix issues found by err113 linter --- pkg/get.go | 5 ++++- pkg/git/finder.go | 14 +++++++++----- pkg/git/finder_test.go | 12 +----------- pkg/list.go | 5 ++++- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pkg/get.go b/pkg/get.go index e59f90b..6878ebb 100644 --- a/pkg/get.go +++ b/pkg/get.go @@ -1,11 +1,14 @@ package pkg import ( + "errors" "fmt" "git-get/pkg/git" "path/filepath" ) +var ErrMissingRepoArg = errors.New("missing argument or --dump flag") + // GetCfg provides configuration for the Get command. type GetCfg struct { Branch string @@ -20,7 +23,7 @@ type GetCfg struct { // Get executes the "git get" command. func Get(conf *GetCfg) error { if conf.URL == "" && conf.Dump == "" { - return fmt.Errorf("missing argument or --dump flag") + return ErrMissingRepoArg } if conf.URL != "" { diff --git a/pkg/git/finder.go b/pkg/git/finder.go index 374aea0..8926649 100644 --- a/pkg/git/finder.go +++ b/pkg/git/finder.go @@ -1,6 +1,7 @@ package git import ( + "errors" "fmt" "io/fs" "os" @@ -12,8 +13,11 @@ import ( // Max number of concurrently running status loading workers. const maxWorkers = 100 -var errDirNoAccess = fmt.Errorf("directory can't be accessed") -var errDirNotExist = fmt.Errorf("directory doesn't exist") +var ( + ErrDirNoAccess = errors.New("directory can't be accessed") + ErrDirNotExist = errors.New("directory doesn't exist") + ErrNoReposFound = errors.New("no git repositories found") +) // Exists returns true if a directory exists. If it doesn't or the directory can't be accessed it returns an error. func Exists(path string) (bool, error) { @@ -23,11 +27,11 @@ func Exists(path string) (bool, error) { } if os.IsNotExist(err) { - return false, fmt.Errorf("can't access %s: %w", path, errDirNotExist) + return false, fmt.Errorf("can't access %s: %w", path, ErrDirNotExist) } // Directory exists but can't be accessed - return true, fmt.Errorf("can't access %s: %w", path, errDirNoAccess) + return true, fmt.Errorf("can't access %s: %w", path, ErrDirNoAccess) } // RepoFinder finds git repositories inside a given path and loads their status. @@ -91,7 +95,7 @@ func (f *RepoFinder) Find() error { } if len(f.repos) == 0 { - return fmt.Errorf("no git repos found in root path %s", f.root) + return fmt.Errorf("%w in root path %s", ErrNoReposFound, f.root) } return nil diff --git a/pkg/git/finder_test.go b/pkg/git/finder_test.go index aa4d7d0..62d5d71 100644 --- a/pkg/git/finder_test.go +++ b/pkg/git/finder_test.go @@ -16,10 +16,6 @@ func TestFinder(t *testing.T) { want int }{ { - name: "no repos", - reposMaker: makeNoRepos, - want: 0, - }, { name: "single repos", reposMaker: makeSingleRepo, want: 1, @@ -59,7 +55,7 @@ func TestExists(t *testing.T) { { name: "dir does not exist", path: "/this/directory/does/not/exist", - want: errDirNotExist, + want: ErrDirNotExist, }, { name: "dir exists", path: os.TempDir(), @@ -76,12 +72,6 @@ func TestExists(t *testing.T) { } } -func makeNoRepos(t *testing.T) string { - root := test.TempDir(t, "") - - return root -} - func makeSingleRepo(t *testing.T) string { root := test.TempDir(t, "") diff --git a/pkg/list.go b/pkg/list.go index 7173b9d..0c3ff72 100644 --- a/pkg/list.go +++ b/pkg/list.go @@ -1,6 +1,7 @@ package pkg import ( + "errors" "fmt" "git-get/pkg/cfg" "git-get/pkg/git" @@ -8,6 +9,8 @@ import ( "strings" ) +var ErrInvalidOutput = errors.New("invalid output format") + // ListCfg provides configuration for the List command. type ListCfg struct { Fetch bool @@ -37,7 +40,7 @@ func List(conf *ListCfg) error { case cfg.OutDump: fmt.Print(print.NewDumpPrinter().Print(printables)) default: - return fmt.Errorf("invalid --out flag; allowed values: [%s]", strings.Join(cfg.AllowedOut, ", ")) + return fmt.Errorf("%w, allowed values: [%s]", ErrInvalidOutput, strings.Join(cfg.AllowedOut, ", ")) } return nil From 88608f9df4e782c88ca8e840cd3e3d151eaac0a0 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 16:57:01 +0200 Subject: [PATCH 14/36] Disable forbidigo linter --- .golangci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.golangci.yml b/.golangci.yml index ad90b31..224e16d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -15,6 +15,7 @@ linters: disable: - depguard # We don't have any packages we need to block - exhaustruct # Too pedantic and impractical. Explicitly setting all struct values goes against Go's zero-value philosophy + - forbidigo # Not suitable for CLI apps where printing to stdout is fine - gochecknoglobals # It's too strict and doesn't distinguish between "bad" globals (mutable shared state) and "good" globals (immutable configuration) - paralleltest # Tests are fast already and paralellizing them adds complexity - wsl # We use wsl_v5 instead From 07a640947497ad90da1ff5d55e8d75e87846c103 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 17:10:38 +0200 Subject: [PATCH 15/36] Disable godox linter temporarily --- .golangci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.golangci.yml b/.golangci.yml index 224e16d..db25b05 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -17,6 +17,7 @@ linters: - exhaustruct # Too pedantic and impractical. Explicitly setting all struct values goes against Go's zero-value philosophy - forbidigo # Not suitable for CLI apps where printing to stdout is fine - gochecknoglobals # It's too strict and doesn't distinguish between "bad" globals (mutable shared state) and "good" globals (immutable configuration) + - godox # TODO: enable it and handle all the remaning TODOs - paralleltest # Tests are fast already and paralellizing them adds complexity - wsl # We use wsl_v5 instead - wrapcheck # Adds too much bloat, many of the errors are contextual enough and don't need wrapping From 7bc9cbb128c374f218c6a3c3d619f666d0a44d0c Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 17:17:34 +0200 Subject: [PATCH 16/36] Fix issues found by goconst linter --- .golangci.yml | 7 +++++++ pkg/url_test.go | 6 +++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index db25b05..35f8c7f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -22,6 +22,12 @@ linters: - wsl # We use wsl_v5 instead - wrapcheck # Adds too much bloat, many of the errors are contextual enough and don't need wrapping + settings: + goconst: + ignore-string-values: + - "get" + - "list" + exclusions: # Typical presets to exclude: https://golangci-lint.run/docs/linters/false-positives/#exclusion-presets presets: @@ -36,6 +42,7 @@ linters: - dupl # We don't mind duplicated code in tests. It helps with clarity - varnamelen # We don't mind short var names in tests. - revive # Complains too much about unused-params, but they help with tests readibility + - funlen # We don't mind long functions in tests - path: test/ linters: - dupl diff --git a/pkg/url_test.go b/pkg/url_test.go index 934c211..8bccd87 100644 --- a/pkg/url_test.go +++ b/pkg/url_test.go @@ -131,11 +131,11 @@ func TestDefaultScheme(t *testing.T) { func TestInvalidURLParse(t *testing.T) { invalidURLs := []string{ "", - //TODO: This Path is technically a correct scp-like syntax. Not sure how to handle it + // TODO: This Path is technically a correct scp-like syntax. Not sure how to handle it "github.com:grdl/git-git.get.git", - //TODO: Is this a valid git Path? - //"git@github.com:1234:grdl/git-get.git", + // TODO: Is this a valid git Path? + // "git@github.com:1234:grdl/git-get.git", } for _, test := range invalidURLs { From eadc2ff45e34018a7fda7428a293da7a133d6b59 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 17:18:03 +0200 Subject: [PATCH 17/36] Fix issues found by godot linter --- pkg/cfg/config.go | 2 +- pkg/dump.go | 2 +- pkg/git/repo.go | 2 +- pkg/git/status.go | 8 ++++---- pkg/git/test/helpers.go | 2 +- pkg/git/test/testrepos.go | 4 ++-- pkg/print/print.go | 2 +- pkg/print/tree.go | 2 +- pkg/url.go | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pkg/cfg/config.go b/pkg/cfg/config.go index 8e0670a..a690a64 100644 --- a/pkg/cfg/config.go +++ b/pkg/cfg/config.go @@ -65,7 +65,7 @@ func Version() string { return fmt.Sprintf("git-get %s", version) } -// Gitconfig represents gitconfig file +// Gitconfig represents gitconfig file. type Gitconfig interface { Get(key string) string } diff --git a/pkg/dump.go b/pkg/dump.go index decc2e0..64d8828 100644 --- a/pkg/dump.go +++ b/pkg/dump.go @@ -48,7 +48,7 @@ func parseDumpFile(path string) ([]parsedLine, error) { } // parseLine splits a dump file line into space-separated segments. -// First part is the URL to clone. Second, optional, is the branch (or tag) to checkout after cloning +// First part is the URL to clone. Second, optional, is the branch (or tag) to checkout after cloning. func parseLine(line string) (parsedLine, error) { var parsed parsedLine diff --git a/pkg/git/repo.go b/pkg/git/repo.go index 9a11861..ac599d2 100644 --- a/pkg/git/repo.go +++ b/pkg/git/repo.go @@ -65,7 +65,7 @@ func Clone(opts *CloneOpts) (*Repo, error) { return Repo, err } -// Fetch preforms a git fetch on all remotes +// Fetch preforms a git fetch on all remotes. func (r *Repo) Fetch() error { err := run.Git("fetch", "--all").OnRepo(r.path).AndShutUp() return err diff --git a/pkg/git/status.go b/pkg/git/status.go index 6f5607c..18b2ac3 100644 --- a/pkg/git/status.go +++ b/pkg/git/status.go @@ -160,22 +160,22 @@ func (s *Status) Branches() []string { return branches } -// BranchStatus returns status of a given branch +// BranchStatus returns status of a given branch. func (s *Status) BranchStatus(branch string) string { return s.branches[branch] } -// WorkTreeStatus returns status of a worktree +// WorkTreeStatus returns status of a worktree. func (s *Status) WorkTreeStatus() string { return s.worktree } -// Remote returns URL to remote repository +// Remote returns URL to remote repository. func (s *Status) Remote() string { return s.remote } -// Errors is a slice of errors that occurred when loading repo status +// Errors is a slice of errors that occurred when loading repo status. func (s *Status) Errors() []string { return s.errors } diff --git a/pkg/git/test/helpers.go b/pkg/git/test/helpers.go index b18f2be..12cd8b2 100644 --- a/pkg/git/test/helpers.go +++ b/pkg/git/test/helpers.go @@ -92,7 +92,7 @@ func checkFatal(t *testing.T, err error) { } } -// removeTestDir removes a test directory +// removeTestDir removes a test directory. func removeTestDir(t *testing.T, dir string) { // Skip cleanup on Windows to avoid file locking issues in CI // The CI runner environment is destroyed after tests anyway diff --git a/pkg/git/test/testrepos.go b/pkg/git/test/testrepos.go index 407af6f..f4aae50 100644 --- a/pkg/git/test/testrepos.go +++ b/pkg/git/test/testrepos.go @@ -172,7 +172,7 @@ func RepoWithBranchAheadAndBehind(t *testing.T) *Repo { return r } -// RepoWithEmptyConfig creates a git repo with empty .git/config file +// RepoWithEmptyConfig creates a git repo with empty .git/config file. func RepoWithEmptyConfig(t *testing.T) *Repo { r := RepoEmpty(t) r.writeFile(filepath.Join(".git", "config"), "") @@ -180,7 +180,7 @@ func RepoWithEmptyConfig(t *testing.T) *Repo { return r } -// RepoWithValidConfig creates a git repo with valid content in .git/config file +// RepoWithValidConfig creates a git repo with valid content in .git/config file. func RepoWithValidConfig(t *testing.T) *Repo { r := RepoEmpty(t) diff --git a/pkg/print/print.go b/pkg/print/print.go index c69e509..1a4d470 100644 --- a/pkg/print/print.go +++ b/pkg/print/print.go @@ -10,7 +10,7 @@ const ( head = "HEAD" ) -// Printable represents a repository which status can be printed +// Printable represents a repository which status can be printed. type Printable interface { Path() string Current() string diff --git a/pkg/print/tree.go b/pkg/print/tree.go index 7021673..06fcb0e 100644 --- a/pkg/print/tree.go +++ b/pkg/print/tree.go @@ -203,7 +203,7 @@ func indentation(node *Node) string { return indent.String() } -// isYoungest checks if the node is the last one in the slice of children +// isYoungest checks if the node is the last one in the slice of children. func (n *Node) isYoungest() bool { if n.parent == nil { return true diff --git a/pkg/url.go b/pkg/url.go index 54f7913..92ef232 100644 --- a/pkg/url.go +++ b/pkg/url.go @@ -89,7 +89,7 @@ func normalizeURL(url *urlpkg.URL, defaultHost string, defaultScheme string) { // Eg, ssh://git@github.com:22/~user/repo.git => github.com/user/repo // // If skipHost is true, it removes the host part from the path. -// Eg, ssh://git@github.com:22/~user/repo.git => user/repo +// Eg, ssh://git@github.com:22/~user/repo.git => user/repo. func URLToPath(url urlpkg.URL, skipHost bool) string { // Remove port numbers from host. url.Host = strings.Split(url.Host, ":")[0] From 3a5fac5c2df2863cfb33eb54667bb62181d207f1 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 17:20:43 +0200 Subject: [PATCH 18/36] Disable testpackage linter temporarily --- .golangci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.golangci.yml b/.golangci.yml index 35f8c7f..c16b2c9 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -19,6 +19,7 @@ linters: - gochecknoglobals # It's too strict and doesn't distinguish between "bad" globals (mutable shared state) and "good" globals (immutable configuration) - godox # TODO: enable it and handle all the remaning TODOs - paralleltest # Tests are fast already and paralellizing them adds complexity + - testpackage # TODO: renable it and refactor tests into separate packages - wsl # We use wsl_v5 instead - wrapcheck # Adds too much bloat, many of the errors are contextual enough and don't need wrapping From c9a5539670d8e8dee77967f76cdf26c133dc515d Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 17:28:21 +0200 Subject: [PATCH 19/36] Fix issues found by testifylint linter --- pkg/git/finder_test.go | 3 +-- pkg/url_test.go | 11 ++++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/git/finder_test.go b/pkg/git/finder_test.go index 62d5d71..e5d1b14 100644 --- a/pkg/git/finder_test.go +++ b/pkg/git/finder_test.go @@ -1,7 +1,6 @@ package git import ( - "errors" "git-get/pkg/git/test" "os" "testing" @@ -67,7 +66,7 @@ func TestExists(t *testing.T) { t.Run(test.name, func(t *testing.T) { _, err := Exists(test.path) - assert.True(t, errors.Is(err, test.want)) + assert.ErrorIs(t, err, test.want) }) } } diff --git a/pkg/url_test.go b/pkg/url_test.go index 8bccd87..f9a6700 100644 --- a/pkg/url_test.go +++ b/pkg/url_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // Following URLs are considered valid according to https://git-scm.com/docs/git-clone#_git_urls: @@ -53,7 +54,7 @@ func TestURLParse(t *testing.T) { for _, test := range tests { url, err := ParseURL(test.in, cfg.Defaults[cfg.KeyDefaultHost], cfg.Defaults[cfg.KeyDefaultScheme]) - assert.NoError(t, err) + require.NoError(t, err) got := URLToPath(*url, false) assert.Equal(t, test.want, got) @@ -93,7 +94,7 @@ func TestURLParseSkipHost(t *testing.T) { for _, test := range tests { url, err := ParseURL(test.in, cfg.Defaults[cfg.KeyDefaultHost], cfg.Defaults[cfg.KeyDefaultScheme]) - assert.NoError(t, err) + require.NoError(t, err) got := URLToPath(*url, true) assert.Equal(t, test.want, got) @@ -119,12 +120,12 @@ func TestDefaultScheme(t *testing.T) { for _, test := range tests { url, err := ParseURL(test.in, cfg.Defaults[cfg.KeyDefaultHost], test.scheme) - assert.NoError(t, err) + require.NoError(t, err) want, err := url.Parse(test.want) - assert.NoError(t, err) + require.NoError(t, err) - assert.Equal(t, url, want) + assert.Equal(t, want, url) } } From d43ea77025e3b578a18aac907ac6478382b5ae85 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 17:33:49 +0200 Subject: [PATCH 20/36] Disable unparam linter --- .golangci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.golangci.yml b/.golangci.yml index c16b2c9..493e930 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -20,6 +20,7 @@ linters: - godox # TODO: enable it and handle all the remaning TODOs - paralleltest # Tests are fast already and paralellizing them adds complexity - testpackage # TODO: renable it and refactor tests into separate packages + - unparam # Impractical, it flags functions that are designed to be general-purpose, but happen to only be used with specific values currently - wsl # We use wsl_v5 instead - wrapcheck # Adds too much bloat, many of the errors are contextual enough and don't need wrapping From b39f5e63fd2c108a6f10b444007ea2f690238ad4 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 17:42:59 +0200 Subject: [PATCH 21/36] Fix issues found by thelper linter --- .golangci.yml | 2 ++ pkg/cfg/config_test.go | 5 +++++ pkg/git/config_test.go | 4 ++++ pkg/git/finder_test.go | 3 +++ pkg/git/repo_test.go | 1 + pkg/git/test/helpers.go | 5 +++++ pkg/git/test/testrepos.go | 15 +++++++++++++++ 7 files changed, 35 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index 493e930..ec4a45a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -29,6 +29,8 @@ linters: ignore-string-values: - "get" - "list" + lll: + line-length: 180 exclusions: # Typical presets to exclude: https://golangci-lint.run/docs/linters/false-positives/#exclusion-presets diff --git a/pkg/cfg/config_test.go b/pkg/cfg/config_test.go index f017152..81563c7 100644 --- a/pkg/cfg/config_test.go +++ b/pkg/cfg/config_test.go @@ -86,24 +86,29 @@ func (c *gitconfigValid) Get(key string) string { } func testConfigEmpty(t *testing.T) { + t.Helper() Init(&gitconfigEmpty{}) } func testConfigOnlyInGitconfig(t *testing.T) { + t.Helper() Init(&gitconfigValid{}) } func testConfigOnlyInEnvVar(t *testing.T) { + t.Helper() Init(&gitconfigEmpty{}) t.Setenv(envVarName, fromEnv) } func testConfigInGitconfigAndEnvVar(t *testing.T) { + t.Helper() Init(&gitconfigValid{}) t.Setenv(envVarName, fromEnv) } func testConfigInFlag(t *testing.T) { + t.Helper() Init(&gitconfigValid{}) t.Setenv(envVarName, fromEnv) diff --git a/pkg/git/config_test.go b/pkg/git/config_test.go index e700395..1af7f14 100644 --- a/pkg/git/config_test.go +++ b/pkg/git/config_test.go @@ -65,12 +65,16 @@ func TestGitConfig(t *testing.T) { } func makeConfigEmpty(t *testing.T) *cfgStub { + t.Helper() + return &cfgStub{ Repo: test.RepoWithEmptyConfig(t), } } func makeConfigValid(t *testing.T) *cfgStub { + t.Helper() + return &cfgStub{ Repo: test.RepoWithValidConfig(t), } diff --git a/pkg/git/finder_test.go b/pkg/git/finder_test.go index e5d1b14..b35a65d 100644 --- a/pkg/git/finder_test.go +++ b/pkg/git/finder_test.go @@ -72,6 +72,7 @@ func TestExists(t *testing.T) { } func makeSingleRepo(t *testing.T) string { + t.Helper() root := test.TempDir(t, "") test.RepoEmptyInDir(t, root) @@ -80,6 +81,7 @@ func makeSingleRepo(t *testing.T) string { } func makeNestedRepo(t *testing.T) string { + t.Helper() // a repo with single nested repo should still be counted as one beacause finder doesn't traverse inside nested repos root := test.TempDir(t, "") @@ -90,6 +92,7 @@ func makeNestedRepo(t *testing.T) string { } func makeMultipleNestedRepos(t *testing.T) string { + t.Helper() root := test.TempDir(t, "") // create two repos inside root - should be counted as 2 diff --git a/pkg/git/repo_test.go b/pkg/git/repo_test.go index baf2d31..f377f14 100644 --- a/pkg/git/repo_test.go +++ b/pkg/git/repo_test.go @@ -411,6 +411,7 @@ func TestRemote(t *testing.T) { } func createTestDirTree(t *testing.T) string { + t.Helper() root := test.TempDir(t, "") err := os.MkdirAll(filepath.Join(root, "a", "b", "c"), os.ModePerm) err = os.MkdirAll(filepath.Join(root, "a", "x", "y"), os.ModePerm) diff --git a/pkg/git/test/helpers.go b/pkg/git/test/helpers.go index 12cd8b2..1be8afa 100644 --- a/pkg/git/test/helpers.go +++ b/pkg/git/test/helpers.go @@ -14,6 +14,8 @@ import ( // TempDir creates a temporary directory inside the parent dir. // If parent is empty, it will use a system default temp dir (usually /tmp). func TempDir(t *testing.T, parent string) string { + t.Helper() + dir, err := ioutil.TempDir(parent, "git-get-repo-") checkFatal(t, err) @@ -87,6 +89,8 @@ func (r *Repo) fetch() { } func checkFatal(t *testing.T, err error) { + t.Helper() + if err != nil { t.Fatalf("failed making test repo: %+v", err) } @@ -94,6 +98,7 @@ func checkFatal(t *testing.T, err error) { // removeTestDir removes a test directory. func removeTestDir(t *testing.T, dir string) { + t.Helper() // Skip cleanup on Windows to avoid file locking issues in CI // The CI runner environment is destroyed after tests anyway if runtime.GOOS == "windows" { diff --git a/pkg/git/test/testrepos.go b/pkg/git/test/testrepos.go index f4aae50..c25434c 100644 --- a/pkg/git/test/testrepos.go +++ b/pkg/git/test/testrepos.go @@ -19,11 +19,13 @@ func (r *Repo) Path() string { // RepoEmpty creates an empty git repo. func RepoEmpty(t *testing.T) *Repo { + t.Helper() return RepoEmptyInDir(t, "") } // RepoEmptyInDir creates an empty git repo inside a given parent dir. func RepoEmptyInDir(t *testing.T, parent string) *Repo { + t.Helper() r := &Repo{ path: TempDir(t, parent), t: t, @@ -36,6 +38,7 @@ func RepoEmptyInDir(t *testing.T, parent string) *Repo { // RepoWithUntracked creates a git repo with a single untracked file. func RepoWithUntracked(t *testing.T) *Repo { + t.Helper() r := RepoEmpty(t) r.writeFile("README.md", "I'm a readme file") @@ -44,6 +47,7 @@ func RepoWithUntracked(t *testing.T) *Repo { // RepoWithStaged creates a git repo with a single staged file. func RepoWithStaged(t *testing.T) *Repo { + t.Helper() r := RepoEmpty(t) r.writeFile("README.md", "I'm a readme file") r.stageFile("README.md") @@ -53,6 +57,7 @@ func RepoWithStaged(t *testing.T) *Repo { // RepoWithCommit creates a git repo with a single commit. func RepoWithCommit(t *testing.T) *Repo { + t.Helper() r := RepoEmpty(t) r.writeFile("README.md", "I'm a readme file") r.stageFile("README.md") @@ -63,6 +68,7 @@ func RepoWithCommit(t *testing.T) *Repo { // RepoWithUncommittedAndUntracked creates a git repo with one staged but uncommitted file and one untracked file. func RepoWithUncommittedAndUntracked(t *testing.T) *Repo { + t.Helper() r := RepoEmpty(t) r.writeFile("README.md", "I'm a readme file") r.stageFile("README.md") @@ -75,6 +81,7 @@ func RepoWithUncommittedAndUntracked(t *testing.T) *Repo { // RepoWithBranch creates a git repo with a new branch. func RepoWithBranch(t *testing.T) *Repo { + t.Helper() r := RepoWithCommit(t) r.branch("feature/branch") r.checkout("feature/branch") @@ -84,6 +91,7 @@ func RepoWithBranch(t *testing.T) *Repo { // RepoWithTag creates a git repo with a new tag. func RepoWithTag(t *testing.T) *Repo { + t.Helper() r := RepoWithCommit(t) r.tag("v0.0.1") r.checkout("v0.0.1") @@ -93,6 +101,7 @@ func RepoWithTag(t *testing.T) *Repo { // RepoWithBranchWithUpstream creates a git repo by cloning another repo and checking out a remote branch. func RepoWithBranchWithUpstream(t *testing.T) *Repo { + t.Helper() origin := RepoWithCommit(t) origin.branch("feature/branch") @@ -104,6 +113,7 @@ func RepoWithBranchWithUpstream(t *testing.T) *Repo { // RepoWithBranchWithoutUpstream creates a git repo by cloning another repo and checking out a new local branch. func RepoWithBranchWithoutUpstream(t *testing.T) *Repo { + t.Helper() origin := RepoWithCommit(t) r := origin.clone() @@ -115,6 +125,7 @@ func RepoWithBranchWithoutUpstream(t *testing.T) *Repo { // RepoWithBranchAhead creates a git repo with a branch being ahead of a remote branch by 1 commit. func RepoWithBranchAhead(t *testing.T) *Repo { + t.Helper() origin := RepoWithCommit(t) origin.branch("feature/branch") @@ -130,6 +141,7 @@ func RepoWithBranchAhead(t *testing.T) *Repo { // RepoWithBranchBehind creates a git repo with a branch being behind a remote branch by 1 commit. func RepoWithBranchBehind(t *testing.T) *Repo { + t.Helper() origin := RepoWithCommit(t) origin.branch("feature/branch") origin.checkout("feature/branch") @@ -148,6 +160,7 @@ func RepoWithBranchBehind(t *testing.T) *Repo { // RepoWithBranchAheadAndBehind creates a git repo with a branch being 2 commits ahead and 1 behind a remote branch. func RepoWithBranchAheadAndBehind(t *testing.T) *Repo { + t.Helper() origin := RepoWithCommit(t) origin.branch("feature/branch") origin.checkout("feature/branch") @@ -174,6 +187,7 @@ func RepoWithBranchAheadAndBehind(t *testing.T) *Repo { // RepoWithEmptyConfig creates a git repo with empty .git/config file. func RepoWithEmptyConfig(t *testing.T) *Repo { + t.Helper() r := RepoEmpty(t) r.writeFile(filepath.Join(".git", "config"), "") @@ -182,6 +196,7 @@ func RepoWithEmptyConfig(t *testing.T) *Repo { // RepoWithValidConfig creates a git repo with valid content in .git/config file. func RepoWithValidConfig(t *testing.T) *Repo { + t.Helper() r := RepoEmpty(t) gitconfig := ` From 2de126166fc546ad24890840bad4745fdafc355c Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 17:51:14 +0200 Subject: [PATCH 22/36] Fix smaller issues found by various linters --- pkg/git/finder.go | 4 ++-- pkg/git/repo.go | 1 + pkg/print/print.go | 2 +- pkg/run/run.go | 4 +++- pkg/url.go | 4 ++-- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pkg/git/finder.go b/pkg/git/finder.go index 8926649..87d4251 100644 --- a/pkg/git/finder.go +++ b/pkg/git/finder.go @@ -105,13 +105,13 @@ func (f *RepoFinder) Find() error { // If fetch equals true, it first fetches from the remote repo before loading the status. // Each repo is loaded concurrently by a separate worker, with max 100 workers being active at the same time. func (f *RepoFinder) LoadAll(fetch bool) []*Status { - var statuses []*Status + statuses := []*Status{} reposChan := make(chan *Repo, f.maxWorkers) statusChan := make(chan *Status, f.maxWorkers) // Fire up workers. They listen on reposChan, load status and send the result to statusChan. - for i := 0; i < f.maxWorkers; i++ { + for range f.maxWorkers { go statusWorker(fetch, reposChan, statusChan) } diff --git a/pkg/git/repo.go b/pkg/git/repo.go index ac599d2..a4dee0a 100644 --- a/pkg/git/repo.go +++ b/pkg/git/repo.go @@ -156,6 +156,7 @@ func (r *Repo) Upstream(branch string) (string, error) { out, err := run.Git("rev-parse", "--abbrev-ref", "--symbolic-full-name", fmt.Sprintf("%s@{upstream}", branch)).OnRepo(r.path).AndCaptureLine() if err != nil { // TODO: no upstream will also throw an error. + // lint:ignore nilerr fix when working on TODO return "", nil } diff --git a/pkg/print/print.go b/pkg/print/print.go index 1a4d470..58fac04 100644 --- a/pkg/print/print.go +++ b/pkg/print/print.go @@ -15,7 +15,7 @@ type Printable interface { Path() string Current() string Branches() []string - BranchStatus(string) string + BranchStatus(branch string) string WorkTreeStatus() string Remote() string Errors() []string diff --git a/pkg/run/run.go b/pkg/run/run.go index 20e8148..390c4e6 100644 --- a/pkg/run/run.go +++ b/pkg/run/run.go @@ -3,6 +3,7 @@ package run import ( "bytes" + "context" "fmt" "os" "os/exec" @@ -32,8 +33,9 @@ type Cmd struct { // Git creates a git command with given arguments. func Git(args ...string) *Cmd { + ctx := context.Background() return &Cmd{ - cmd: exec.Command("git", args...), + cmd: exec.CommandContext(ctx, "git", args...), args: strings.Join(args, " "), } } diff --git a/pkg/url.go b/pkg/url.go index 92ef232..fe0c39b 100644 --- a/pkg/url.go +++ b/pkg/url.go @@ -19,8 +19,8 @@ var scpSyntax = regexp.MustCompile(`^([a-zA-Z0-9_]+)@([a-zA-Z0-9._-]+):(.*)$`) // ParseURL parses given rawURL string into a URL. // When the parsed URL has an empty host, use the defaultHost. // When the parsed URL has an empty scheme, use the defaultScheme. -func ParseURL(rawURL string, defaultHost string, defaultScheme string) (url *urlpkg.URL, err error) { - url, err = parseRawURL(rawURL) +func ParseURL(rawURL string, defaultHost string, defaultScheme string) (*urlpkg.URL, error) { + url, err := parseRawURL(rawURL) if err != nil { return nil, err } From 0db5ba6f5ab2f1dbb7dd917e91954d28be2fc3ae Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 18:00:59 +0200 Subject: [PATCH 23/36] Fix issues found by ineffassign linter --- pkg/git/repo_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/git/repo_test.go b/pkg/git/repo_test.go index f377f14..a99b314 100644 --- a/pkg/git/repo_test.go +++ b/pkg/git/repo_test.go @@ -414,7 +414,14 @@ func createTestDirTree(t *testing.T) string { t.Helper() root := test.TempDir(t, "") err := os.MkdirAll(filepath.Join(root, "a", "b", "c"), os.ModePerm) + if err != nil { + t.Fatal(err) + } + err = os.MkdirAll(filepath.Join(root, "a", "x", "y"), os.ModePerm) + if err != nil { + t.Fatal(err) + } _, err = os.Create(filepath.Join(root, "a", "x", "y", "file.txt")) if err != nil { From 08f3264cd0e1b6753b9cd2181c774321e4508484 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 18:01:42 +0200 Subject: [PATCH 24/36] Fix issues found by mirror linter --- pkg/cfg/config.go | 2 +- pkg/git/repo_test.go | 1 + pkg/git/test/helpers.go | 2 +- pkg/run/run.go | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/cfg/config.go b/pkg/cfg/config.go index a690a64..f362fe6 100644 --- a/pkg/cfg/config.go +++ b/pkg/cfg/config.go @@ -93,7 +93,7 @@ func readGitconfig(cfg Gitconfig) { viper.SetConfigType("env") - if err := viper.ReadConfig(bytes.NewBuffer([]byte(strings.Join(lines, "\n")))); err != nil { + if err := viper.ReadConfig(bytes.NewBufferString(strings.Join(lines, "\n"))); err != nil { // Log error but don't fail - configuration is optional fmt.Fprintf(os.Stderr, "Warning: failed to read git config: %v\n", err) } diff --git a/pkg/git/repo_test.go b/pkg/git/repo_test.go index a99b314..b67a974 100644 --- a/pkg/git/repo_test.go +++ b/pkg/git/repo_test.go @@ -413,6 +413,7 @@ func TestRemote(t *testing.T) { func createTestDirTree(t *testing.T) string { t.Helper() root := test.TempDir(t, "") + err := os.MkdirAll(filepath.Join(root, "a", "b", "c"), os.ModePerm) if err != nil { t.Fatal(err) diff --git a/pkg/git/test/helpers.go b/pkg/git/test/helpers.go index 1be8afa..14e0104 100644 --- a/pkg/git/test/helpers.go +++ b/pkg/git/test/helpers.go @@ -39,7 +39,7 @@ func (r *Repo) writeFile(filename string, content string) { file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) checkFatal(r.t, err) - _, err = file.Write([]byte(content)) + _, err = file.WriteString(content) checkFatal(r.t, err) } diff --git a/pkg/run/run.go b/pkg/run/run.go index 390c4e6..4c104c8 100644 --- a/pkg/run/run.go +++ b/pkg/run/run.go @@ -34,6 +34,7 @@ type Cmd struct { // Git creates a git command with given arguments. func Git(args ...string) *Cmd { ctx := context.Background() + return &Cmd{ cmd: exec.CommandContext(ctx, "git", args...), args: strings.Join(args, " "), From 8b8c814ced8f73024da80439fa1ae153d29ca76a Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 18:05:15 +0200 Subject: [PATCH 25/36] Fix issues found by staticcheck linter --- pkg/git/test/helpers.go | 5 +++-- pkg/print/print.go | 4 +--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pkg/git/test/helpers.go b/pkg/git/test/helpers.go index 14e0104..e71077a 100644 --- a/pkg/git/test/helpers.go +++ b/pkg/git/test/helpers.go @@ -4,7 +4,6 @@ package test import ( "fmt" "git-get/pkg/run" - "io/ioutil" "os" "path/filepath" "runtime" @@ -16,7 +15,9 @@ import ( func TempDir(t *testing.T, parent string) string { t.Helper() - dir, err := ioutil.TempDir(parent, "git-get-repo-") + // t.TempDir() is not enough in this case, we need to be able to create dirs inside the parent dir + //nolint:usetesting + dir, err := os.MkdirTemp(parent, "git-get-repo-") checkFatal(t, err) // Automatically remove temp dir when the test is over. diff --git a/pkg/print/print.go b/pkg/print/print.go index 58fac04..f628373 100644 --- a/pkg/print/print.go +++ b/pkg/print/print.go @@ -27,9 +27,7 @@ func Errors(repos []Printable) string { errors := []string{} for _, repo := range repos { - for _, err := range repo.Errors() { - errors = append(errors, err) - } + errors = append(errors, repo.Errors()...) } if len(errors) == 0 { From aa1da8e3da455527dc863f6af5bc1ba15c3ce2aa Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 18:20:18 +0200 Subject: [PATCH 26/36] Refactor package print into out to fix failing predeclared linter --- pkg/list.go | 11 ++++++----- pkg/{print => out}/dump.go | 2 +- pkg/{print => out}/flat.go | 2 +- pkg/{print => out}/print.go | 4 ++-- pkg/{print => out}/tree.go | 2 +- 5 files changed, 11 insertions(+), 10 deletions(-) rename pkg/{print => out}/dump.go (98%) rename pkg/{print => out}/flat.go (98%) rename pkg/{print => out}/print.go (93%) rename pkg/{print => out}/tree.go (99%) diff --git a/pkg/list.go b/pkg/list.go index 0c3ff72..9037978 100644 --- a/pkg/list.go +++ b/pkg/list.go @@ -5,7 +5,7 @@ import ( "fmt" "git-get/pkg/cfg" "git-get/pkg/git" - "git-get/pkg/print" + "git-get/pkg/out" "strings" ) @@ -27,18 +27,19 @@ func List(conf *ListCfg) error { statuses := finder.LoadAll(conf.Fetch) - printables := make([]print.Printable, len(statuses)) + printables := make([]out.Printable, len(statuses)) + for i := range statuses { printables[i] = statuses[i] } switch conf.Output { case cfg.OutFlat: - fmt.Print(print.NewFlatPrinter().Print(printables)) + fmt.Print(out.NewFlatPrinter().Print(printables)) case cfg.OutTree: - fmt.Print(print.NewTreePrinter().Print(conf.Root, printables)) + fmt.Print(out.NewTreePrinter().Print(conf.Root, printables)) case cfg.OutDump: - fmt.Print(print.NewDumpPrinter().Print(printables)) + fmt.Print(out.NewDumpPrinter().Print(printables)) default: return fmt.Errorf("%w, allowed values: [%s]", ErrInvalidOutput, strings.Join(cfg.AllowedOut, ", ")) } diff --git a/pkg/print/dump.go b/pkg/out/dump.go similarity index 98% rename from pkg/print/dump.go rename to pkg/out/dump.go index 293b3f3..c3878fd 100644 --- a/pkg/print/dump.go +++ b/pkg/out/dump.go @@ -1,4 +1,4 @@ -package print +package out import ( "strings" diff --git a/pkg/print/flat.go b/pkg/out/flat.go similarity index 98% rename from pkg/print/flat.go rename to pkg/out/flat.go index 24576bb..0edcf3d 100644 --- a/pkg/print/flat.go +++ b/pkg/out/flat.go @@ -1,4 +1,4 @@ -package print +package out import ( "fmt" diff --git a/pkg/print/print.go b/pkg/out/print.go similarity index 93% rename from pkg/print/print.go rename to pkg/out/print.go index f628373..7ad59d5 100644 --- a/pkg/print/print.go +++ b/pkg/out/print.go @@ -1,5 +1,5 @@ -// Package print implements different outputs for git-list command -package print +// Package out implements different outputs for git-list command +package out import ( "fmt" diff --git a/pkg/print/tree.go b/pkg/out/tree.go similarity index 99% rename from pkg/print/tree.go rename to pkg/out/tree.go index 06fcb0e..48c8443 100644 --- a/pkg/print/tree.go +++ b/pkg/out/tree.go @@ -1,4 +1,4 @@ -package print +package out import ( "fmt" From 1bf52c4c32c30b910fa2b5968afb97dd489c0e4b Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 18:21:17 +0200 Subject: [PATCH 27/36] Fix issues found by perfsprint linter --- pkg/cfg/config.go | 2 +- pkg/git/repo.go | 2 +- pkg/git/repo_test.go | 4 ++-- pkg/out/tree.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/cfg/config.go b/pkg/cfg/config.go index f362fe6..c24aa6f 100644 --- a/pkg/cfg/config.go +++ b/pkg/cfg/config.go @@ -62,7 +62,7 @@ func Version() string { return fmt.Sprintf("git-get %s (%s)", version, commit[:7]) } - return fmt.Sprintf("git-get %s", version) + return "git-get " + version } // Gitconfig represents gitconfig file. diff --git a/pkg/git/repo.go b/pkg/git/repo.go index a4dee0a..201d31d 100644 --- a/pkg/git/repo.go +++ b/pkg/git/repo.go @@ -153,7 +153,7 @@ func (r *Repo) Branches() ([]string, error) { // Upstream returns the name of an upstream branch if a given branch is tracking one. // Otherwise it returns an empty string. func (r *Repo) Upstream(branch string) (string, error) { - out, err := run.Git("rev-parse", "--abbrev-ref", "--symbolic-full-name", fmt.Sprintf("%s@{upstream}", branch)).OnRepo(r.path).AndCaptureLine() + out, err := run.Git("rev-parse", "--abbrev-ref", "--symbolic-full-name", branch+"@{upstream}").OnRepo(r.path).AndCaptureLine() if err != nil { // TODO: no upstream will also throw an error. // lint:ignore nilerr fix when working on TODO diff --git a/pkg/git/repo_test.go b/pkg/git/repo_test.go index b67a974..9ee8510 100644 --- a/pkg/git/repo_test.go +++ b/pkg/git/repo_test.go @@ -1,11 +1,11 @@ package git import ( - "fmt" "git-get/pkg/git/test" "os" "path/filepath" "reflect" + "strconv" "testing" "github.com/stretchr/testify/assert" @@ -339,7 +339,7 @@ func TestCleanupFailedClone(t *testing.T) { } for i, test := range tests { - t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + t.Run(strconv.Itoa(i), func(t *testing.T) { root := createTestDirTree(t) path := filepath.Join(root, test.path) diff --git a/pkg/out/tree.go b/pkg/out/tree.go index 48c8443..8bbe097 100644 --- a/pkg/out/tree.go +++ b/pkg/out/tree.go @@ -20,7 +20,7 @@ func NewTreePrinter() *TreePrinter { // Print generates a tree view of repos and their statuses. func (p *TreePrinter) Print(root string, repos []Printable) string { if len(repos) == 0 { - return fmt.Sprintf("There are no git repos under %s", root) + return "There are no git repos under " + root } tree := buildTree(root, repos) From 3b85a64c2d38370133b58e21341ea1b120db108e Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 18:27:41 +0200 Subject: [PATCH 28/36] Disable mnd linter --- .golangci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.golangci.yml b/.golangci.yml index ec4a45a..56737a3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -18,6 +18,7 @@ linters: - forbidigo # Not suitable for CLI apps where printing to stdout is fine - gochecknoglobals # It's too strict and doesn't distinguish between "bad" globals (mutable shared state) and "good" globals (immutable configuration) - godox # TODO: enable it and handle all the remaning TODOs + - mnd # Impractical. We deal with numbers like file permissions here, it's much clearer to see them explicitly. - paralleltest # Tests are fast already and paralellizing them adds complexity - testpackage # TODO: renable it and refactor tests into separate packages - unparam # Impractical, it flags functions that are designed to be general-purpose, but happen to only be used with specific values currently From ca8681f9972961c218df60fd2f4c01cee48d56b0 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 18:28:09 +0200 Subject: [PATCH 29/36] Fix issues found by nlreturn linter --- pkg/git/finder.go | 1 + pkg/git/repo.go | 2 ++ pkg/git/status.go | 1 + pkg/git/test/testrepos.go | 1 + pkg/out/flat.go | 1 + pkg/out/tree.go | 1 + pkg/run/run.go | 1 + 7 files changed, 8 insertions(+) diff --git a/pkg/git/finder.go b/pkg/git/finder.go index 87d4251..cfef9da 100644 --- a/pkg/git/finder.go +++ b/pkg/git/finder.go @@ -85,6 +85,7 @@ func (f *RepoFinder) Find() error { gitPath := filepath.Join(path, dotgit) if _, err := os.Stat(gitPath); err == nil { f.addIfOk(path) + return fs.SkipDir // Skip this directory's contents since it's a repo } diff --git a/pkg/git/repo.go b/pkg/git/repo.go index 201d31d..8095481 100644 --- a/pkg/git/repo.go +++ b/pkg/git/repo.go @@ -57,6 +57,7 @@ func Clone(opts *CloneOpts) (*Repo, error) { if err != nil { cleanupFailedClone(opts.Path) + return nil, err } @@ -68,6 +69,7 @@ func Clone(opts *CloneOpts) (*Repo, error) { // Fetch preforms a git fetch on all remotes. func (r *Repo) Fetch() error { err := run.Git("fetch", "--all").OnRepo(r.path).AndShutUp() + return err } diff --git a/pkg/git/status.go b/pkg/git/status.go index 18b2ac3..7c644d1 100644 --- a/pkg/git/status.go +++ b/pkg/git/status.go @@ -65,6 +65,7 @@ func (r *Repo) loadBranches() (map[string]string, []error) { branches, err := r.Branches() if err != nil { errors = append(errors, err) + return statuses, errors } diff --git a/pkg/git/test/testrepos.go b/pkg/git/test/testrepos.go index c25434c..6dd0ce1 100644 --- a/pkg/git/test/testrepos.go +++ b/pkg/git/test/testrepos.go @@ -20,6 +20,7 @@ func (r *Repo) Path() string { // RepoEmpty creates an empty git repo. func RepoEmpty(t *testing.T) *Repo { t.Helper() + return RepoEmptyInDir(t, "") } diff --git a/pkg/out/flat.go b/pkg/out/flat.go index 0edcf3d..be9d1a4 100644 --- a/pkg/out/flat.go +++ b/pkg/out/flat.go @@ -23,6 +23,7 @@ func (p *FlatPrinter) Print(repos []Printable) string { if len(repo.Errors()) > 0 { str.WriteString(" " + red("error") + "\n") + continue } diff --git a/pkg/out/tree.go b/pkg/out/tree.go index 8bbe097..b046a68 100644 --- a/pkg/out/tree.go +++ b/pkg/out/tree.go @@ -216,6 +216,7 @@ func (n *Node) isYoungest() bool { for i, sis := range sisters { if sis.val == n.val { myIndex = i + break } } diff --git a/pkg/run/run.go b/pkg/run/run.go index 4c104c8..a84fdcd 100644 --- a/pkg/run/run.go +++ b/pkg/run/run.go @@ -133,5 +133,6 @@ func (e GitError) Error() string { func lines(output []byte) []string { lines := strings.TrimSuffix(string(output), "\n") + return strings.Split(lines, "\n") } From 046a84723000213154d32dc7851a790fa0adafdf Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 18:39:34 +0200 Subject: [PATCH 30/36] Disable noinlineerr linter and gci formatter --- .golangci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.golangci.yml b/.golangci.yml index 56737a3..b401034 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -5,7 +5,6 @@ run: formatters: enable: - - gci - gofmt - goimports @@ -19,6 +18,7 @@ linters: - gochecknoglobals # It's too strict and doesn't distinguish between "bad" globals (mutable shared state) and "good" globals (immutable configuration) - godox # TODO: enable it and handle all the remaning TODOs - mnd # Impractical. We deal with numbers like file permissions here, it's much clearer to see them explicitly. + - noinlineerr # Impractical. Inline error handling is a common and idiomatic practice - paralleltest # Tests are fast already and paralellizing them adds complexity - testpackage # TODO: renable it and refactor tests into separate packages - unparam # Impractical, it flags functions that are designed to be general-purpose, but happen to only be used with specific values currently From 16e4814460a3b284a7bc47befa8f5445d4dac881 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 18:41:52 +0200 Subject: [PATCH 31/36] Re-enable linting in CI actions --- .github/workflows/ci.yml | 10 ++++------ .github/workflows/release.yml | 8 +++----- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aeb42c6..2f69cfd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,13 +69,11 @@ jobs: with: go-version: '1.24' cache: true - - - name: Run golangci-lint - uses: golangci/golangci-lint-action@v6 - # TODO: Fix linting errors - continue-on-error: true + + - name: Run lints + uses: golangci/golangci-lint-action@v8 with: - version: latest + version: v2.4.0 args: --timeout=5m security: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a7edeb7..950395c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,11 +41,9 @@ jobs: run: go test -race ./... - name: Run lints - uses: golangci/golangci-lint-action@v6 - # TODO: Fix linting errors - continue-on-error: true + uses: golangci/golangci-lint-action@v8 with: - version: latest + version: v2.4.0 args: --timeout=5m - name: Validate GoReleaser config @@ -74,7 +72,7 @@ jobs: - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: - version: latest + version: '~> v2' args: release --clean env: GITHUB_TOKEN: ${{ secrets.GORELEASER_TOKEN }} From 42df73c75e327bfde3b15422a61c0762fe7c3996 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 20:00:49 +0200 Subject: [PATCH 32/36] Set git user and email in tests helper instead of CI jobs. Simplify CI --- .github/workflows/ci.yml | 56 +++++----------------------------------- pkg/git/test/helpers.go | 16 +++++++++++- 2 files changed, 21 insertions(+), 51 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f69cfd..31ebdb6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,23 +38,13 @@ jobs: - name: Verify dependencies run: go mod verify + + - name: Build binary + run: go build -v -o bin/git-get ./cmd/ + + - name: Run tests + run: go test -race ./... - - name: Set up Git (for tests) - run: | - git config --global user.email "test@example.com" - git config --global user.name "CI Test" - - - name: Run tests with coverage - run: go test -race -coverprofile coverage.out -covermode=atomic ./... - - - name: Upload coverage to Codecov - if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.24' - uses: codecov/codecov-action@v5 - with: - file: ./coverage.out - flags: unittests - name: codecov-umbrella - fail_ci_if_error: false lint: name: Lint @@ -104,37 +94,3 @@ jobs: with: sarif_file: 'trivy-results.sarif' - build: - name: Build - needs: [test, lint, security] - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.24' - cache: true - - - name: Build binary - run: | - go build -v -o bin/git-get ./cmd/ - - - name: Test binary and symlink behavior - run: | - ./bin/git-get --version - # Test symlink functionality - ln -sf git-get bin/git-list - ./bin/git-list --version - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: binary - path: bin/ - retention-days: 30 diff --git a/pkg/git/test/helpers.go b/pkg/git/test/helpers.go index e71077a..fdac6ac 100644 --- a/pkg/git/test/helpers.go +++ b/pkg/git/test/helpers.go @@ -31,6 +31,17 @@ func TempDir(t *testing.T, parent string) string { func (r *Repo) init() { err := run.Git("init", "--quiet", "--initial-branch=main", r.path).AndShutUp() checkFatal(r.t, err) + + r.setupGitConfig() +} + +// setupGitConfig sets up local git config for test repository only. +func (r *Repo) setupGitConfig() { + err := run.Git("config", "user.name", "Test User").OnRepo(r.path).AndShutUp() + checkFatal(r.t, err) + + err = run.Git("config", "user.email", "test@example.com").OnRepo(r.path).AndShutUp() + checkFatal(r.t, err) } // writeFile writes the content string into a file. If file doesn't exists, it will create it. @@ -50,7 +61,7 @@ func (r *Repo) stageFile(path string) { } func (r *Repo) commit(msg string) { - err := run.Git("commit", "-m", fmt.Sprintf("%q", msg), "--author=\"user \"").OnRepo(r.path).AndShutUp() + err := run.Git("commit", "-m", fmt.Sprintf("%q", msg)).OnRepo(r.path).AndShutUp() checkFatal(r.t, err) } @@ -81,6 +92,9 @@ func (r *Repo) clone() *Repo { t: r.t, } + // Set up git config in the cloned repository + clone.setupGitConfig() + return clone } From 1b8c05dd92e228c51b14c333becd3aecedc8a1f5 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 20:40:12 +0200 Subject: [PATCH 33/36] Clean up Github workflows --- .github/dependabot.yml | 39 ----------------------------- .github/workflows/ci.yml | 29 +--------------------- .github/workflows/codeql.yml | 46 ----------------------------------- .github/workflows/release.yml | 43 ++------------------------------ 4 files changed, 3 insertions(+), 154 deletions(-) delete mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/codeql.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index a40d8a3..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,39 +0,0 @@ -version: 2 -updates: - # Go modules - - package-ecosystem: "gomod" - directory: "/" - schedule: - interval: "weekly" - day: "monday" - time: "09:00" - open-pull-requests-limit: 5 - reviewers: - - "grdl" - assignees: - - "grdl" - commit-message: - prefix: "deps" - include: "scope" - labels: - - "dependencies" - - "go" - - # GitHub Actions - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - day: "monday" - time: "09:00" - open-pull-requests-limit: 3 - reviewers: - - "grdl" - assignees: - - "grdl" - commit-message: - prefix: "ci" - include: "scope" - labels: - - "dependencies" - - "github-actions" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31ebdb6..92b0097 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: [master, main] pull_request: branches: [master, main] + workflow_call: permissions: contents: read @@ -66,31 +67,3 @@ jobs: version: v2.4.0 args: --timeout=5m - security: - name: Security - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v5 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.24' - cache: true - - - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@master - with: - scan-type: 'fs' - scan-ref: '.' - format: 'sarif' - output: 'trivy-results.sarif' - - - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v3 - if: always() - with: - sarif_file: 'trivy-results.sarif' - diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index d7de2dd..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: "CodeQL Security Analysis" - -on: - push: - branches: [master, main] - pull_request: - branches: [master, main] - schedule: - - cron: '30 2 * * 1' # Run weekly on Mondays at 2:30 AM UTC - -permissions: - actions: read - contents: read - security-events: write - -jobs: - analyze: - name: Analyze (${{ matrix.language }}) - runs-on: ubuntu-latest - timeout-minutes: 360 - - strategy: - fail-fast: false - matrix: - include: - - language: go - build-mode: autobuild - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 2 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - # Enable additional security-and-quality query pack - queries: +security-and-quality - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 - with: - category: "/language:${{matrix.language}}" \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 950395c..92afb3a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,47 +10,8 @@ permissions: security-events: write jobs: - validate: - name: Validate Release - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.24' - cache: true - - - name: Download dependencies - run: go mod download - - - name: Verify dependencies - run: go mod verify - - - name: Set up Git (for tests) - run: | - git config --global user.email "test@example.com" - git config --global user.name "CI Test" - - - name: Run tests - run: go test -race ./... - - - name: Run lints - uses: golangci/golangci-lint-action@v8 - with: - version: v2.4.0 - args: --timeout=5m - - - name: Validate GoReleaser config - uses: goreleaser/goreleaser-action@v6 - with: - version: latest - args: check + build-and-test: + uses: ./.github/workflows/ci.yml release: name: GoReleaser From f9ff6e0dbde98adf168d275ee3ea109b5d256d53 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 20:56:07 +0200 Subject: [PATCH 34/36] Re-enable windows tests --- .github/workflows/ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92b0097..c84ed6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,9 +17,7 @@ jobs: strategy: matrix: go-version: ['1.24'] - os: [ubuntu-latest, macos-latest] - # TODO: fix tests on windows - # os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} steps: From 5922fd8e3c1d350c22b7aab3895fa610d7bc8d2d Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 21:10:13 +0200 Subject: [PATCH 35/36] Fix incorrect tests in repo_test --- pkg/git/repo_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/git/repo_test.go b/pkg/git/repo_test.go index 9ee8510..12c61d6 100644 --- a/pkg/git/repo_test.go +++ b/pkg/git/repo_test.go @@ -73,12 +73,12 @@ func TestUntracked(t *testing.T) { { name: "single untracked", repoMaker: test.RepoWithUntracked, - want: 0, + want: 1, }, { name: "single tracked ", repoMaker: test.RepoWithStaged, - want: 1, + want: 0, }, { name: "committed", @@ -96,7 +96,7 @@ func TestUntracked(t *testing.T) { t.Run(test.name, func(t *testing.T) { r, _ := Open(test.repoMaker(t).Path()) - got, err := r.Uncommitted() + got, err := r.Untracked() if err != nil { t.Errorf("got error %q", err) } From ea178b2ccdb8830981647e61b7438be4f4a65ee7 Mon Sep 17 00:00:00 2001 From: Grzegorz Dlugoszewski Date: Sun, 24 Aug 2025 22:14:22 +0200 Subject: [PATCH 36/36] Add sync mechanism to git operations to avoid race conditions when testing on Windows --- pkg/git/repo_test.go | 5 ----- pkg/git/test/helpers.go | 27 +++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/pkg/git/repo_test.go b/pkg/git/repo_test.go index 12c61d6..1d806a1 100644 --- a/pkg/git/repo_test.go +++ b/pkg/git/repo_test.go @@ -114,11 +114,6 @@ func TestCurrentBranch(t *testing.T) { repoMaker func(*testing.T) *test.Repo want string }{ - { - name: "empty repo without commits", - repoMaker: test.RepoEmpty, - want: "main", - }, { name: "only main branch", repoMaker: test.RepoWithCommit, diff --git a/pkg/git/test/helpers.go b/pkg/git/test/helpers.go index fdac6ac..582666c 100644 --- a/pkg/git/test/helpers.go +++ b/pkg/git/test/helpers.go @@ -28,11 +28,21 @@ func TempDir(t *testing.T, parent string) string { return dir } +// syncGitIndex forces git to refresh its index and ensures file system operations are flushed. +// This helps to prevent race-condition issues when running tests on Windows. +func (r *Repo) syncGitIndex() { + // Force git to refresh its index - this makes git re-scan the working directory + _ = run.Git("update-index", "--refresh").OnRepo(r.path).AndShutUp() + // Run status to ensure git has processed any pending changes + _ = run.Git("status", "--porcelain").OnRepo(r.path).AndShutUp() +} + func (r *Repo) init() { err := run.Git("init", "--quiet", "--initial-branch=main", r.path).AndShutUp() checkFatal(r.t, err) r.setupGitConfig() + r.syncGitIndex() } // setupGitConfig sets up local git config for test repository only. @@ -53,31 +63,46 @@ func (r *Repo) writeFile(filename string, content string) { _, err = file.WriteString(content) checkFatal(r.t, err) + + // Ensure data is written to disk before closing + err = file.Sync() + checkFatal(r.t, err) + + err = file.Close() + checkFatal(r.t, err) + + // Force git to recognize the file changes + r.syncGitIndex() } func (r *Repo) stageFile(path string) { err := run.Git("add", path).OnRepo(r.path).AndShutUp() checkFatal(r.t, err) + r.syncGitIndex() } func (r *Repo) commit(msg string) { err := run.Git("commit", "-m", fmt.Sprintf("%q", msg)).OnRepo(r.path).AndShutUp() checkFatal(r.t, err) + r.syncGitIndex() } func (r *Repo) branch(name string) { err := run.Git("branch", name).OnRepo(r.path).AndShutUp() checkFatal(r.t, err) + r.syncGitIndex() } func (r *Repo) tag(name string) { err := run.Git("tag", "-a", name, "-m", name).OnRepo(r.path).AndShutUp() checkFatal(r.t, err) + r.syncGitIndex() } func (r *Repo) checkout(name string) { err := run.Git("checkout", name).OnRepo(r.path).AndShutUp() checkFatal(r.t, err) + r.syncGitIndex() } func (r *Repo) clone() *Repo { @@ -94,6 +119,7 @@ func (r *Repo) clone() *Repo { // Set up git config in the cloned repository clone.setupGitConfig() + clone.syncGitIndex() return clone } @@ -101,6 +127,7 @@ func (r *Repo) clone() *Repo { func (r *Repo) fetch() { err := run.Git("fetch", "--all").OnRepo(r.path).AndShutUp() checkFatal(r.t, err) + r.syncGitIndex() } func checkFatal(t *testing.T, err error) {