git_prole/git/worktree/
mod.rsuse std::ffi::OsStr;
use std::fmt::Debug;
use std::process::Command;
use camino::Utf8Path;
use camino::Utf8PathBuf;
use command_error::CommandExt;
use command_error::OutputContext;
use miette::miette;
use miette::IntoDiagnostic;
use rustc_hash::FxHashMap as HashMap;
use tap::Tap;
use tracing::instrument;
use utf8_command::Utf8Output;
use super::Git;
use super::LocalBranchRef;
mod resolve_unique_names;
mod parse;
pub use parse::Worktree;
pub use parse::WorktreeHead;
pub use parse::Worktrees;
pub use resolve_unique_names::RenamedWorktree;
pub use resolve_unique_names::ResolveUniqueNameOpts;
#[repr(transparent)]
pub struct GitWorktree<'a>(&'a Git);
impl Debug for GitWorktree<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(self.0, f)
}
}
impl<'a> GitWorktree<'a> {
pub fn new(git: &'a Git) -> Self {
Self(git)
}
#[instrument(level = "trace")]
pub fn main(&self) -> miette::Result<Worktree> {
Ok(self.list()?.into_main())
}
#[instrument(level = "trace")]
pub fn container(&self) -> miette::Result<Utf8PathBuf> {
let mut path = self.main()?.path;
if !path.pop() {
Err(miette!("Main worktree path has no parent: {path}"))
} else {
Ok(path)
}
}
#[instrument(level = "trace")]
pub fn list(&self) -> miette::Result<Worktrees> {
self.0
.command()
.args(["worktree", "list", "--porcelain", "-z"])
.output_checked_as(|context: OutputContext<Utf8Output>| {
if !context.status().success() {
Err(context.error())
} else {
let output = &context.output().stdout;
match Worktrees::parse(self.0, output) {
Ok(worktrees) => Ok(worktrees),
Err(err) => {
let err = miette!("{err}");
Err(context.error_msg(err))
}
}
}
})
.into_diagnostic()
}
#[instrument(level = "trace")]
pub fn add(&self, path: &Utf8Path, options: &AddWorktreeOpts<'_>) -> miette::Result<()> {
self.add_command(path, options)
.status_checked()
.into_diagnostic()?;
Ok(())
}
#[instrument(level = "trace")]
pub fn add_command(&self, path: &Utf8Path, options: &AddWorktreeOpts<'_>) -> Command {
let mut command = self.0.command();
command.args(["worktree", "add"]);
if let Some(branch) = options.create_branch {
command.arg(if options.force_branch { "-B" } else { "-b" });
command.arg(branch.branch_name());
}
if !options.checkout {
command.arg("--no-checkout");
}
if options.guess_remote {
command.arg("--guess-remote");
}
if options.track {
command.arg("--track");
}
command.arg(path.as_str());
if let Some(start_point) = options.start_point {
command.arg(start_point);
}
command
}
#[instrument(level = "trace")]
pub fn rename(&self, from: &Utf8Path, to: &Utf8Path) -> miette::Result<()> {
self.0
.command()
.current_dir(from)
.args(["worktree", "move", from.as_str(), to.as_str()])
.status_checked()
.into_diagnostic()?;
Ok(())
}
#[instrument(level = "trace")]
pub fn repair(
&self,
paths: impl IntoIterator<Item = impl AsRef<OsStr>> + Debug,
) -> miette::Result<()> {
self.0
.command()
.args(["worktree", "repair"])
.args(paths)
.output_checked_utf8()
.into_diagnostic()?;
Ok(())
}
pub fn dirname_for<'b>(&self, branch: &'b str) -> &'b str {
match branch.rsplit_once('/') {
Some((_left, right)) => right,
None => branch,
}
}
#[instrument(level = "trace")]
pub fn path_for(&self, branch: &str) -> miette::Result<Utf8PathBuf> {
Ok(self
.container()?
.tap_mut(|p| p.push(self.dirname_for(branch))))
}
#[instrument(level = "trace")]
pub fn resolve_unique_names(
&self,
opts: ResolveUniqueNameOpts<'_>,
) -> miette::Result<HashMap<Utf8PathBuf, RenamedWorktree>> {
resolve_unique_names::resolve_unique_worktree_names(self.0, opts)
}
}
#[derive(Clone, Copy, Debug)]
pub struct AddWorktreeOpts<'a> {
pub force_branch: bool,
pub create_branch: Option<&'a LocalBranchRef>,
pub checkout: bool,
pub guess_remote: bool,
pub track: bool,
pub start_point: Option<&'a str>,
}
impl<'a> Default for AddWorktreeOpts<'a> {
fn default() -> Self {
Self {
force_branch: false,
create_branch: None,
checkout: true,
guess_remote: false,
track: false,
start_point: None,
}
}
}