-
Notifications
You must be signed in to change notification settings - Fork 203
Expand file tree
/
Copy pathmod.rs
More file actions
2134 lines (1647 loc) · 83.5 KB
/
Copy pathmod.rs
File metadata and controls
2134 lines (1647 loc) · 83.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
mod config;
mod hook;
mod list;
mod step;
pub(crate) use config::{
ApprovalsCommand, CiStatusAction, ConfigAliasCommand, ConfigCommand,
ConfigPluginsClaudeCommand, ConfigPluginsCodexCommand, ConfigPluginsCommand,
ConfigPluginsOpencodeCommand, ConfigShellCommand, DefaultBranchAction, HintsAction, LogsAction,
MarkerAction, PreviousBranchAction, StateCommand, StateWrite, VarsAction,
};
pub(crate) use hook::{HOOK_TYPE_NAMES, HookCommand, HookOptions, parse_hook_type};
pub(crate) use list::ListSubcommand;
pub(crate) use step::StepCommand;
use clap::builder::styling::{AnsiColor, Color, Styles};
use clap::{Args, Command, CommandFactory, Parser, Subcommand, ValueEnum};
use std::ffi::OsString;
use std::sync::OnceLock;
use crate::commands::Shell;
/// Parse KEY=VALUE string for `wt config state vars set`.
///
/// Like `parse_key_val`, but without hyphen→underscore canonicalization.
/// Key validation is deferred to `validate_vars_key` in the command handler.
pub(super) fn parse_vars_assignment(s: &str) -> Result<(String, String), String> {
let (key, value) = s
.split_once('=')
.ok_or_else(|| format!("invalid KEY=VALUE: no `=` found in `{s}`"))?;
if key.is_empty() {
return Err("invalid KEY=VALUE: key cannot be empty".to_string());
}
Ok((key.to_string(), value.to_string()))
}
/// Custom styles for help output - matches worktrunk's color scheme
pub(crate) fn help_styles() -> Styles {
Styles::styled()
.header(
anstyle::Style::new()
.bold()
.fg_color(Some(Color::Ansi(AnsiColor::Green))),
)
.usage(
anstyle::Style::new()
.bold()
.fg_color(Some(Color::Ansi(AnsiColor::Green))),
)
.literal(
anstyle::Style::new()
.bold()
.fg_color(Some(Color::Ansi(AnsiColor::Cyan))),
)
.placeholder(anstyle::Style::new().fg_color(Some(Color::Ansi(AnsiColor::Cyan))))
.error(
anstyle::Style::new()
.bold()
.fg_color(Some(Color::Ansi(AnsiColor::Red))),
)
.valid(
anstyle::Style::new()
.bold()
.fg_color(Some(Color::Ansi(AnsiColor::Green))),
)
.invalid(
anstyle::Style::new()
.bold()
.fg_color(Some(Color::Ansi(AnsiColor::Yellow))),
)
}
/// Default command name for worktrunk
const DEFAULT_COMMAND_NAME: &str = "wt";
/// Help template for commands
const HELP_TEMPLATE: &str = "\
{before-help}{name} - {about-with-newline}
Usage: {usage}
{all-args}{after-help}";
/// Cached value_name for Shell enum (e.g., "bash|fish|zsh|powershell")
///
/// TODO: There should be a simpler way to show ValueEnum variants in clap's "missing required
/// argument" error. Clap auto-generates `[possible values: ...]` in help and completions from
/// ValueEnum, but doesn't use it for value_name. We use mut_subcommand to set it dynamically,
/// but this feels overly complex. Revisit if clap adds better support.
fn shell_value_name() -> &'static str {
static CACHE: OnceLock<String> = OnceLock::new();
CACHE
.get_or_init(|| {
Shell::value_variants()
.iter()
.filter_map(|v| v.to_possible_value())
.map(|v| v.get_name().to_owned())
.collect::<Vec<_>>()
.join("|")
})
.as_str()
}
/// Build a clap Command for Cli with the shared help template applied recursively.
pub(crate) fn build_command() -> Command {
let cmd = apply_help_template_recursive(Cli::command(), DEFAULT_COMMAND_NAME);
// Set value_name for Shell args to show options in usage/errors
let shell_name = shell_value_name();
cmd.mut_subcommand("config", |c| {
c.mut_subcommand("shell", |c| {
c.mut_subcommand("init", |c| c.mut_arg("shell", |a| a.value_name(shell_name)))
.mut_subcommand("install", |c| {
c.mut_arg("shell", |a| a.value_name(shell_name))
})
.mut_subcommand("uninstall", |c| {
c.mut_arg("shell", |a| a.value_name(shell_name))
})
})
})
}
/// Parent commands whose subcommands can be suggested for unrecognized top-level commands.
const NESTED_COMMAND_PARENTS: &[&str] = &["step", "hook"];
/// Check if an unrecognized subcommand matches a nested subcommand.
///
/// Returns the full command path if found, e.g., "wt step squash" for "squash".
pub(crate) fn suggest_nested_subcommand(cmd: &Command, unknown: &str) -> Option<String> {
for parent in NESTED_COMMAND_PARENTS {
if let Some(parent_cmd) = cmd.get_subcommands().find(|c| c.get_name() == *parent)
&& parent_cmd
.get_subcommands()
.any(|s| s.get_name() == unknown)
{
return Some(format!("wt {parent} {unknown}"));
}
}
// Hook types aren't clap subcommands of `hook` (they're caught by
// `external_subcommand`), so the structural search above misses them.
// Check the canonical name list directly so `wt pre-merge` → `wt hook
// pre-merge` still suggests correctly.
if HOOK_TYPE_NAMES.contains(&unknown) {
return Some(format!("wt hook {unknown}"));
}
None
}
fn apply_help_template_recursive(mut cmd: Command, path: &str) -> Command {
cmd = cmd.help_template(HELP_TEMPLATE).display_name(path);
for sub in cmd.get_subcommands_mut() {
let sub_cmd = std::mem::take(sub);
let sub_path = format!("{} {}", path, sub_cmd.get_name());
let sub_cmd = apply_help_template_recursive(sub_cmd, &sub_path);
*sub = sub_cmd;
}
cmd
}
/// Get the version string for display.
///
/// Returns the git describe version if available (e.g., "v0.8.5-3-gabcdef"),
/// otherwise falls back to the cargo package version (e.g., "0.8.5").
pub(crate) fn version_str() -> &'static str {
static VERSION: OnceLock<String> = OnceLock::new();
VERSION.get_or_init(|| {
let git_version = env!("VERGEN_GIT_DESCRIBE");
let cargo_version = env!("CARGO_PKG_VERSION");
if git_version.contains("IDEMPOTENT") {
cargo_version.to_string()
} else {
git_version.to_string()
}
})
}
/// Output format for commands with text + JSON modes (e.g., `wt switch`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub(crate) enum SwitchFormat {
/// Human-readable text output
Text,
/// JSON output
Json,
}
// TODO: ClaudeCode is statusline-specific but lives in this shared enum, forcing
// unrelated codepaths to handle it. Consider a dedicated StatuslineFormat enum.
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub(crate) enum OutputFormat {
/// Human-readable table format
Table,
/// JSON output
Json,
/// Claude Code statusline mode (reads context from stdin)
#[value(name = "claude-code")]
ClaudeCode,
}
#[derive(Parser)]
#[command(name = "wt")]
#[command(about = "Git worktree management for parallel AI agent workflows", long_about = None)]
#[command(version = version_str())]
#[command(disable_help_subcommand = true)]
#[command(styles = help_styles())]
#[command(arg_required_else_help = true)]
// Disable clap's text wrapping - we handle wrapping in the markdown renderer.
// This prevents clap from breaking markdown tables by wrapping their rows.
#[command(term_width = 0)]
#[command(after_long_help = "\
Getting started
wt switch --create feature # Create worktree and branch
wt switch feature # Switch to worktree
wt list # Show all worktrees
wt remove # Remove worktree; delete branch if merged
Run `wt config shell install` to set up directory switching.
Run `wt config create` to customize worktree locations.
Docs: https://worktrunk.dev
GitHub: https://github.com/max-sixty/worktrunk")]
pub(crate) struct Cli {
/// Working directory for this command
#[arg(
short = 'C',
global = true,
value_name = "path",
display_order = 100,
help_heading = "Global Options"
)]
pub directory: Option<std::path::PathBuf>,
/// User config file path
#[arg(
long,
global = true,
value_name = "path",
display_order = 101,
help_heading = "Global Options"
)]
pub config: Option<std::path::PathBuf>,
/// Verbose output (-v: info logs + hook/alias template variable & output; -vv: debug logs + diagnostic report + trace.log/output.log under .git/wt/logs/)
#[arg(
long,
short = 'v',
global = true,
action = clap::ArgAction::Count,
display_order = 102,
help_heading = "Global Options"
)]
pub verbose: u8,
/// Skip approval prompts
#[arg(
long,
short = 'y',
global = true,
display_order = 103,
help_heading = "Global Options"
)]
pub yes: bool,
#[command(subcommand)]
pub command: Option<Commands>,
}
/// Shared `--no-hooks` / `--no-verify` flags for commands that resolve hook
/// skipping to a plain `bool` (`switch`, `remove`, `step commit`,
/// `step squash`).
///
/// `wt merge` does not flatten this struct: its hooks flag is tri-state
/// (`Option<bool>`, so config `[merge] verify` can still apply) and it carries
/// a positive `--verify` override. It declares its own flags but routes the
/// `--no-verify` deprecation through `crate::warn_no_verify_deprecated`, so the
/// warning text lives in exactly one place.
#[derive(Args)]
pub(crate) struct HookFlags {
/// Skip hooks
#[arg(long = "no-hooks", action = clap::ArgAction::SetFalse, default_value_t = true, help_heading = "Automation")]
pub(crate) verify: bool,
/// Skip hooks (deprecated alias for --no-hooks)
#[arg(long = "no-verify", hide = true)]
pub(crate) no_verify_deprecated: bool,
}
impl HookFlags {
/// Resolve to the effective verify value, emitting the deprecation warning
/// once if `--no-verify` was used.
pub(crate) fn resolve(&self) -> bool {
if self.no_verify_deprecated {
crate::warn_no_verify_deprecated();
false
} else {
self.verify
}
}
}
#[derive(Args)]
pub(crate) struct SwitchArgs {
/// Branch name or shortcut
///
/// Opens interactive picker if omitted.
/// Shortcuts: '^' (default branch), '-' (previous), '@' (current), 'pr:{N}' (GitHub PR), 'mr:{N}' (GitLab MR)
#[arg(add = crate::completion::worktree_branch_completer())]
pub(crate) branch: Option<String>,
/// Include branches without worktrees
#[arg(long, help_heading = "Picker Options", conflicts_with_all = ["create", "base", "execute", "execute_args", "clobber"])]
pub(crate) branches: bool,
/// Include remote branches
#[arg(long, help_heading = "Picker Options", conflicts_with_all = ["create", "base", "execute", "execute_args", "clobber"])]
pub(crate) remotes: bool,
/// Create a new branch
#[arg(short = 'c', long, requires = "branch")]
pub(crate) create: bool,
/// Base branch
///
/// Defaults to default branch. Supports the same shortcuts as the branch
/// argument: `^`, `@`, `-`, `pr:{N}`, `mr:{N}`.
#[arg(short = 'b', long, requires = "branch", add = crate::completion::branch_value_completer())]
pub(crate) base: Option<String>,
/// Command to run after switch
///
/// Replaces the wt process with the command after switching, giving
/// it full terminal control. Useful for launching editors, AI agents,
/// or other interactive tools.
///
/// Supports [hook template variables](@/hook.md#template-variables)
/// (`{{ branch }}`, `{{ worktree_path }}`, etc.) and filters.
/// `{{ base }}` and `{{ base_worktree_path }}` require `--create`.
///
/// Especially useful with shell aliases:
///
/// ```sh
/// alias wsc='wt switch --create -x claude'
/// wsc feature-branch -- 'Fix GH #322'
/// ```
///
/// Then `wsc feature-branch` creates the worktree and launches Claude
/// Code. Arguments after `--` are passed to the command, so
/// `wsc feature -- 'Fix GH #322'` runs `claude 'Fix GH #322'`,
/// starting Claude with a prompt.
///
/// Template example: `-x 'code {{ worktree_path }}'` opens VS Code
/// at the worktree, `-x 'tmux new -s {{ branch | sanitize }}'` starts
/// a tmux session named after the branch.
#[arg(short = 'x', long, requires = "branch")]
pub(crate) execute: Option<String>,
/// Additional arguments for --execute command (after --)
///
/// Arguments after `--` are appended to the execute command.
/// Each argument is expanded for templates, then POSIX shell-escaped.
#[arg(last = true, requires = "execute")]
pub(crate) execute_args: Vec<String>,
/// Remove stale paths at target
#[arg(long, requires = "branch")]
pub(crate) clobber: bool,
/// Skip directory change after switching
///
/// Hooks still run normally. Useful when hooks handle navigation
/// (e.g., tmux workflows) or for CI/automation. Use --cd to override.
#[arg(long, overrides_with = "cd")]
pub(crate) no_cd: bool,
/// Change directory after switching
#[arg(long, overrides_with = "no_cd", hide = true)]
pub(crate) cd: bool,
#[command(flatten)]
pub(crate) hooks: HookFlags,
/// Output format
///
/// JSON prints structured result to stdout. Designed for tool
/// integration (e.g., Claude Code WorktreeCreate hooks).
#[arg(long, default_value = "text", help_heading = "Automation")]
pub(crate) format: SwitchFormat,
}
#[derive(Args)]
pub(crate) struct ListArgs {
#[command(subcommand)]
pub(crate) subcommand: Option<ListSubcommand>,
/// Output format (table, json)
#[arg(long, value_enum, default_value = "table", hide_possible_values = true)]
pub(crate) format: OutputFormat,
/// Include branches without worktrees
#[arg(long)]
pub(crate) branches: bool,
/// Include remote branches
#[arg(long)]
pub(crate) remotes: bool,
/// Show CI, diff analysis, and LLM summaries
#[arg(long)]
pub(crate) full: bool,
/// Show fast info immediately, update with slow info
///
/// Displays local data (branches, paths, status) first, then updates
/// with remote data (CI, upstream) as it arrives. Use --no-progressive
/// to force buffered rendering. Auto-enabled for TTY.
#[arg(long, overrides_with = "no_progressive")]
pub(crate) progressive: bool,
/// Force buffered rendering
#[arg(long = "no-progressive", overrides_with = "progressive", hide = true)]
pub(crate) no_progressive: bool,
}
#[derive(Args)]
pub(crate) struct RemoveArgs {
/// Branch name [default: current]
#[arg(add = crate::completion::local_branches_completer())]
pub(crate) branches: Vec<String>,
/// Keep branch after removal
#[arg(long = "no-delete-branch", overrides_with = "delete_branch")]
pub(crate) no_delete_branch: bool,
/// Delete branch after removal (overrides config `[remove] delete-branch = false`)
#[arg(
long = "delete-branch",
overrides_with = "no_delete_branch",
hide = true
)]
pub(crate) delete_branch: bool,
/// Delete unmerged branches
#[arg(short = 'D', long = "force-delete")]
pub(crate) force_delete: bool,
/// Run removal in foreground (block until complete)
#[arg(long)]
pub(crate) foreground: bool,
#[command(flatten)]
pub(crate) hooks: HookFlags,
/// Force worktree removal
///
/// Remove worktrees even if they contain untracked files (like build
/// artifacts). Without this flag, removal fails if untracked files exist.
#[arg(short, long)]
pub(crate) force: bool,
/// Output format
///
/// JSON prints structured result to stdout after removal completes.
#[arg(long, default_value = "text", help_heading = "Automation")]
pub(crate) format: SwitchFormat,
}
#[derive(Args)]
pub(crate) struct MergeArgs {
/// Target branch
///
/// Defaults to default branch.
#[arg(add = crate::completion::branch_value_completer())]
pub(crate) target: Option<String>,
/// Force commit squashing
#[arg(long, overrides_with = "no_squash", hide = true)]
pub(crate) squash: bool,
/// Skip commit squashing
#[arg(long = "no-squash", overrides_with = "squash")]
pub(crate) no_squash: bool,
/// Force commit and squash
#[arg(long, overrides_with = "no_commit", hide = true)]
pub(crate) commit: bool,
/// Skip commit and squash
#[arg(long = "no-commit", overrides_with = "commit")]
pub(crate) no_commit: bool,
/// Force rebasing onto target
#[arg(long, overrides_with = "no_rebase", hide = true)]
pub(crate) rebase: bool,
/// Skip rebase (fail if not already rebased)
#[arg(long = "no-rebase", overrides_with = "rebase")]
pub(crate) no_rebase: bool,
/// Force worktree removal after merge
#[arg(long, overrides_with = "no_remove", hide = true)]
pub(crate) remove: bool,
/// Keep worktree after merge
#[arg(long = "no-remove", overrides_with = "remove")]
pub(crate) no_remove: bool,
/// Create a merge commit (no fast-forward)
#[arg(long = "no-ff", overrides_with = "ff")]
pub(crate) no_ff: bool,
/// Allow fast-forward (default)
#[arg(long, overrides_with = "no_ff", hide = true)]
pub(crate) ff: bool,
/// Force running hooks
#[arg(long, overrides_with_all = ["no_hooks", "no_verify"], hide = true)]
pub(crate) verify: bool,
/// Skip hooks
#[arg(
long = "no-hooks",
overrides_with_all = ["verify", "no_verify"],
help_heading = "Automation"
)]
pub(crate) no_hooks: bool,
/// Skip hooks (deprecated alias for --no-hooks)
#[arg(long = "no-verify", overrides_with_all = ["verify", "no_hooks"], hide = true)]
pub(crate) no_verify: bool,
/// What to stage before committing [default: all]
#[arg(long)]
pub(crate) stage: Option<crate::commands::commit::StageMode>,
/// Output format
///
/// JSON prints structured result to stdout after merge completes.
#[arg(long, default_value = "text", help_heading = "Automation")]
pub(crate) format: SwitchFormat,
}
// Ordering: by "core-ness". Primitive worktree operations first (switch, list,
// remove), then composites built on top (merge), then subcommand namespaces
// (step, hook, config). `remove` is a primitive and more core than `merge`,
// which wraps it. Hidden commands last.
#[derive(Subcommand)]
pub(crate) enum Commands {
/// Switch to a worktree; create if needed
#[command(
after_long_help = r#"Worktrees are addressed by branch name; paths are computed from a configurable template. Unlike `git switch`, this navigates between worktrees rather than changing branches in place.
<!-- demo: wt-switch.gif 1600x900 -->
## Examples
```console
$ wt switch feature-auth # Switch to worktree
$ wt switch - # Previous worktree (like cd -)
$ wt switch --create new-feature # Create new branch and worktree
$ wt switch --create hotfix --base production
$ wt switch pr:123 # Switch to PR #123's branch
```
## Creating a branch
The `--create` flag creates a new branch from `--base` — the default branch unless specified. Without `--create`, the branch must already exist. Switching to a remote branch (e.g., `wt switch feature` when only `origin/feature` exists) creates a local tracking branch.
## Creating worktrees
If the branch already has a worktree, `wt switch` changes directories to it. Otherwise, it creates one:
1. Runs [pre-switch hooks](@/hook.md#hook-types), blocking until complete
2. Creates worktree at configured path
3. Switches to new directory
4. Runs [pre-start hooks](@/hook.md#hook-types), blocking until complete
5. Spawns [post-start](@/hook.md#hook-types) and [post-switch hooks](@/hook.md#hook-types) in the background
```console
$ wt switch feature # Existing branch → creates worktree
$ wt switch --create feature # New branch and worktree
$ wt switch --create fix --base release # New branch from release
$ wt switch --create temp --no-hooks # Skip hooks
```
## Shortcuts
| Shortcut | Meaning |
|----------|---------|
| `^` | Default branch (`main`/`master`) |
| `@` | Current branch/worktree |
| `-` | Previous worktree (like `cd -`) |
| `pr:{N}` | GitHub PR #N's branch |
| `mr:{N}` | GitLab MR !N's branch |
```console
$ wt switch - # Back to previous
$ wt switch ^ # Default branch worktree
$ wt switch --create fix --base=@ # Branch from current HEAD
$ wt switch --create fix --base=pr:123 # Branch from PR #123's head
$ wt switch pr:123 # PR #123's branch
$ wt switch mr:101 # MR !101's branch
```
Shortcuts also apply to `--base`. For a fork PR/MR, the head commit is fetched and used as the base SHA without creating a tracking branch.
## Interactive picker
When called without arguments, `wt switch` opens an interactive picker to browse and select worktrees with live preview.
<!-- demo: wt-switch-picker.gif 1600x800 -->
**Keybindings:**
| Key | Action |
|-----|--------|
| `↑`/`↓` | Navigate worktree list |
| (type) | Filter worktrees |
| `Enter` | Switch to selected worktree |
| `Alt-c` | Create new worktree named as entered text |
| `Esc` | Cancel |
| `1`–`5` | Switch preview tab |
| `Alt-p` | Toggle preview panel |
| `Ctrl-u`/`Ctrl-d` | Scroll preview up/down |
<!-- Alt-r (remove worktree) works but is omitted: cursor resets after skim reload (#1695). Add once fixed. See #1881. -->
**Preview tabs** — toggle with number keys:
1. **HEAD±** — Diff of uncommitted changes
2. **log** — Recent commits; commits already on the default branch have dimmed hashes
3. **main…±** — Diff of changes since the merge-base with the default branch
4. **remote⇅** — Ahead/behind diff vs upstream tracking branch
5. **summary** — LLM-generated branch summary; requires `[list] summary = true` and `[commit.generation]`
**Pager configuration:** The preview panel pipes diff output through git's pager. Override in user config:
```toml
[switch.picker]
pager = "delta --paging=never --width=$COLUMNS"
```
Available on Unix only (macOS, Linux). On Windows, use `wt list` or `wt switch <branch>` directly.
## Pull requests and merge requests
The `pr:<number>` and `mr:<number>` shortcuts resolve a GitHub PR or GitLab MR to its branch. For same-repo PRs/MRs, worktrunk switches to the branch directly. For fork PRs/MRs, it fetches the ref (`refs/pull/N/head` or `refs/merge-requests/N/head`) and configures `pushRemote` to the fork URL.
```console
$ wt switch pr:101 # GitHub PR #101
$ wt switch mr:101 # GitLab MR !101
```
Requires `gh` (GitHub) or `glab` (GitLab) CLI to be installed and authenticated. The `--create` flag cannot be used with `pr:`/`mr:` syntax since the branch already exists.
**Forks:** The local branch uses the PR/MR's branch name directly (e.g., `feature-fix`), so `git push` works normally. If a local branch with that name already exists tracking something else, rename it first.
**Gitea (experimental):** `pr:` is also compatible with Gitea via the `tea` CLI. Set `[forge] platform = "gitea"` in `.config/wt.toml` to opt in; worktrunk also auto-detects Gitea when the remote host contains `gitea` or when `tea login add` has been run for the host.
**Azure DevOps (experimental):** `pr:` is also compatible with Azure DevOps via the `az` CLI (with the `azure-devops` extension). Set `[forge] platform = "azure-devops"` in `.config/wt.toml` to opt in; worktrunk also auto-detects Azure DevOps from `dev.azure.com` and `*.visualstudio.com` remotes.
## When wt switch fails
- **Branch doesn't exist** — Use `--create`, or check `wt list --branches`
- **Path occupied** — Another worktree is at the target path; switch to it or remove it
- **Stale directory** — Use `--clobber` to remove a non-worktree directory at the target path
To change which branch a worktree is on, use `git switch` inside that worktree.
## See also
- [`wt list`](@/list.md) — View all worktrees
- [`wt remove`](@/remove.md) — Delete worktrees when done
- [`wt merge`](@/merge.md) — Integrate changes back to the default branch
"#
)]
Switch(SwitchArgs),
/// List worktrees and their status
#[command(
after_long_help = r#"Shows uncommitted changes, divergence from the default branch and remote, and optional CI status and LLM summaries.
<!-- demo: wt-list.gif 1600x900 -->
The table renders progressively: branch names, paths, and commit hashes appear immediately, then status, divergence, and other columns fill in as background git operations complete.
## Full mode
`--full` adds columns that require network access or LLM calls: [CI status](#ci-status) (GitHub/GitLab pipeline pass/fail), line diffs since the merge-base, and [LLM-generated summaries](#llm-summaries) of each branch's changes.
## Examples
List all worktrees:
<!-- wt list -->
```console
$ wt list
Branch Status HEAD± main↕ Remote⇅ Commit Age Message
@ feature-api + ↕⇡ +54 -5 ↑4 ↓1 ⇡3 6814f02a 30m Add API tests
^ main ^⇅ ⇡1 ⇣1 41ee0834 4d Merge fix-auth: hardened to…
+ fix-auth ↕| ↑2 ↓1 | b772e68b 5h Add secure token storage
+ fix-typos _| | 41ee0834 4d Merge fix-auth: hardened to…
○ Showing 4 worktrees, 1 with changes, 2 ahead, 1 column hidden
```
Include CI status, line diffs, and LLM summaries:
<!-- wt list --full -->
```console
$ wt list --full
Branch Status HEAD± main↕ main…± Summary Remote⇅ CI Commit
@ feature-api + ↕⇡ +54 -5 ↑4 ↓1 +234 -24 Refactor API to REST architecture with middleware ⇡3 ● 6814f02a
^ main ^⇅ ⇡1 ⇣1 ● 41ee0834
+ fix-auth ↕| ↑2 ↓1 +25 -11 Harden auth with constant-time token validation | ● b772e68b
+ fix-typos _| | ● 41ee0834
○ Showing 4 worktrees, 1 with changes, 2 ahead, 3 columns hidden
```
Include branches that don't have worktrees:
<!-- wt list --branches --full -->
```console
$ wt list --branches --full
Branch Status HEAD± main↕ main…± Summary Remote⇅ CI Commit
@ feature-api + ↕⇡ +54 -5 ↑4 ↓1 +234 -24 Refactor API to REST architecture with middleware ⇡3 ● 6814f02a
^ main ^⇅ ⇡1 ⇣1 ● 41ee0834
+ fix-auth ↕| ↑2 ↓1 +25 -11 Harden auth with constant-time token validation | ● b772e68b
+ fix-typos _| | ● 41ee0834
exp /↕ ↑2 ↓1 +137 Explore GraphQL schema and resolvers 96379229
wip /↕ ↑1 ↓1 +33 Start API documentation b40716dc
○ Showing 4 worktrees, 2 branches, 1 with changes, 4 ahead, 3 columns hidden
```
Output as JSON for scripting:
```console
$ wt list --format=json
```
## Columns
| Column | Shows |
|--------|-------|
| Branch | Branch name |
| Status | Compact symbols (see below) |
| HEAD± | Uncommitted changes: +added -deleted lines |
| main↕ | Commits ahead/behind default branch |
| main…± | Line diffs since the merge-base with the default branch; `--full` only |
| Summary | LLM-generated branch summary; requires `--full`, `summary = true`, and [`commit.generation`](@/config.md#commit) [experimental] |
| Remote⇅ | Commits ahead/behind tracking branch |
| CI | Pipeline status; `--full` only |
| Path | Worktree directory |
| URL | Dev server URL from project config; dimmed if port is not listening |
| Commit | Short hash (8 chars) |
| Age | Time since last commit |
| Message | Last commit message (truncated) |
Note: `main↕` and `main…±` refer to the default branch — the header label stays `main` for compactness. `main…±` uses a merge-base (three-dot) diff.
### CI status
The CI column shows GitHub/GitLab pipeline status:
| Indicator | Meaning |
|-----------|---------|
| `●` green | All checks passed |
| `●` blue | Checks running |
| `●` red | Checks failed |
| `●` yellow | Merge conflicts with base |
| `●` gray | No checks configured |
| `⚠` yellow | Fetch error (rate limit, network) |
| (blank) | No upstream or no PR/MR |
CI indicators are clickable links to the PR or pipeline page. Any CI dot appears dimmed when unpushed local changes make the status stale. PRs/MRs are checked first, then branch workflows/pipelines for branches with an upstream. Local-only branches show blank; remote-only branches — visible with `--remotes` — get CI status detection. Results are cached for 30-60 seconds; use `wt config state` to view or clear.
### LLM summaries [experimental]
Reuses the [`commit.generation`](@/config.md#commit) command — the same LLM that generates commit messages. Enable with `summary = true` in `[list]` config; requires `--full`. Results are cached until the branch's diff changes.
## Status symbols
The Status column has multiple subcolumns. Within each, only the first matching symbol is shown (listed in priority order):
| Subcolumn | Symbol | Meaning |
|-----------|--------|---------|
| Working tree (1) | `+` | Staged files |
| Working tree (2) | `!` | Modified files (unstaged) |
| Working tree (3) | `?` | Untracked files |
| Worktree | `✘` | Merge conflicts |
| | `⤴` | Rebase in progress |
| | `⤵` | Merge in progress |
| | `/` | Branch without worktree |
| | `⚑` | Branch-worktree mismatch (branch name doesn't match worktree path) |
| | `⊟` | Prunable (directory missing) |
| | `⊞` | Locked worktree |
| Default branch | `^` | Is the default branch |
| | `∅` | Orphan branch (no common ancestor with the default branch) |
| | `✗` | Would conflict if merged to the default branch; with `--full`, includes uncommitted changes |
| | `_` | Same commit as the default branch, clean |
| | `–` | Same commit as the default branch, uncommitted changes |
| | `⊂` | Content [integrated](@/remove.md#branch-cleanup) into the default branch or target |
| | `↕` | Diverged from the default branch |
| | `↑` | Ahead of the default branch |
| | `↓` | Behind the default branch |
| Remote | `\|` | In sync with remote |
| | `⇅` | Diverged from remote |
| | `⇡` | Ahead of remote |
| | `⇣` | Behind remote |
Rows are dimmed when [safe to delete](@/remove.md#branch-cleanup) (`_` same commit with clean working tree or `⊂` content integrated).
### Placeholder symbols
These appear across all columns while the table is loading:
| Symbol | Meaning |
|--------|---------|
| `·` | Data is loading, or collection timed out / branch too stale |
---
## JSON output
Query structured data with `--format=json`:
```console
# Current worktree path (for scripts)
$ wt list --format=json | jq -r '.[] | select(.is_current) | .path'
# Branches with uncommitted changes
$ wt list --format=json | jq '.[] | select(.working_tree.modified)'
# Worktrees with merge conflicts
$ wt list --format=json | jq '.[] | select(.operation_state == "conflicts")'
# Branches ahead of main (needs merging)
$ wt list --format=json | jq '.[] | select(.main.ahead > 0) | .branch'
# Integrated branches (safe to remove)
$ wt list --format=json | jq '.[] | select(.main_state == "integrated" or .main_state == "empty") | .branch'
# Branches without worktrees
$ wt list --format=json --branches | jq '.[] | select(.kind == "branch") | .branch'
# Worktrees ahead of remote (needs pushing)
$ wt list --format=json | jq '.[] | select(.remote.ahead > 0) | {branch, ahead: .remote.ahead}'
# Stale CI (local changes not reflected in CI)
$ wt list --format=json --full | jq '.[] | select(.ci.stale) | .branch'
```
**Fields:**
| Field | Type | Description |
|-------|------|-------------|
| `branch` | string/null | Branch name (null for detached HEAD) |
| `path` | string | Worktree path (absent for branches without worktrees) |
| `kind` | string | `"worktree"` or `"branch"` |
| `commit` | object | Commit info (see below) |
| `working_tree` | object | Working tree state (see below) |
| `main_state` | string | Relation to the default branch (see below) |
| `integration_reason` | string | Why branch is integrated (see below) |
| `operation_state` | string | `"conflicts"`, `"rebase"`, or `"merge"`; absent when clean |
| `main` | object | Relationship to the default branch (see below); absent when is_main |
| `remote` | object | Tracking branch info (see below); absent when no tracking |
| `worktree` | object | Worktree metadata (see below) |
| `is_main` | boolean | Is the main worktree |
| `is_current` | boolean | Is the current worktree |
| `is_previous` | boolean | Previous worktree from wt switch |
| `ci` | object | CI status (see below); absent when no CI |
| `url` | string | Dev server URL from project config; absent when not configured |
| `url_active` | boolean | Whether the URL's port is listening; absent when not configured |
| `summary` | string | LLM-generated branch summary; absent when not configured or no summary |
| `statusline` | string | Pre-formatted status with ANSI colors |
| `symbols` | string | Raw status symbols without colors (e.g., `"!?↓"`) |
| `vars` | object | Per-branch variables from [`wt config state vars`](@/config.md#wt-config-state-vars) (absent when empty) |
### Commit object
| Field | Type | Description |
|-------|------|-------------|
| `sha` | string | Full commit SHA (40 chars) |
| `short_sha` | string | Short commit SHA, abbreviated per `core.abbrev` (auto-extends for ambiguous prefixes) |
| `message` | string | Commit message (first line) |
| `timestamp` | number | Unix timestamp |
### working_tree object
| Field | Type | Description |
|-------|------|-------------|
| `staged` | boolean | Has staged files |
| `modified` | boolean | Has modified files (unstaged) |
| `untracked` | boolean | Has untracked files |
| `renamed` | boolean | Has renamed files |
| `deleted` | boolean | Has deleted files |
| `diff` | object | Lines changed vs HEAD: `{added, deleted}` |
### main object
| Field | Type | Description |
|-------|------|-------------|
| `ahead` | number | Commits ahead of the default branch |
| `behind` | number | Commits behind the default branch |
| `diff` | object | Lines changed vs the default branch: `{added, deleted}` |
### remote object
| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Remote name (e.g., `"origin"`) |
| `branch` | string | Remote branch name |
| `ahead` | number | Commits ahead of remote |
| `behind` | number | Commits behind remote |
### worktree object
| Field | Type | Description |
|-------|------|-------------|
| `state` | string | `"no_worktree"`, `"branch_worktree_mismatch"`, `"prunable"`, `"locked"` (absent when normal) |
| `reason` | string | Reason for locked/prunable state |
| `detached` | boolean | HEAD is detached |
### ci object
| Field | Type | Description |
|-------|------|-------------|
| `status` | string | CI status (see below) |
| `source` | string | `"pr"` (PR/MR) or `"branch"` (branch workflow) |
| `stale` | boolean | Local HEAD differs from remote (unpushed changes) |
| `url` | string | URL to the PR/MR page |
### main_state values
These values describe the relation to the default branch.
`"is_main"` `"orphan"` `"would_conflict"` `"empty"` `"same_commit"` `"integrated"` `"diverged"` `"ahead"` `"behind"`
### integration_reason values
When `main_state == "integrated"`: `"ancestor"` `"trees_match"` `"no_added_changes"` `"merge_adds_nothing"` `"patch-id-match"`
### ci.status values
`"passed"` `"running"` `"failed"` `"conflicts"` `"no-ci"` `"error"`
Missing a field that would be generally useful? Open an issue at https://github.com/max-sixty/worktrunk.
## See also
- [`wt switch`](@/switch.md) — Switch worktrees or open interactive picker
"#
)]
// TODO: `args_conflicts_with_subcommands` causes confusing errors for unknown
// subcommands ("cannot be used with --branches") instead of "unknown subcommand".
// Could fix with external_subcommand + post-parse validation, but not worth the
// code. The `statusline` subcommand may move elsewhere anyway.
#[command(args_conflicts_with_subcommands = true)]
List(ListArgs),
/// Remove worktree; delete branch if merged
///
/// Defaults to the current worktree.
#[command(after_long_help = r#"## Examples
Remove current worktree:
<!-- wt remove (docs-example) -->
```console
$ wt remove
◎ Running pre-remove project:cleanup
flyctl scale count 0
Scaling app to 0 machines
◎ Removing api worktree & branch in background (same commit as main, _)
○ Switched to worktree for main @ ~/repo
```
Remove specific worktrees / branches:
```console
$ wt remove feature-branch
$ wt remove old-feature another-branch
```
Keep the branch:
```console
$ wt remove --no-delete-branch feature-branch
```
Force-delete an unmerged branch:
```console
$ wt remove -D experimental
```
## Branch cleanup
By default, branches are deleted when they would add no changes to the default branch if merged. This works with both unchanged git histories, and squash-merge or rebase workflows where commit history differs but file changes match.
Worktrunk checks six conditions (in order of cost):