pub mod commands;
pub mod config;
pub mod gh;
pub mod git;
pub mod shell;
pub mod status;
pub mod ui;
pub mod worktree;
use clap::{Parser, Subcommand};
use clap_complete::Shell;
use std::path::PathBuf;
use crate::commands::{
clean, completions, doctor, fetch, go, init, install_man, list, new, pick, pr, rm, sync,
};
use crate::git::GitRepo;
use crate::ui::UiOptions;
pub const ABOUT: &str = "Git worktrees as daily-driver workspaces
workty makes Git worktrees feel like workspaces/tabs. Switch context without
stashing or WIP commits, see everything in flight with a dashboard, and clean
up merged work safely.";
pub const AFTER_HELP: &str = "EXAMPLES:
git workty Show dashboard of all worktrees
git workty new feat/login Create new workspace for feat/login
git workty go feat/login Print path to feat/login worktree
git workty pick Fuzzy select a worktree (interactive)
git workty rm feat/login Remove the feat/login worktree
git workty clean --merged Remove all merged worktrees
SHELL INTEGRATION:
Add to your shell config:
eval \"$(git workty init zsh)\"
This provides:
wcd - fuzzy select and cd to a worktree
wnew - create new worktree and cd into it
wgo - go to a worktree by name";
#[derive(Parser)]
#[command(name = "git-workty", bin_name = "git workty")]
#[command(author, version, about = ABOUT, after_help = AFTER_HELP)]
#[command(propagate_version = true)]
pub struct Cli {
#[arg(long, global = true, env = "NO_COLOR")]
pub no_color: bool,
#[arg(long, global = true)]
pub ascii: bool,
#[arg(long, global = true)]
pub json: bool,
#[arg(short = 'C', global = true, value_name = "PATH")]
pub directory: Option<PathBuf>,
#[arg(long, short = 'y', global = true)]
pub yes: bool,
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Subcommand)]
pub enum Commands {
#[command(visible_alias = "ls")]
List {
#[arg(long)]
fast: bool,
},
#[command(after_help = "EXAMPLES:
git workty new feat/login
git workty new hotfix --from main
git workty new feature --no-fetch --no-push")]
New {
name: String,
#[arg(long, short = 'f')]
from: Option<String>,
#[arg(long, short = 'p')]
path: Option<PathBuf>,
#[arg(long)]
print_path: bool,
#[arg(long, short = 'o')]
open: bool,
#[arg(long)]
no_fetch: bool,
#[arg(long)]
no_push: bool,
},
#[command(after_help = "EXAMPLES:
cd \"$(git workty go feat/login)\"
git workty go main")]
Go {
name: String,
},
#[command(after_help = "EXAMPLES:
cd \"$(git workty pick)\"")]
Pick,
#[command(after_help = "EXAMPLES:
git workty rm feat/login
git workty rm feat/login --delete-branch
git workty rm feat/login --force")]
Rm {
name: String,
#[arg(long, short = 'f')]
force: bool,
#[arg(long, short = 'd')]
delete_branch: bool,
},
#[command(after_help = "EXAMPLES:
git workty clean --merged --dry-run
git workty clean --gone --yes
git workty clean --stale 30")]
Clean {
#[arg(long)]
merged: bool,
#[arg(long)]
gone: bool,
#[arg(long, value_name = "DAYS")]
stale: Option<u32>,
#[arg(long, short = 'n')]
dry_run: bool,
},
#[command(after_help = "EXAMPLES:
eval \"$(git workty init zsh)\"
git workty init bash >> ~/.bashrc")]
Init {
shell: String,
#[arg(long)]
wrap_git: bool,
#[arg(long)]
no_cd: bool,
},
Doctor,
#[command(after_help = "EXAMPLES:
git workty completions zsh > _git-workty
git workty completions bash > /etc/bash_completion.d/git-workty")]
Completions {
shell: Shell,
},
#[command(after_help = "EXAMPLES:
git workty pr 123
cd \"$(git workty pr 123 --print-path)\"")]
Pr {
number: u32,
#[arg(long)]
print_path: bool,
#[arg(long, short = 'o')]
open: bool,
},
#[command(after_help = "EXAMPLES:
git workty fetch
git workty fetch --all")]
Fetch {
#[arg(long, short = 'a')]
all: bool,
},
#[command(after_help = "EXAMPLES:
git workty sync --dry-run
git workty sync --fetch")]
Sync {
#[arg(long, short = 'n')]
dry_run: bool,
#[arg(long, short = 'f')]
fetch: bool,
},
InstallMan,
}
pub fn run_cli() {
let cli = Cli::parse();
let ui_opts = UiOptions {
color: !cli.no_color && supports_color(),
ascii: cli.ascii,
json: cli.json,
};
let result = run(cli, &ui_opts);
if let Err(e) = result {
ui::print_error(&format!("{:#}", e), None);
std::process::exit(1);
}
}
fn run(cli: Cli, ui_opts: &UiOptions) -> anyhow::Result<()> {
let start_path = cli.directory.as_deref();
match cli.command {
None => {
let repo = GitRepo::discover(start_path)?;
list::execute(&repo, ui_opts, false)
}
Some(Commands::List { fast }) => {
let repo = GitRepo::discover(start_path)?;
list::execute(&repo, ui_opts, fast)
}
Some(Commands::New {
name,
from,
path,
print_path,
open,
no_fetch,
no_push,
}) => {
let repo = GitRepo::discover(start_path)?;
new::execute(
&repo,
new::NewOptions {
name,
from,
path,
print_path,
open,
no_fetch,
no_push,
},
)
}
Some(Commands::Go { name }) => {
let repo = GitRepo::discover(start_path)?;
go::execute(&repo, &name)
}
Some(Commands::Pick) => {
let repo = GitRepo::discover(start_path)?;
pick::execute(&repo, ui_opts)
}
Some(Commands::Rm {
name,
force,
delete_branch,
}) => {
let repo = GitRepo::discover(start_path)?;
rm::execute(
&repo,
rm::RmOptions {
name,
force,
delete_branch,
yes: cli.yes,
},
)
}
Some(Commands::Clean {
merged,
gone,
stale,
dry_run,
}) => {
let repo = GitRepo::discover(start_path)?;
clean::execute(
&repo,
clean::CleanOptions {
merged,
gone,
stale_days: stale,
dry_run,
yes: cli.yes,
},
)
}
Some(Commands::Init {
shell,
wrap_git,
no_cd,
}) => {
init::execute(init::InitOptions {
shell,
wrap_git,
no_cd,
});
Ok(())
}
Some(Commands::Doctor) => {
doctor::execute(start_path);
Ok(())
}
Some(Commands::Completions { shell }) => {
completions::execute::<Cli>(shell);
Ok(())
}
Some(Commands::Pr {
number,
print_path,
open,
}) => {
let repo = GitRepo::discover(start_path)?;
pr::execute(
&repo,
pr::PrOptions {
number,
print_path,
open,
},
)
}
Some(Commands::Fetch { all }) => {
let repo = GitRepo::discover(start_path)?;
fetch::execute(&repo, all)
}
Some(Commands::Sync { dry_run, fetch }) => {
let repo = GitRepo::discover(start_path)?;
sync::execute(&repo, sync::SyncOptions { dry_run, fetch })
}
Some(Commands::InstallMan) => install_man::execute(cli.yes),
}
}
fn supports_color() -> bool {
use is_terminal::IsTerminal;
if std::env::var("NO_COLOR").is_ok() {
return false;
}
std::io::stdout().is_terminal()
}