-
Notifications
You must be signed in to change notification settings - Fork 3
Add git status detection when navigating into git repos #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
e3167ea
Add git status detection when navigating into git repos
himattm 984b1fd
Fix git status detection: add timeouts, rename helpers, and harden tests
himattm f65bc81
Extract shared git helper package and add git plugin tests
himattm 7d5fdc5
Fix test flakiness and file mode consistency in git_test.go
himattm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| package git | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "os/exec" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/himattm/prism/internal/cache" | ||
| ) | ||
|
|
||
| const gitTimeout = 500 * time.Millisecond | ||
|
|
||
| // IsRepo checks if a directory is inside a git repository. | ||
| func IsRepo(ctx context.Context, dir string) bool { | ||
| ctx, cancel := context.WithTimeout(ctx, gitTimeout) | ||
| defer cancel() | ||
| cmd := exec.CommandContext(ctx, "git", "--no-optional-locks", "rev-parse", "--git-dir") | ||
| cmd.Dir = dir | ||
| return cmd.Run() == nil | ||
| } | ||
|
|
||
| // FindRoot runs git rev-parse --show-toplevel from the given directory | ||
| // to find the git root. Returns empty string if not in a git repo. | ||
| func FindRoot(ctx context.Context, dir string) string { | ||
| ctx, cancel := context.WithTimeout(ctx, gitTimeout) | ||
| defer cancel() | ||
| cmd := exec.CommandContext(ctx, "git", "--no-optional-locks", "rev-parse", "--show-toplevel") | ||
| cmd.Dir = dir | ||
| var out bytes.Buffer | ||
| cmd.Stdout = &out | ||
|
|
||
| if err := cmd.Run(); err != nil { | ||
| return "" | ||
| } | ||
|
|
||
| return strings.TrimSpace(out.String()) | ||
| } | ||
|
|
||
| // EffectiveDir returns the best directory for git operations. | ||
| // Tries projectDir first, falls back to finding git root from currentDir. | ||
| // Results are cached using the provided cache (if non-nil). | ||
| func EffectiveDir(ctx context.Context, projectDir, currentDir string, c *cache.Cache) string { | ||
| // Fast path: ProjectDir is set and is a git repo | ||
| if projectDir != "" { | ||
| if IsRepo(ctx, projectDir) { | ||
| return projectDir | ||
| } | ||
| } | ||
|
|
||
| // Fallback: find git root from CurrentDir | ||
| if currentDir == "" { | ||
| return "" | ||
| } | ||
|
|
||
| cacheKey := "git:effective:" + currentDir | ||
| if c != nil { | ||
| if cached, ok := c.Get(cacheKey); ok { | ||
| return cached | ||
| } | ||
| } | ||
|
|
||
| gitRoot := FindRoot(ctx, currentDir) | ||
|
|
||
| if c != nil { | ||
| c.Set(cacheKey, gitRoot, cache.GitTTL) | ||
| } | ||
|
|
||
| return gitRoot | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| package plugins | ||
|
|
||
| import ( | ||
| "context" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/himattm/prism/internal/cache" | ||
| "github.com/himattm/prism/internal/plugin" | ||
| ) | ||
|
|
||
| // setupGitRepo initializes a git repo in the given directory with a single commit. | ||
| func setupGitRepo(t *testing.T, dir string) { | ||
| t.Helper() | ||
|
|
||
| commands := [][]string{ | ||
| {"git", "init"}, | ||
| {"git", "config", "user.email", "test@test.com"}, | ||
| {"git", "config", "user.name", "Test"}, | ||
| {"git", "checkout", "-B", "main"}, | ||
| {"git", "commit", "--allow-empty", "-m", "initial"}, | ||
| } | ||
|
|
||
| for _, args := range commands { | ||
| cmd := exec.Command(args[0], args[1:]...) | ||
| cmd.Dir = dir | ||
| out, err := cmd.CombinedOutput() | ||
| if err != nil { | ||
| t.Fatalf("setup command %v failed: %v\n%s", args, err, out) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestGitPlugin_Execute_NonGitProjectDir_GitCurrentDir(t *testing.T) { | ||
| // Parent directory is NOT a git repo | ||
| parentDir := t.TempDir() | ||
|
|
||
| // Create a subdirectory that IS a git repo | ||
| gitRepoDir := filepath.Join(parentDir, "repo") | ||
| if err := os.Mkdir(gitRepoDir, 0755); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| setupGitRepo(t, gitRepoDir) | ||
|
|
||
| p := &GitPlugin{} | ||
| p.SetCache(cache.New()) | ||
|
|
||
| ctx := context.Background() | ||
| input := plugin.Input{ | ||
| Prism: plugin.PrismContext{ | ||
| ProjectDir: parentDir, | ||
| CurrentDir: gitRepoDir, | ||
| }, | ||
| Colors: map[string]string{ | ||
| "yellow": "", | ||
| "reset": "", | ||
| }, | ||
| } | ||
|
|
||
| result, err := p.Execute(ctx, input) | ||
| if err != nil { | ||
| t.Fatalf("unexpected error: %v", err) | ||
| } | ||
|
|
||
| // Should fall back to CurrentDir and find the branch | ||
| if result == "" { | ||
| t.Fatal("expected non-empty result when CurrentDir is a git repo, got empty") | ||
| } | ||
|
|
||
| if !strings.Contains(result, "main") && !strings.Contains(result, "master") { | ||
| t.Errorf("expected result to contain branch name (main or master), got %q", result) | ||
| } | ||
| } | ||
|
|
||
| func TestGitPlugin_Execute_NonGitProjectDir_NonGitCurrentDir(t *testing.T) { | ||
| // Neither directory is a git repo | ||
| tempDir := t.TempDir() | ||
|
|
||
| p := &GitPlugin{} | ||
| p.SetCache(cache.New()) | ||
|
|
||
| ctx := context.Background() | ||
| input := plugin.Input{ | ||
| Prism: plugin.PrismContext{ | ||
| ProjectDir: tempDir, | ||
| CurrentDir: tempDir, | ||
| }, | ||
| Colors: map[string]string{ | ||
| "yellow": "", | ||
| "reset": "", | ||
| }, | ||
| } | ||
|
|
||
| result, err := p.Execute(ctx, input) | ||
| if err != nil { | ||
| t.Fatalf("unexpected error: %v", err) | ||
| } | ||
|
|
||
| if result != "" { | ||
| t.Errorf("expected empty result when neither dir is a git repo, got %q", result) | ||
| } | ||
| } | ||
|
|
||
| func TestGitPlugin_Execute_GitProjectDir(t *testing.T) { | ||
| // ProjectDir itself is a git repo | ||
| gitRepoDir := t.TempDir() | ||
| setupGitRepo(t, gitRepoDir) | ||
|
|
||
| p := &GitPlugin{} | ||
| p.SetCache(cache.New()) | ||
|
|
||
| ctx := context.Background() | ||
| input := plugin.Input{ | ||
| Prism: plugin.PrismContext{ | ||
| ProjectDir: gitRepoDir, | ||
| CurrentDir: gitRepoDir, | ||
| }, | ||
| Colors: map[string]string{ | ||
| "yellow": "", | ||
| "reset": "", | ||
| }, | ||
| } | ||
|
|
||
| result, err := p.Execute(ctx, input) | ||
| if err != nil { | ||
| t.Fatalf("unexpected error: %v", err) | ||
| } | ||
|
|
||
| if result == "" { | ||
| t.Fatal("expected non-empty result when ProjectDir is a git repo, got empty") | ||
| } | ||
|
|
||
| if !strings.Contains(result, "main") && !strings.Contains(result, "master") { | ||
| t.Errorf("expected result to contain branch name (main or master), got %q", result) | ||
| } | ||
| } | ||
|
|
||
| func TestGitPlugin_Execute_SubdirOfGitRepo(t *testing.T) { | ||
| // Parent is NOT a git repo, but CurrentDir is a subdirectory of one | ||
| parentDir := t.TempDir() | ||
|
|
||
| gitRepoDir := filepath.Join(parentDir, "repo") | ||
| if err := os.Mkdir(gitRepoDir, 0755); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| setupGitRepo(t, gitRepoDir) | ||
|
|
||
| subDir := filepath.Join(gitRepoDir, "subdir") | ||
| if err := os.Mkdir(subDir, 0755); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
|
|
||
| p := &GitPlugin{} | ||
| p.SetCache(cache.New()) | ||
|
|
||
| ctx := context.Background() | ||
| input := plugin.Input{ | ||
| Prism: plugin.PrismContext{ | ||
| ProjectDir: parentDir, | ||
| CurrentDir: subDir, | ||
| }, | ||
| Colors: map[string]string{ | ||
| "yellow": "", | ||
| "reset": "", | ||
| }, | ||
| } | ||
|
|
||
| result, err := p.Execute(ctx, input) | ||
| if err != nil { | ||
| t.Fatalf("unexpected error: %v", err) | ||
| } | ||
|
|
||
| // Should find the git repo via rev-parse from the subdirectory | ||
| if result == "" { | ||
| t.Fatal("expected non-empty result when CurrentDir is inside a git repo, got empty") | ||
| } | ||
|
|
||
| if !strings.Contains(result, "main") && !strings.Contains(result, "master") { | ||
| t.Errorf("expected result to contain branch name (main or master), got %q", result) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getEffectiveGitDirusescontext.Background()which means these git probes can take up to the full internal timeout(s) independent of the statusline render timeout, andgetGitDiffStatsstill runs git without any context/timeout. Consider threading a single timeout context throughgetEffectiveGitDir(and switchinggetGitDiffStatstoexec.CommandContext) so a slow/hung git call can't stall statusline rendering.