#[allow(unused_imports)]
use clap::CommandFactory;
use clap::{Parser, Subcommand};
use clap_complete::engine::{ArgValueCompleter, CompletionCandidate};
use std::collections::HashSet;
use std::ffi::OsStr;
use std::path::PathBuf;
use std::process::Command;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Cli {
#[arg(long, value_name = "WHEN", global = true, ignore_case = true)]
pub color: Option<crate::color::ColorMode>,
#[arg(long, short = 'v', global = true)]
pub verbose: bool,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Add {
branch: String,
#[arg(add = ArgValueCompleter::new(list_git_refs))]
start_point: Option<String>,
#[arg(long, conflicts_with = "no_tmux")]
tmux: bool,
#[arg(long, conflicts_with = "tmux")]
no_tmux: bool,
},
Create {
branch: String,
#[arg(add = ArgValueCompleter::new(list_git_refs))]
start_point: Option<String>,
},
Ls {
#[arg(long)]
show_path: bool,
},
Rm {
#[arg(num_args = 0.., value_name = "TARGET", add = ArgValueCompleter::new(list_git_worktrees))]
targets: Vec<String>,
},
Cd {
#[arg(add = ArgValueCompleter::new(list_git_worktrees))]
name: Option<String>,
},
Init {
#[arg(long, conflicts_with = "local")]
global: bool,
#[arg(long, conflicts_with = "global")]
local: bool,
#[arg(short, long)]
force: bool,
},
Completion {
shell: String,
},
ShellInit {
shell: String,
},
Open {
#[arg(long, conflicts_with = "window")]
pane: bool,
#[arg(long, conflicts_with = "pane")]
window: bool,
},
Sync {
#[arg(long)]
run: bool,
#[arg(long)]
copy: bool,
#[arg(long)]
link: bool,
},
}
#[must_use]
pub fn list_git_refs(current: &OsStr) -> Vec<CompletionCandidate> {
let output = Command::new("git")
.args([
"for-each-ref",
"--format=%(refname:short)%09%(symref)",
"refs/heads",
"refs/remotes",
"refs/tags",
])
.output();
let Ok(output) = output else {
return Vec::new();
};
if !output.status.success() {
return Vec::new();
}
let prefix = current.to_string_lossy();
String::from_utf8_lossy(&output.stdout)
.lines()
.filter_map(|line| {
let parts: Vec<&str> = line.split('\t').collect();
let refname = parts.first()?.trim();
let symref = parts.get(1).map_or("", |s| s.trim());
if !symref.is_empty() {
return None;
}
if !refname.starts_with(&*prefix) {
return None;
}
Some(CompletionCandidate::new(refname))
})
.collect()
}
#[must_use]
#[allow(dead_code)] pub fn list_git_branches(current: &OsStr) -> Vec<CompletionCandidate> {
let output = Command::new("git")
.args([
"for-each-ref",
"--format=%(refname:short)%09%(symref)",
"refs/heads",
"refs/remotes",
])
.output();
let Ok(output) = output else {
return Vec::new();
};
if !output.status.success() {
return Vec::new();
}
let prefix = current.to_string_lossy();
String::from_utf8_lossy(&output.stdout)
.lines()
.filter_map(|line| {
let parts: Vec<&str> = line.split('\t').collect();
let refname = parts.first()?.trim();
let symref = parts.get(1).map_or("", |s| s.trim());
if !symref.is_empty() {
return None;
}
if !refname.starts_with(&*prefix) {
return None;
}
Some(CompletionCandidate::new(refname))
})
.collect()
}
pub fn list_git_worktrees(current: &OsStr) -> Vec<CompletionCandidate> {
let output = Command::new("git")
.args(["worktree", "list", "--porcelain"])
.output();
let Ok(output) = output else {
return Vec::new();
};
if !output.status.success() {
return Vec::new();
}
let prefix = current.to_string_lossy();
let stdout = String::from_utf8_lossy(&output.stdout);
let mut candidates_set = HashSet::new();
if "@".starts_with(&*prefix) {
candidates_set.insert("@".to_string());
}
for branch in parse_worktree_list(&stdout) {
candidates_set.insert(branch);
}
if let Ok(repo_root) = crate::commands::common::get_main_repo_root() {
if crate::config::Config::load_from_repo_root(&repo_root).is_ok() {
let entries = crate::domain::worktree::parse_worktree_entries(&stdout, None);
let worktree_paths: Vec<PathBuf> = entries
.iter()
.skip(1)
.map(|entry| PathBuf::from(&entry.path))
.collect();
if let Some(worktree_root) =
crate::domain::worktree::calculate_worktree_root_from_paths(&worktree_paths)
{
for entry in entries.iter().skip(1) {
let worktree_path = PathBuf::from(&entry.path);
if let Some(rel_path) = crate::domain::worktree::calculate_relative_path(
&worktree_path,
&worktree_root,
) {
candidates_set.insert(rel_path);
}
}
}
}
}
candidates_set
.into_iter()
.filter(|name| name.starts_with(&*prefix))
.map(CompletionCandidate::new)
.collect()
}
#[must_use]
pub fn parse_worktree_list(output: &str) -> Vec<String> {
let mut branches = Vec::new();
let mut worktree_index = 0;
let mut current_branch: Option<String> = None;
for line in output.lines() {
if line.starts_with("worktree ") {
if let Some(branch) = current_branch.take() {
if worktree_index > 0 {
branches.push(branch);
}
}
worktree_index += 1;
} else if line.starts_with("branch ") {
if let Some(branch_ref) = line.strip_prefix("branch ") {
let branch = branch_ref.strip_prefix("refs/heads/").unwrap_or(branch_ref);
current_branch = Some(branch.to_string());
}
} else if line.is_empty() {
if let Some(branch) = current_branch.take() {
if worktree_index > 1 {
branches.push(branch);
}
}
}
}
if let Some(branch) = current_branch {
if worktree_index > 1 {
branches.push(branch);
}
}
branches
}