1pub mod commands;
2pub mod config;
3pub mod gh;
4pub mod git;
5pub mod shell;
6pub mod status;
7pub mod ui;
8pub mod worktree;
9
10use clap::{Parser, Subcommand};
11use clap_complete::Shell;
12use std::path::PathBuf;
13
14use crate::commands::{
15 clean, completions, doctor, fetch, go, init, install_man, list, new, pick, pr, rm, sync,
16};
17use crate::git::GitRepo;
18use crate::ui::UiOptions;
19
20pub const ABOUT: &str = "Git worktrees as daily-driver workspaces
21
22workty makes Git worktrees feel like workspaces/tabs. Switch context without
23stashing or WIP commits, see everything in flight with a dashboard, and clean
24up merged work safely.";
25
26pub const AFTER_HELP: &str = "EXAMPLES:
27 git workty Show dashboard of all worktrees
28 git workty new feat/login Create new workspace for feat/login
29 git workty go feat/login Print path to feat/login worktree
30 git workty pick Fuzzy select a worktree (interactive)
31 git workty rm feat/login Remove the feat/login worktree
32 git workty clean --merged Remove all merged worktrees
33
34SHELL INTEGRATION:
35 Add to your shell config:
36 eval \"$(git workty init zsh)\"
37
38 This provides:
39 wcd - fuzzy select and cd to a worktree
40 wnew - create new worktree and cd into it
41 wgo - go to a worktree by name";
42
43#[derive(Parser)]
44#[command(name = "git-workty", bin_name = "git workty")]
45#[command(author, version, about = ABOUT, after_help = AFTER_HELP)]
46#[command(propagate_version = true)]
47pub struct Cli {
48 #[arg(long, global = true, env = "NO_COLOR")]
50 pub no_color: bool,
51
52 #[arg(long, global = true)]
54 pub ascii: bool,
55
56 #[arg(long, global = true)]
58 pub json: bool,
59
60 #[arg(short = 'C', global = true, value_name = "PATH")]
62 pub directory: Option<PathBuf>,
63
64 #[arg(long, short = 'y', global = true)]
66 pub yes: bool,
67
68 #[command(subcommand)]
69 pub command: Option<Commands>,
70}
71
72#[derive(Subcommand)]
73pub enum Commands {
74 #[command(visible_alias = "ls")]
76 List {
77 #[arg(long)]
79 fast: bool,
80 },
81
82 #[command(after_help = "EXAMPLES:
84 git workty new feat/login
85 git workty new hotfix --from main
86 git workty new feature --no-fetch --no-push")]
87 New {
88 name: String,
90
91 #[arg(long, short = 'f')]
93 from: Option<String>,
94
95 #[arg(long, short = 'p')]
97 path: Option<PathBuf>,
98
99 #[arg(long)]
101 print_path: bool,
102
103 #[arg(long, short = 'o')]
105 open: bool,
106
107 #[arg(long)]
109 no_fetch: bool,
110
111 #[arg(long)]
113 no_push: bool,
114 },
115
116 #[command(after_help = "EXAMPLES:
118 cd \"$(git workty go feat/login)\"
119 git workty go main")]
120 Go {
121 name: String,
123 },
124
125 #[command(after_help = "EXAMPLES:
127 cd \"$(git workty pick)\"")]
128 Pick,
129
130 #[command(after_help = "EXAMPLES:
132 git workty rm feat/login
133 git workty rm feat/login --delete-branch
134 git workty rm feat/login --force")]
135 Rm {
136 name: String,
138
139 #[arg(long, short = 'f')]
141 force: bool,
142
143 #[arg(long, short = 'd')]
145 delete_branch: bool,
146 },
147
148 #[command(after_help = "EXAMPLES:
150 git workty clean --merged --dry-run
151 git workty clean --gone --yes
152 git workty clean --stale 30")]
153 Clean {
154 #[arg(long)]
156 merged: bool,
157
158 #[arg(long)]
160 gone: bool,
161
162 #[arg(long, value_name = "DAYS")]
164 stale: Option<u32>,
165
166 #[arg(long, short = 'n')]
168 dry_run: bool,
169 },
170
171 #[command(after_help = "EXAMPLES:
173 eval \"$(git workty init zsh)\"
174 git workty init bash >> ~/.bashrc")]
175 Init {
176 shell: String,
178
179 #[arg(long)]
181 wrap_git: bool,
182
183 #[arg(long)]
185 no_cd: bool,
186 },
187
188 Doctor,
190
191 #[command(after_help = "EXAMPLES:
193 git workty completions zsh > _git-workty
194 git workty completions bash > /etc/bash_completion.d/git-workty")]
195 Completions {
196 shell: Shell,
198 },
199
200 #[command(after_help = "EXAMPLES:
202 git workty pr 123
203 cd \"$(git workty pr 123 --print-path)\"")]
204 Pr {
205 number: u32,
207
208 #[arg(long)]
210 print_path: bool,
211
212 #[arg(long, short = 'o')]
214 open: bool,
215 },
216
217 #[command(after_help = "EXAMPLES:
219 git workty fetch
220 git workty fetch --all")]
221 Fetch {
222 #[arg(long, short = 'a')]
224 all: bool,
225 },
226
227 #[command(after_help = "EXAMPLES:
229 git workty sync --dry-run
230 git workty sync --fetch")]
231 Sync {
232 #[arg(long, short = 'n')]
234 dry_run: bool,
235
236 #[arg(long, short = 'f')]
238 fetch: bool,
239 },
240
241 InstallMan,
243}
244
245pub fn run_cli() {
246 let cli = Cli::parse();
247
248 let ui_opts = UiOptions {
249 color: !cli.no_color && supports_color(),
250 ascii: cli.ascii,
251 json: cli.json,
252 };
253
254 let result = run(cli, &ui_opts);
255
256 if let Err(e) = result {
257 ui::print_error(&format!("{:#}", e), None);
258 std::process::exit(1);
259 }
260}
261
262fn run(cli: Cli, ui_opts: &UiOptions) -> anyhow::Result<()> {
263 let start_path = cli.directory.as_deref();
264
265 match cli.command {
266 None => {
267 let repo = GitRepo::discover(start_path)?;
268 list::execute(&repo, ui_opts, false)
269 }
270
271 Some(Commands::List { fast }) => {
272 let repo = GitRepo::discover(start_path)?;
273 list::execute(&repo, ui_opts, fast)
274 }
275
276 Some(Commands::New {
277 name,
278 from,
279 path,
280 print_path,
281 open,
282 no_fetch,
283 no_push,
284 }) => {
285 let repo = GitRepo::discover(start_path)?;
286 new::execute(
287 &repo,
288 new::NewOptions {
289 name,
290 from,
291 path,
292 print_path,
293 open,
294 no_fetch,
295 no_push,
296 },
297 )
298 }
299
300 Some(Commands::Go { name }) => {
301 let repo = GitRepo::discover(start_path)?;
302 go::execute(&repo, &name)
303 }
304
305 Some(Commands::Pick) => {
306 let repo = GitRepo::discover(start_path)?;
307 pick::execute(&repo, ui_opts)
308 }
309
310 Some(Commands::Rm {
311 name,
312 force,
313 delete_branch,
314 }) => {
315 let repo = GitRepo::discover(start_path)?;
316 rm::execute(
317 &repo,
318 rm::RmOptions {
319 name,
320 force,
321 delete_branch,
322 yes: cli.yes,
323 },
324 )
325 }
326
327 Some(Commands::Clean {
328 merged,
329 gone,
330 stale,
331 dry_run,
332 }) => {
333 let repo = GitRepo::discover(start_path)?;
334 clean::execute(
335 &repo,
336 clean::CleanOptions {
337 merged,
338 gone,
339 stale_days: stale,
340 dry_run,
341 yes: cli.yes,
342 },
343 )
344 }
345
346 Some(Commands::Init {
347 shell,
348 wrap_git,
349 no_cd,
350 }) => {
351 init::execute(init::InitOptions {
352 shell,
353 wrap_git,
354 no_cd,
355 });
356 Ok(())
357 }
358
359 Some(Commands::Doctor) => {
360 doctor::execute(start_path);
361 Ok(())
362 }
363
364 Some(Commands::Completions { shell }) => {
365 completions::execute::<Cli>(shell);
366 Ok(())
367 }
368
369 Some(Commands::Pr {
370 number,
371 print_path,
372 open,
373 }) => {
374 let repo = GitRepo::discover(start_path)?;
375 pr::execute(
376 &repo,
377 pr::PrOptions {
378 number,
379 print_path,
380 open,
381 },
382 )
383 }
384
385 Some(Commands::Fetch { all }) => {
386 let repo = GitRepo::discover(start_path)?;
387 fetch::execute(&repo, all)
388 }
389
390 Some(Commands::Sync { dry_run, fetch }) => {
391 let repo = GitRepo::discover(start_path)?;
392 sync::execute(&repo, sync::SyncOptions { dry_run, fetch })
393 }
394
395 Some(Commands::InstallMan) => install_man::execute(cli.yes),
396 }
397}
398
399fn supports_color() -> bool {
400 use is_terminal::IsTerminal;
401
402 if std::env::var("NO_COLOR").is_ok() {
403 return false;
404 }
405
406 std::io::stdout().is_terminal()
407}