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 --print-path")]
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
108 #[command(after_help = "EXAMPLES:
110 cd \"$(git workty go feat/login)\"
111 git workty go main")]
112 Go {
113 name: String,
115 },
116
117 #[command(after_help = "EXAMPLES:
119 cd \"$(git workty pick)\"")]
120 Pick,
121
122 #[command(after_help = "EXAMPLES:
124 git workty rm feat/login
125 git workty rm feat/login --delete-branch
126 git workty rm feat/login --force")]
127 Rm {
128 name: String,
130
131 #[arg(long, short = 'f')]
133 force: bool,
134
135 #[arg(long, short = 'd')]
137 delete_branch: bool,
138 },
139
140 #[command(after_help = "EXAMPLES:
142 git workty clean --merged --dry-run
143 git workty clean --gone --yes
144 git workty clean --stale 30")]
145 Clean {
146 #[arg(long)]
148 merged: bool,
149
150 #[arg(long)]
152 gone: bool,
153
154 #[arg(long, value_name = "DAYS")]
156 stale: Option<u32>,
157
158 #[arg(long, short = 'n')]
160 dry_run: bool,
161 },
162
163 #[command(after_help = "EXAMPLES:
165 eval \"$(git workty init zsh)\"
166 git workty init bash >> ~/.bashrc")]
167 Init {
168 shell: String,
170
171 #[arg(long)]
173 wrap_git: bool,
174
175 #[arg(long)]
177 no_cd: bool,
178 },
179
180 Doctor,
182
183 #[command(after_help = "EXAMPLES:
185 git workty completions zsh > _git-workty
186 git workty completions bash > /etc/bash_completion.d/git-workty")]
187 Completions {
188 shell: Shell,
190 },
191
192 #[command(after_help = "EXAMPLES:
194 git workty pr 123
195 cd \"$(git workty pr 123 --print-path)\"")]
196 Pr {
197 number: u32,
199
200 #[arg(long)]
202 print_path: bool,
203
204 #[arg(long, short = 'o')]
206 open: bool,
207 },
208
209 #[command(after_help = "EXAMPLES:
211 git workty fetch
212 git workty fetch --all")]
213 Fetch {
214 #[arg(long, short = 'a')]
216 all: bool,
217 },
218
219 #[command(after_help = "EXAMPLES:
221 git workty sync --dry-run
222 git workty sync --fetch")]
223 Sync {
224 #[arg(long, short = 'n')]
226 dry_run: bool,
227
228 #[arg(long, short = 'f')]
230 fetch: bool,
231 },
232
233 InstallMan,
235}
236
237pub fn run_cli() {
238 let cli = Cli::parse();
239
240 let ui_opts = UiOptions {
241 color: !cli.no_color && supports_color(),
242 ascii: cli.ascii,
243 json: cli.json,
244 };
245
246 let result = run(cli, &ui_opts);
247
248 if let Err(e) = result {
249 ui::print_error(&format!("{:#}", e), None);
250 std::process::exit(1);
251 }
252}
253
254fn run(cli: Cli, ui_opts: &UiOptions) -> anyhow::Result<()> {
255 let start_path = cli.directory.as_deref();
256
257 match cli.command {
258 None => {
259 let repo = GitRepo::discover(start_path)?;
260 list::execute(&repo, ui_opts, false)
261 }
262
263 Some(Commands::List { fast }) => {
264 let repo = GitRepo::discover(start_path)?;
265 list::execute(&repo, ui_opts, fast)
266 }
267
268 Some(Commands::New {
269 name,
270 from,
271 path,
272 print_path,
273 open,
274 }) => {
275 let repo = GitRepo::discover(start_path)?;
276 new::execute(
277 &repo,
278 new::NewOptions {
279 name,
280 from,
281 path,
282 print_path,
283 open,
284 },
285 )
286 }
287
288 Some(Commands::Go { name }) => {
289 let repo = GitRepo::discover(start_path)?;
290 go::execute(&repo, &name)
291 }
292
293 Some(Commands::Pick) => {
294 let repo = GitRepo::discover(start_path)?;
295 pick::execute(&repo, ui_opts)
296 }
297
298 Some(Commands::Rm {
299 name,
300 force,
301 delete_branch,
302 }) => {
303 let repo = GitRepo::discover(start_path)?;
304 rm::execute(
305 &repo,
306 rm::RmOptions {
307 name,
308 force,
309 delete_branch,
310 yes: cli.yes,
311 },
312 )
313 }
314
315 Some(Commands::Clean {
316 merged,
317 gone,
318 stale,
319 dry_run,
320 }) => {
321 let repo = GitRepo::discover(start_path)?;
322 clean::execute(
323 &repo,
324 clean::CleanOptions {
325 merged,
326 gone,
327 stale_days: stale,
328 dry_run,
329 yes: cli.yes,
330 },
331 )
332 }
333
334 Some(Commands::Init {
335 shell,
336 wrap_git,
337 no_cd,
338 }) => {
339 init::execute(init::InitOptions {
340 shell,
341 wrap_git,
342 no_cd,
343 });
344 Ok(())
345 }
346
347 Some(Commands::Doctor) => {
348 doctor::execute(start_path);
349 Ok(())
350 }
351
352 Some(Commands::Completions { shell }) => {
353 completions::execute::<Cli>(shell);
354 Ok(())
355 }
356
357 Some(Commands::Pr {
358 number,
359 print_path,
360 open,
361 }) => {
362 let repo = GitRepo::discover(start_path)?;
363 pr::execute(
364 &repo,
365 pr::PrOptions {
366 number,
367 print_path,
368 open,
369 },
370 )
371 }
372
373 Some(Commands::Fetch { all }) => {
374 let repo = GitRepo::discover(start_path)?;
375 fetch::execute(&repo, all)
376 }
377
378 Some(Commands::Sync { dry_run, fetch }) => {
379 let repo = GitRepo::discover(start_path)?;
380 sync::execute(&repo, sync::SyncOptions { dry_run, fetch })
381 }
382
383 Some(Commands::InstallMan) => install_man::execute(cli.yes),
384 }
385}
386
387fn supports_color() -> bool {
388 use is_terminal::IsTerminal;
389
390 if std::env::var("NO_COLOR").is_ok() {
391 return false;
392 }
393
394 std::io::stdout().is_terminal()
395}