Skip to content

Commit 050f795

Browse files
committed
feat: allow discovery of linked worktree git dirs (#301)
This also works if the work-tree can't be found but it is otherwise a valid git dir.
1 parent 4cff7a8 commit 050f795

File tree

4 files changed

+90
-31
lines changed

4 files changed

+90
-31
lines changed

git-discover/src/is.rs

+51-16
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,23 @@ pub fn bare(git_dir_candidate: impl AsRef<Path>) -> bool {
2020
/// * an objects directory
2121
/// * a refs directory
2222
pub fn git(git_dir: impl AsRef<Path>) -> Result<crate::repository::Kind, crate::is_git::Error> {
23-
let (dot_git, common_dir, git_dir_is_linked_worktree) = match crate::path::from_gitdir_file(git_dir.as_ref()) {
23+
#[derive(Eq, PartialEq)]
24+
enum Kind {
25+
MaybeRepo,
26+
LinkedWorkTreeDir,
27+
WorkTreeGitDir { work_dir: std::path::PathBuf },
28+
}
29+
#[cfg(not(windows))]
30+
fn is_directory(err: &std::io::Error) -> bool {
31+
err.raw_os_error() == Some(21)
32+
}
33+
// TODO: use ::IsDirectory as well when stabilized, but it's permission denied on windows
34+
#[cfg(windows)]
35+
fn is_directory(err: &std::io::Error) -> bool {
36+
err.kind() == std::io::ErrorKind::PermissionDenied
37+
}
38+
let git_dir = git_dir.as_ref();
39+
let (dot_git, common_dir, kind) = match crate::path::from_gitdir_file(git_dir) {
2440
Ok(private_git_dir) => {
2541
let common_dir = private_git_dir.join("commondir");
2642
let common_dir = crate::path::from_plain_file(&common_dir)
@@ -29,16 +45,31 @@ pub fn git(git_dir: impl AsRef<Path>) -> Result<crate::repository::Kind, crate::
2945
})?
3046
.map_err(|_| crate::is_git::Error::MissingCommonDir { missing: common_dir })?;
3147
let common_dir = private_git_dir.join(common_dir);
32-
(Cow::Owned(private_git_dir), Cow::Owned(common_dir), true)
33-
}
34-
// TODO: use ::IsDirectory as well when stabilized, but it's permission denied on windows
35-
#[cfg(not(windows))]
36-
Err(crate::path::from_gitdir_file::Error::Io(err)) if err.raw_os_error() == Some(21) => {
37-
(Cow::Borrowed(git_dir.as_ref()), Cow::Borrowed(git_dir.as_ref()), false)
48+
(
49+
Cow::Owned(private_git_dir),
50+
Cow::Owned(common_dir),
51+
Kind::LinkedWorkTreeDir,
52+
)
3853
}
39-
#[cfg(windows)]
40-
Err(crate::path::from_gitdir_file::Error::Io(err)) if err.kind() == std::io::ErrorKind::PermissionDenied => {
41-
(Cow::Borrowed(git_dir.as_ref()), Cow::Borrowed(git_dir.as_ref()), false)
54+
Err(crate::path::from_gitdir_file::Error::Io(err)) if is_directory(&err) => {
55+
let common_dir = git_dir.join("commondir");
56+
match crate::path::from_plain_file(common_dir)
57+
.and_then(Result::ok)
58+
.and_then(|cd| {
59+
crate::path::from_plain_file(git_dir.join("gitdir"))
60+
.and_then(Result::ok)
61+
.map(|worktree_gitfile| (crate::path::without_dot_git_dir(worktree_gitfile), cd))
62+
}) {
63+
Some((work_dir, common_dir)) => {
64+
let common_dir = git_dir.join(common_dir);
65+
(
66+
Cow::Borrowed(git_dir),
67+
Cow::Owned(common_dir),
68+
Kind::WorkTreeGitDir { work_dir },
69+
)
70+
}
71+
None => (Cow::Borrowed(git_dir), Cow::Borrowed(git_dir), Kind::MaybeRepo),
72+
}
4273
}
4374
Err(err) => return Err(err.into()),
4475
};
@@ -74,13 +105,17 @@ pub fn git(git_dir: impl AsRef<Path>) -> Result<crate::repository::Kind, crate::
74105
}
75106
}
76107

77-
Ok(if git_dir_is_linked_worktree {
78-
crate::repository::Kind::WorkTree {
108+
Ok(match kind {
109+
Kind::LinkedWorkTreeDir => crate::repository::Kind::WorkTree {
79110
linked_git_dir: Some(dot_git.into_owned()),
111+
},
112+
Kind::WorkTreeGitDir { work_dir } => crate::repository::Kind::WorkTreeGitDir { work_dir },
113+
Kind::MaybeRepo => {
114+
if bare(git_dir) {
115+
crate::repository::Kind::Bare
116+
} else {
117+
crate::repository::Kind::WorkTree { linked_git_dir: None }
118+
}
80119
}
81-
} else if bare(git_dir) {
82-
crate::repository::Kind::Bare
83-
} else {
84-
crate::repository::Kind::WorkTree { linked_git_dir: None }
85120
})
86121
}

git-discover/src/path.rs

+8
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,11 @@ pub fn from_gitdir_file(path: impl AsRef<std::path::Path>) -> Result<PathBuf, fr
5858
}
5959
Ok(gitdir)
6060
}
61+
62+
/// Conditionally pop a trailing `.git` dir if present.
63+
pub fn without_dot_git_dir(mut path: PathBuf) -> PathBuf {
64+
if path.file_name().and_then(|n| n.to_str()) == Some(".git") {
65+
path.pop();
66+
}
67+
path
68+
}

git-discover/src/repository.rs

+13-6
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ pub enum Path {
1212
},
1313
/// The currently checked out or nascent work tree of a git repository
1414
WorkTree(PathBuf),
15-
/// The git repository itself
15+
/// The git repository itself, typically bare and without known worktree.
16+
///
17+
/// Note that it might still have linked work-trees which can be accessed later, weather bare or not.
1618
Repository(PathBuf),
1719
}
1820

1921
mod path {
22+
use crate::path::without_dot_git_dir;
2023
use crate::repository::{Kind, Path};
2124
use std::path::PathBuf;
2225

@@ -47,14 +50,11 @@ mod path {
4750

4851
let dir = dir.into();
4952
match kind {
53+
Kind::WorkTreeGitDir { work_dir } => Path::LinkedWorkTree { git_dir: dir, work_dir },
5054
Kind::WorkTree { linked_git_dir } => match linked_git_dir {
5155
Some(git_dir) => Path::LinkedWorkTree {
5256
git_dir,
53-
work_dir: {
54-
let mut dir = absolutize_on_trailing_parent(dir);
55-
dir.pop(); // ".git" portion
56-
dir
57-
},
57+
work_dir: without_dot_git_dir(absolutize_on_trailing_parent(dir)),
5858
},
5959
None => {
6060
let mut dir = absolutize_on_trailing_parent(dir);
@@ -92,13 +92,20 @@ mod path {
9292
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
9393
pub enum Kind {
9494
/// A bare repository does not have a work tree, that is files on disk beyond the `git` repository itself.
95+
///
96+
/// Note that this is merely a guess at this point as we didn't read the configuration yet.
9597
Bare,
9698
/// A `git` repository along with a checked out files in a work tree.
9799
WorkTree {
98100
/// If set, this is the git dir associated with this _linked_ worktree.
99101
/// If `None`, the git_dir is the `.git` directory inside the _main_ worktree we represent.
100102
linked_git_dir: Option<PathBuf>,
101103
},
104+
/// A worktree's git directory in the common`.git` directory in `worktrees/<name>`.
105+
WorkTreeGitDir {
106+
/// Path to the worktree directory.
107+
work_dir: PathBuf,
108+
},
102109
}
103110

104111
impl Kind {

git-discover/tests/upwards/mod.rs

+18-9
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,21 @@ fn from_non_existing_worktree() {
130130
}
131131

132132
#[test]
133-
fn from_existing_worktree() {
133+
fn from_existing_worktree_inside_dot_git() {
134134
let top_level_repo = repo_path().unwrap();
135+
let (path, _trust) = git_discover::upwards(top_level_repo.join(".git/worktrees/a")).unwrap();
136+
let suffix = std::path::Path::new(top_level_repo.file_name().unwrap())
137+
.join("worktrees")
138+
.join("a");
139+
assert!(
140+
matches!(path, git_discover::repository::Path::LinkedWorkTree { work_dir, .. } if work_dir.ends_with(suffix)),
141+
"we can handle to start from within a (somewhat partial) worktree git dir"
142+
);
143+
}
144+
145+
#[test]
146+
fn from_existing_worktree() -> crate::Result {
147+
let top_level_repo = repo_path()?;
135148
for (discover_path, expected_worktree_path, expected_git_dir) in [
136149
(top_level_repo.join("worktrees/a"), "worktrees/a", ".git/worktrees/a"),
137150
(
@@ -140,7 +153,7 @@ fn from_existing_worktree() {
140153
"bare.git/worktrees/c",
141154
),
142155
] {
143-
let (path, trust) = git_discover::upwards(discover_path).unwrap();
156+
let (path, trust) = git_discover::upwards(discover_path)?;
144157
assert!(matches!(path, git_discover::repository::Path::LinkedWorkTree { .. }));
145158

146159
assert_eq!(trust, expected_trust());
@@ -153,8 +166,8 @@ fn from_existing_worktree() {
153166
);
154167
#[cfg(windows)]
155168
assert_eq!(
156-
git_dir.canonicalize().unwrap(),
157-
top_level_repo.join(expected_git_dir).canonicalize().unwrap(),
169+
git_dir.canonicalize()?,
170+
top_level_repo.join(expected_git_dir).canonicalize()?,
158171
"we don't skip over worktrees and discover their git dir (gitdir is absolute in file)"
159172
);
160173
let worktree = worktree.expect("linked worktree is set");
@@ -163,12 +176,8 @@ fn from_existing_worktree() {
163176
Ok(std::path::Path::new(expected_worktree_path)),
164177
"the worktree path is the .git file's directory"
165178
);
166-
167-
assert!(
168-
git_discover::is_git(&git_dir).is_err(),
169-
"we aren't able to detect git directories from private worktrees and that's by design"
170-
);
171179
}
180+
Ok(())
172181
}
173182

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

0 commit comments

Comments
 (0)