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::{clean, completions, doctor, go, init, install_man, list, new, pick, pr, rm};
15use crate::git::GitRepo;
16use crate::ui::UiOptions;
17
18pub const ABOUT: &str = "Git worktrees as daily-driver workspaces
19
20workty makes Git worktrees feel like workspaces/tabs. Switch context without
21stashing or WIP commits, see everything in flight with a dashboard, and clean
22up merged work safely.";
23
24pub const AFTER_HELP: &str = "EXAMPLES:
25 git workty Show dashboard of all worktrees
26 git workty new feat/login Create new workspace for feat/login
27 git workty go feat/login Print path to feat/login worktree
28 git workty pick Fuzzy select a worktree (interactive)
29 git workty rm feat/login Remove the feat/login worktree
30 git workty clean --merged Remove all merged worktrees
31
32SHELL INTEGRATION:
33 Add to your shell config:
34 eval \"$(git workty init zsh)\"
35
36 This provides:
37 wcd - fuzzy select and cd to a worktree
38 wnew - create new worktree and cd into it
39 wgo - go to a worktree by name";
40
41#[derive(Parser)]
42#[command(name = "git-workty", bin_name = "git workty")]
43#[command(author, version, about = ABOUT, after_help = AFTER_HELP)]
44#[command(propagate_version = true)]
45pub struct Cli {
46 #[arg(long, global = true, env = "NO_COLOR")]
48 pub no_color: bool,
49
50 #[arg(long, global = true)]
52 pub ascii: bool,
53
54 #[arg(long, global = true)]
56 pub json: bool,
57
58 #[arg(short = 'C', global = true, value_name = "PATH")]
60 pub directory: Option<PathBuf>,
61
62 #[arg(long, short = 'y', global = true)]
64 pub yes: bool,
65
66 #[command(subcommand)]
67 pub command: Option<Commands>,
68}
69
70#[derive(Subcommand)]
71pub enum Commands {
72 #[command(visible_alias = "ls")]
74 List,
75
76 #[command(after_help = "EXAMPLES:
78 git workty new feat/login
79 git workty new hotfix --from main
80 git workty new feature --print-path")]
81 New {
82 name: String,
84
85 #[arg(long, short = 'f')]
87 from: Option<String>,
88
89 #[arg(long, short = 'p')]
91 path: Option<PathBuf>,
92
93 #[arg(long)]
95 print_path: bool,
96
97 #[arg(long, short = 'o')]
99 open: bool,
100 },
101
102 #[command(after_help = "EXAMPLES:
104 cd \"$(git workty go feat/login)\"
105 git workty go main")]
106 Go {
107 name: String,
109 },
110
111 #[command(after_help = "EXAMPLES:
113 cd \"$(git workty pick)\"")]
114 Pick,
115
116 #[command(after_help = "EXAMPLES:
118 git workty rm feat/login
119 git workty rm feat/login --delete-branch
120 git workty rm feat/login --force")]
121 Rm {
122 name: String,
124
125 #[arg(long, short = 'f')]
127 force: bool,
128
129 #[arg(long, short = 'd')]
131 delete_branch: bool,
132 },
133
134 #[command(after_help = "EXAMPLES:
136 git workty clean --merged --dry-run
137 git workty clean --merged --yes")]
138 Clean {
139 #[arg(long)]
141 merged: bool,
142
143 #[arg(long, short = 'n')]
145 dry_run: bool,
146 },
147
148 #[command(after_help = "EXAMPLES:
150 eval \"$(git workty init zsh)\"
151 git workty init bash >> ~/.bashrc")]
152 Init {
153 shell: String,
155
156 #[arg(long)]
158 wrap_git: bool,
159
160 #[arg(long)]
162 no_cd: bool,
163 },
164
165 Doctor,
167
168 #[command(after_help = "EXAMPLES:
170 git workty completions zsh > _git-workty
171 git workty completions bash > /etc/bash_completion.d/git-workty")]
172 Completions {
173 shell: Shell,
175 },
176
177 #[command(after_help = "EXAMPLES:
179 git workty pr 123
180 cd \"$(git workty pr 123 --print-path)\"")]
181 Pr {
182 number: u32,
184
185 #[arg(long)]
187 print_path: bool,
188
189 #[arg(long, short = 'o')]
191 open: bool,
192 },
193
194 InstallMan,
196}
197
198pub fn run_cli() {
199 let cli = Cli::parse();
200
201 let ui_opts = UiOptions {
202 color: !cli.no_color && supports_color(),
203 ascii: cli.ascii,
204 json: cli.json,
205 };
206
207 let result = run(cli, &ui_opts);
208
209 if let Err(e) = result {
210 ui::print_error(&format!("{:#}", e), None);
211 std::process::exit(1);
212 }
213}
214
215fn run(cli: Cli, ui_opts: &UiOptions) -> anyhow::Result<()> {
216 let start_path = cli.directory.as_deref();
217
218 match cli.command {
219 None | Some(Commands::List) => {
220 let repo = GitRepo::discover(start_path)?;
221 list::execute(&repo, ui_opts)
222 }
223
224 Some(Commands::New {
225 name,
226 from,
227 path,
228 print_path,
229 open,
230 }) => {
231 let repo = GitRepo::discover(start_path)?;
232 new::execute(
233 &repo,
234 new::NewOptions {
235 name,
236 from,
237 path,
238 print_path,
239 open,
240 },
241 )
242 }
243
244 Some(Commands::Go { name }) => {
245 let repo = GitRepo::discover(start_path)?;
246 go::execute(&repo, &name)
247 }
248
249 Some(Commands::Pick) => {
250 let repo = GitRepo::discover(start_path)?;
251 pick::execute(&repo, ui_opts)
252 }
253
254 Some(Commands::Rm {
255 name,
256 force,
257 delete_branch,
258 }) => {
259 let repo = GitRepo::discover(start_path)?;
260 rm::execute(
261 &repo,
262 rm::RmOptions {
263 name,
264 force,
265 delete_branch,
266 yes: cli.yes,
267 },
268 )
269 }
270
271 Some(Commands::Clean { merged, dry_run }) => {
272 let repo = GitRepo::discover(start_path)?;
273 clean::execute(
274 &repo,
275 clean::CleanOptions {
276 merged,
277 dry_run,
278 yes: cli.yes,
279 },
280 )
281 }
282
283 Some(Commands::Init {
284 shell,
285 wrap_git,
286 no_cd,
287 }) => {
288 init::execute(init::InitOptions {
289 shell,
290 wrap_git,
291 no_cd,
292 });
293 Ok(())
294 }
295
296 Some(Commands::Doctor) => {
297 doctor::execute(start_path);
298 Ok(())
299 }
300
301 Some(Commands::Completions { shell }) => {
302 completions::execute::<Cli>(shell);
303 Ok(())
304 }
305
306 Some(Commands::Pr {
307 number,
308 print_path,
309 open,
310 }) => {
311 let repo = GitRepo::discover(start_path)?;
312 pr::execute(
313 &repo,
314 pr::PrOptions {
315 number,
316 print_path,
317 open,
318 },
319 )
320 }
321
322 Some(Commands::InstallMan) => install_man::execute(cli.yes),
323 }
324}
325
326fn supports_color() -> bool {
327 use is_terminal::IsTerminal;
328
329 if std::env::var("NO_COLOR").is_ok() {
330 return false;
331 }
332
333 std::io::stdout().is_terminal()
334}