Skip to content

Commit

Permalink
Convert multiple worktrees (#21)
Browse files Browse the repository at this point in the history
Closes #4
  • Loading branch information
9999years authored Oct 18, 2024
1 parent 736974a commit 1397a2a
Show file tree
Hide file tree
Showing 23 changed files with 1,069 additions and 312 deletions.
1 change: 1 addition & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ impl App {
self.git()?,
ConvertPlanOpts {
default_branch: args.default_branch.clone(),
destination: args.destination.clone(),
},
)?
.execute()?,
Expand Down
4 changes: 4 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ pub struct ConvertArgs {
/// A default branch to create a worktree for.
#[arg(long)]
pub default_branch: Option<String>,

/// The directory to place the worktrees into.
#[arg()]
pub destination: Option<Utf8PathBuf>,
}

#[derive(Args, Clone, Debug)]
Expand Down
1 change: 1 addition & 0 deletions src/clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub fn clone(git: AppGit<'_>, args: CloneArgs) -> miette::Result<()> {
git.with_directory(destination),
ConvertPlanOpts {
default_branch: None,
destination: None,
},
)?
.execute()?;
Expand Down
833 changes: 531 additions & 302 deletions src/convert.rs

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions src/git/worktree/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::ffi::OsStr;
use std::fmt::Debug;
use std::process::Command;

Expand Down Expand Up @@ -139,11 +140,15 @@ impl<'a> GitWorktree<'a> {
}

#[instrument(level = "trace")]
pub fn repair(&self) -> miette::Result<()> {
pub fn repair(
&self,
paths: impl IntoIterator<Item = impl AsRef<OsStr>> + Debug,
) -> miette::Result<()> {
self.0
.command()
.args(["worktree", "repair"])
.status_checked()
.args(paths)
.output_checked_utf8()
.into_diagnostic()?;
Ok(())
}
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ mod gh;
mod git;
mod install_tracing;
mod normal_path;
mod only_paths_in_parent_directory;
mod parse;
mod topological_sort;
mod utf8tempdir;
Expand Down Expand Up @@ -55,4 +56,5 @@ pub use git::Worktree;
pub use git::WorktreeHead;
pub use git::Worktrees;
pub use normal_path::NormalPath;
pub use only_paths_in_parent_directory::only_paths_in_parent_directory;
pub use utf8tempdir::Utf8TempDir;
64 changes: 64 additions & 0 deletions src/only_paths_in_parent_directory.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use std::fmt::Debug;

use camino::Utf8Path;
use miette::IntoDiagnostic;
use rustc_hash::FxHashSet;
use tracing::instrument;

/// Check if a set of paths all have the same parent directory and they are the only paths in that
/// directory (other than dotfiles).
#[instrument(level = "trace")]
pub fn only_paths_in_parent_directory<'p, I, P>(paths: I) -> Option<&'p Utf8Path>
where
I: IntoIterator<Item = &'p P> + Debug,
P: AsRef<Utf8Path> + 'p + ?Sized,
{
let mut paths = paths.into_iter();
let mut names = FxHashSet::default();
let first = paths.next()?.as_ref();
let parent = first.parent()?;
names.insert(first.file_name()?);

for path in paths {
let path = path.as_ref();
if path.parent()? != parent {
return None;
}
names.insert(path.file_name()?);
}

match path_contains_only_names_and_dotfiles(parent, &names) {
Ok(true) => Some(parent),
Ok(false) => None,
Err(error) => {
tracing::debug!(
directory=%parent,
error=%error,
"Error while listing directory"
);
None
}
}
}

/// Check if a path contains only files listed in the given set of names and dotfiles.
#[instrument(level = "trace")]
fn path_contains_only_names_and_dotfiles(
path: &Utf8Path,
names: &FxHashSet<&str>,
) -> miette::Result<bool> {
for entry in path.read_dir_utf8().into_diagnostic()? {
let entry = entry.into_diagnostic()?;
let name = entry.file_name();
if !name.starts_with('.') && !names.contains(name) {
tracing::debug!(
directory=%path,
entry=%name,
"Directory entry is not a dotfile or listed in known paths"
);
return Ok(false);
}
}

Ok(true)
}
1 change: 0 additions & 1 deletion src/topological_sort.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ use rustc_hash::FxHashSet as HashSet;
/// This implements Kahn's algorithm.
///
/// See: <https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm>
#[cfg_attr(not(test), expect(dead_code))]
pub fn topological_sort<P>(paths: &[P]) -> miette::Result<Vec<Utf8PathBuf>>
where
P: AsRef<Utf8Path>,
Expand Down
10 changes: 7 additions & 3 deletions tests/clone_simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ use test_harness::GitProle;
use test_harness::WorktreeState;

#[test]
fn clone_simple() {
let prole = GitProle::new().unwrap();
prole.setup_repo("remote/my-repo").unwrap();
fn clone_simple() -> miette::Result<()> {
let prole = GitProle::new()?;
prole.setup_repo("remote/my-repo")?;
prole
.cmd()
.args(["clone", "remote/my-repo"])
.status_checked()
.unwrap();

prole.sh("ls -la && ls -la my-repo")?;

prole
.repo_state("my-repo")
.worktrees([
Expand All @@ -25,4 +27,6 @@ fn clone_simple() {
),
])
.assert();

Ok(())
}
2 changes: 1 addition & 1 deletion tests/config_remotes_default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use test_harness::GitProle;
use test_harness::WorktreeState;

#[test]
fn convert_multiple_remotes() -> miette::Result<()> {
fn config_remotes_default() -> miette::Result<()> {
let prole = GitProle::new()?;
setup_repo_multiple_remotes(&prole, "my-remotes/my-repo", "my-repo")?;

Expand Down
41 changes: 41 additions & 0 deletions tests/convert_bare_dot_git.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use command_error::CommandExt;
use miette::IntoDiagnostic;
use test_harness::GitProle;
use test_harness::WorktreeState;

#[test]
fn convert_bare_dot_git() -> miette::Result<()> {
let prole = GitProle::new()?;
prole.sh(r#"
mkdir -p my-repo/.git
cd my-repo/.git || exit
git init --bare
git worktree add ../main
cd ../main || exit
echo "puppy doggy" > README.md
git add .
git commit -m "Initial commit"
git worktree add ../puppy
git worktree add --detach ../doggy
"#)?;

prole
.cd_cmd("my-repo/main")
.arg("convert")
.status_checked()
.into_diagnostic()?;

prole
.repo_state("my-repo")
.worktrees([
WorktreeState::new_bare(),
WorktreeState::new("main").branch("main"),
WorktreeState::new("puppy").branch("puppy"),
WorktreeState::new("doggy").detached("4023d080"),
])
.assert();

Ok(())
}
41 changes: 41 additions & 0 deletions tests/convert_bare_ends_with_dot_git.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use command_error::CommandExt;
use miette::IntoDiagnostic;
use test_harness::GitProle;
use test_harness::WorktreeState;

#[test]
fn convert_bare_ends_with_dot_git() -> miette::Result<()> {
let prole = GitProle::new()?;
prole.sh(r#"
mkdir -p my-repo.git
cd my-repo.git || exit
git init --bare
git worktree add ../main
cd ../main || exit
echo "puppy doggy" > README.md
git add .
git commit -m "Initial commit"
git worktree add ../puppy
git worktree add --detach ../doggy
"#)?;

prole
.cd_cmd("my-repo.git")
.arg("convert")
.status_checked()
.into_diagnostic()?;

prole
.repo_state("my-repo")
.worktrees([
WorktreeState::new_bare(),
WorktreeState::new("main").branch("main"),
WorktreeState::new("puppy").branch("puppy"),
WorktreeState::new("doggy").detached("4023d080"),
])
.assert();

Ok(())
}
41 changes: 41 additions & 0 deletions tests/convert_bare_no_dot.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use command_error::CommandExt;
use miette::IntoDiagnostic;
use test_harness::GitProle;
use test_harness::WorktreeState;

#[test]
fn convert_bare_no_dot() -> miette::Result<()> {
let prole = GitProle::new()?;
prole.sh(r#"
mkdir -p my-repo
cd my-repo || exit
git init --bare
git worktree add ../main
cd ../main || exit
echo "puppy doggy" > README.md
git add .
git commit -m "Initial commit"
git worktree add ../puppy
git worktree add --detach ../doggy
"#)?;

prole
.cd_cmd("main")
.arg("convert")
.status_checked()
.into_diagnostic()?;

prole
.repo_state("my-repo")
.worktrees([
WorktreeState::new_bare(),
WorktreeState::new("main").branch("main"),
WorktreeState::new("puppy").branch("puppy"),
WorktreeState::new("doggy").detached("4023d080"),
])
.assert();

Ok(())
}
41 changes: 41 additions & 0 deletions tests/convert_bare_starts_with_dot.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use command_error::CommandExt;
use miette::IntoDiagnostic;
use test_harness::GitProle;
use test_harness::WorktreeState;

#[test]
fn convert_bare_starts_with_dot() -> miette::Result<()> {
let prole = GitProle::new()?;
prole.sh(r#"
mkdir -p my-repo/.bare
cd my-repo/.bare || exit
git init --bare
git worktree add ../main
cd ../main || exit
echo "puppy doggy" > README.md
git add .
git commit -m "Initial commit"
git worktree add ../puppy
git worktree add --detach ../doggy
"#)?;

prole
.cd_cmd("my-repo/main")
.arg("convert")
.status_checked()
.into_diagnostic()?;

prole
.repo_state("my-repo")
.worktrees([
WorktreeState::new_bare(),
WorktreeState::new("main").branch("main"),
WorktreeState::new("puppy").branch("puppy"),
WorktreeState::new("doggy").detached("4023d080"),
])
.assert();

Ok(())
}
34 changes: 34 additions & 0 deletions tests/convert_common_parent.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use command_error::CommandExt;
use miette::IntoDiagnostic;
use test_harness::GitProle;
use test_harness::WorktreeState;

#[test]
fn convert_common_parent() -> miette::Result<()> {
let prole = GitProle::new()?;
prole.setup_repo("my-prefix/my-repo")?;

prole.sh(r#"
cd my-prefix/my-repo
git worktree add ../puppy
git worktree add ../doggy
"#)?;

prole
.cd_cmd("my-prefix/my-repo")
.arg("convert")
.status_checked()
.into_diagnostic()?;

prole
.repo_state("my-prefix")
.worktrees([
WorktreeState::new_bare(),
WorktreeState::new("main").branch("main"),
WorktreeState::new("puppy").branch("puppy"),
WorktreeState::new("doggy").branch("doggy"),
])
.assert();

Ok(())
}
Loading

0 comments on commit 1397a2a

Please sign in to comment.