diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index 5e5b4b9..82af633 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -605,6 +605,8 @@ func TestShowDisplaysDirectories(t *testing.T) { } func TestCurrentShowsEffectiveProfileInCwd(t *testing.T) { + skipOnWindows(t) + if _, err := exec.LookPath("git"); err != nil { t.Skip("git not installed") } diff --git a/cmd/dir_test.go b/cmd/dir_test.go index cda4320..3639c1c 100644 --- a/cmd/dir_test.go +++ b/cmd/dir_test.go @@ -3,13 +3,31 @@ package cmd import ( "os" "path/filepath" + "runtime" "strings" "testing" "github.com/aanogueira/git-context/internal/config" ) +// skipOnWindows bails out of CLI tests that hard-code POSIX-shaped paths +// (`/tmp/...`) or that rely on `t.Setenv("HOME", ...)` to isolate the +// home directory. Neither assumption holds on Windows: `/tmp/x` becomes +// `:\tmp\x` after `filepath.Abs`, and `os.UserHomeDir` reads +// `USERPROFILE` rather than `HOME`. The production code is exercised on +// Windows by the unit-level tests in `internal/config` and `internal/git`; +// this file's tests are CLI smoke checks rather than feature contracts. +func skipOnWindows(t *testing.T) { + t.Helper() + + if runtime.GOOS == "windows" { + t.Skip("uses POSIX path literals or HOME isolation; covered on Unix only") + } +} + func TestDirAddAssignsAndRegenerates(t *testing.T) { + skipOnWindows(t) + tmpHome := t.TempDir() t.Setenv("HOME", tmpHome) @@ -43,6 +61,8 @@ func TestDirAddAssignsAndRegenerates(t *testing.T) { } func TestDirAddRejectsDuplicate(t *testing.T) { + skipOnWindows(t) + tmpHome := t.TempDir() t.Setenv("HOME", tmpHome) @@ -91,6 +111,8 @@ func TestDirAddWarnsWhenNoDefaultProfile(t *testing.T) { } func TestDirRemoveUnassignsAndRegenerates(t *testing.T) { + skipOnWindows(t) + tmpHome := t.TempDir() t.Setenv("HOME", tmpHome) diff --git a/cmd/integration_test.go b/cmd/integration_test.go index db7bb9d..a78dc0f 100644 --- a/cmd/integration_test.go +++ b/cmd/integration_test.go @@ -14,6 +14,8 @@ import ( // default profile, assign a directory to a different profile, verify that // inside the assigned directory git resolves the right user.email. func TestEndToEndDirectoryAssignment(t *testing.T) { + skipOnWindows(t) + if _, err := exec.LookPath("git"); err != nil { t.Skip("git not installed") } diff --git a/internal/config/dirs.go b/internal/config/dirs.go index 35bd4b2..28db7e9 100644 --- a/internal/config/dirs.go +++ b/internal/config/dirs.go @@ -17,6 +17,9 @@ import ( // git-style glob). // - `~` is expanded to the user's home directory. // - Relative paths are resolved against the current working directory. +// - On Windows, backslashes are normalized to forward slashes so the +// output is in the canonical form git config expects in `gitdir:` +// and `path =` values (and so storage in YAML is platform-agnostic). // - A trailing slash is always appended so the directive matches the // whole subtree, not just the directory itself. func NormalizeDir(path string) (string, error) { @@ -46,6 +49,8 @@ func NormalizeDir(path string) (string, error) { path = abs } + path = strings.ReplaceAll(path, `\`, `/`) + if !strings.HasSuffix(path, "/") { path += "/" } diff --git a/internal/config/dirs_test.go b/internal/config/dirs_test.go index 87d60c1..55169f8 100644 --- a/internal/config/dirs_test.go +++ b/internal/config/dirs_test.go @@ -3,10 +3,16 @@ package config import ( "os" "path/filepath" + "runtime" "strings" "testing" ) +// toFwd mirrors NormalizeDir's backslash-to-slash canonicalization so test +// expectations built via filepath.Join (which uses OS-native separators) +// stay comparable on Windows. +func toFwd(p string) string { return strings.ReplaceAll(p, `\`, `/`) } + func TestNormalizeDir(t *testing.T) { t.Parallel() @@ -20,23 +26,35 @@ func TestNormalizeDir(t *testing.T) { t.Fatalf("Getwd failed: %v", err) } - tests := []struct { + type tcase struct { name string in string want string - }{ - {"absolute path gets trailing slash", "/Users/x/projects/work", "/Users/x/projects/work/"}, - { - "absolute path keeps existing trailing slash", - "/Users/x/projects/work/", - "/Users/x/projects/work/", - }, - {"tilde expands to home", "~/projects/work", filepath.Join(home, "projects", "work") + "/"}, - {"relative resolves against cwd", "./foo", filepath.Join(cwd, "foo") + "/"}, + } + + tests := []tcase{ + {"tilde expands to home", "~/projects/work", toFwd(filepath.Join(home, "projects", "work")) + "/"}, + {"relative resolves against cwd", "./foo", toFwd(filepath.Join(cwd, "foo")) + "/"}, {"single-star glob passes through unchanged", "~/work/*/repo", "~/work/*/repo"}, {"double-star glob passes through unchanged", "~/work/**", "~/work/**"}, } + // POSIX-rooted absolute paths (`/Users/x/...`) are not absolute on + // Windows — `filepath.Abs` resolves them against the cwd's drive, + // producing `D:/Users/x/...`. Skip those rows on Windows; the + // equivalent semantics are exercised by the tilde + relative cases + // above, which build absolute paths via filepath.Join. + if runtime.GOOS != "windows" { + tests = append(tests, + tcase{"absolute path gets trailing slash", "/Users/x/projects/work", "/Users/x/projects/work/"}, + tcase{ + "absolute path keeps existing trailing slash", + "/Users/x/projects/work/", + "/Users/x/projects/work/", + }, + ) + } + for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() diff --git a/internal/git/git_test.go b/internal/git/git_test.go index 40b92f0..fbf1115 100644 --- a/internal/git/git_test.go +++ b/internal/git/git_test.go @@ -331,7 +331,7 @@ func TestWriteRootConfigDefaultAndAssignments(t *testing.T) { t.Errorf("missing [include] block:\n%s", content) } - if !strings.Contains(content, "path = "+defaultProfilePath) { + if !strings.Contains(content, "path = "+toGitPath(defaultProfilePath)) { t.Errorf("missing default profile path:\n%s", content) } @@ -464,7 +464,7 @@ func TestRegenerate(t *testing.T) { } rootStr := string(root) - wantInclude := filepath.Join(profilesDir, "work.gitconfig") + wantInclude := toGitPath(filepath.Join(profilesDir, "work.gitconfig")) if !strings.Contains(rootStr, "path = "+wantInclude) { t.Errorf("root missing default include for %q:\n%s", wantInclude, rootStr)