Skip to content

Commit

Permalink
feat: allow discovery of linked worktree git dirs (#301)
Browse files Browse the repository at this point in the history
This also works if the work-tree can't be found but it is otherwise
a valid git dir.
  • Loading branch information
Byron committed May 18, 2022
1 parent 4cff7a8 commit 050f795
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 31 deletions.
67 changes: 51 additions & 16 deletions git-discover/src/is.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,23 @@ pub fn bare(git_dir_candidate: impl AsRef<Path>) -> bool {
/// * an objects directory
/// * a refs directory
pub fn git(git_dir: impl AsRef<Path>) -> Result<crate::repository::Kind, crate::is_git::Error> {
let (dot_git, common_dir, git_dir_is_linked_worktree) = match crate::path::from_gitdir_file(git_dir.as_ref()) {
#[derive(Eq, PartialEq)]
enum Kind {
MaybeRepo,
LinkedWorkTreeDir,
WorkTreeGitDir { work_dir: std::path::PathBuf },
}
#[cfg(not(windows))]
fn is_directory(err: &std::io::Error) -> bool {
err.raw_os_error() == Some(21)
}
// TODO: use ::IsDirectory as well when stabilized, but it's permission denied on windows
#[cfg(windows)]
fn is_directory(err: &std::io::Error) -> bool {
err.kind() == std::io::ErrorKind::PermissionDenied
}
let git_dir = git_dir.as_ref();
let (dot_git, common_dir, kind) = match crate::path::from_gitdir_file(git_dir) {
Ok(private_git_dir) => {
let common_dir = private_git_dir.join("commondir");
let common_dir = crate::path::from_plain_file(&common_dir)
Expand All @@ -29,16 +45,31 @@ pub fn git(git_dir: impl AsRef<Path>) -> Result<crate::repository::Kind, crate::
})?
.map_err(|_| crate::is_git::Error::MissingCommonDir { missing: common_dir })?;
let common_dir = private_git_dir.join(common_dir);
(Cow::Owned(private_git_dir), Cow::Owned(common_dir), true)
}
// TODO: use ::IsDirectory as well when stabilized, but it's permission denied on windows
#[cfg(not(windows))]
Err(crate::path::from_gitdir_file::Error::Io(err)) if err.raw_os_error() == Some(21) => {
(Cow::Borrowed(git_dir.as_ref()), Cow::Borrowed(git_dir.as_ref()), false)
(
Cow::Owned(private_git_dir),
Cow::Owned(common_dir),
Kind::LinkedWorkTreeDir,
)
}
#[cfg(windows)]
Err(crate::path::from_gitdir_file::Error::Io(err)) if err.kind() == std::io::ErrorKind::PermissionDenied => {
(Cow::Borrowed(git_dir.as_ref()), Cow::Borrowed(git_dir.as_ref()), false)
Err(crate::path::from_gitdir_file::Error::Io(err)) if is_directory(&err) => {
let common_dir = git_dir.join("commondir");
match crate::path::from_plain_file(common_dir)
.and_then(Result::ok)
.and_then(|cd| {
crate::path::from_plain_file(git_dir.join("gitdir"))
.and_then(Result::ok)
.map(|worktree_gitfile| (crate::path::without_dot_git_dir(worktree_gitfile), cd))
}) {
Some((work_dir, common_dir)) => {
let common_dir = git_dir.join(common_dir);
(
Cow::Borrowed(git_dir),
Cow::Owned(common_dir),
Kind::WorkTreeGitDir { work_dir },
)
}
None => (Cow::Borrowed(git_dir), Cow::Borrowed(git_dir), Kind::MaybeRepo),
}
}
Err(err) => return Err(err.into()),
};
Expand Down Expand Up @@ -74,13 +105,17 @@ pub fn git(git_dir: impl AsRef<Path>) -> Result<crate::repository::Kind, crate::
}
}

Ok(if git_dir_is_linked_worktree {
crate::repository::Kind::WorkTree {
Ok(match kind {
Kind::LinkedWorkTreeDir => crate::repository::Kind::WorkTree {
linked_git_dir: Some(dot_git.into_owned()),
},
Kind::WorkTreeGitDir { work_dir } => crate::repository::Kind::WorkTreeGitDir { work_dir },
Kind::MaybeRepo => {
if bare(git_dir) {
crate::repository::Kind::Bare
} else {
crate::repository::Kind::WorkTree { linked_git_dir: None }
}
}
} else if bare(git_dir) {
crate::repository::Kind::Bare
} else {
crate::repository::Kind::WorkTree { linked_git_dir: None }
})
}
8 changes: 8 additions & 0 deletions git-discover/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,11 @@ pub fn from_gitdir_file(path: impl AsRef<std::path::Path>) -> Result<PathBuf, fr
}
Ok(gitdir)
}

/// Conditionally pop a trailing `.git` dir if present.
pub fn without_dot_git_dir(mut path: PathBuf) -> PathBuf {
if path.file_name().and_then(|n| n.to_str()) == Some(".git") {
path.pop();
}
path
}
19 changes: 13 additions & 6 deletions git-discover/src/repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ pub enum Path {
},
/// The currently checked out or nascent work tree of a git repository
WorkTree(PathBuf),
/// The git repository itself
/// The git repository itself, typically bare and without known worktree.
///
/// Note that it might still have linked work-trees which can be accessed later, weather bare or not.
Repository(PathBuf),
}

mod path {
use crate::path::without_dot_git_dir;
use crate::repository::{Kind, Path};
use std::path::PathBuf;

Expand Down Expand Up @@ -47,14 +50,11 @@ mod path {

let dir = dir.into();
match kind {
Kind::WorkTreeGitDir { work_dir } => Path::LinkedWorkTree { git_dir: dir, work_dir },
Kind::WorkTree { linked_git_dir } => match linked_git_dir {
Some(git_dir) => Path::LinkedWorkTree {
git_dir,
work_dir: {
let mut dir = absolutize_on_trailing_parent(dir);
dir.pop(); // ".git" portion
dir
},
work_dir: without_dot_git_dir(absolutize_on_trailing_parent(dir)),
},
None => {
let mut dir = absolutize_on_trailing_parent(dir);
Expand Down Expand Up @@ -92,13 +92,20 @@ mod path {
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum Kind {
/// A bare repository does not have a work tree, that is files on disk beyond the `git` repository itself.
///
/// Note that this is merely a guess at this point as we didn't read the configuration yet.
Bare,
/// A `git` repository along with a checked out files in a work tree.
WorkTree {
/// If set, this is the git dir associated with this _linked_ worktree.
/// If `None`, the git_dir is the `.git` directory inside the _main_ worktree we represent.
linked_git_dir: Option<PathBuf>,
},
/// A worktree's git directory in the common`.git` directory in `worktrees/<name>`.
WorkTreeGitDir {
/// Path to the worktree directory.
work_dir: PathBuf,
},
}

impl Kind {
Expand Down
27 changes: 18 additions & 9 deletions git-discover/tests/upwards/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,21 @@ fn from_non_existing_worktree() {
}

#[test]
fn from_existing_worktree() {
fn from_existing_worktree_inside_dot_git() {
let top_level_repo = repo_path().unwrap();
let (path, _trust) = git_discover::upwards(top_level_repo.join(".git/worktrees/a")).unwrap();
let suffix = std::path::Path::new(top_level_repo.file_name().unwrap())
.join("worktrees")
.join("a");
assert!(
matches!(path, git_discover::repository::Path::LinkedWorkTree { work_dir, .. } if work_dir.ends_with(suffix)),
"we can handle to start from within a (somewhat partial) worktree git dir"
);
}

#[test]
fn from_existing_worktree() -> crate::Result {
let top_level_repo = repo_path()?;
for (discover_path, expected_worktree_path, expected_git_dir) in [
(top_level_repo.join("worktrees/a"), "worktrees/a", ".git/worktrees/a"),
(
Expand All @@ -140,7 +153,7 @@ fn from_existing_worktree() {
"bare.git/worktrees/c",
),
] {
let (path, trust) = git_discover::upwards(discover_path).unwrap();
let (path, trust) = git_discover::upwards(discover_path)?;
assert!(matches!(path, git_discover::repository::Path::LinkedWorkTree { .. }));

assert_eq!(trust, expected_trust());
Expand All @@ -153,8 +166,8 @@ fn from_existing_worktree() {
);
#[cfg(windows)]
assert_eq!(
git_dir.canonicalize().unwrap(),
top_level_repo.join(expected_git_dir).canonicalize().unwrap(),
git_dir.canonicalize()?,
top_level_repo.join(expected_git_dir).canonicalize()?,
"we don't skip over worktrees and discover their git dir (gitdir is absolute in file)"
);
let worktree = worktree.expect("linked worktree is set");
Expand All @@ -163,12 +176,8 @@ fn from_existing_worktree() {
Ok(std::path::Path::new(expected_worktree_path)),
"the worktree path is the .git file's directory"
);

assert!(
git_discover::is_git(&git_dir).is_err(),
"we aren't able to detect git directories from private worktrees and that's by design"
);
}
Ok(())
}

fn repo_path() -> crate::Result<PathBuf> {
Expand Down

0 comments on commit 050f795

Please sign in to comment.