Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changes/22.enh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Every error now ends with how to fix it: an already checked-out branch points at `wt go`, config errors name both files the bad value could live in, unknown config keys and failing setup/refresh hooks say what to change, and a failed `wt sync` fast-forward lists the local commits in the way. (#22)
2 changes: 1 addition & 1 deletion PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -783,7 +783,7 @@ stays short):
(Homebrew cask detected through the PATH symlink;
the rc file's actual eval lines quoted by line number).
- [ ] End-to-end `brew uninstall` re-verification (needs the `v0.1.0` cask).
- [ ] Failure-message audit: every error names the fix
- [x] Failure-message audit: every error names the fix
(`error: branch 'x' already checked out in ../acme.trees/x — wt go x?`).
- [ ] Cut `v0.1.0` — the first real release:
the cask publishes to the tap;
Expand Down
3 changes: 2 additions & 1 deletion internal/cli/claim.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ func runClaim(cmd *cobra.Command, branch, baseFlag string, noFetch bool) error {
return err
}
if t, ok := treeHoldingBranch(trees, branch); ok {
return preconditionf("branch %q is already checked out in %s", branch, t.Path)
return preconditionf(
"branch %q is already checked out in %s — `wt go %s` jumps there", branch, t.Path, branch)
}
if err := checkBase(ctx, p.g, base); err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func runConfigEdit(cmd *cobra.Command) error {
edit.Stdout = os.Stdout
edit.Stderr = os.Stderr
if err := edit.Run(); err != nil {
return fmt.Errorf("editor %q: %w", editor, err)
return fmt.Errorf("editor %q: %w — set $VISUAL or $EDITOR to a working editor", editor, err)
}

// Validate right away: a typo surfaces now, at the terminal
Expand Down
8 changes: 6 additions & 2 deletions internal/cli/copies.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func copyFiles(
continue
}
if err != nil {
return fmt.Errorf("copy %s: %w", name, err)
return copyError(name, err)
}
fmt.Fprintf(chatter, "copy: %s\n", name)
}
Expand Down Expand Up @@ -147,7 +147,11 @@ func readCopy(root, name string) (data []byte, ok bool, err error) {
return nil, false, nil
}
if err != nil {
return nil, false, fmt.Errorf("copy %s: %w", name, err)
return nil, false, copyError(name, err)
}
return data, true, nil
}

func copyError(name string, err error) error {
return fmt.Errorf("copy %s: %w — fix the file, or drop it from copy in wt.toml", name, err)
}
3 changes: 2 additions & 1 deletion internal/cli/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ func runDoctor(cmd *cobra.Command, info BuildInfo, jsonOut, offline bool) error
// No locational claim in the message: under --json the fixes are
// in the payload, not "above".
if view.Issues > 0 {
return preconditionf("%d %s found", view.Issues, plural(view.Issues, "issue"))
return preconditionf("%d %s found — each fix is listed above",
view.Issues, plural(view.Issues, "issue"))
}
return nil
}
Expand Down
3 changes: 1 addition & 2 deletions internal/cli/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,7 @@ func provisionInitialPool(ctx context.Context, r *repo.Repo, chatter io.Writer)
// exists: a resize to the size already configured returns
// early, while every claim provisions what it finds missing.
return fmt.Errorf(
"%w — the config is saved; fix the cause (`wt config --edit` for hooks), "+
"and the missing slots provision on the next claim", err)
"%w; the config is saved, and the missing slots provision on the next claim", err)
}
return nil
}
Expand Down
3 changes: 2 additions & 1 deletion internal/cli/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ func runNew(cmd *cobra.Command, branch, baseFlag string, noFetch bool) error {
// R4: when the branch lives in some tree already,
// the error must point straight at it.
if t, ok := treeHoldingBranch(trees, branch); ok {
return preconditionf("branch %q is already checked out in %s", branch, t.Path)
return preconditionf("branch %q is already checked out in %s — `wt go %s` jumps there",
branch, t.Path, branch)
}
// In pool mode the natural resume for an existing branch,
// say one left by a claim that failed after its branch
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func runRoot(cmd *cobra.Command, _ []string) error {
err := runJump(cmd, "")
var notRepo *repo.NotARepoError
if errors.As(err, &notRepo) {
return fmt.Errorf("%w — `wt --help` shows usage", err)
return fmt.Errorf("%w; `wt --help` shows usage", err)
}
return err
}
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/shellinit.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func newShellInitCmd() *cobra.Command {
Short: "Emit the shell integration for eval in ~/.zshrc",
Args: cobra.MatchAll(cobra.ExactArgs(1), func(_ *cobra.Command, args []string) error {
if args[0] != "zsh" {
return fmt.Errorf("unsupported shell %q — v1 supports zsh only", args[0])
return fmt.Errorf("unsupported shell %q — wt supports zsh only: `wt shell-init zsh`", args[0])
}
return nil
}),
Expand Down
3 changes: 2 additions & 1 deletion internal/cli/slots.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,8 @@ func (p *poolRepo) pinForRelease(
// lease reaches Repin as "expect nothing" without further ado.
held, err := lease.Get(leases, slot)
if err == nil && held == nil && claimRequired {
return nil, preconditionf("%s is not claimed — nothing to release", slot)
return nil, preconditionf(
"%s is not claimed — nothing to release; `wt pool ls` shows the claimed slots", slot)
}
pinned, err := lease.Repin(leases, slot, branch, held)
if err != nil {
Expand Down
6 changes: 4 additions & 2 deletions internal/cli/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ func runSync(cmd *cobra.Command, all bool) error {
}

if err := g.Fetch(ctx, remote); err != nil {
return fmt.Errorf("fetching %s: %w", remote, err)
return fmt.Errorf("fetching %s: %w — check `git remote -v` and your network, then rerun",
remote, err)
}
fmt.Fprintf(chatter, "fetched %s\n", remote)
// Best-effort: the fetch already succeeded, so a state-write
Expand Down Expand Up @@ -144,7 +145,8 @@ func fastForwardBase(
}
if ffErr != nil {
return preconditionf(
"%s could not fast-forward to %s (%v) — resolve it by hand", base, up, ffErr)
"%s could not fast-forward to %s (%v) — `git log %s..%s` lists the local commits "+
"in the way; move them to a branch, then rerun", base, up, ffErr, up, base)
}
fmt.Fprintf(chatter, "%s fast-forwarded %s to %s\n", base, commits(behind), up)
return nil
Expand Down
47 changes: 47 additions & 0 deletions internal/cli/testdata/script/errors-name-fix.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Every error names its fix: the user should never have to guess
# the next command. One case per family of failure.

[!exec:git] skip 'git not on PATH'

# outside a repository
exitcode 4 wt ls
stderr 'not inside a git repository — cd into one first'

# unsupported shell
exitcode 2 wt shell-init bash
stderr 'wt supports zsh only: `wt shell-init zsh`'

exec git init -q -b main acme
cd acme
exec git commit -q --allow-empty -m 'initial'
exec wt init --yes

# a branch already checked out points at the tree that holds it
exec wt new feature/login
exitcode 3 wt new feature/login
stderr 'already checked out in .*feature-login — `wt go feature/login` jumps there'

# an invalid config value names both files it could live in
cp $WORK/bad-staleness.toml .git/wt.toml
exitcode 1 wt config
stderr 'staleness_hours must not be negative, got -1 — fix it in .*\.git[/\\]wt\.toml or '

# an unknown key says what to do about it
cp $WORK/unknown-key.toml .git/wt.toml
exitcode 1 wt config
stderr 'unknown key "colour" .* — remove it or fix its spelling'

# a failing setup hook names the key to fix
cp $WORK/bad-hook.toml .git/wt.toml
! exec wt new feature/pay
stderr 'setup hook failed: .* — fix hooks.setup with `wt config --edit`'

-- bad-staleness.toml --
staleness_hours = -1

-- unknown-key.toml --
colour = "auto"

-- bad-hook.toml --
[hooks]
setup = "exit 7"
3 changes: 2 additions & 1 deletion internal/cli/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ func resolveTree(ctx context.Context, trees []gitx.Worktree, name string) (gitx.
if t, ok := findTree(trees, top); ok {
return t, nil
}
return gitx.Worktree{}, fmt.Errorf("git does not list the current tree %s", top)
return gitx.Worktree{}, fmt.Errorf(
"git does not list the current tree %s — `git worktree repair` re-registers it", top)
}
// Every tree is a candidate here, bare entries included:
// exact-name commands may need to name states a jump never
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/warm.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func finishFresh(
}
fmt.Fprintf(chatter, "running setup hook: %s\n", setup)
if err := runHook(ctx, dest, setup, chatter); err != nil {
return fmt.Errorf("setup hook failed: %w", err)
return fmt.Errorf("setup hook failed: %w — fix hooks.setup with `wt config --edit`", err)
}
if files := cfg.Hooks.RefreshIfChanged; len(files) > 0 {
hash, err := pool.Hash(dest, files)
Expand Down Expand Up @@ -76,7 +76,7 @@ func refreshTree(
}
fmt.Fprintf(chatter, "running refresh hook: %s\n", refresh)
if err := runHook(ctx, dest, refresh, chatter); err != nil {
return fmt.Errorf("refresh hook failed: %w", err)
return fmt.Errorf("refresh hook failed: %w — fix hooks.refresh with `wt config --edit`", err)
}
if len(files) > 0 {
return st.WriteRefreshHash(name, current)
Expand Down
2 changes: 1 addition & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func Load(globalPath, repoPath string) (Config, error) {
normalize(&cfg)

if err := validate(cfg); err != nil {
return Config{}, err
return Config{}, fmt.Errorf("%w — fix it in %s or %s", err, repoPath, globalPath)
}
return cfg, nil
}
Expand Down
3 changes: 2 additions & 1 deletion internal/config/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ func positionedError(path string, err error) error {
// beats a wall of positions.
e := strict.Errors[0]
row, col := e.Position()
return fmt.Errorf("%s:%d:%d: unknown key %q (not part of wt's config)",
return fmt.Errorf(
"%s:%d:%d: unknown key %q (not part of wt's config) — remove it or fix its spelling",
path, row, col, strings.Join(e.Key(), "."))
}
var decode *toml.DecodeError
Expand Down
5 changes: 3 additions & 2 deletions internal/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ func Find(ctx context.Context, dir string) (*Repo, error) {
common, bare := lines[0], lines[1] == "true"
if bare {
return nil, fmt.Errorf(
"%s is a bare repository; wt needs a main checkout to anchor config and trees", common)
"%s is a bare repository; wt needs a main checkout to anchor config and trees — "+
"run wt from a clone with a working tree", common)
}
if filepath.Base(common) == ".git" {
return &Repo{CommonDir: common, Root: filepath.Dir(common)}, nil
Expand Down Expand Up @@ -143,7 +144,7 @@ type NotARepoError struct {
}

func (e *NotARepoError) Error() string {
return fmt.Sprintf("%s is not inside a git repository", e.Dir)
return fmt.Sprintf("%s is not inside a git repository — cd into one first", e.Dir)
}

// WtExitCode maps to D13's "not a wt repo" contract code;
Expand Down
Loading