Archived
Update a pin that is behind with u; switch fast-forwards
`u` moves the pin of the selection to the tip of its ref when the pin is only behind it: a switch to that branch with exactly the tip the row shows, nothing fetched; if the ref moved meanwhile, nothing happens. On the superproject it fast-forwards the branch to its upstream and restores the submodules (git pull --ff-only from the last fetch). Switch now fast-forwards a local branch that is only behind the chosen remote branch, unless another worktree has it checked out, and keeps a checked-out branch that tracks it under another name. A branch with commits of its own is still never moved. Stage pin is dropped: switching to the branch already stages it. The detail pane's palette wraps instead of cutting off keys. Add D29 and F59. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+5
-4
@@ -14,7 +14,7 @@ bare commit hash.
|
||||
| [state-model.md](state-model.md) | How a submodule's state is determined, summarized into issues, and how pins are resolved to refs |
|
||||
| [ux-guide.md](ux-guide.md) | Layout, attention hierarchy, colors, wording, navigation and key bindings |
|
||||
| [architecture.md](architecture.md) | Package layout, the provider boundary, async loading, testing |
|
||||
| [actions.md](actions.md) | the write actions restore and switch branch (done) and stage pin (draft) |
|
||||
| [actions.md](actions.md) | the write actions restore, switch branch and update pin |
|
||||
| [decisions.md](decisions.md) | Decision log with rationale (ADR style) |
|
||||
|
||||
## Install
|
||||
@@ -33,10 +33,11 @@ Installs `submarine` and the alias `sub` (linux/macOS, amd64/arm64) to
|
||||
through a git provider (`internal/submodule/git`): `submarine [dir]`. It
|
||||
fetches only when asked (`f`) and follows changes made in other shells. `submarine --mock` shows
|
||||
the built-in mock data (`internal/submodule/mock`). `!` shows only
|
||||
submodules with problems. `x` restores the selection and `b` switches its
|
||||
branch ([actions.md](actions.md)); stage pin is drafted there.
|
||||
submodules with problems. `x` restores the selection, `b` switches its
|
||||
branch and `u` updates a pin that is behind ([actions.md](actions.md)).
|
||||
|
||||
Next steps, roughly in order:
|
||||
|
||||
1. Stage pin, see [actions.md](actions.md). (Restore and switch branch are done.)
|
||||
1. (done) Write actions: restore, switch branch, update pin, see
|
||||
[actions.md](actions.md). Stage pin was dropped (D29).
|
||||
2. (done) "Problems only" (`!`) view, reusing the tree's filter.
|
||||
|
||||
+77
-58
@@ -1,8 +1,9 @@
|
||||
# Actions (draft)
|
||||
|
||||
**Status:** [Restore](#restore) (key `x`, [D23](decisions.md)) and
|
||||
[Switch branch](#switch-branch) (key `b`, [D24](decisions.md)) are implemented. [Stage pin](#stage-pin) is still a draft; its open
|
||||
questions are [at the end](#open-questions).
|
||||
**Status:** [Restore](#restore) (key `x`, [D23](decisions.md)),
|
||||
[Switch branch](#switch-branch) (key `b`, [D24](decisions.md)) and
|
||||
[Update pin](#update-pin) (key `u`, [D29](decisions.md)) are implemented.
|
||||
Stage pin was dropped ([D29](decisions.md)).
|
||||
|
||||
## Principle
|
||||
|
||||
@@ -13,12 +14,12 @@ a difference in one of two directions:
|
||||
| Action | Direction | Key |
|
||||
|---|---|---|
|
||||
| [Restore](#restore) | The worktree follows the recorded state | `x` |
|
||||
| [Stage pin](#stage-pin) | The recorded state follows the worktree | `s` |
|
||||
| [Switch branch](#switch-branch) | Both: the branch tip is recorded, then restored to | `b` |
|
||||
| [Update pin](#update-pin) | Both: a switch to the tip of the pin's ref | `u` |
|
||||
|
||||
There is nothing else for now. Committing, pushing and bumping a pin to the
|
||||
tip are done outside submarine. A bump is "check out the tip in the
|
||||
submodule, then `s`" (or switch to the branch with `b`).
|
||||
There is nothing else for now. Committing and pushing are done outside
|
||||
submarine. Recording a commit made in a submodule is a switch to its branch
|
||||
(`b`); a commit on no branch must get one first.
|
||||
|
||||
## Restore
|
||||
|
||||
@@ -258,9 +259,14 @@ For a **submodule**, with the tip `T` the list showed:
|
||||
2. The submodule, and everything below it, is [restored](#restore) to `T`.
|
||||
3. HEAD is put on the branch (the branch name is the local one):
|
||||
- a local branch: checked out;
|
||||
- a remote branch `origin/feat`: the local branch `feat` is created,
|
||||
tracking it; if `feat` exists at `T` it is checked out; if it exists
|
||||
**elsewhere** it is not moved and HEAD stays detached at `T`.
|
||||
- a remote branch `origin/feat`: the local branch is the one checked out
|
||||
before if it tracks `origin/feat` (e.g. `dev`), else `feat`. It is
|
||||
created, tracking it, if it does not exist; if it exists at `T` it is
|
||||
checked out; if it is only
|
||||
**behind** `T` (an ancestor of it) it is fast-forwarded to `T` and
|
||||
checked out ([D29](decisions.md)); if it has commits of its own, or is
|
||||
checked out in another worktree, it is **not moved** and HEAD stays
|
||||
detached at `T`.
|
||||
|
||||
For the **superproject** step 1 is left out: the branch is checked out via the
|
||||
restore, so its submodules follow the pins of the new commit (staged pins of
|
||||
@@ -279,7 +285,7 @@ only if something would be lost. A submodule that is not initialized can't be
|
||||
switched (`src/app is not initialized; restore it first`). Network: only what
|
||||
is missing below, like restore. Lines above the footer: `checking …`,
|
||||
`switching src/app to origin/feat…`; flash `switched src/app to feat` (or
|
||||
`… to origin/feat (detached: local feat is elsewhere)`); failure
|
||||
`… to origin/feat (detached: local feat can't be fast-forwarded)`); failure
|
||||
`switch of src/app to origin/feat failed: …`, until the next key. One
|
||||
restore or switch runs at a time.
|
||||
|
||||
@@ -287,72 +293,80 @@ restore or switch runs at a time.
|
||||
|
||||
`git.Provider.Switch` (`internal/submodule/git/switch.go`): verify the ref is
|
||||
still `T`; `update-index --cacheinfo` in the parent; `restoreRepo` (see
|
||||
restore); then `switch` / `switch -c <local> --track <remote-branch>`. The restore leaves HEAD of the
|
||||
restore); then `switch` / `switch -c <local> --track <remote-branch>`; a local branch
|
||||
behind `T` is first moved with `merge-base --is-ancestor <local> T`, a look
|
||||
at `worktree list --porcelain` and `update-ref refs/heads/<local> T <old>`
|
||||
(which fails if the branch moved meanwhile). The restore leaves HEAD of the
|
||||
switched repository alone (Switch puts it on the chosen branch); the
|
||||
submodules below it are attached as in a restore.
|
||||
`SwitchLoss` is `lossOf` with `T` as the target. `Branches` is one
|
||||
`for-each-ref`, without the `origin/HEAD` symref.
|
||||
|
||||
## Stage pin
|
||||
## Update pin
|
||||
|
||||
Records the commit checked out in a submodule as its new pin in the
|
||||
parent's index. It is the same as `git add <path>` in the parent, but only
|
||||
when it is safe.
|
||||
Moves the pin of a submodule to the tip of its ref, the branch the row shows
|
||||
(`origin/main ↓2`). It is a [switch](#switch-branch) to that branch, with the
|
||||
tip the row shows: nothing is fetched, so `u` gives exactly the commits the
|
||||
row counts. `f` is what brings newer ones.
|
||||
|
||||
On the **superproject** the pin is HEAD and the ref is the upstream of its
|
||||
branch (the header's `main ↓1`): `u` fast-forwards the branch to it, like
|
||||
`git pull --ff-only`, and restores the submodules to the pins of the new
|
||||
commit.
|
||||
|
||||
### Available when
|
||||
|
||||
All of:
|
||||
|
||||
- the selected node is a submodule, not the superproject;
|
||||
- it is `off pin` (`Checkout == CheckoutDiffers`): HEAD differs from the
|
||||
pin in the parent's index;
|
||||
- its worktree is **clean** (`Worktree == WorktreeClean`). Uncommitted
|
||||
changes, untracked files and conflicts count as dirty, the same rule as
|
||||
the `changed` issue. A moved nested pin counts as a change too, so nested
|
||||
submodules are staged bottom-up: first inside the submodule, then the
|
||||
submodule itself (after a commit in it, outside submarine).
|
||||
|
||||
Changes *inside* nested submodules don't count. Like the `changed` issue,
|
||||
staging only looks at the submodule's own worktree.
|
||||
- it is initialized and its state is loaded;
|
||||
- its ref is a remote branch (not a tag, a SHA or a local branch) and has a
|
||||
tip; for the superproject: its branch has an upstream;
|
||||
- the pin is **behind** the ref and not ahead (`Behind > 0`, `Ahead == 0`).
|
||||
A diverged pin (`↑n↓m`) is left to `b`: moving it would drop the pin's
|
||||
own commits from the record.
|
||||
|
||||
### Effect
|
||||
|
||||
- The parent's index records the submodule's HEAD as the gitlink of its
|
||||
path. Nothing else changes: no worktree, no other index entry, no commit.
|
||||
- The submodule then shows `staged` (or `●` next to an issue), and the
|
||||
`off pin` issue is gone.
|
||||
- If the checkout equals the pin in the parent's `HEAD`, staging removes
|
||||
the staged pin instead (the index equals `HEAD` again).
|
||||
- If the checked-out commit is not pushed, it is staged anyway. The row
|
||||
then shows `unpushed` (Danger), which is the warning.
|
||||
- There is no confirmation: nothing is lost.
|
||||
- Undo: restore the submodule and choose `c` (committed pin), see
|
||||
[Target](#target).
|
||||
The same as [switching](#switch-branch) to the ref: the tip is staged
|
||||
as the pin, the submodule and everything below it are restored to it, and
|
||||
HEAD is put on the local branch. A local branch that is only behind is
|
||||
fast-forwarded, so the common case (local `main` at the old pin) ends on
|
||||
`main`. A checked-out branch that tracks the ref under another name (`dev`
|
||||
tracking `origin/main`) is the one used, so the update stays on it. For the
|
||||
superproject staged pins are lost, as in a switch (and counted).
|
||||
|
||||
If the ref no longer points at the tip the row showed (a fetch meanwhile),
|
||||
nothing happens: `can't update src/app: origin/main moved; check the new
|
||||
state`. The watcher has updated the row by then; `u` again takes the new
|
||||
tip.
|
||||
|
||||
### Confirmation, lines, errors
|
||||
|
||||
As for switch: the question appears only if something would be lost
|
||||
(`Update src/app to origin/main? Discards … y update · n cancel`). Lines
|
||||
above the footer: `checking src/app…`, `updating src/app to origin/main…`;
|
||||
flash `updated src/app to main`; failure `update of src/app to origin/main
|
||||
failed: …`.
|
||||
|
||||
### Implementation note
|
||||
|
||||
```sh
|
||||
# in the submodule
|
||||
git rev-parse HEAD # must still be the SHA shown in the UI, else abort
|
||||
# in the parent
|
||||
git update-index --cacheinfo 160000,<sha>,<path>
|
||||
```
|
||||
|
||||
`update-index` stages exactly the SHA that was checked, independent of the
|
||||
worktree. Flash: `staged pin of src/app` on success; git's message on
|
||||
failure.
|
||||
The UI only: `startPinUpdate` (`internal/tui/pinupdate.go`) reads the
|
||||
branches (`Switcher.Branches`), takes the one named like `Pin.Ref`, checks
|
||||
that its tip still starts with the abbreviated `Status.Tip.SHA`, and then
|
||||
runs the switch flow with it (`SwitchLoss`, `Switch`).
|
||||
|
||||
## Keys and palette
|
||||
|
||||
Both are submodule keys (`keyMap.Submodule`), shown in the detail pane's
|
||||
palette, or in the footer when the pane can't show it. Restore is always
|
||||
offered: it is idempotent, and the palette is static.
|
||||
All three are submodule keys (`keyMap.Submodule`), shown in the detail
|
||||
pane's palette, or in the footer when the pane can't show it. They are
|
||||
always offered, the palette is static: restore is idempotent, and `u` says
|
||||
why it can't update.
|
||||
|
||||
| Key | Action | Pressed when not possible (flash) |
|
||||
|---|---|---|
|
||||
| `x` restore | Restore the selection | `this data source can't restore` (mock); `wait for the restore of … to finish` |
|
||||
| `b` switch branch | [Switch branch](#switch-branch) | `this data source can't switch branches`; `wait for the restore of … to finish`; `src/app is not initialized; restore it first` |
|
||||
| `s` stage pin | [Stage pin](#stage-pin), not implemented | `src/app is on its pin` / `src/app has uncommitted changes; commit or restore them first` / `the superproject has no pin` |
|
||||
| `u` update pin | [Update pin](#update-pin) | `this data source can't update pins`; `wait for the restore of … to finish`; `the superproject has no upstream branch`; `src/app is still loading`; `src/app is not initialized; restore it first`; `the pin of src/app is not on a branch`; `the pin of src/app has diverged from origin/main; b switches the branch`; `the pin of src/app is ahead of origin/main`; `src/app is up to date with origin/main` (the superproject without `the pin of`); after the check `can't update src/app: main is a local branch; b switches to it` |
|
||||
|
||||
`R` would sit next to `r` (reload), a dangerous mix-up for a destructive
|
||||
action, hence `x`.
|
||||
@@ -376,12 +390,19 @@ action, hence `x`.
|
||||
- UI (`internal/tui/restore_test.go`): restores at once without a loss, asks
|
||||
with one (`y` restores, anything else cancels), the mock can't restore.
|
||||
- Switch: provider (`switch_test.go`): list without `origin/HEAD`, local
|
||||
first; a remote pick stages the tip and creates a tracking branch; an
|
||||
existing local branch elsewhere is not moved; a moved ref aborts; the
|
||||
first; a remote pick stages the tip and creates a tracking branch; a
|
||||
local branch that is only behind is fast-forwarded; one with commits of
|
||||
its own, or checked out in another worktree, is not moved; a moved ref aborts; the
|
||||
superproject switch ends exact; the loss is the restore's. Picker
|
||||
(`branches_test.go`): filter, selection, scrolling. UI (`switch_test.go`):
|
||||
opens, filters, enter switches, asks with a loss, esc closes, the mock
|
||||
can't.
|
||||
- Update pin: UI (`pinupdate_test.go`): switches to the tip of the ref, asks
|
||||
with a loss, aborts if the ref moved, refuses (with its flash) a pin that
|
||||
is up to date, ahead, diverged, on a tag, on a local branch or not
|
||||
initialized; updates the superproject, not one without an upstream.
|
||||
Provider: the superproject's branch is fast-forwarded; a checked-out
|
||||
branch that tracks the remote branch under another name is kept.
|
||||
- By hand in tmux on a fixture, see
|
||||
[architecture.md](architecture.md#testing).
|
||||
|
||||
@@ -392,7 +413,5 @@ submodule is restored to the index gitlink, so a staged pin is its target and
|
||||
survives; restore may clone and fetch what is missing; the key is `x`; it
|
||||
asks only if something would be lost.
|
||||
|
||||
Open (stage pin):
|
||||
|
||||
1. **Key.** `s`?
|
||||
2. **Palette.** Show it always (like restore), or only while it applies?
|
||||
Open (update pin): updating several submodules at once (on the superproject
|
||||
or a collapsed parent), deliberately left out for now.
|
||||
|
||||
@@ -20,7 +20,7 @@ internal/submodule/git/ provider using the git CLI (reads, fetch, watch)
|
||||
fetch.go Fetch (network), Fetchable
|
||||
watch.go Watch: polls git metadata for changes
|
||||
restore.go Restore, RestoreLoss (network: only what is missing)
|
||||
switch.go Branches, SwitchLoss, Switch (stage the tip, restore, attach the branch)
|
||||
switch.go Branches, SwitchLoss, Switch (stage the tip, restore, attach or fast-forward the branch)
|
||||
gitdir.go resolves .git dirs and gitdir: files
|
||||
internal/submodule/mock/ fixture provider with artificial latency
|
||||
internal/update/ self-update from the package registry; NO UI imports
|
||||
@@ -28,6 +28,7 @@ internal/tui/ Bubble Tea application
|
||||
app.go root model: layout, message routing, loading
|
||||
keys.go key bindings (also feed the help view)
|
||||
restore.go, switch.go restore / switch flows; a switch reuses restoreState
|
||||
pinupdate.go update pin (u): a switch to the pin's ref, at the tip the row shows
|
||||
overlay.go draws the picker over the finished view at the position the picker reports
|
||||
msgs.go tree/status/fetch/change messages
|
||||
refresh.go reloader: one load per key, coalesced reruns
|
||||
|
||||
+38
-1
@@ -362,7 +362,7 @@ Enter on a branch of a submodule stages its tip as the pin in the parent's
|
||||
index and restores the submodule to it (D23, recursion included); then HEAD
|
||||
is put on the branch. On the superproject it checks the branch out and
|
||||
restores below, without staging.
|
||||
- *Key `b`*, not `s`, which stays reserved for stage pin.
|
||||
- *Key `b`*, not `s`, which stays reserved for stage pin (dropped, D29).
|
||||
- *Remote branch.* A local branch of that name is created, tracking it. If
|
||||
one exists at another commit it is **not moved**: HEAD stays detached at
|
||||
the tip and the flash says so (restore never moves branches, D23).
|
||||
@@ -530,3 +530,40 @@ the pin, the ref is resolved (as for `Status`) and one `switch` runs. No
|
||||
network. `detached HEAD on pin` is now left only where no branch is at the
|
||||
pin. Switch keeps its own rule for the repository it switches; the
|
||||
submodules below it follow this one.
|
||||
|
||||
## D29 — Update pin instead of stage pin; switch fast-forwards a branch behind
|
||||
|
||||
**Context.** Stage pin (record the checkout as the pin) was drafted, but a
|
||||
commit made in a submodule is on a branch, and switching to that branch
|
||||
(`b`) already stages it. The common job is a different one: a pin that is
|
||||
behind its branch (`origin/main ↓2`) should move to the tip. With `b` that
|
||||
is a search in the popup, and it often ended detached, because the local
|
||||
`main` sat at the old pin and was never moved (D24).
|
||||
**Decision.** `u` updates the pin of a submodule to the tip of its ref, only
|
||||
when the pin is behind it and not ahead. It is a switch to that branch (D24)
|
||||
with the tip the row shows: nothing is fetched, and if the ref moved
|
||||
meanwhile nothing happens. Switch (and so `u`) now fast-forwards a local
|
||||
branch that is an ancestor of the tip, unless a worktree has it checked out;
|
||||
`update-ref` with the old tip makes the move fail if the branch moved. Stage
|
||||
pin is dropped.
|
||||
- *No fetch.* The user sees `↓2` and gets those two commits; a fetch first
|
||||
would update to something the screen never showed. `f` is separate.
|
||||
- *Only behind.* A diverged pin has commits the ref lacks; moving it would
|
||||
drop them from the record, so that is a deliberate `b`.
|
||||
- *The superproject too.* Its pin is HEAD and its ref the upstream, so `u`
|
||||
there is `git pull --ff-only` plus a restore of the submodules. Switch
|
||||
keeps a checked-out branch that tracks the chosen remote branch under
|
||||
another name (`dev` tracking `origin/main`), so an update never changes
|
||||
the branch.
|
||||
- *One at a time.* Updating several at once is left for later.
|
||||
- *Fast-forward only.* A local branch with commits of its own is never
|
||||
moved, as before. Restore (D23, D28) still never moves a branch: it goes
|
||||
back to the recorded pin, where moving branches has no place.
|
||||
**Alternatives.** Keeping stage pin: it covers only a commit on no branch,
|
||||
which then is `unpushed` anyway. `u` fetching first (a real pull): rejected,
|
||||
see above. Resetting a local branch with commits of its own: loses them from
|
||||
the branch.
|
||||
**Consequences.** Amends D24 ("not moved") and D28's note that switch keeps
|
||||
its own rule. No new provider interface: `u` is `Branches`, `SwitchLoss` and
|
||||
`Switch`. Since `Status.Tip.SHA` is abbreviated, the UI compares it as a
|
||||
prefix of the branch's full SHA.
|
||||
|
||||
@@ -156,7 +156,13 @@ commit hashes. It doesn't answer the questions users actually have:
|
||||
with everything below; HEAD ends on the branch. It asks for confirmation
|
||||
only if something would be lost. See [actions.md](actions.md#switch-branch).
|
||||
|
||||
Stage pin is drafted in [actions.md](actions.md#stage-pin).
|
||||
- **F59** `u` updates the pin of the selection to the tip of its ref, when
|
||||
the pin is only behind it (`origin/main ↓2`): a switch (**F57**) to that
|
||||
branch, with exactly the tip the row shows. For the superproject that is
|
||||
a fast-forward of its branch to its upstream (`git pull --ff-only`). Nothing is fetched; if
|
||||
the ref moved meanwhile, nothing happens. A local branch that is only
|
||||
behind is fast-forwarded, in a switch too. Otherwise the footer says why
|
||||
not. See [actions.md](actions.md#update-pin).
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
+15
-3
@@ -46,9 +46,10 @@ Keys come in three groups (`keyMap.Navigation`, `.Submodule`, `.Global`):
|
||||
release exists, `q quit and update to vX` follows them as the right-most
|
||||
item, on a highlighted background (`Theme.Highlight`: black on Primary), and
|
||||
plain quit is `Q` (see D27).
|
||||
- **Submodule** (act on the selected one, e.g. `c`): a palette on the last
|
||||
line of the detail pane. It is left out if the pane is shorter than 12
|
||||
lines. Whenever the palette isn't shown (pane hidden or too short), these
|
||||
- **Submodule** (act on the selected one, e.g. `c`): a palette at the
|
||||
bottom of the detail pane, wrapped onto a second line if the pane is too
|
||||
narrow for all keys (none is cut off). It is left out if the pane is
|
||||
shorter than 12 lines. Whenever the palette isn't shown (pane hidden or too short), these
|
||||
keys move into the footer after the navigation keys.
|
||||
|
||||
If the footer is too narrow, keys are dropped from the left group's end
|
||||
@@ -393,6 +394,16 @@ than `Muted`, so the popup has the focus.
|
||||
[restore](#restore): `checking`, the question `Switch src/app to
|
||||
origin/feat? Discards … y switch · n cancel`, `switching …`.
|
||||
|
||||
### Update pin
|
||||
|
||||
`u` runs without a popup, on the line above the footer like a
|
||||
[switch](#switch-branch): `checking src/app…`, the question `Update src/app
|
||||
to origin/main? Discards … y update · n cancel` only if something would be
|
||||
lost, `updating src/app to origin/main…`, then the flash `updated src/app to
|
||||
main`. On the header the name is `the superproject` (`updated the
|
||||
superproject to main`). Where it can't update, a flash says why (see
|
||||
[actions.md](actions.md#keys-and-palette)).
|
||||
|
||||
### Key reference
|
||||
|
||||
Every key in the UI, where it is shown. Update this
|
||||
@@ -410,6 +421,7 @@ where the palette can't be shown, see [Key bar and palette](#key-bar-and-palette
|
||||
| `home` / `g`, `end` / `G` | Header / last submodule | Navigation | Help |
|
||||
| `pgup`, `pgdn` | Move by one screen | Navigation | Help |
|
||||
| `c` | Copy path | Submodule | Palette, Help |
|
||||
| `u` | Update pin to the tip of its ref (superproject: fast-forward to upstream) | Submodule | Palette, Help |
|
||||
| `b` | Switch branch (popup) | Submodule | Palette, Help |
|
||||
| `x` | Restore | Submodule | Palette, Help |
|
||||
| `f` | Fetch all repositories | Global | Help |
|
||||
|
||||
Reference in New Issue
Block a user