package git import ( "context" "errors" "os" "os/exec" "path/filepath" "strings" "testing" "time" ) func TestDiscoverRepositoryRoot(t *testing.T) { repo := newRemoteFixture(t) child := filepath.Join(repo.work, "child") if err := os.Mkdir(child, 0755); err != nil { t.Fatal(err) } alias := filepath.Join(t.TempDir(), "alias") if err := os.Symlink(repo.work, alias); err != nil { t.Fatal(err) } worktree := filepath.Join(t.TempDir(), "worktree") gitRun(t, repo.work, "worktree", "add", "--detach", worktree) for _, tc := range []struct{ dir, want string }{{repo.work, repo.work}, {child, repo.work}, {alias, repo.work}, {worktree, worktree}} { root, found, err := DiscoverRepositoryRoot(context.Background(), tc.dir) want, e := filepath.EvalSymlinks(tc.want) if e != nil { t.Fatal(e) } if err != nil || !found || root != want { t.Fatalf("Discover(%s) = %q, %t, %v; want %q", tc.dir, root, found, err, want) } } standalone := t.TempDir() if root, found, err := DiscoverRepositoryRoot(context.Background(), standalone); err != nil || found || root != "" { t.Fatalf("standalone = %q, %t, %v", root, found, err) } if _, err := RepositoryRoot(context.Background(), standalone); err == nil { t.Fatal("strict lookup accepted standalone") } if _, found, err := DiscoverRepositoryRoot(context.Background(), repo.bare); err == nil || found { t.Fatalf("bare repository: found %t, error %v", found, err) } broken := t.TempDir() if err := os.WriteFile(filepath.Join(broken, ".git"), []byte("gitdir: missing\n"), 0600); err != nil { t.Fatal(err) } if _, found, err := DiscoverRepositoryRoot(context.Background(), broken); err == nil || found { t.Fatalf("broken marker: found %t, error %v", found, err) } if _, found, err := DiscoverRepositoryRoot(context.Background(), filepath.Join(standalone, "missing")); err == nil || found { t.Fatalf("missing directory: found %t, error %v", found, err) } } func TestNoRepositoryBoundary(t *testing.T) { for _, tc := range []struct { message string want bool }{ {"fatal: not a git repository (or any of the parent directories): .git\n", true}, {"fatal: not a git repository (or any parent up to mount point /some path)\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).\n", true}, {"fatal: not a git repository: '/bad'", false}, {"fatal: detected dubious ownership in repository", false}, } { if _, got := noRepositoryBoundary(tc.message); got != tc.want { t.Errorf("diagnostic %q = %t", tc.message, got) } } } func TestDiscoverRepositoryRootHonorsDiscoveryBoundaries(t *testing.T) { repo := newRemoteFixture(t) child := filepath.Join(repo.work, "child") if err := os.Mkdir(child, 0o755); err != nil { t.Fatal(err) } alias := filepath.Join(t.TempDir(), "alias") if err := os.Symlink(repo.work, alias); err != nil { t.Fatal(err) } if err := os.Remove(filepath.Join(repo.work, ".git", "HEAD")); err != nil { t.Fatal(err) } for _, tc := range []struct { name, directory, ceiling string standalone bool }{ {"damaged ancestor", child, "", false}, {"excluded ancestor", child, repo.work, true}, {"physical ceiling alias", child, alias, true}, {"literal ceiling alias", child, string(os.PathListSeparator) + alias, false}, {"ceiling includes current directory", repo.work, repo.work, false}, } { t.Run(tc.name, func(t *testing.T) { t.Setenv("GIT_CEILING_DIRECTORIES", tc.ceiling) _, found, err := DiscoverRepositoryRoot(t.Context(), tc.directory) if tc.standalone { if err != nil || found { t.Fatalf("excluded checkout: found %t, error %v", found, err) } } else { var exitErr *exec.ExitError if found || !errors.As(err, &exitErr) || exitErr.ExitCode() != 128 { t.Fatalf("damaged checkout: found %t, error %v", found, err) } } }) } bin := t.TempDir() script := "#!/bin/sh\nprintf 'fatal: not a git repository (or any parent up to mount point %s)\\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).\\n' \"$DISCOVERY_BOUNDARY\" >&2\nexit 128\n" if err := os.WriteFile(filepath.Join(bin, "git"), []byte(script), 0o755); err != nil { t.Fatal(err) } t.Setenv("PATH", bin) t.Setenv("GIT_CEILING_DIRECTORIES", "") t.Setenv("DISCOVERY_BOUNDARY", repo.work) if _, found, err := DiscoverRepositoryRoot(t.Context(), child); err != nil || found { t.Fatalf("filesystem boundary: found %t, error %v", found, err) } } func TestDiscoverRepositoryRootFailures(t *testing.T) { dir := t.TempDir() bin := t.TempDir() script := "#!/bin/sh\nprintf '%s\\n' 'fatal: detected dubious ownership in repository' >&2\nexit 128\n" if err := os.WriteFile(filepath.Join(bin, "git"), []byte(script), 0755); err != nil { t.Fatal(err) } t.Setenv("PATH", bin) _, found, err := DiscoverRepositoryRoot(context.Background(), dir) var exitErr *exec.ExitError if found || !errors.As(err, &exitErr) || exitErr.ExitCode() != 128 || !strings.Contains(err.Error(), "dubious ownership") { t.Fatalf("found %t, error %v", found, err) } if err := os.WriteFile(filepath.Join(bin, "git"), []byte("#!/bin/sh\nprintf '%s\\n' 'fatal: permission denied' >&2\nexit 128\n"), 0o755); err != nil { t.Fatal(err) } _, found, err = DiscoverRepositoryRoot(context.Background(), dir) if found || !errors.As(err, &exitErr) || exitErr.ExitCode() != 128 || !strings.Contains(err.Error(), "permission denied") { t.Fatalf("found %t, error %v", found, err) } ctx, cancel := context.WithCancel(context.Background()) cancel() if _, _, err := DiscoverRepositoryRoot(ctx, dir); !errors.Is(err, context.Canceled) { t.Fatalf("cancellation = %v", err) } } func TestDiscoverRepositoryRootInterruptedGit(t *testing.T) { dir := t.TempDir() bin := t.TempDir() ready := filepath.Join(t.TempDir(), "ready") script := "#!/bin/sh\n[ \"$LC_ALL\" = C ] || exit 42\n: > \"$DISCOVERY_READY\"\nexec /bin/sleep 60\n" if err := os.WriteFile(filepath.Join(bin, "git"), []byte(script), 0o755); err != nil { t.Fatal(err) } t.Setenv("PATH", bin) t.Setenv("DISCOVERY_READY", ready) t.Setenv("LC_ALL", "different-locale") ctx, cancel := context.WithCancel(context.Background()) defer cancel() done := make(chan error, 1) go func() { _, _, err := DiscoverRepositoryRoot(ctx, dir); done <- err }() timer := time.NewTimer(5 * time.Second) defer timer.Stop() ticker := time.NewTicker(10 * time.Millisecond) defer ticker.Stop() for { if _, err := os.Stat(ready); err == nil { break } select { case err := <-done: t.Fatalf("Git ended before readiness: %v", err) case <-timer.C: cancel() err := <-done t.Fatalf("Git did not become ready: %v", err) case <-ticker.C: } } cancel() if err := <-done; !errors.Is(err, context.Canceled) { t.Fatalf("cancellation = %v", err) } if got := os.Getenv("LC_ALL"); got != "different-locale" { t.Fatalf("parent locale changed to %q", got) } } func TestDiscoverRepositoryRootWithoutGit(t *testing.T) { repo := newRemoteFixture(t) standalone := t.TempDir() t.Setenv("PATH", t.TempDir()) if _, found, err := DiscoverRepositoryRoot(context.Background(), standalone); found || err != nil { t.Fatalf("standalone: found %t, error %v", found, err) } if _, found, err := DiscoverRepositoryRoot(context.Background(), repo.work); found || !errors.Is(err, exec.ErrNotFound) { t.Fatalf("checkout: found %t, error %v", found, err) } broken := t.TempDir() if err := os.WriteFile(filepath.Join(broken, ".git"), []byte("broken"), 0600); err != nil { t.Fatal(err) } if _, found, err := DiscoverRepositoryRoot(context.Background(), broken); found || !errors.Is(err, exec.ErrNotFound) { t.Fatalf("broken marker: found %t, error %v", found, err) } t.Setenv("GIT_DIR", "invalid") if _, found, err := DiscoverRepositoryRoot(context.Background(), standalone); found || !errors.Is(err, exec.ErrNotFound) { t.Fatalf("explicit context: found %t, error %v", found, err) } }