mod branches;
mod cleaner;
mod cli;
mod config;
mod git;
mod ui;
mod worktrees;
use anyhow::Result;
use clap::Parser;
use cli::{Cli, Command, ConfigAction};
fn main() -> Result<()> {
let cli = Cli::parse();
let git = git::Git::new(cli.verbose);
let ui = ui::Ui::new();
match cli.command {
Some(Command::Config { action }) => handle_config_command(&git, &ui, action),
None => handle_clean(&git, &ui, &cli),
}
}
fn handle_config_command(git: &git::Git, ui: &ui::Ui, action: ConfigAction) -> Result<()> {
match action {
ConfigAction::List => {
match config::Config::load(git)? {
Some(cfg) => {
ui.heading("Current configuration [sync]:");
ui.blank();
ui.line(&format!(
" {} {}",
ui.bold.apply_to("protected:"),
if cfg.protected.is_empty() {
"(none)".to_string()
} else {
cfg.protected.join(", ")
}
));
ui.line(&format!(
" {} {}",
ui.bold.apply_to("remotes:"),
match &cfg.remotes {
Some(r) => r.join(", "),
None => "(all)".to_string(),
}
));
let branch_protected = git.branch_protected_list()?;
ui.line(&format!(
" {} {}",
ui.bold.apply_to("branch protected:"),
if branch_protected.is_empty() {
"(none)".to_string()
} else {
branch_protected.join(", ")
}
));
ui.line(&format!(
" {} {}",
ui.bold.apply_to("worktrunk:"),
match cfg.worktrunk {
Some(true) => "enabled",
Some(false) => "disabled",
None => "(auto-detect)",
}
));
}
None => {
ui.muted("No configuration found. Run `git sync` to start the setup wizard.");
}
}
Ok(())
}
ConfigAction::Set { key, value } => {
let full_key = format!("sync.{key}");
git.config_set(&full_key, &value)?;
ui.success(&format!("Set {key} = {value}"));
Ok(())
}
ConfigAction::AddProtected { pattern } => {
git.config_add("sync.protected", &pattern)?;
ui.success(&format!("Added protected pattern: {pattern}"));
Ok(())
}
ConfigAction::RemoveProtected { pattern } => {
let mut protected = git.config_get_all("sync.protected")?;
protected.retain(|p| p != &pattern);
git.config_unset_all("sync.protected")?;
for p in &protected {
git.config_add("sync.protected", p)?;
}
ui.success(&format!("Removed protected pattern: {pattern}"));
Ok(())
}
ConfigAction::AddRemote { name } => {
git.config_add("sync.remote", &name)?;
ui.success(&format!("Added remote: {name}"));
Ok(())
}
ConfigAction::RemoveRemote { name } => {
let mut remotes = git.config_get_all("sync.remote")?;
remotes.retain(|r| r != &name);
git.config_unset_all("sync.remote")?;
for r in &remotes {
git.config_add("sync.remote", r)?;
}
ui.success(&format!("Removed remote: {name}"));
Ok(())
}
ConfigAction::Protect { branch } => {
git.set_branch_protected(&branch, true)?;
ui.success(&format!("Branch '{branch}' marked as protected"));
Ok(())
}
ConfigAction::Unprotect { branch } => {
git.set_branch_protected(&branch, false)?;
ui.success(&format!("Branch '{branch}' is no longer protected"));
Ok(())
}
ConfigAction::Setup => {
config::Config::interactive_setup(git, ui)?;
Ok(())
}
}
}
fn handle_clean(git: &git::Git, ui: &ui::Ui, cli: &Cli) -> Result<()> {
let cfg = config::load_or_setup(git, ui)?;
let use_worktrunk = resolve_worktrunk(git, ui, cli, &cfg)?;
let opts = cleaner::CleanerOptions {
yes: cli.yes,
dry_run: cli.dry_run,
no_fetch: cli.no_fetch,
local_only: cli.local_only,
remote_only: cli.remote_only,
no_worktrees: cli.no_worktrees,
use_worktrunk,
};
cleaner::run(git, &cfg, ui, &opts)
}
fn resolve_worktrunk(git: &git::Git, ui: &ui::Ui, cli: &Cli, cfg: &config::Config) -> Result<bool> {
if cli.worktrunk {
if !git::worktrunk_available() {
anyhow::bail!(
"Worktrunk (wt) not found on $PATH. \
Install it from https://worktrunk.dev or remove --worktrunk."
);
}
return Ok(true);
}
if cli.no_worktrunk {
return Ok(false);
}
if let Some(val) = cfg.worktrunk {
if val && !git::worktrunk_available() {
anyhow::bail!(
"sync.worktrunk is enabled but worktrunk (wt) is not found on $PATH. \
Install it from https://worktrunk.dev or run: \
git sync config set worktrunk false"
);
}
return Ok(val);
}
if git.worktrunk_config_exists()? && git::worktrunk_available() {
if cli.yes {
return Ok(true);
}
return ui.confirm(
"Worktrunk detected. Use it for worktree removal (triggers pre/post-remove hooks)?",
true,
);
}
Ok(false)
}