Archived
Preview uncommitted changes in the detail pane
For a dirty submodule, list the changed files so the user can remember
what they were doing there:
Changes 5 files · 2M 1A 1D 1R · +37 −12
──────────────────────────────────
M render.go src/render/ +20 −4 3d ago
? scratch.txt 5m ago
- Status.Changes: files changed against HEAD (staged and unstaged
combined) with kind, line counts and modification time; sorted by kind,
then by size; capped at 200 with totals over all files.
- Git provider: parse `status --porcelain=v2 -z` and one
`diff HEAD --numstat -z -M`, only for dirty submodules.
- Detail pane: flat list with a colored status letter per file, like IDE
source-control views; counts and age in compact columns; up to 10 files,
every kind keeps a line, the rest is summarized as "n more".
- Fixture gains libs/work with every kind of change; mock data extended.
- Docs: F24, change preview in the UX guide, D17.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
64fe34b5b0
commit
19c6f17eb2
@@ -15,16 +15,16 @@ internal/submodule/ domain model; NO UI imports
|
||||
provider.go Provider interface
|
||||
internal/submodule/git/ read-only provider using the git CLI
|
||||
exec.go git runner (environment, errors)
|
||||
parse.go pure parsers for git output
|
||||
parse.go pure parsers for git output (status, numstat, …)
|
||||
provider.go Tree, Status, pin → ref resolution
|
||||
internal/submodule/mock/ fixture provider with artificial latency
|
||||
internal/tui/ Bubble Tea application
|
||||
app.go root model: layout, message routing, loading
|
||||
keys.go key bindings (also feed the help view)
|
||||
msgs.go treeLoadedMsg, statusLoadedMsg
|
||||
theme/ colors, glyphs, issue words, level styles
|
||||
theme/ colors, glyphs, issue words, change letters, level styles
|
||||
tree/ tree component: rows, navigation, mouse, view
|
||||
detail/ detail pane
|
||||
detail/ detail pane; changes.go: change preview
|
||||
header/ header line
|
||||
footer/ help and flash messages
|
||||
```
|
||||
|
||||
@@ -128,3 +128,21 @@ processes.
|
||||
**Decision.** The git provider limits concurrent `Status` calls to
|
||||
`runtime.NumCPU()` with a semaphore that respects `ctx`.
|
||||
**Consequences.** The UI stays unchanged; rows fill in steadily.
|
||||
|
||||
## D17 — Change preview: flat letter list against HEAD
|
||||
|
||||
**Context.** `changed` doesn't tell users what they changed or why; they
|
||||
want to remember what they were doing in a submodule.
|
||||
**Decision.** `Status.Changes` lists the changed files against HEAD (staged
|
||||
and unstaged combined) with line counts. The detail pane shows them as a
|
||||
flat list with a colored status letter in front, like IDE source-control
|
||||
views, instead of group headers. Sorted by kind, then by size; the 10-line
|
||||
budget keeps one line for every kind present and summarizes the rest.
|
||||
Placed directly below the issue lines, because the pane is cut by height.
|
||||
One summary line for all files, then strictly one line per file with its
|
||||
own age: a second summary line ("+1 · edited 19m ago") read as if it
|
||||
belonged to the first file.
|
||||
Dirt inside nested submodules is ignored (D7); unstaged moves show as
|
||||
removed + untracked.
|
||||
**Consequences.** One extra `git diff` per dirty submodule, none for clean
|
||||
ones. Not included: staged/unstaged markers, diff bodies, stash count.
|
||||
|
||||
@@ -61,6 +61,13 @@ commit hashes. It doesn't answer the questions users actually have:
|
||||
- **F22** Lists the commits between pin and tip (SHA, subject, author, age)
|
||||
with a title stating ahead/behind/diverged.
|
||||
- **F23** Shows child count, issues below, and the remote URL.
|
||||
- **F24** For a submodule with uncommitted changes, previews the changed
|
||||
files so the user can remember what they were doing: a flat list with a
|
||||
colored letter per file (`M` modified, `A` added, `D` removed, `R`
|
||||
renamed, `?` untracked, `!` conflict), line counts (`+n −m`) per file and
|
||||
in total, and how long ago each file was edited. Only a handful of files are
|
||||
shown; the rest is summarized as "n more" with their kinds. Staged and
|
||||
unstaged changes are combined.
|
||||
|
||||
### Header and footer
|
||||
|
||||
|
||||
+27
-1
@@ -32,10 +32,29 @@ in several at once.
|
||||
| `Pin` | `Ref`, `Source`, `Ahead`, `Behind`, `Pushed` |
|
||||
| `Pinned`, `CheckedOut`, `Tip` | Commits (SHA, subject, author, time) |
|
||||
| `Between` | Commits that make up the pin ↔ tip difference |
|
||||
| `Changes` | Changed files of the worktree (nil if clean), see below |
|
||||
|
||||
`Status == nil` means "still loading". `Submodule.Err` is set if loading
|
||||
failed.
|
||||
|
||||
## Changes
|
||||
|
||||
`Status.Changes` describes *what* is behind `Worktree`: one `FileChange` per
|
||||
path with a `ChangeKind` (conflicted, modified, added, removed, renamed,
|
||||
untracked), added/removed line counts, and flags for binary files and moved
|
||||
nested submodule pointers. It compares the worktree **with HEAD**, so staged
|
||||
and unstaged changes are combined (a file added to the index and edited
|
||||
afterwards is one added file).
|
||||
|
||||
- `Files` is sorted by kind (that order), then by changed lines (largest
|
||||
first), then by path, and capped at `MaxChangeFiles` (200). `Total`,
|
||||
`Added` and `Removed` cover all files.
|
||||
- `FileChange.Modified` is the file's modification time (zero for removed
|
||||
files).
|
||||
- `Worktree` is derived from the same list: conflict, else any tracked
|
||||
change → modified, else untracked.
|
||||
- It is a preview, not an issue: it adds no issue kinds and changes no levels.
|
||||
|
||||
## Issues: summarizing the dimensions
|
||||
|
||||
The UI never shows raw dimensions in the tree. `Status.Issues()` summarizes
|
||||
@@ -107,7 +126,9 @@ are local; `<pin>` is the index gitlink.
|
||||
| `Pinned`, `PinStaged` | parent: `ls-files -s -- <path>` vs `ls-tree HEAD -- <path>` |
|
||||
| `Presence` | directory missing → `DirMissing`; no `.git` in it → `NotInitialized` |
|
||||
| `Checkout` | `rev-parse HEAD`, `symbolic-ref -q HEAD` (no branch → detached) |
|
||||
| `Worktree` | `status --porcelain=v2 --ignore-submodules=dirty` |
|
||||
| `Worktree`, `Changes` kinds | `status --porcelain=v2 -z --untracked-files=normal --ignore-submodules=dirty` |
|
||||
| `Changes` line counts | `diff HEAD --numstat -z -M --ignore-submodules=dirty`, only if a tracked file changed; failure leaves the counts at 0 |
|
||||
| `FileChange.Modified` | `stat` of each changed file that still exists |
|
||||
| `Pushed` | `for-each-ref --contains <pin> refs/remotes` is not empty |
|
||||
| `Pin.Ref` | rules above; the remote is `origin`, else the first remote |
|
||||
| `Ahead`, `Behind` | `rev-list --left-right --count <pin>...<ref>` |
|
||||
@@ -123,6 +144,11 @@ Details:
|
||||
- Rule 6 shows the short SHA; there is no tip and no distance.
|
||||
- `--ignore-submodules=dirty`: changes inside a nested submodule do not make
|
||||
its parent `changed` (D7), but a moved nested pin does.
|
||||
- Renames are detected only for staged moves (`git mv`); an unstaged move
|
||||
is a removed file plus an untracked file. Untracked directories are one
|
||||
entry (`dir/`) without line counts.
|
||||
- `--untracked-files=normal` overrides `status.showUntrackedFiles`, so the
|
||||
result doesn't depend on the user's config.
|
||||
- A missing submodule only gets a `Pinned` SHA; nested submodules of an
|
||||
uninitialized submodule are unknown and not shown.
|
||||
- If the pinned commit doesn't exist in the submodule (not fetched), loading
|
||||
|
||||
+50
-4
@@ -61,12 +61,58 @@ indent ▸ name [n] status ref delta
|
||||
2. One line per issue: word (colored, 10 columns) + explanation;
|
||||
`in sync nothing to do` if there are none; `staged` and
|
||||
`detached HEAD on pin` as information
|
||||
3. `Ref` with distance and `via <rule>`
|
||||
4. `Pinned`, `Checkout`, `Tip`: SHA + subject; the checkout SHA is yellow if it
|
||||
3. `Changes` (only with uncommitted changes), see
|
||||
[Change preview](#change-preview)
|
||||
4. `Ref` with distance and `via <rule>`
|
||||
5. `Pinned`, `Checkout`, `Tip`: SHA + subject; the checkout SHA is yellow if it
|
||||
differs from the pin
|
||||
5. "Pin is n commits behind/ahead of …" / "Pin and … have diverged": commit list
|
||||
6. `Children` (count and issues below), `URL`
|
||||
6. "Pin is n commits behind/ahead of …" / "Pin and … have diverged": commit list
|
||||
7. `Children` (count and issues below), `URL`
|
||||
|
||||
#### Change preview
|
||||
|
||||
```
|
||||
Changes 5 files · 2M 1A 1D 1R · +37 −12
|
||||
──────────────────────────────────────
|
||||
M render.go src/render/ +20 −4 3d ago
|
||||
M README.md +3 −1 20m ago
|
||||
A notes.md docs/ +14 2h ago
|
||||
D old.go src/ −8
|
||||
R new.go ← old.go 1d ago
|
||||
? scratch.txt 5m ago
|
||||
… 3 more (2M 1?)
|
||||
```
|
||||
|
||||
- A flat list, no group headers: IDEs (VS Code, JetBrains) do the same, and
|
||||
headers would use up the few lines the preview has. The letter is the
|
||||
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.
|
||||
- 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
|
||||
rest is filled in sort order. `… n more (<kinds>)` summarizes the hidden
|
||||
files. With 11 files all are shown: "1 more" would take the same line as
|
||||
the file itself.
|
||||
- The `Changes` line summarizes all files: count, files per kind, line
|
||||
totals. Everything below it describes one file per line, so no line can
|
||||
be mistaken for a file's detail. In a narrow pane the line totals are
|
||||
dropped rather than cut.
|
||||
- A muted rule (`─`, `-` in ASCII mode) separates the summary from the
|
||||
files. It starts in the value column and is as wide as the summary or
|
||||
the file table, whichever is wider.
|
||||
- The age (muted) is the file's modification time, so the most recently
|
||||
touched files stand out. Removed files have no age.
|
||||
- The list starts in the value column (under `5 files`), like every other
|
||||
value in the pane.
|
||||
- File name bold, directory muted. Counts form a right-aligned column
|
||||
directly after the widest path (max 48 columns), not at the pane's edge,
|
||||
so they stay next to their file in wide panes. If space is short the age
|
||||
column goes first, then the directory is cut from the left
|
||||
(`…ernal/client/`), the name last. For
|
||||
renames the muted part is `← old/path`.
|
||||
- Untracked directories are shown as one entry (`tmp/`).
|
||||
### Header
|
||||
|
||||
`path branch ↑↓ · n submodules · <count> <issue>… · n pin staged · n loading · fetched <age>`
|
||||
|
||||
Reference in New Issue
Block a user