Skip to main content

gwm/
error.rs

1use thiserror::Error;
2
3pub type Result<T> = std::result::Result<T, GwmError>;
4
5/// Which side of a `gwm link` / `gwm open` pair is missing on a branch.
6/// Carried by [`GwmError::LinkMissing`] so the user sees whether the
7/// issue or the PR slot is empty — both share the same git-config
8/// shape (`branch.<name>.gwm-issue` / `branch.<name>.gwm-pr`) so the
9/// error message must spell out which one was queried.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum LinkKind {
12  Issue,
13  Pr,
14}
15
16impl LinkKind {
17  fn as_str(self) -> &'static str {
18    match self {
19      LinkKind::Issue => "issue",
20      LinkKind::Pr => "PR",
21    }
22  }
23}
24
25impl std::fmt::Display for LinkKind {
26  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27    f.write_str(self.as_str())
28  }
29}
30
31#[derive(Debug, Error)]
32pub enum GwmError {
33  #[error("not inside a git repository")]
34  NotInGitRepo,
35
36  #[error("git error: {0}")]
37  Git(#[from] git2::Error),
38
39  #[error("io error: {0}")]
40  Io(#[from] std::io::Error),
41
42  #[error("toml parse error: {0}")]
43  TomlParse(#[from] toml::de::Error),
44
45  #[error("toml serialize error: {0}")]
46  TomlSer(#[from] toml::ser::Error),
47
48  #[error("regex error: {0}")]
49  Regex(#[from] regex::Error),
50
51  #[error("shell expand error: {0}")]
52  ShellExpand(#[from] shellexpand::LookupError<std::env::VarError>),
53
54  #[error("invalid branch type '{got}' (allowed: {allowed})")]
55  InvalidBranchType { got: String, allowed: String },
56
57  #[error("invalid issue number '{0}' (digits only)")]
58  InvalidIssue(String),
59
60  #[error("invalid description '{0}' (kebab-case alphanumeric)")]
61  InvalidDescription(String),
62
63  #[error("worktree '{0}' not found")]
64  WorktreeNotFound(String),
65
66  #[error("worktree '{0}' already exists at {1}")]
67  WorktreeExists(String, String),
68
69  /// `gwm create` refuses to silently reuse a pre-existing local branch
70  /// (issue #99). The caller must opt in explicitly (`--reuse-branch` /
71  /// `reuse_branch: true`) to attach the new worktree to the existing
72  /// branch tip; otherwise this surfaces so the user can delete the
73  /// stale ref or rename their request rather than ending up on
74  /// whatever commit the stale branch resurrected.
75  #[error(
76    "branch '{name}' already exists at {oid} — pass --reuse-branch to attach the worktree to it, or delete the stale branch first"
77  )]
78  BranchExists { name: String, oid: String },
79
80  #[error("guard '{name}' tripped: file {file} matches deny pattern")]
81  GuardTripped { name: String, file: String },
82
83  /// Generic command/spawn failure. The variant is shared between
84  /// every subcommand that shells out (bootstrap steps, `gwm tmux`,
85  /// `gwm zellij`, the `git log` / `git status` previews in the TUI
86  /// sidebar, …); callers prepend their own operation name into the
87  /// inner string so the rendered message stays attributable to the
88  /// verb the user actually typed.
89  #[error("command failed: {0}")]
90  CommandFailed(String),
91
92  #[error("config error: {0}")]
93  Config(String),
94
95  /// Issue #105: HEAD is unborn (no commits yet) or detached when a
96  /// command that needs the current branch shorthand is invoked.
97  /// Split out of `Other` so callers (and the TUI status line) can
98  /// distinguish "no current branch" from arbitrary string errors.
99  #[error("{reason}")]
100  UnbornHead { reason: String },
101
102  /// Issue #105: failed to deserialize a `gh` CLI JSON payload.
103  /// `kind` names the payload (`"issue"`, `"pr"`, `"pr list"`,
104  /// `"labels"`, `"milestones"`) so the user can grep for which
105  /// gh contract changed; `source` carries the underlying
106  /// `serde_json::Error` for downstream introspection.
107  #[error("failed to parse {kind} json: {source}")]
108  GhJsonParse {
109    kind: &'static str,
110    #[source]
111    source: serde_json::Error,
112  },
113
114  /// Issue #38: failed to serialize a `--format=json` / daemon JSON-RPC
115  /// payload. The output DTOs in `json_api` are plain structs so this is
116  /// effectively unreachable, but surfacing it as a typed error keeps the
117  /// JSON output paths off `unwrap`/`expect` (CLAUDE.md house rule).
118  #[error("failed to serialize json output: {0}")]
119  JsonSerialize(#[from] serde_json::Error),
120
121  /// Issue #105: `gwm open` / `gwm link` was asked for an issue or PR
122  /// linked to a branch but no such link is recorded in git-config.
123  /// `kind` names which side (issue vs PR) is missing; `branch` is
124  /// the branch shorthand the user queried.
125  #[error("no {kind} linked to branch '{branch}'")]
126  LinkMissing { kind: LinkKind, branch: String },
127
128  /// Issue #36: `--workspace <dir>` pointed at a directory that holds no
129  /// git repos directly below it. Surfaced rather than opening an empty
130  /// table / TUI so the user can tell a wrong path from an empty root.
131  #[error("no git repos found directly under workspace root '{root}'")]
132  EmptyWorkspace { root: String },
133
134  /// Issue #36: `gwm create` in workspace mode is ambiguous without an
135  /// explicit target repo — list the candidates so the user can pick one.
136  #[error("workspace mode: `gwm create` requires --repo <name> (one of: {available})")]
137  WorkspaceRepoRequired { available: String },
138
139  /// Issue #36: `--repo <name>` named a repo that is not present directly
140  /// under the workspace root.
141  #[error("repo '{name}' not found in workspace (available: {available})")]
142  WorkspaceRepoNotFound { name: String, available: String },
143
144  /// Issue #36: `--workspace` is a global flag, so clap accepts it for every
145  /// subcommand, but only `list`, `create` and bare `gwm` (the TUI) implement
146  /// it. Reject it elsewhere rather than silently ignoring it and acting on
147  /// the current single repo — a wrong-target footgun for destructive
148  /// commands like `gwm remove` (Codex review #303 P2).
149  #[error("--workspace is only supported with `gwm list`, `gwm create`, `gwm exec`, `gwm clean`, or bare `gwm` (the TUI) — refusing to run this subcommand against a single repo")]
150  WorkspaceUnsupportedCommand,
151
152  #[error("{0}")]
153  Other(String),
154}
155
156impl From<anyhow::Error> for GwmError {
157  fn from(e: anyhow::Error) -> Self {
158    GwmError::Other(e.to_string())
159  }
160}