From c56765ce262fb7a207ede5d6c9a2385ed5188349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Fri, 17 Jun 2022 01:44:47 +0200 Subject: [PATCH] Match branches with worktrees always, even with slashes --- src/grm/main.rs | 18 +--------- src/tree.rs | 2 +- src/worktree.rs | 90 ++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 83 insertions(+), 27 deletions(-) diff --git a/src/grm/main.rs b/src/grm/main.rs index 6c7ab86..5972102 100644 --- a/src/grm/main.rs +++ b/src/grm/main.rs @@ -502,25 +502,9 @@ fn main() { None => None, }; - let mut name: &str = &action_args.name; - let subdirectory; - let split = name.split_once('/'); - match split { - None => subdirectory = None, - Some(split) => { - if split.0.is_empty() || split.1.is_empty() { - print_error("Worktree name cannot start or end with a slash"); - process::exit(1); - } else { - (subdirectory, name) = (Some(Path::new(split.0)), split.1); - } - } - } - match worktree::add_worktree( &cwd, - name, - subdirectory, + &action_args.name, track, action_args.no_track, ) { diff --git a/src/tree.rs b/src/tree.rs index d765298..e8a0741 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -222,7 +222,7 @@ fn sync_repo(root_path: &Path, repo: &repo::Repo, init_worktree: bool) -> Result if newly_created && repo.worktree_setup && init_worktree { match repo_handle.default_branch() { Ok(branch) => { - worktree::add_worktree(&repo_path, &branch.name()?, None, None, false)?; + worktree::add_worktree(&repo_path, &branch.name()?, None, false)?; } Err(_error) => print_repo_error( &repo.name, diff --git a/src/worktree.rs b/src/worktree.rs index 35db51f..631aa79 100644 --- a/src/worktree.rs +++ b/src/worktree.rs @@ -21,10 +21,17 @@ pub const GIT_MAIN_WORKTREE_DIRECTORY: &str = ".git-main-working-tree"; pub fn add_worktree( directory: &Path, name: &str, - subdirectory: Option<&Path>, track: Option<(&str, &str)>, no_track: bool, ) -> Result<(), String> { + // A branch name must never start or end with a slash. Everything else is ok. + if name.starts_with('/') || name.ends_with('/') { + return Err(format!( + "Invalid worktree name: {}. It cannot start or end with a slash", + name + )); + } + let repo = repo::RepoHandle::open(directory, true).map_err(|error| match error.kind { repo::RepoErrorKind::NotFound => { String::from("Current directory does not contain a worktree setup") @@ -38,11 +45,6 @@ pub fn add_worktree( return Err(format!("Worktree {} already exists", &name)); } - let path = match subdirectory { - Some(dir) => directory.join(dir).join(name), - None => directory.join(Path::new(name)), - }; - let mut remote_branch_exists = false; let mut target_branch = match repo.find_local_branch(name) { @@ -193,10 +195,80 @@ pub fn add_worktree( } } - if let Some(subdirectory) = subdirectory { - std::fs::create_dir_all(subdirectory).map_err(|error| error.to_string())?; + // We have to create subdirectories first, otherwise adding the worktree + // will fail + if name.contains('/') { + let path = Path::new(&name); + if let Some(base) = path.parent() { + // This is a workaround of a bug in libgit2 (?) + // + // When *not* doing this, we will receive an error from the `Repository::worktree()` + // like this: + // + // > failed to make directory '/{repo}/.git-main-working-tree/worktrees/dir/test + // + // This is a discrepancy between the behaviour of libgit2 and the + // git CLI when creating worktrees with slashes: + // + // The git CLI will create the worktree's configuration directory + // inside {git_dir}/worktrees/{last_path_component}. Look at this: + // + // ``` + // $ git worktree add 1/2/3 -b 1/2/3 + // $ ls .git/worktrees + // 3 + // ``` + // + // Interesting: When adding a worktree with a different name but the + // same final path component, git starts adding a counter suffix to + // the worktree directories: + // + // ``` + // $ git worktree add 1/3/3 -b 1/3/3 + // $ git worktree add 1/4/3 -b 1/4/3 + // $ ls .git/worktrees + // 3 + // 31 + // 32 + // ``` + // + // I *guess* that the mapping back from the worktree directory under .git to the actual + // worktree directory is done via the `gitdir` file inside `.git/worktrees/{worktree}. + // This means that the actual directory would not matter. You can verify this by + // just renaming it: + // + // ``` + // $ mv .git/worktrees/3 .git/worktrees/foobar + // $ git worktree list + // /tmp/ fcc8a2a7 [master] + // /tmp/1/2/3 fcc8a2a7 [1/2/3] + // /tmp/1/3/3 fcc8a2a7 [1/3/3] + // /tmp/1/4/3 fcc8a2a7 [1/4/3] + // ``` + // + // => Still works + // + // Anyway, libgit2 does not do this: It tries to create the worktree + // directory inside .git with the exact name of the worktree, including + // any slashes. It should be this code: + // + // https://github.com/libgit2/libgit2/blob/f98dd5438f8d7bfd557b612fdf1605b1c3fb8eaf/src/libgit2/worktree.c#L346 + // + // As a workaround, we can create the base directory manually for now. + // + // Tracking upstream issue: https://github.com/libgit2/libgit2/issues/6327 + std::fs::create_dir_all( + directory + .join(GIT_MAIN_WORKTREE_DIRECTORY) + .join("worktrees") + .join(base), + ) + .map_err(|error| error.to_string())?; + std::fs::create_dir_all(base).map_err(|error| error.to_string())?; + } } - repo.new_worktree(name, &path, &target_branch)?; + + repo.new_worktree(name, &directory.join(&name), &target_branch)?; Ok(()) }