From af6d03a5b1cab8b726927c2b9f58ed440644a0ea Mon Sep 17 00:00:00 2001 From: Simon Franken Date: Wed, 21 Oct 2026 20:12:13 +0200 Subject: [PATCH] Mark untracked directories that are git repositories Co-Authored-By: Claude Sonnet 5.5 --- docs/state-model.md | 2 +- docs/ux-guide.md | 4 +++- internal/submodule/git/provider.go | 3 +++ internal/submodule/model.go | 1 + internal/submodule/status.go | 17 ++++++++++++++++- internal/submodule/status_test.go | 19 +++++++++++++++++++ internal/tui/detail/changes.go | 2 ++ 7 files changed, 45 insertions(+), 3 deletions(-) diff --git a/docs/state-model.md b/docs/state-model.md index 47d62fa..95e9290 100644 --- a/docs/state-model.md +++ b/docs/state-model.md @@ -72,7 +72,7 @@ truth for what counts as a problem. | `conflict` | Merge conflict in the worktree | Danger | | `missing` | Not initialized; directory missing | Warn | | `off pin` | Checkout differs from pin | Warn | -| `changed` | Uncommitted changes; untracked files | Warn | +| `changed` | Uncommitted changes; untracked files (worded "untracked repository" if all of them are git repositories) | Warn | Rules: diff --git a/docs/ux-guide.md b/docs/ux-guide.md index b5e193f..4064e4f 100644 --- a/docs/ux-guide.md +++ b/docs/ux-guide.md @@ -91,7 +91,9 @@ Changes 5 files · 2M 1A 1D 1R · +37 −12 meaning, the color only supports it (N4). - Letters and colors: `!` red bold, `M` yellow, `A` green, `D` red, `R` cyan, `?` muted. Line counts: green `+n`, red `−n`; `bin` for binary - files, `submodule` for a moved nested pin, nothing for untracked files. + files, `submodule` for a moved nested pin, `repository` for an untracked + directory that is a git repository itself (cloned, never added as a + submodule), nothing for other untracked files. - Order: kind (`!`, `M`, `A`, `D`, `R`, `?`), then changed lines, largest first, because the biggest edits are the ones people remember. - Budget: 10 files. Every kind that occurs gets at least one line, the diff --git a/internal/submodule/git/provider.go b/internal/submodule/git/provider.go index 22bd70d..e9c41d9 100644 --- a/internal/submodule/git/provider.go +++ b/internal/submodule/git/provider.go @@ -387,6 +387,9 @@ func changes(ctx context.Context, dir string, entries []entry) (*submodule.Chang c.Added += n.added c.Removed += n.removed } + if e.kind == submodule.ChangeUntracked && strings.HasSuffix(e.path, "/") { + f.Repo = initialized(filepath.Join(dir, filepath.FromSlash(e.path))) + } if e.kind != submodule.ChangeRemoved { if fi, err := os.Stat(filepath.Join(dir, filepath.FromSlash(e.path))); err == nil { f.Modified = fi.ModTime() diff --git a/internal/submodule/model.go b/internal/submodule/model.go index b658c90..ed3b455 100644 --- a/internal/submodule/model.go +++ b/internal/submodule/model.go @@ -132,6 +132,7 @@ type FileChange struct { Removed int // removed lines Binary bool // line counts are unknown Submodule bool // a nested submodule pointer changed + Repo bool // an untracked directory that is a git repository itself Modified time.Time // modification time of the file; zero if it doesn't exist } diff --git a/internal/submodule/status.go b/internal/submodule/status.go index 810edc3..f0b41d0 100644 --- a/internal/submodule/status.go +++ b/internal/submodule/status.go @@ -55,7 +55,7 @@ func (s *Status) Issues() []Issue { case WorktreeModified: out = append(out, Issue{IssueChanged, LevelWarn, "uncommitted changes"}) case WorktreeUntracked: - out = append(out, Issue{IssueChanged, LevelWarn, "untracked files"}) + out = append(out, Issue{IssueChanged, LevelWarn, s.untrackedText()}) } return out } @@ -79,3 +79,18 @@ func Walk(mods []*Submodule, fn func(*Submodule)) { Walk(m.Children, fn) } } + +// untrackedText words the untracked-files issue: "untracked repository" if +// everything untracked is a git repository of its own, e.g. a clone that was +// never added as a submodule. +func (s *Status) untrackedText() string { + if s.Changes == nil || s.Changes.Total != len(s.Changes.Files) { + return "untracked files" + } + for _, f := range s.Changes.Files { + if f.Kind != ChangeUntracked || !f.Repo { + return "untracked files" + } + } + return "untracked repository" +} diff --git a/internal/submodule/status_test.go b/internal/submodule/status_test.go index 3fdcfcf..4b38123 100644 --- a/internal/submodule/status_test.go +++ b/internal/submodule/status_test.go @@ -50,3 +50,22 @@ func TestHiddenIssues(t *testing.T) { t.Errorf("HiddenIssues() = %d, %v; want 2, LevelDanger", count, worst) } } + +func TestUntrackedRepositoryWording(t *testing.T) { + repo := FileChange{Path: "lib/x/", Kind: ChangeUntracked, Repo: true} + file := FileChange{Path: "a.txt", Kind: ChangeUntracked} + for _, c := range []struct { + files []FileChange + want string + }{ + {[]FileChange{repo}, "untracked repository"}, + {[]FileChange{repo, file}, "untracked files"}, + {[]FileChange{file}, "untracked files"}, + } { + s := &Status{Worktree: WorktreeUntracked, Changes: &Changes{Total: len(c.files), Files: c.files}} + got := s.Issues() + if len(got) == 0 || got[len(got)-1].Detail != c.want { + t.Errorf("%v: got %v, want %q", c.files, got, c.want) + } + } +} diff --git a/internal/tui/detail/changes.go b/internal/tui/detail/changes.go index d74a12a..cadc55d 100644 --- a/internal/tui/detail/changes.go +++ b/internal/tui/detail/changes.go @@ -163,6 +163,8 @@ func (m Model) fileLines(files []submodule.FileChange) []string { switch { case f.Submodule: r.counts = muted.Render("submodule") + case f.Repo: + r.counts = muted.Render("repository") case f.Binary: r.counts = muted.Render("bin") default: