Archived
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8aa12774eb | ||
|
|
5cb809428b |
@@ -31,7 +31,7 @@ internal/tui/ Bubble Tea application
|
|||||||
tree/ tree component: rows, navigation, mouse, view
|
tree/ tree component: rows, navigation, mouse, view
|
||||||
detail/ detail pane; changes.go: change preview
|
detail/ detail pane; changes.go: change preview
|
||||||
header/ header line
|
header/ header line
|
||||||
footer/ help and flash messages
|
footer/ key bar (navigation left, global right) and flash messages
|
||||||
```
|
```
|
||||||
|
|
||||||
### Layering rules
|
### Layering rules
|
||||||
@@ -64,6 +64,12 @@ type Watcher interface {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Locator interface {
|
||||||
|
Dir(path string) string // absolute directory, "" if unknown
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- `Tree` returns the superproject and all submodules with `Parent` and
|
- `Tree` returns the superproject and all submodules with `Parent` and
|
||||||
`Children` linked. `Status` fields are nil.
|
`Children` linked. `Status` fields are nil.
|
||||||
- `Status` is called once per submodule, concurrently. It must be safe to
|
- `Status` is called once per submodule, concurrently. It must be safe to
|
||||||
|
|||||||
+32
-1
@@ -7,7 +7,8 @@ Each entry: context, decision, consequences. Newest last.
|
|||||||
**Context.** Hashes don't tell users whether a pin is current.
|
**Context.** Hashes don't tell users whether a pin is current.
|
||||||
**Decision.** Resolve each pin to a ref and show the distance (`origin/main ↓1`).
|
**Decision.** Resolve each pin to a ref and show the distance (`origin/main ↓1`).
|
||||||
The resolution order is fixed and the applied rule is shown in the detail pane
|
The resolution order is fixed and the applied rule is shown in the detail pane
|
||||||
(see [state-model.md](state-model.md#resolving-a-pin-to-a-ref)).
|
(see [state-model.md](state-model.md#resolving-a-pin-to-a-ref); amended:
|
||||||
|
the closest branch wins, see "Pin resolves to the closest branch").
|
||||||
**Consequences.** Resolution is a heuristic; showing the rule keeps it
|
**Consequences.** Resolution is a heuristic; showing the rule keeps it
|
||||||
transparent.
|
transparent.
|
||||||
|
|
||||||
@@ -200,3 +201,33 @@ package so versions stay clean.
|
|||||||
corrupt or partial downloads, not a compromised registry (no signatures).
|
corrupt or partial downloads, not a compromised registry (no signatures).
|
||||||
The update check is a deliberate, outside-the-provider network use: only
|
The update check is a deliberate, outside-the-provider network use: only
|
||||||
on `update` and a once-a-day background check that can be disabled.
|
on `update` and a once-a-day background check that can be disabled.
|
||||||
|
|
||||||
|
## Copy path with `c` via OSC 52
|
||||||
|
|
||||||
|
`c` copies the selected submodule's absolute directory (optional
|
||||||
|
`submodule.Locator`; the relative path as fallback). The absolute path is
|
||||||
|
what a shell needs (`cd <paste>`). OSC 52 instead of a clipboard tool
|
||||||
|
(`xclip`, `pbcopy`): no external dependency and it works over SSH. The cost:
|
||||||
|
terminals that block OSC 52 copy nothing silently; the footer still says
|
||||||
|
`copied`, since the application can't know.
|
||||||
|
|
||||||
|
## Key groups: global, navigation, submodule
|
||||||
|
|
||||||
|
Keys that act on the selected submodule (copy path, later checkout, bump) are
|
||||||
|
shown in a palette in the detail pane, next to what they act on; global keys
|
||||||
|
(`?`, `q`) sit right in the footer. Where the pane can't show the palette
|
||||||
|
(hidden below 90 columns, or under 12 lines) the footer shows these keys
|
||||||
|
instead, so small terminals need no extra UI (no overlay or action menu).
|
||||||
|
Keys never depend on what is visible.
|
||||||
|
|
||||||
|
## Pin resolves to the closest branch
|
||||||
|
|
||||||
|
A pin on a pushed feature branch showed as diverged from `origin/main`,
|
||||||
|
because `branch = main` in `.gitmodules` won before any distance was looked
|
||||||
|
at. Now the closest branch containing the pin wins; the `.gitmodules`
|
||||||
|
branch, the upstream and the remote default branch only break ties (and are
|
||||||
|
always compared, even past the 16-branch cap). An unpushed pin compares only
|
||||||
|
those preferred refs, by commits on either side: scanning every branch's
|
||||||
|
merge base would be too slow. Cost: a pin on main far behind its tip shows a
|
||||||
|
younger branch that contains it if that branch's tip is closer, so "n behind
|
||||||
|
main" is no longer visible there; `via <rule>` still tells which ref it is.
|
||||||
|
|||||||
+28
-14
@@ -104,17 +104,30 @@ A descendant whose load failed (`error`) counts as one danger.
|
|||||||
## Resolving a pin to a ref
|
## Resolving a pin to a ref
|
||||||
|
|
||||||
The pin is displayed as a ref, not a hash. A commit can be on several
|
The pin is displayed as a ref, not a hash. A commit can be on several
|
||||||
branches, on none, or deep in the history, so the git provider resolves it by
|
branches, on none, or deep in the history. The git provider shows the
|
||||||
this order and records which rule applied in `PinInfo.Source`:
|
**closest** branch and records which rule proposed it in `PinInfo.Source`.
|
||||||
|
|
||||||
|
Preferred refs, in this order (each only if it exists):
|
||||||
|
|
||||||
|
| Preference | Ref | `RefSource` |
|
||||||
|
|---|---|---|
|
||||||
|
| a | `branch = …` in `.gitmodules` | `RefFromGitmodules` |
|
||||||
|
| b | Upstream of the branch checked out in the submodule | `RefFromCurrentBranch` |
|
||||||
|
| c | Remote default branch (`<remote>/HEAD`) | `RefFromDefaultBranch` |
|
||||||
|
|
||||||
|
Resolution:
|
||||||
|
|
||||||
| # | Rule | `RefSource` |
|
| # | Rule | `RefSource` |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 1 | `branch = …` in `.gitmodules` | `RefFromGitmodules` |
|
| 1 | Pin pushed: the branch containing the pin with the fewest commits after it. Candidates are the preferred refs that contain the pin plus any remote branch containing it (`for-each-ref --contains`); a tie goes to the preferred ref | preferred ref's, else `RefFromContains` |
|
||||||
| 2 | Branch currently checked out in the submodule | `RefFromCurrentBranch` |
|
| 2 | Pin unpushed: the preferred ref with the fewest commits on either side (`↑n↓m`), ties by preference | preferred ref's |
|
||||||
| 3 | Remote default branch, if it contains the pin | `RefFromDefaultBranch` |
|
| 3 | A tag (`git describe`) | `RefFromTag` |
|
||||||
| 4 | Any remote branch containing the pin, preferring the closest tip (`git branch -r --contains`) | `RefFromContains` |
|
| 4 | Short SHA | `RefFromSHA` |
|
||||||
| 5 | A tag (`git describe`) | `RefFromTag` |
|
|
||||||
| 6 | Short SHA | `RefFromSHA` |
|
So a `.gitmodules` branch is shown when it is as close as any other branch,
|
||||||
|
but a pin that sits on a feature branch shows that branch rather than
|
||||||
|
"diverged from main". The trade-off: a pin on main that a younger branch
|
||||||
|
also contains shows the younger branch if its tip is fewer commits away.
|
||||||
|
|
||||||
Distances: `git rev-list --left-right --count <pin>...<ref>` gives
|
Distances: `git rev-list --left-right --count <pin>...<ref>` gives
|
||||||
`Ahead` (left) and `Behind` (right).
|
`Ahead` (left) and `Behind` (right).
|
||||||
@@ -143,12 +156,13 @@ are local; `<pin>` is the index gitlink.
|
|||||||
|
|
||||||
Details:
|
Details:
|
||||||
|
|
||||||
- Rule 1: `branch = .` means the superproject's current branch. The rule
|
- Preference a: `branch = .` means the superproject's current branch. The
|
||||||
applies only if `<remote>/<branch>` exists locally.
|
ref counts only if `<remote>/<branch>` exists locally.
|
||||||
- Rule 2 uses the checked-out branch's upstream (`<branch>@{upstream}`).
|
- Preference b uses the checked-out branch's upstream (`<branch>@{upstream}`).
|
||||||
- Rule 4 compares at most 16 containing branches (oldest tip first) and
|
- Rule 1 counts `rev-list --count <pin>..<ref>`. Besides the preferred refs
|
||||||
picks the one with the fewest commits after the pin.
|
it compares at most 16 containing branches (oldest tip first).
|
||||||
- Rule 6 shows the short SHA; there is no tip and no distance.
|
- Rule 2 counts `rev-list --count <pin>...<ref>`.
|
||||||
|
- Rule 4 shows the short SHA; there is no tip and no distance.
|
||||||
- `--ignore-submodules=dirty`: changes inside a nested submodule do not make
|
- `--ignore-submodules=dirty`: changes inside a nested submodule do not make
|
||||||
its parent `changed` (D7), but a moved nested pin does.
|
its parent `changed` (D7), but a moved nested pin does.
|
||||||
- Renames are detected only for staged moves (`git mv`); an unstaged move
|
- Renames are detected only for staged moves (`git mv`); an unstaged move
|
||||||
|
|||||||
@@ -36,6 +36,21 @@
|
|||||||
| Detail pane | 40% of the width, at least 36 columns; hidden below 90 columns |
|
| Detail pane | 40% of the width, at least 36 columns; hidden below 90 columns |
|
||||||
| Footer | 1 line (more with full help) |
|
| Footer | 1 line (more with full help) |
|
||||||
|
|
||||||
|
### Key bar and palette
|
||||||
|
|
||||||
|
Keys come in three groups (`keyMap.Navigation`, `.Submodule`, `.Global`):
|
||||||
|
|
||||||
|
- **Navigation** (move, fold, toggle): left in the footer.
|
||||||
|
- **Global** (`?`, `q`): right-aligned in the footer, always kept.
|
||||||
|
- **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
|
||||||
|
keys move into the footer after the navigation keys.
|
||||||
|
|
||||||
|
If the footer is too narrow, keys are dropped from the left group's end
|
||||||
|
(submodule keys first); the global keys stay. Full help (`?`) lists all keys
|
||||||
|
in columns, whatever the size. Keys work whether or not they are shown.
|
||||||
|
|
||||||
### Tree row
|
### Tree row
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -191,6 +206,7 @@ colors in components.
|
|||||||
| `q`, `ctrl+c` | Quit |
|
| `q`, `ctrl+c` | Quit |
|
||||||
| `f` | Fetch all repositories (does nothing while a fetch runs) |
|
| `f` | Fetch all repositories (does nothing while a fetch runs) |
|
||||||
| `r` | Reload everything (e.g. after editing files) |
|
| `r` | Reload everything (e.g. after editing files) |
|
||||||
|
| `c` | Copy the absolute directory of the selected submodule to the clipboard (OSC 52, so it also works over SSH; needs terminal support, in tmux `set -g set-clipboard on`). The footer confirms with `copied <path>`. Without a local provider (mock) the path relative to the superproject is copied |
|
||||||
| `u`, `b`, `/`, `!` | Planned actions (see requirements) |
|
| `u`, `b`, `/`, `!` | Planned actions (see requirements) |
|
||||||
|
|
||||||
"First child" always means the first child **as displayed** (children are
|
"First child" always means the first child **as displayed** (children are
|
||||||
@@ -200,6 +216,32 @@ Collapsing an ancestor of the selection moves the selection to that ancestor.
|
|||||||
The view scrolls so the selection is always visible; when the first submodule
|
The view scrolls so the selection is always visible; when the first submodule
|
||||||
is selected, headers above it are shown too.
|
is selected, headers above it are shown too.
|
||||||
|
|
||||||
|
### Key reference
|
||||||
|
|
||||||
|
Every key in the UI, where it is shown, and whether it works. Update this
|
||||||
|
table when a key is added, moved or implemented. "Bar" is the short footer
|
||||||
|
bar, "Palette" the detail pane's palette (the footer takes its keys over
|
||||||
|
where the palette can't be shown, see [Key bar and palette](#key-bar-and-palette)),
|
||||||
|
"Help" the full help (`?`).
|
||||||
|
|
||||||
|
| Key | Action | Group | Shown in | Implemented |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `↑` / `k`, `↓` / `j` | Move selection | Navigation | Bar, Help | yes |
|
||||||
|
| `←` / `h`, `→` / `l` | Fold / unfold, parent / first child | Navigation | Bar, Help | yes |
|
||||||
|
| `H` / `⇧←`, `L` / `⇧→` | Collapse / expand all | Navigation | Bar, Help | yes |
|
||||||
|
| `space` | Toggle expand/collapse | Navigation | Bar, Help | yes |
|
||||||
|
| `home` / `g`, `end` / `G` | First / last submodule | Navigation | Help | yes |
|
||||||
|
| `pgup`, `pgdn` | Move by one screen | Navigation | Help | yes |
|
||||||
|
| `c` | Copy path | Submodule | Palette, Help | yes |
|
||||||
|
| `u` | Checkout pin | Submodule | Help | no (flashes "not implemented") |
|
||||||
|
| `b` | Bump pin to tip | Submodule | Help | no (flashes "not implemented") |
|
||||||
|
| `f` | Fetch all repositories | Global | Help | yes |
|
||||||
|
| `r` | Reload everything | Global | Help | yes |
|
||||||
|
| `/` | Filter | Global | Help | no (flashes "not implemented") |
|
||||||
|
| `!` | Problems only | Global | Help | no (flashes "not implemented") |
|
||||||
|
| `?` | Toggle full help | Global | Bar, Help | yes |
|
||||||
|
| `q`, `ctrl+c` | Quit | Global | Bar, Help | yes |
|
||||||
|
|
||||||
### Mouse
|
### Mouse
|
||||||
|
|
||||||
- Click a row: select it.
|
- Click a row: select it.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ module submarine
|
|||||||
go 1.27.3
|
go 1.27.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1
|
||||||
github.com/charmbracelet/bubbles v1.0.0
|
github.com/charmbracelet/bubbles v1.0.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
@@ -11,7 +12,6 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
|
||||||
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
|
|||||||
@@ -36,6 +36,17 @@ func (p *Provider) Fetchable(path string) bool {
|
|||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dir returns the absolute directory of path ("" is the superproject),
|
||||||
|
// whether or not it is initialized.
|
||||||
|
func (p *Provider) Dir(path string) string {
|
||||||
|
if path == "" {
|
||||||
|
return p.root
|
||||||
|
}
|
||||||
|
p.mu.RLock()
|
||||||
|
defer p.mu.RUnlock()
|
||||||
|
return p.mods[path].dir
|
||||||
|
}
|
||||||
|
|
||||||
// repoDir returns the worktree of path if it is initialized.
|
// repoDir returns the worktree of path if it is initialized.
|
||||||
func (p *Provider) repoDir(path string) (string, bool) {
|
func (p *Provider) repoDir(path string) (string, bool) {
|
||||||
if path == "" {
|
if path == "" {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// maxContainsCandidates limits how many remote branches containing the pin
|
// maxContainsCandidates limits how many remote branches containing the pin
|
||||||
// are compared when resolving the pin by RefFromContains.
|
// are compared besides the preferred refs when resolving a pushed pin.
|
||||||
const maxContainsCandidates = 16
|
const maxContainsCandidates = 16
|
||||||
|
|
||||||
// maxBetween limits the commits listed between pin and tip.
|
// maxBetween limits the commits listed between pin and tip.
|
||||||
@@ -269,80 +269,130 @@ func status(ctx context.Context, in info) (*submodule.Status, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resolve finds the ref that describes the pin, following the order in
|
// resolve finds the ref that describes the pin, following the order in
|
||||||
// docs/state-model.md. containing lists the remote branches containing the
|
// docs/state-model.md: the closest branch wins, the preferred refs (see
|
||||||
// pin, oldest tip first.
|
// preferredRefs) break ties. containing lists the remote branches containing
|
||||||
|
// the pin, oldest tip first.
|
||||||
func resolve(ctx context.Context, in info, pin, branch string, containing []string) (string, submodule.RefSource, error) {
|
func resolve(ctx context.Context, in info, pin, branch string, containing []string) (string, submodule.RefSource, error) {
|
||||||
remote, err := remoteName(ctx, in.dir)
|
preferred, err := preferredRefs(ctx, in, branch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, err
|
return "", 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. branch from .gitmodules
|
// 1. pushed: the branch containing the pin with the fewest commits after it
|
||||||
if b := in.branch; b != "" && remote != "" {
|
if len(containing) > 0 {
|
||||||
if b == "." {
|
var cands []candidate
|
||||||
b, _, err = try(ctx, in.superDir, "symbolic-ref", "--short", "-q", "HEAD")
|
for _, c := range preferred {
|
||||||
if err != nil {
|
if _, ok, err := try(ctx, in.dir, "merge-base", "--is-ancestor", pin, c.ref); err != nil {
|
||||||
return "", 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if b != "" {
|
|
||||||
if _, ok, err := try(ctx, in.dir, "rev-parse", "--verify", "-q", "refs/remotes/"+remote+"/"+b); err != nil {
|
|
||||||
return "", 0, err
|
return "", 0, err
|
||||||
} else if ok {
|
} else if ok {
|
||||||
return remote + "/" + b, submodule.RefFromGitmodules, nil
|
cands = append(cands, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
for _, r := range containing[:min(len(containing), maxContainsCandidates)] {
|
||||||
|
if !slices.ContainsFunc(cands, func(c candidate) bool { return c.ref == r }) {
|
||||||
// 2. upstream of the checked-out branch
|
cands = append(cands, candidate{r, submodule.RefFromContains})
|
||||||
if branch != "" {
|
}
|
||||||
if up, ok, err := try(ctx, in.dir, "rev-parse", "--abbrev-ref", "--symbolic-full-name", branch+"@{upstream}"); err != nil {
|
|
||||||
return "", 0, err
|
|
||||||
} else if ok && up != "" {
|
|
||||||
return up, submodule.RefFromCurrentBranch, nil
|
|
||||||
}
|
}
|
||||||
}
|
if c, ok, err := closest(ctx, in.dir, pin+"..", cands); err != nil {
|
||||||
|
|
||||||
// 3. remote default branch, if it contains the pin
|
|
||||||
if remote != "" {
|
|
||||||
if def, ok, err := try(ctx, in.dir, "symbolic-ref", "--short", "-q", "refs/remotes/"+remote+"/HEAD"); err != nil {
|
|
||||||
return "", 0, err
|
return "", 0, err
|
||||||
} else if ok {
|
} else if ok {
|
||||||
if _, ok, err := try(ctx, in.dir, "merge-base", "--is-ancestor", pin, def); err != nil {
|
return c.ref, c.source, nil
|
||||||
return "", 0, err
|
|
||||||
} else if ok {
|
|
||||||
return def, submodule.RefFromDefaultBranch, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. remote branch containing the pin with the closest tip
|
// 2. unpushed: the preferred ref with the fewest commits on either side
|
||||||
best, bestN := "", -1
|
if c, ok, err := closest(ctx, in.dir, pin+"...", preferred); err != nil {
|
||||||
for _, r := range containing[:min(len(containing), maxContainsCandidates)] {
|
return "", 0, err
|
||||||
out, err := run(ctx, in.dir, "rev-list", "--count", pin+".."+r)
|
} else if ok {
|
||||||
if err != nil {
|
return c.ref, c.source, nil
|
||||||
return "", 0, err
|
|
||||||
}
|
|
||||||
var n int
|
|
||||||
if _, err := fmt.Sscan(out, &n); err == nil && (bestN < 0 || n < bestN) {
|
|
||||||
best, bestN = r, n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if best != "" {
|
|
||||||
return best, submodule.RefFromContains, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. nearest tag
|
// 3. nearest tag
|
||||||
if tag, ok, err := try(ctx, in.dir, "describe", "--tags", "--abbrev=0", pin); err != nil {
|
if tag, ok, err := try(ctx, in.dir, "describe", "--tags", "--abbrev=0", pin); err != nil {
|
||||||
return "", 0, err
|
return "", 0, err
|
||||||
} else if ok && tag != "" {
|
} else if ok && tag != "" {
|
||||||
return tag, submodule.RefFromTag, nil
|
return tag, submodule.RefFromTag, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. the commit itself
|
// 4. the commit itself
|
||||||
return "", submodule.RefFromSHA, nil
|
return "", submodule.RefFromSHA, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// candidate is a ref the pin may resolve to and the rule that proposed it.
|
||||||
|
type candidate struct {
|
||||||
|
ref string
|
||||||
|
source submodule.RefSource
|
||||||
|
}
|
||||||
|
|
||||||
|
// preferredRefs returns the refs the user most likely means, in order of
|
||||||
|
// preference and without duplicates: the .gitmodules branch, the upstream of
|
||||||
|
// the checked-out branch, the remote default branch. Each must exist.
|
||||||
|
func preferredRefs(ctx context.Context, in info, branch string) ([]candidate, error) {
|
||||||
|
remote, err := remoteName(ctx, in.dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var refs []candidate
|
||||||
|
add := func(ref string, source submodule.RefSource) {
|
||||||
|
if ref != "" && !slices.ContainsFunc(refs, func(c candidate) bool { return c.ref == ref }) {
|
||||||
|
refs = append(refs, candidate{ref, source})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// branch from .gitmodules
|
||||||
|
if b := in.branch; b != "" && remote != "" {
|
||||||
|
if b == "." {
|
||||||
|
b, _, err = try(ctx, in.superDir, "symbolic-ref", "--short", "-q", "HEAD")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if b != "" {
|
||||||
|
if _, ok, err := try(ctx, in.dir, "rev-parse", "--verify", "-q", "refs/remotes/"+remote+"/"+b); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if ok {
|
||||||
|
add(remote+"/"+b, submodule.RefFromGitmodules)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// upstream of the checked-out branch
|
||||||
|
if branch != "" {
|
||||||
|
if up, ok, err := try(ctx, in.dir, "rev-parse", "--abbrev-ref", "--symbolic-full-name", branch+"@{upstream}"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if ok {
|
||||||
|
add(up, submodule.RefFromCurrentBranch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remote default branch
|
||||||
|
if remote != "" {
|
||||||
|
if def, ok, err := try(ctx, in.dir, "symbolic-ref", "--short", "-q", "refs/remotes/"+remote+"/HEAD"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if ok {
|
||||||
|
add(def, submodule.RefFromDefaultBranch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return refs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// closest returns the candidate with the fewest commits in the range
|
||||||
|
// `<from><ref>` (from is e.g. "<pin>.."); the first one wins a tie.
|
||||||
|
func closest(ctx context.Context, dir, from string, cands []candidate) (candidate, bool, error) {
|
||||||
|
best, bestN := candidate{}, -1
|
||||||
|
for _, c := range cands {
|
||||||
|
out, err := run(ctx, dir, "rev-list", "--count", from+c.ref)
|
||||||
|
if err != nil {
|
||||||
|
return candidate{}, false, err
|
||||||
|
}
|
||||||
|
var n int
|
||||||
|
if _, err := fmt.Sscan(out, &n); err == nil && (bestN < 0 || n < bestN) {
|
||||||
|
best, bestN = c, n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best, bestN >= 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
// remoteName returns "origin" if it exists, else the first remote, else "".
|
// remoteName returns "origin" if it exists, else the first remote, else "".
|
||||||
func remoteName(ctx context.Context, dir string) (string, error) {
|
func remoteName(ctx context.Context, dir string) (string, error) {
|
||||||
out, err := run(ctx, dir, "remote")
|
out, err := run(ctx, dir, "remote")
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func TestTree(t *testing.T) {
|
|||||||
submodule.Walk(repo.Submodules, func(s *submodule.Submodule) { paths = append(paths, s.Path) })
|
submodule.Walk(repo.Submodules, func(s *submodule.Submodule) { paths = append(paths, s.Path) })
|
||||||
want := []string{
|
want := []string{
|
||||||
"src/core", "src/ui", "src/net", "libs/store", "libs/log", "libs/log/modules/nested",
|
"src/core", "src/ui", "src/net", "libs/store", "libs/log", "libs/log/modules/nested",
|
||||||
"libs/feature", "libs/conflict", "libs/staged", "libs/missing", "vendor", "libs/work",
|
"libs/feature", "libs/conflict", "libs/staged", "libs/missing", "vendor", "libs/work", "libs/topic",
|
||||||
}
|
}
|
||||||
if !slices.Equal(paths, want) {
|
if !slices.Equal(paths, want) {
|
||||||
t.Errorf("paths = %v\nwant %v", paths, want)
|
t.Errorf("paths = %v\nwant %v", paths, want)
|
||||||
@@ -106,6 +106,7 @@ func TestStatus(t *testing.T) {
|
|||||||
{path: "libs/log", ref: "origin/main", source: submodule.RefFromCurrentBranch},
|
{path: "libs/log", ref: "origin/main", source: submodule.RefFromCurrentBranch},
|
||||||
{path: "libs/log/modules/nested", issues: []submodule.IssueKind{submodule.IssueChanged}, ref: "origin/main", source: submodule.RefFromDefaultBranch, checkout: submodule.CheckoutDetached},
|
{path: "libs/log/modules/nested", issues: []submodule.IssueKind{submodule.IssueChanged}, ref: "origin/main", source: submodule.RefFromDefaultBranch, checkout: submodule.CheckoutDetached},
|
||||||
{path: "libs/feature", ref: "origin/feat/search", source: submodule.RefFromContains, behind: 1, checkout: submodule.CheckoutDetached},
|
{path: "libs/feature", ref: "origin/feat/search", source: submodule.RefFromContains, behind: 1, checkout: submodule.CheckoutDetached},
|
||||||
|
{path: "libs/topic", ref: "origin/feat/search", source: submodule.RefFromContains, checkout: submodule.CheckoutDetached},
|
||||||
{path: "libs/conflict", issues: []submodule.IssueKind{submodule.IssueConflict}, ref: "origin/main", source: submodule.RefFromCurrentBranch, staged: true},
|
{path: "libs/conflict", issues: []submodule.IssueKind{submodule.IssueConflict}, ref: "origin/main", source: submodule.RefFromCurrentBranch, staged: true},
|
||||||
{path: "libs/staged", ref: "origin/main", source: submodule.RefFromCurrentBranch, staged: true},
|
{path: "libs/staged", ref: "origin/main", source: submodule.RefFromCurrentBranch, staged: true},
|
||||||
{path: "libs/missing", issues: []submodule.IssueKind{submodule.IssueMissing}},
|
{path: "libs/missing", issues: []submodule.IssueKind{submodule.IssueMissing}},
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ type Watcher interface {
|
|||||||
Watch(ctx context.Context) <-chan Change
|
Watch(ctx context.Context) <-chan Change
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Locator is implemented by providers whose submodules live on the local
|
||||||
|
// disk. Dir returns the absolute directory of path ("" is the superproject),
|
||||||
|
// or "" if it is unknown.
|
||||||
|
type Locator interface {
|
||||||
|
Dir(path string) string
|
||||||
|
}
|
||||||
|
|
||||||
// Change reports that the git metadata of a repository changed.
|
// Change reports that the git metadata of a repository changed.
|
||||||
type Change struct {
|
type Change struct {
|
||||||
Path string // "" is the superproject
|
Path string // "" is the superproject
|
||||||
|
|||||||
+22
-1
@@ -73,7 +73,7 @@ func New(ctx context.Context, p submodule.Provider, opts Options) Model {
|
|||||||
loads: newReloader(),
|
loads: newReloader(),
|
||||||
ticking: true, // Init schedules the first tick
|
ticking: true, // Init schedules the first tick
|
||||||
tree: tree.New(t),
|
tree: tree.New(t),
|
||||||
detail: detail.New(t),
|
detail: detail.New(t, k.Submodule()),
|
||||||
footer: footer.New(t, k),
|
footer: footer.New(t, k),
|
||||||
spinner: sp,
|
spinner: sp,
|
||||||
}
|
}
|
||||||
@@ -353,6 +353,8 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m, m.startFetch()
|
return m, m.startFetch()
|
||||||
case key.Matches(msg, k.Reload):
|
case key.Matches(msg, k.Reload):
|
||||||
return m, m.reloadTree(true)
|
return m, m.reloadTree(true)
|
||||||
|
case key.Matches(msg, k.Copy):
|
||||||
|
return m.copyPath()
|
||||||
case key.Matches(msg, k.Update, k.Bump, k.Filter, k.Issues):
|
case key.Matches(msg, k.Update, k.Bump, k.Filter, k.Issues):
|
||||||
cmd := m.footer.Flash("“" + msg.String() + "” is not implemented in the prototype")
|
cmd := m.footer.Flash("“" + msg.String() + "” is not implemented in the prototype")
|
||||||
m.layout()
|
m.layout()
|
||||||
@@ -361,6 +363,24 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copyPath copies the absolute directory of the selected submodule to the
|
||||||
|
// clipboard, or its path in the superproject if the provider can't tell.
|
||||||
|
func (m Model) copyPath() (tea.Model, tea.Cmd) {
|
||||||
|
s := m.tree.Selected()
|
||||||
|
if s == nil {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
path := s.Path
|
||||||
|
if l, ok := m.provider.(submodule.Locator); ok {
|
||||||
|
if dir := l.Dir(s.Path); dir != "" {
|
||||||
|
path = dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd := tea.Batch(copyToClipboard(path), m.footer.Flash("copied "+path))
|
||||||
|
m.layout()
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
// layout distributes the terminal size among the components.
|
// layout distributes the terminal size among the components.
|
||||||
func (m *Model) layout() {
|
func (m *Model) layout() {
|
||||||
m.footer.SetWidth(m.width)
|
m.footer.SetWidth(m.width)
|
||||||
@@ -373,6 +393,7 @@ func (m *Model) layout() {
|
|||||||
treeWidth = m.width - detailWidth
|
treeWidth = m.width - detailWidth
|
||||||
m.detail.SetSize(detailWidth-2, bodyHeight) // border + padding
|
m.detail.SetSize(detailWidth-2, bodyHeight) // border + padding
|
||||||
}
|
}
|
||||||
|
m.footer.SetActions(!m.showDetail || !m.detail.PaletteVisible())
|
||||||
m.tree.SetSize(treeWidth, bodyHeight)
|
m.tree.SetSize(treeWidth, bodyHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/aymanbagabas/go-osc52/v2"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
// copyToClipboard sets the system clipboard with the OSC 52 escape sequence,
|
||||||
|
// which works over SSH and in terminals that allow it; others ignore it.
|
||||||
|
func copyToClipboard(text string) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
seq := osc52.New(text)
|
||||||
|
if os.Getenv("TMUX") != "" {
|
||||||
|
seq = seq.Tmux()
|
||||||
|
}
|
||||||
|
_, _ = seq.WriteTo(os.Stderr)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/help"
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/charmbracelet/x/ansi"
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
|
||||||
@@ -20,9 +22,20 @@ type Model struct {
|
|||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
spinner string
|
spinner string
|
||||||
|
help help.Model
|
||||||
|
actions []key.Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(t theme.Theme) Model { return Model{theme: t} }
|
// paletteMinHeight is the pane height below which the palette is left out:
|
||||||
|
// the information is worth more than the hints.
|
||||||
|
const paletteMinHeight = 12
|
||||||
|
|
||||||
|
func New(t theme.Theme, actions []key.Binding) Model {
|
||||||
|
return Model{theme: t, help: theme.NewHelp(t), actions: actions}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaletteVisible reports whether the palette of submodule actions fits.
|
||||||
|
func (m Model) PaletteVisible() bool { return m.height >= paletteMinHeight }
|
||||||
|
|
||||||
func (m *Model) SetSize(w, h int) { m.width, m.height = w, h }
|
func (m *Model) SetSize(w, h int) { m.width, m.height = w, h }
|
||||||
func (m *Model) SetSpinner(frame string) { m.spinner = frame }
|
func (m *Model) SetSpinner(frame string) { m.spinner = frame }
|
||||||
@@ -38,10 +51,21 @@ func (m Model) View(s *submodule.Submodule) string {
|
|||||||
for i, l := range lines {
|
for i, l := range lines {
|
||||||
lines[i] = ansi.Truncate(l, m.width, "…")
|
lines[i] = ansi.Truncate(l, m.width, "…")
|
||||||
}
|
}
|
||||||
if len(lines) > m.height {
|
height := m.height
|
||||||
lines = lines[:m.height]
|
var palette string
|
||||||
|
if m.PaletteVisible() {
|
||||||
|
height-- // the palette is the last line
|
||||||
|
m.help.Width = 0
|
||||||
|
palette = ansi.Truncate(m.help.ShortHelpView(m.actions), m.width, "…")
|
||||||
}
|
}
|
||||||
return lipgloss.NewStyle().Width(m.width).Height(m.height).Render(strings.Join(lines, "\n"))
|
if len(lines) > height {
|
||||||
|
lines = lines[:height]
|
||||||
|
}
|
||||||
|
body := lipgloss.NewStyle().Width(m.width).Height(height).Render(strings.Join(lines, "\n"))
|
||||||
|
if palette == "" {
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
return body + "\n" + palette
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) lines(s *submodule.Submodule) []string {
|
func (m Model) lines(s *submodule.Submodule) []string {
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
package footer
|
package footer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/help"
|
"github.com/charmbracelet/bubbles/help"
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
|
||||||
"submarine/internal/tui/theme"
|
"submarine/internal/tui/theme"
|
||||||
)
|
)
|
||||||
@@ -15,26 +18,34 @@ const flashDuration = 2 * time.Second
|
|||||||
|
|
||||||
type clearFlashMsg struct{ id int }
|
type clearFlashMsg struct{ id int }
|
||||||
|
|
||||||
|
// Keys provides the key groups. The short bar shows Navigation (and
|
||||||
|
// Submodule, if requested) on the left and Global on the right; full help
|
||||||
|
// shows FullHelp.
|
||||||
|
type Keys interface {
|
||||||
|
help.KeyMap
|
||||||
|
Navigation() []key.Binding
|
||||||
|
Submodule() []key.Binding
|
||||||
|
Global() []key.Binding
|
||||||
|
}
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
theme theme.Theme
|
theme theme.Theme
|
||||||
help help.Model
|
help help.Model
|
||||||
keys help.KeyMap
|
keys Keys
|
||||||
width int
|
width int
|
||||||
flash string
|
flash string
|
||||||
flashID int
|
flashID int
|
||||||
|
actions bool // show the submodule bindings in the short bar
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(t theme.Theme, keys help.KeyMap) Model {
|
func New(t theme.Theme, keys Keys) Model {
|
||||||
h := help.New()
|
return Model{theme: t, help: theme.NewHelp(t), keys: keys}
|
||||||
h.Styles.ShortKey = theme.Fg(t.Text).Bold(true)
|
|
||||||
h.Styles.ShortDesc = theme.Fg(t.Muted)
|
|
||||||
h.Styles.ShortSeparator = theme.Fg(t.Border)
|
|
||||||
h.Styles.FullKey = h.Styles.ShortKey
|
|
||||||
h.Styles.FullDesc = h.Styles.ShortDesc
|
|
||||||
h.Styles.FullSeparator = h.Styles.ShortSeparator
|
|
||||||
return Model{theme: t, help: h, keys: keys}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetActions shows the submodule bindings in the short bar. Set it if the
|
||||||
|
// detail pane doesn't show them.
|
||||||
|
func (m *Model) SetActions(on bool) { m.actions = on }
|
||||||
|
|
||||||
func (m *Model) SetWidth(w int) {
|
func (m *Model) SetWidth(w int) {
|
||||||
m.width = w
|
m.width = w
|
||||||
m.help.Width = w - 2
|
m.help.Width = w - 2
|
||||||
@@ -62,5 +73,29 @@ func (m Model) View() string {
|
|||||||
if m.flash != "" {
|
if m.flash != "" {
|
||||||
return style.Foreground(m.theme.Warn).Render(m.flash)
|
return style.Foreground(m.theme.Warn).Render(m.flash)
|
||||||
}
|
}
|
||||||
return style.Render(m.help.View(m.keys))
|
if m.help.ShowAll {
|
||||||
|
return style.Render(m.help.View(m.keys))
|
||||||
|
}
|
||||||
|
return style.Render(m.shortBar(m.width - 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
// shortBar puts the global keys on the right and as many of the others on the
|
||||||
|
// left as fit; the submodule keys are dropped first.
|
||||||
|
func (m Model) shortBar(w int) string {
|
||||||
|
h := m.help
|
||||||
|
h.Width = 0
|
||||||
|
right := h.ShortHelpView(m.keys.Global())
|
||||||
|
left := m.keys.Navigation()
|
||||||
|
if m.actions {
|
||||||
|
left = append(left[:len(left):len(left)], m.keys.Submodule()...)
|
||||||
|
}
|
||||||
|
for ; ; left = left[:len(left)-1] {
|
||||||
|
l := h.ShortHelpView(left)
|
||||||
|
if gap := w - lipgloss.Width(l) - lipgloss.Width(right); gap >= 2 || len(left) == 0 {
|
||||||
|
if gap < 0 { // not even the global keys fit
|
||||||
|
return ansi.Truncate(right, w, "…")
|
||||||
|
}
|
||||||
|
return l + strings.Repeat(" ", gap) + right
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-4
@@ -7,7 +7,7 @@ type keyMap struct {
|
|||||||
CollapseAll, ExpandAll key.Binding
|
CollapseAll, ExpandAll key.Binding
|
||||||
Home, End, PageUp, PageDown key.Binding
|
Home, End, PageUp, PageDown key.Binding
|
||||||
Update, Bump, Fetch, Reload, Filter, Issues key.Binding
|
Update, Bump, Fetch, Reload, Filter, Issues key.Binding
|
||||||
Help, Quit key.Binding
|
Copy, Help, Quit key.Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultKeys() keyMap {
|
func defaultKeys() keyMap {
|
||||||
@@ -29,16 +29,29 @@ func defaultKeys() keyMap {
|
|||||||
Reload: key.NewBinding(key.WithKeys("r"), key.WithHelp("r", "reload")),
|
Reload: key.NewBinding(key.WithKeys("r"), key.WithHelp("r", "reload")),
|
||||||
Filter: key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "filter")),
|
Filter: key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "filter")),
|
||||||
Issues: key.NewBinding(key.WithKeys("!"), key.WithHelp("!", "problems only")),
|
Issues: key.NewBinding(key.WithKeys("!"), key.WithHelp("!", "problems only")),
|
||||||
|
Copy: key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "copy path")),
|
||||||
Help: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "help")),
|
Help: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "help")),
|
||||||
Quit: key.NewBinding(key.WithKeys("q", "ctrl+c"), key.WithHelp("q", "quit")),
|
Quit: key.NewBinding(key.WithKeys("q", "ctrl+c"), key.WithHelp("q", "quit")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k keyMap) ShortHelp() []key.Binding {
|
// Navigation returns the bindings that move the selection or fold the tree.
|
||||||
|
func (k keyMap) Navigation() []key.Binding {
|
||||||
move := key.NewBinding(key.WithKeys("up"), key.WithHelp("↑↓/jk", "move"))
|
move := key.NewBinding(key.WithKeys("up"), key.WithHelp("↑↓/jk", "move"))
|
||||||
fold := key.NewBinding(key.WithKeys("left"), key.WithHelp("←→/hl", "fold"))
|
fold := key.NewBinding(key.WithKeys("left"), key.WithHelp("←→/hl", "fold"))
|
||||||
all := key.NewBinding(key.WithKeys("H"), key.WithHelp("⇧←→/HL", "fold all"))
|
all := key.NewBinding(key.WithKeys("H"), key.WithHelp("⇧←→/HL", "fold all"))
|
||||||
return []key.Binding{move, fold, all, k.Toggle, k.Help, k.Quit}
|
return []key.Binding{move, fold, all, k.Toggle}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submodule returns the bindings that act on the selected submodule. They
|
||||||
|
// are shown in the detail pane, or in the footer if the pane can't show them.
|
||||||
|
func (k keyMap) Submodule() []key.Binding { return []key.Binding{k.Copy} }
|
||||||
|
|
||||||
|
// Global returns the bindings that don't depend on the selection.
|
||||||
|
func (k keyMap) Global() []key.Binding { return []key.Binding{k.Help, k.Quit} }
|
||||||
|
|
||||||
|
func (k keyMap) ShortHelp() []key.Binding {
|
||||||
|
return append(append(k.Navigation(), k.Submodule()...), k.Global()...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k keyMap) FullHelp() [][]key.Binding {
|
func (k keyMap) FullHelp() [][]key.Binding {
|
||||||
@@ -46,7 +59,8 @@ func (k keyMap) FullHelp() [][]key.Binding {
|
|||||||
{k.Up, k.Down, k.Left, k.Right, k.Toggle},
|
{k.Up, k.Down, k.Left, k.Right, k.Toggle},
|
||||||
{k.CollapseAll, k.ExpandAll},
|
{k.CollapseAll, k.ExpandAll},
|
||||||
{k.Home, k.End, k.PageUp, k.PageDown},
|
{k.Home, k.End, k.PageUp, k.PageDown},
|
||||||
{k.Update, k.Bump, k.Fetch, k.Reload, k.Filter, k.Issues},
|
{k.Copy, k.Update, k.Bump},
|
||||||
|
{k.Fetch, k.Reload, k.Filter, k.Issues},
|
||||||
{k.Help, k.Quit},
|
{k.Help, k.Quit},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package theme
|
||||||
|
|
||||||
|
import "github.com/charmbracelet/bubbles/help"
|
||||||
|
|
||||||
|
// NewHelp returns a help renderer in the theme's colors.
|
||||||
|
func NewHelp(t Theme) help.Model {
|
||||||
|
h := help.New()
|
||||||
|
h.Styles.ShortKey = Fg(t.Text).Bold(true)
|
||||||
|
h.Styles.ShortDesc = Fg(t.Muted)
|
||||||
|
h.Styles.ShortSeparator = Fg(t.Border)
|
||||||
|
h.Styles.FullKey = h.Styles.ShortKey
|
||||||
|
h.Styles.FullDesc = h.Styles.ShortDesc
|
||||||
|
h.Styles.FullSeparator = h.Styles.ShortSeparator
|
||||||
|
return h
|
||||||
|
}
|
||||||
@@ -72,6 +72,8 @@ g "$w" push origin main
|
|||||||
g "$root/work/log" submodule add "$root/remotes/nested.git" modules/nested
|
g "$root/work/log" submodule add "$root/remotes/nested.git" modules/nested
|
||||||
g "$root/work/log" commit -m "Add nested"
|
g "$root/work/log" commit -m "Add nested"
|
||||||
g "$root/work/log" push origin main
|
g "$root/work/log" push origin main
|
||||||
|
# "archive" ties with main (same tip, sorts first) -> the preferred ref wins
|
||||||
|
g "$root/work/log" push origin main:archive
|
||||||
|
|
||||||
super="$root/super"
|
super="$root/super"
|
||||||
git init -q "$super"
|
git init -q "$super"
|
||||||
@@ -88,6 +90,7 @@ add ui libs/staged
|
|||||||
add net libs/missing
|
add net libs/missing
|
||||||
add ui vendor
|
add ui vendor
|
||||||
add work libs/work
|
add work libs/work
|
||||||
|
add feature libs/topic -b main
|
||||||
g "$super" submodule update --init --recursive
|
g "$super" submodule update --init --recursive
|
||||||
g "$super" commit -m "Add submodules"
|
g "$super" commit -m "Add submodules"
|
||||||
|
|
||||||
@@ -109,6 +112,13 @@ g "$super/libs/feature" checkout origin/feat/search~1
|
|||||||
g "$super" add libs/feature
|
g "$super" add libs/feature
|
||||||
g "$super" commit -m "Pin feature search"
|
g "$super" commit -m "Pin feature search"
|
||||||
|
|
||||||
|
# libs/topic: .gitmodules says main, but the pin is the tip of feat/search
|
||||||
|
# -> the closest branch wins over the configured one
|
||||||
|
g "$super/libs/topic" fetch
|
||||||
|
g "$super/libs/topic" checkout origin/feat/search
|
||||||
|
g "$super" add libs/topic
|
||||||
|
g "$super" commit -m "Pin topic search"
|
||||||
|
|
||||||
# src/core: checkout moved away from the pin -> off pin
|
# src/core: checkout moved away from the pin -> off pin
|
||||||
g "$super/src/core" checkout HEAD~1
|
g "$super/src/core" checkout HEAD~1
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user