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
78 #[command(after_help = "EXAMPLES:
80 git workty new feat/login
81 git workty new hotfix --from main
82 git workty new feature --print-path")]
83 New {
84 name: String,
86
87 #[arg(long, short = 'f')]
89 from: Option<String>,
90
91 #[arg(long, short = 'p')]
93 path: Option<PathBuf>,
94
95 #[arg(long)]
97 print_path: bool,
98
99 #[arg(long, short = 'o')]
101 open: bool,
102 },
103
104 #[command(after_help = "EXAMPLES:
106 cd \"$(git workty go feat/login)\"
107 git workty go main")]
108 Go {
109 name: String,
111 },
112
113 #[command(after_help = "EXAMPLES:
115 cd \"$(git workty pick)\"")]
116 Pick,
117
118 #[command(after_help = "EXAMPLES:
120 git workty rm feat/login
121 git workty rm feat/login --delete-branch
122 git workty rm feat/login --force")]
123 Rm {
124 name: String,
126
127 #[arg(long, short = 'f')]
129 force: bool,
130
131 #[arg(long, short = 'd')]
133 delete_branch: bool,
134 },
135
136 #[command(after_help = "EXAMPLES:
138 git workty clean --merged --dry-run
139 git workty clean --merged --yes")]
140 Clean {
141 #[arg(long)]
143 merged: bool,
144
145 #[arg(long, short = 'n')]
147 dry_run: bool,
148 },
149
150 #[command(after_help = "EXAMPLES:
152 eval \"$(git workty init zsh)\"
153 git workty init bash >> ~/.bashrc")]
154 Init {
155 shell: String,
157
158 #[arg(long)]
160 wrap_git: bool,
161
162 #[arg(long)]
164 no_cd: bool,
165 },
166
167 Doctor,
169
170 #[command(after_help = "EXAMPLES:
172 git workty completions zsh > _git-workty
173 git workty completions bash > /etc/bash_completion.d/git-workty")]
174 Completions {
175 shell: Shell,
177 },
178
179 #[command(after_help = "EXAMPLES:
181 git workty pr 123
182 cd \"$(git workty pr 123 --print-path)\"")]
183 Pr {
184 number: u32,
186
187 #[arg(long)]
189 print_path: bool,
190
191 #[arg(long, short = 'o')]
193 open: bool,
194 },
195
196 #[command(after_help = "EXAMPLES:
198 git workty fetch
199 git workty fetch --all")]
200 Fetch {
201 #[arg(long, short = 'a')]
203 all: bool,
204 },
205
206 #[command(after_help = "EXAMPLES:
208 git workty sync --dry-run
209 git workty sync --fetch")]
210 Sync {
211 #[arg(long, short = 'n')]
213 dry_run: bool,
214
215 #[arg(long, short = 'f')]
217 fetch: bool,
218 },
219
220 InstallMan,
222}
223
224pub fn run_cli() {
225 let cli = Cli::parse();
226
227 let ui_opts = UiOptions {
228 color: !cli.no_color && supports_color(),
229 ascii: cli.ascii,
230 json: cli.json,
231 };
232
233 let result = run(cli, &ui_opts);
234
235 if let Err(e) = result {
236 ui::print_error(&format!("{:#}", e), None);
237 std::process::exit(1);
238 }
239}
240
241fn run(cli: Cli, ui_opts: &UiOptions) -> anyhow::Result<()> {
242 let start_path = cli.directory.as_deref();
243
244 match cli.command {
245 None | Some(Commands::List) => {
246 let repo = GitRepo::discover(start_path)?;
247 list::execute(&repo, ui_opts)
248 }
249
250 Some(Commands::New {
251 name,
252 from,
253 path,
254 print_path,
255 open,
256 }) => {
257 let repo = GitRepo::discover(start_path)?;
258 new::execute(
259 &repo,
260 new::NewOptions {
261 name,
262 from,
263 path,
264 print_path,
265 open,
266 },
267 )
268 }
269
270 Some(Commands::Go { name }) => {
271 let repo = GitRepo::discover(start_path)?;
272 go::execute(&repo, &name)
273 }
274
275 Some(Commands::Pick) => {
276 let repo = GitRepo::discover(start_path)?;
277 pick::execute(&repo, ui_opts)
278 }
279
280 Some(Commands::Rm {
281 name,
282 force,
283 delete_branch,
284 }) => {
285 let repo = GitRepo::discover(start_path)?;
286 rm::execute(
287 &repo,
288 rm::RmOptions {
289 name,
290 force,
291 delete_branch,
292 yes: cli.yes,
293 },
294 )
295 }
296
297 Some(Commands::Clean { merged, dry_run }) => {
298 let repo = GitRepo::discover(start_path)?;
299 clean::execute(
300 &repo,
301 clean::CleanOptions {
302 merged,
303 dry_run,
304 yes: cli.yes,
305 },
306 )
307 }
308
309 Some(Commands::Init {
310 shell,
311 wrap_git,
312 no_cd,
313 }) => {
314 init::execute(init::InitOptions {
315 shell,
316 wrap_git,
317 no_cd,
318 });
319 Ok(())
320 }
321
322 Some(Commands::Doctor) => {
323 doctor::execute(start_path);
324 Ok(())
325 }
326
327 Some(Commands::Completions { shell }) => {
328 completions::execute::<Cli>(shell);
329 Ok(())
330 }
331
332 Some(Commands::Pr {
333 number,
334 print_path,
335 open,
336 }) => {
337 let repo = GitRepo::discover(start_path)?;
338 pr::execute(
339 &repo,
340 pr::PrOptions {
341 number,
342 print_path,
343 open,
344 },
345 )
346 }
347
348 Some(Commands::Fetch { all }) => {
349 let repo = GitRepo::discover(start_path)?;
350 fetch::execute(&repo, all)
351 }
352
353 Some(Commands::Sync { dry_run, fetch }) => {
354 let repo = GitRepo::discover(start_path)?;
355 sync::execute(&repo, sync::SyncOptions { dry_run, fetch })
356 }
357
358 Some(Commands::InstallMan) => install_man::execute(cli.yes),
359 }
360}
361
362fn supports_color() -> bool {
363 use is_terminal::IsTerminal;
364
365 if std::env::var("NO_COLOR").is_ok() {
366 return false;
367 }
368
369 std::io::stdout().is_terminal()
370}