Archived
Add switch branch (b): stage the tip and restore to it
b opens a centered popup with a search line and the local and remote branches of the selection. Enter stages the branch tip as the pin of a submodule, restores it (and everything below) to it, and puts HEAD on the branch; a remote branch gets a local tracking branch unless one exists elsewhere. Works on the superproject too. Losses are asked like restore. The backdrop behind the popup is dimmed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+4
-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) | **Draft:** the write actions restore and stage pin |
|
||||
| [actions.md](actions.md) | the write actions restore and switch branch (done) and stage pin (draft) |
|
||||
| [decisions.md](decisions.md) | Decision log with rationale (ADR style) |
|
||||
|
||||
## Install
|
||||
@@ -33,10 +33,10 @@ 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
|
||||
([actions.md](actions.md)); stage pin is drafted there.
|
||||
submodules with problems. `x` restores the selection and `b` switches its
|
||||
branch ([actions.md](actions.md)); stage pin is drafted there.
|
||||
|
||||
Next steps, roughly in order:
|
||||
|
||||
1. Stage pin, see [actions.md](actions.md). (Restore is done.)
|
||||
1. Stage pin, see [actions.md](actions.md). (Restore and switch branch are done.)
|
||||
2. (done) "Problems only" (`!`) view, reusing the tree's filter.
|
||||
|
||||
+80
-5
@@ -1,7 +1,7 @@
|
||||
# Actions (draft)
|
||||
|
||||
**Status:** [Restore](#restore) is implemented (key `x`, decisions in
|
||||
[D23](decisions.md)). [Stage pin](#stage-pin) is still a draft; its open
|
||||
**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).
|
||||
|
||||
## Principle
|
||||
@@ -14,10 +14,11 @@ a difference in one of two directions:
|
||||
|---|---|---|
|
||||
| [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` |
|
||||
|
||||
There is nothing else for now. Committing, pushing, switching branches and
|
||||
bumping a pin to the tip are done outside submarine. A bump is "check out
|
||||
the tip in the submodule, then `s`".
|
||||
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`).
|
||||
|
||||
## Restore
|
||||
|
||||
@@ -196,6 +197,72 @@ target. A directory in the untracked list is searched for `.git`
|
||||
*directories* (a standalone repository); an absorbed submodule leaves only a
|
||||
`.git` file and loses just its worktree.
|
||||
|
||||
## Switch branch
|
||||
|
||||
Moves the selected repository to another branch, in one step: the tip of the
|
||||
branch becomes the pin, and the repository is restored to it.
|
||||
|
||||
### The popup
|
||||
|
||||
`b` opens a popup in the middle of the screen: a search line on top, below it
|
||||
the branches of the selection, local ones first, then remote ones
|
||||
(`origin/…`), each group with the newest tip first. Each row shows the name
|
||||
(`*` marks the checked-out branch), the short SHA and the age of the tip.
|
||||
The list is read from the disk; fetch (`f`) is what brings new remote
|
||||
branches.
|
||||
|
||||
| Key | Effect |
|
||||
|---|---|
|
||||
| text | Filters: every word must be in the name, case-insensitive. The selection stays on its branch if it still matches |
|
||||
| `↑` `↓`, `pgup` `pgdn` | Select |
|
||||
| `enter` | Switch to the selected branch |
|
||||
| `esc` | Close |
|
||||
|
||||
Mouse and all other keys are ignored while the popup is open. A header
|
||||
selection switches the superproject.
|
||||
|
||||
### Effect
|
||||
|
||||
For a **submodule**, with the tip `T` the list showed:
|
||||
|
||||
1. The parent's index records `T` as the pin (a staged pin; if `T` is the
|
||||
committed pin, the index equals `HEAD` again).
|
||||
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`.
|
||||
|
||||
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
|
||||
the old state are lost, and counted).
|
||||
|
||||
Staging comes first, so a failed restore can be repeated with `x`. If the
|
||||
branch moved since the list was read (a fetch), the switch aborts with
|
||||
`origin/feat moved; open the list again`; the staged SHA is always the one
|
||||
shown.
|
||||
|
||||
### Confirmation, network, errors
|
||||
|
||||
As for restore: the loss is read from the disk and the question
|
||||
(`Switch src/app to origin/feat? Discards … y switch · n cancel`) appears
|
||||
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
|
||||
`switch of src/app to origin/feat failed: …`, until the next key. One
|
||||
restore or switch runs at a time.
|
||||
|
||||
### Implementation note
|
||||
|
||||
`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>`.
|
||||
`SwitchLoss` is `lossOf` with `T` as the target. `Branches` is one
|
||||
`for-each-ref`, without the `origin/HEAD` symref.
|
||||
|
||||
## Stage pin
|
||||
|
||||
Records the commit checked out in a submodule as its new pin in the
|
||||
@@ -254,6 +321,7 @@ offered: it is idempotent, and the palette is static.
|
||||
| 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` |
|
||||
|
||||
`R` would sit next to `r` (reload), a dangerous mix-up for a destructive
|
||||
@@ -272,6 +340,13 @@ action, hence `x`.
|
||||
detached commits (not those on a branch) and standalone repositories.
|
||||
- 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
|
||||
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.
|
||||
- By hand in tmux on a fixture, see
|
||||
[architecture.md](architecture.md#testing).
|
||||
|
||||
|
||||
+14
-1
@@ -12,25 +12,30 @@ cmd/submarine/main.go wiring: choose provider (git or --mock), start the
|
||||
internal/submodule/ domain model; NO UI imports
|
||||
model.go Repo, Submodule, Status, PinInfo, Commit, enums
|
||||
status.go Issues(), HiddenIssues(), Walk()
|
||||
provider.go Provider interface
|
||||
provider.go Provider interface; optional Fetcher, Watcher, Locator, Restorer, Switcher
|
||||
internal/submodule/git/ provider using the git CLI (reads, fetch, watch)
|
||||
exec.go git runner (environment, errors)
|
||||
parse.go pure parsers for git output (status, numstat, …)
|
||||
provider.go Tree, Status, pin → ref resolution
|
||||
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)
|
||||
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
|
||||
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
|
||||
overlay.go draws the picker centered over the finished view
|
||||
msgs.go tree/status/fetch/change messages
|
||||
refresh.go reloader: one load per key, coalesced reruns
|
||||
theme/ colors, glyphs, issue words, change letters, level styles
|
||||
tree/ tree component: rows, navigation, mouse, view
|
||||
detail/ detail pane; changes.go: change preview
|
||||
header/ header line
|
||||
branches/ branch picker popup (search line + list)
|
||||
footer/ key bar (navigation left, global right) and flash messages
|
||||
```
|
||||
|
||||
@@ -71,6 +76,14 @@ type Restorer interface {
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
type Switcher interface {
|
||||
Branches(ctx context.Context, path string) ([]Branch, error) // local + remote, no network
|
||||
SwitchLoss(ctx context.Context, path string, b Branch) (Loss, error) // as RestoreLoss
|
||||
Switch(ctx context.Context, path string, b Branch) (string, error) // branch HEAD ends on, "" if detached
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
type Locator interface {
|
||||
Dir(path string) string // absolute directory, "" if unknown
|
||||
|
||||
@@ -349,3 +349,38 @@ the same for every submodule of the target tree, initializing missing ones.
|
||||
does not implement it. Restore is idempotent and can be repeated after a
|
||||
failure. It takes the time of its slowest clone; there is no per-repository
|
||||
progress yet.
|
||||
|
||||
## D24 — Switch branch: stage the tip, then restore
|
||||
|
||||
**Context.** Switching branches was left to the shell. Users of a repository
|
||||
with many submodules want to move one to another branch and have the
|
||||
superproject record it.
|
||||
**Decision.** `b` opens a popup (search line, local then remote branches).
|
||||
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.
|
||||
- *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).
|
||||
- *Same loss rules as restore.* The loss is read from the disk; the question
|
||||
appears only if something is lost. Staged first, so a failed restore can be
|
||||
repeated with `x`. The pin question of restore (`s` / `c`) is skipped: the
|
||||
staged pin is being replaced.
|
||||
- *What the picker showed is what is staged.* The branch carries its SHA; a
|
||||
fetch may move the ref meanwhile, then the switch aborts
|
||||
(`origin/feat moved; open the list again`).
|
||||
- *Network.* Extends D23: a tip often pins nested commits that are not on
|
||||
disk, so switch may clone and fetch what is missing, like restore. The
|
||||
list itself uses none; `f` refreshes remote branches.
|
||||
- *Popup.* The first overlay: drawn over the finished view (after
|
||||
`zone.Scan`, so no zone marker is cut); the keys go to the picker while it
|
||||
is open. The backdrop is dimmed: stripped of its styles and drawn in
|
||||
`Backdrop`, an adaptive 256-color grey (238 dark / 252 light). `Muted`
|
||||
(ANSI 8) was too bright to set the popup apart, and ANSI faint (SGR 2) is
|
||||
ignored by some terminals. Like `Bar` and `Selected` it carries no meaning,
|
||||
so it may leave the 16 ANSI colors.
|
||||
**Consequences.** `Switcher` is a new optional provider interface; the mock
|
||||
does not implement it. The switch runs as the restore does (one write
|
||||
operation at a time, same lines above the footer).
|
||||
|
||||
@@ -137,6 +137,13 @@ commit hashes. It doesn't answer the questions users actually have:
|
||||
staged or the committed pin (the latter unstages it). It asks for
|
||||
confirmation only if something would be lost. See [actions.md](actions.md#restore).
|
||||
|
||||
- **F57** `b` opens a popup in the middle of the screen with a search line
|
||||
and the local and remote branches of the selection (a submodule, or the
|
||||
superproject). `↑` / `↓` select, `enter` switches, `esc` cancels. For a
|
||||
submodule the branch tip becomes its staged pin and it is restored to it,
|
||||
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).
|
||||
|
||||
## Usage
|
||||
@@ -161,7 +168,7 @@ submarine --mock # built-in mock data
|
||||
- **N7** The UI is independent of the data source: the mock and the git
|
||||
provider are interchangeable without UI changes.
|
||||
- **N8** The git provider only uses the network in `Fetch` and, for objects
|
||||
that are missing, in `Restore`. It never prompts (no password, passphrase
|
||||
that are missing, in `Restore` and `Switch`. It never prompts (no password, passphrase
|
||||
or host-key questions; those fail with git's message). It uses the `git`
|
||||
command line, so the user's git configuration applies. Apart from fetching
|
||||
and the actions of [actions.md](actions.md), it is read-only: it writes
|
||||
|
||||
@@ -183,6 +183,7 @@ colors in components.
|
||||
| Border | ANSI `8` grey | pane and help separators |
|
||||
| Bar | adaptive 254 (light) / 235 (dark) | header background |
|
||||
| Selected | adaptive 253 (light) / 237 (dark) | selected row background |
|
||||
| Backdrop | adaptive 252 (light) / 238 (dark) | the screen behind a popup |
|
||||
|
||||
- Background shades have no ANSI-16 equivalent, so they use `AdaptiveColor`.
|
||||
The background is detected once in `main` before Bubble Tea starts.
|
||||
@@ -293,6 +294,34 @@ question is open. After `c` the name reads `src/app to the committed pin`. Witho
|
||||
superproject`. This line takes precedence over the filter line and the
|
||||
problems-only notice.
|
||||
|
||||
### Switch branch
|
||||
|
||||
`b` opens a popup in the middle of the screen (see
|
||||
[actions.md](actions.md#switch-branch)). The screen around it stays visible
|
||||
but dimmed: its colors are dropped and it is drawn in `Backdrop`, darker
|
||||
than `Muted`, so the popup has the focus.
|
||||
|
||||
```
|
||||
╭──────────────────────────────────────────────────────────────────────╮
|
||||
│ Switch libs/feature │
|
||||
│ / filter branches │
|
||||
│ ──────────────────────────────────────────────────────────────────── │
|
||||
│ * main 92d1583 3d ago │
|
||||
│ origin/feat/search 58a2bf1 2h ago │
|
||||
│ ↑↓ select · enter switch · esc cancel │
|
||||
╰──────────────────────────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
- Border `Border`, title in `Primary` bold, search prompt `Primary`, the
|
||||
selected row on the `Selected` background, SHA and age `Muted`, the remote
|
||||
prefix `Muted`, the checked-out branch bold with `*`.
|
||||
- Width at most 72 columns, height as the list (at most 14 rows), so
|
||||
filtering does not resize it. While reading: spinner and `reading
|
||||
branches…`; no match: `no branch matches`; error: red, inside the popup.
|
||||
- Enter closes the popup and continues on the line above the footer like a
|
||||
[restore](#restore): `checking`, the question `Switch src/app to
|
||||
origin/feat? Discards … y switch · n cancel`, `switching …`.
|
||||
|
||||
### Key reference
|
||||
|
||||
Every key in the UI, where it is shown. Update this
|
||||
@@ -310,6 +339,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 |
|
||||
| `b` | Switch branch (popup) | Submodule | Palette, Help |
|
||||
| `x` | Restore | Submodule | Palette, Help |
|
||||
| `f` | Fetch all repositories | Global | Help |
|
||||
| `r` | Reload everything | Global | Help |
|
||||
|
||||
Reference in New Issue
Block a user