From 9e323303f63c43330b6c3aa7f03a9c1b6308a57c Mon Sep 17 00:00:00 2001 From: Rebecca Turner Date: Tue, 15 Oct 2024 16:43:50 -0700 Subject: [PATCH] Split up `git::worktree` --- src/git/worktree/mod.rs | 221 +++++++++++++++++++++ src/git/{worktree.rs => worktree/parse.rs} | 220 +------------------- 2 files changed, 227 insertions(+), 214 deletions(-) create mode 100644 src/git/worktree/mod.rs rename src/git/{worktree.rs => worktree/parse.rs} (61%) diff --git a/src/git/worktree/mod.rs b/src/git/worktree/mod.rs new file mode 100644 index 0000000..1302be9 --- /dev/null +++ b/src/git/worktree/mod.rs @@ -0,0 +1,221 @@ +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 tap::Tap; +use tracing::instrument; +use utf8_command::Utf8Output; +use winnow::Parser; + +use super::Git; +use super::LocalBranchRef; + +mod parse; +pub use parse::Worktree; +pub use parse::WorktreeHead; +pub use parse::Worktrees; + +/// Git methods for dealing with worktrees. +#[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) + } + + /// Get the 'main' worktree. There can only be one main worktree, and it contains the + /// common `.git` directory. + /// + /// See: + #[instrument(level = "trace")] + pub fn main(&self) -> miette::Result { + // Kinda wasteful; we parse all the worktrees and then throw them away. + Ok(self.list()?.into_main()) + } + + /// Get the worktree container directory. + /// + /// This is the main worktree's parent, and is usually where all the other worktrees are + /// cloned as well. + #[instrument(level = "trace")] + pub fn container(&self) -> miette::Result { + // TODO: Write `.git-prole` to indicate worktree container root? + let main = self.main()?; + let mut path = if main.head == WorktreeHead::Bare { + // Git has a bug(?) where `git worktree list` will show the _parent_ of a + // bare worktree in a directory named `.git`. Work around it by getting the + // `.git` directory manually. + // + // See: https://lore.kernel.org/git/8f961645-2b70-4d45-a9f9-72e71c07bc11@app.fastmail.com/T/ + self.0.with_directory(main.path).path().git_common_dir()? + } else { + main.path + }; + + if !path.pop() { + Err(miette!("Main worktree path has no parent: {path}")) + } else { + Ok(path) + } + } + + /// List Git worktrees. + #[instrument(level = "trace")] + pub fn list(&self) -> miette::Result { + self.0 + .command() + .args(["worktree", "list", "--porcelain", "-z"]) + .output_checked_as(|context: OutputContext| { + if !context.status().success() { + Err(context.error()) + } else { + let output = &context.output().stdout; + match Worktrees::parser.parse(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) -> miette::Result<()> { + self.0 + .command() + .args(["worktree", "repair"]) + .status_checked() + .into_diagnostic()?; + Ok(()) + } + + /// The directory name, nested under the worktree parent directory, where the given + /// branch's worktree will be placed. + /// + /// E.g. to convert a repo `~/puppy` with default branch `main`, this will return `main`, + /// to indicate a worktree to be placed in `~/puppy/main`. + /// + /// TODO: Should support some configurable regex filtering or other logic? + pub fn dirname_for<'b>(&self, branch: &'b str) -> &'b str { + match branch.rsplit_once('/') { + Some((_left, right)) => { + tracing::warn!( + %branch, + worktree = %right, + "Branch contains a `/`, using trailing component for worktree directory name" + ); + right + } + None => branch, + } + } + + /// Get the full path for a new worktree with the given branch name. + /// + /// This appends the [`Self::dirname_for`] to the [`Self::container`]. + #[instrument(level = "trace")] + pub fn path_for(&self, branch: &str) -> miette::Result { + Ok(self + .container()? + .tap_mut(|p| p.push(self.dirname_for(branch)))) + } +} + +/// Options for `git worktree add`. +#[derive(Clone, Copy, Debug)] +pub struct AddWorktreeOpts<'a> { + /// If true, use `-B` instead of `-b` for `create_branch`. + /// Default false. + pub force_branch: bool, + /// Create a new branch. + pub create_branch: Option<&'a LocalBranchRef>, + /// If false, use `--no-checkout`. + /// Default true. + pub checkout: bool, + /// If true, use `--guess-remote`. + /// Default false. + pub guess_remote: bool, + /// If true, use `--track`. + /// Default false. + pub track: bool, + /// The start point for the new worktree. + 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, + } + } +} diff --git a/src/git/worktree.rs b/src/git/worktree/parse.rs similarity index 61% rename from src/git/worktree.rs rename to src/git/worktree/parse.rs index 3218ec7..a7d0e8e 100644 --- a/src/git/worktree.rs +++ b/src/git/worktree/parse.rs @@ -1,21 +1,13 @@ use std::collections::HashMap; -use std::fmt::Debug; use std::fmt::Display; use std::ops::Deref; -use std::process::Command; use std::str::FromStr; use camino::Utf8Path; use camino::Utf8PathBuf; -use command_error::CommandExt; -use command_error::OutputContext; use miette::miette; -use miette::IntoDiagnostic; use owo_colors::OwoColorize; use owo_colors::Stream; -use tap::Tap; -use tracing::instrument; -use utf8_command::Utf8Output; use winnow::combinator::alt; use winnow::combinator::cut_err; use winnow::combinator::eof; @@ -25,213 +17,9 @@ use winnow::PResult; use winnow::Parser; use crate::parse::till_null; +use crate::CommitHash; use crate::NormalPath; - -use super::commit_hash::CommitHash; -use super::Git; -use super::LocalBranchRef; -use super::Ref; - -/// Git methods for dealing with worktrees. -#[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) - } - - /// Get the 'main' worktree. There can only be one main worktree, and it contains the - /// common `.git` directory. - /// - /// See: - #[instrument(level = "trace")] - pub fn main(&self) -> miette::Result { - // Kinda wasteful; we parse all the worktrees and then throw them away. - let mut worktrees = self.list()?; - Ok(worktrees.inner.remove(&worktrees.main).unwrap()) - } - - /// Get the worktree container directory. - /// - /// This is the main worktree's parent, and is usually where all the other worktrees are - /// cloned as well. - #[instrument(level = "trace")] - pub fn container(&self) -> miette::Result { - // TODO: Write `.git-prole` to indicate worktree container root? - let main = self.main()?; - let mut path = if main.head == WorktreeHead::Bare { - // Git has a bug(?) where `git worktree list` will show the _parent_ of a - // bare worktree in a directory named `.git`. Work around it by getting the - // `.git` directory manually. - // - // See: https://lore.kernel.org/git/8f961645-2b70-4d45-a9f9-72e71c07bc11@app.fastmail.com/T/ - self.0.with_directory(main.path).path().git_common_dir()? - } else { - main.path - }; - - if !path.pop() { - Err(miette!("Main worktree path has no parent: {path}")) - } else { - Ok(path) - } - } - - /// List Git worktrees. - #[instrument(level = "trace")] - pub fn list(&self) -> miette::Result { - self.0 - .command() - .args(["worktree", "list", "--porcelain", "-z"]) - .output_checked_as(|context: OutputContext| { - if !context.status().success() { - Err(context.error()) - } else { - let output = &context.output().stdout; - match Worktrees::parser.parse(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) -> miette::Result<()> { - self.0 - .command() - .args(["worktree", "repair"]) - .status_checked() - .into_diagnostic()?; - Ok(()) - } - - /// The directory name, nested under the worktree parent directory, where the given - /// branch's worktree will be placed. - /// - /// E.g. to convert a repo `~/puppy` with default branch `main`, this will return `main`, - /// to indicate a worktree to be placed in `~/puppy/main`. - /// - /// TODO: Should support some configurable regex filtering or other logic? - pub fn dirname_for<'b>(&self, branch: &'b str) -> &'b str { - match branch.rsplit_once('/') { - Some((_left, right)) => { - tracing::warn!( - %branch, - worktree = %right, - "Branch contains a `/`, using trailing component for worktree directory name" - ); - right - } - None => branch, - } - } - - /// Get the full path for a new worktree with the given branch name. - /// - /// This appends the [`Self::dirname_for`] to the [`Self::container`]. - #[instrument(level = "trace")] - pub fn path_for(&self, branch: &str) -> miette::Result { - Ok(self - .container()? - .tap_mut(|p| p.push(self.dirname_for(branch)))) - } -} - -/// Options for `git worktree add`. -#[derive(Clone, Copy, Debug)] -pub struct AddWorktreeOpts<'a> { - /// If true, use `-B` instead of `-b` for `create_branch`. - /// Default false. - pub force_branch: bool, - /// Create a new branch. - pub create_branch: Option<&'a LocalBranchRef>, - /// If false, use `--no-checkout`. - /// Default true. - pub checkout: bool, - /// If true, use `--guess-remote`. - /// Default false. - pub guess_remote: bool, - /// If true, use `--track`. - /// Default false. - pub track: bool, - /// The start point for the new worktree. - 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, - } - } -} +use crate::Ref; /// A set of Git worktrees. /// @@ -249,6 +37,10 @@ impl Worktrees { &self.main } + pub fn into_main(mut self) -> Worktree { + self.inner.remove(&self.main).unwrap() + } + pub fn parser(input: &mut &str) -> PResult { let mut main = Worktree::parser.parse_next(input)?; main.is_main = true;