Skip to content

Commit 208cf5a

Browse files
committed
fix: properly classify non-bare repositories without worktree.
Such repositories claim they are not bare, but also don't have a workdir with a .git directory inside of it (which is always needed unless certain variables are set). Now they are classified as repository that needs more detailed checking later, while indicating that it 'usually' would be having a worktree that is configured via git configuration.
1 parent 85e18d2 commit 208cf5a

File tree

6 files changed

+65
-118
lines changed

6 files changed

+65
-118
lines changed

gix-discover/src/is.rs

Lines changed: 17 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -9,107 +9,6 @@ pub fn bare(git_dir_candidate: &Path) -> bool {
99
!(git_dir_candidate.join("index").exists() || (git_dir_candidate.file_name() == Some(OsStr::new(DOT_GIT_DIR))))
1010
}
1111

12-
/// Parse `<git_dir_candidate>/config` quickly to evaluate the value of the `bare` line, or return `true` if the file doesn't exist
13-
/// similar to what`guess_repository_type` seems to be doing.
14-
/// Return `None` if the `bare` line can't be found or the value of `bare` can't be determined.
15-
fn bare_by_config(git_dir_candidate: &Path) -> std::io::Result<Option<bool>> {
16-
match std::fs::read(git_dir_candidate.join("config")) {
17-
Ok(buf) => Ok(config::parse_bare(&buf)),
18-
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(Some(true)),
19-
Err(err) => Err(err),
20-
}
21-
}
22-
23-
// Copied and adapted from `gix-config-value::boolean`.
24-
mod config {
25-
use bstr::{BStr, ByteSlice};
26-
27-
/// Note that we intentionally turn repositories that have a worktree configuration into bare repos,
28-
/// as we don't actually parse the worktree from the config file and expect the caller to do the right
29-
/// think when seemingly seeing bare repository.
30-
/// The reason we do this is to not incorrectly pretend this is a worktree.
31-
pub(crate) fn parse_bare(buf: &[u8]) -> Option<bool> {
32-
let mut is_bare = None;
33-
let mut has_worktree_configuration = false;
34-
for line in buf.lines() {
35-
if is_bare.is_none() {
36-
if let Some(line) = line.trim().strip_prefix(b"bare") {
37-
is_bare = match line.first() {
38-
None => Some(true),
39-
Some(c) if *c == b'=' => parse_bool(line.get(1..)?.trim_start().as_bstr()),
40-
Some(c) if c.is_ascii_whitespace() => match line.split_once_str(b"=") {
41-
Some((_left, right)) => parse_bool(right.trim_start().as_bstr()),
42-
None => Some(true),
43-
},
44-
Some(_other_char_) => None,
45-
};
46-
continue;
47-
}
48-
}
49-
if line.trim().strip_prefix(b"worktree").is_some() {
50-
has_worktree_configuration = true;
51-
break;
52-
}
53-
}
54-
is_bare.map(|bare| bare || has_worktree_configuration)
55-
}
56-
57-
fn parse_bool(value: &BStr) -> Option<bool> {
58-
Some(if parse_true(value) {
59-
true
60-
} else if parse_false(value) {
61-
false
62-
} else {
63-
use std::str::FromStr;
64-
if let Some(integer) = value.to_str().ok().and_then(|s| i64::from_str(s).ok()) {
65-
integer != 0
66-
} else {
67-
return None;
68-
}
69-
})
70-
}
71-
72-
fn parse_true(value: &BStr) -> bool {
73-
value.eq_ignore_ascii_case(b"yes") || value.eq_ignore_ascii_case(b"on") || value.eq_ignore_ascii_case(b"true")
74-
}
75-
76-
fn parse_false(value: &BStr) -> bool {
77-
value.eq_ignore_ascii_case(b"no")
78-
|| value.eq_ignore_ascii_case(b"off")
79-
|| value.eq_ignore_ascii_case(b"false")
80-
|| value.is_empty()
81-
}
82-
83-
#[cfg(test)]
84-
mod tests {
85-
use super::*;
86-
87-
#[test]
88-
fn various() {
89-
for (input, expected) in [
90-
("bare=true", Some(true)),
91-
("bare=1", Some(true)),
92-
("bare =1", Some(true)),
93-
("bare= yes", Some(true)),
94-
("bare=false", Some(false)),
95-
("bare=0", Some(false)),
96-
("bare=blah", None),
97-
("bare=", Some(false)),
98-
("bare= \n", Some(false)),
99-
("bare = true \n", Some(true)),
100-
("\t bare = false \n", Some(false)),
101-
("\n\tbare=true", Some(true)),
102-
("\n\tbare=true\n\tfoo", Some(true)),
103-
("\n\tbare ", Some(true)),
104-
("\n\tbare", Some(true)),
105-
("not found\nreally", None),
106-
] {
107-
assert_eq!(parse_bare(input.as_bytes()), expected, "{input:?}");
108-
}
109-
}
110-
}
111-
}
112-
11312
/// Returns true if `git_dir` is located within a `.git/modules` directory, indicating it's a submodule clone.
11413
pub fn submodule_git_dir(git_dir: &Path) -> bool {
11514
let mut last_comp = None;
@@ -141,12 +40,15 @@ pub fn git(git_dir: &Path) -> Result<crate::repository::Kind, crate::is_git::Err
14140
source: err,
14241
path: git_dir.into(),
14342
})?;
144-
git_with_metadata(git_dir, git_dir_metadata)
43+
// precompose-unicode can't be known here, so we just default it to false, hoping it won't matter.
44+
let cwd = gix_fs::current_dir(false)?;
45+
git_with_metadata(git_dir, git_dir_metadata, &cwd)
14546
}
14647

14748
pub(crate) fn git_with_metadata(
14849
git_dir: &Path,
14950
git_dir_metadata: std::fs::Metadata,
51+
cwd: &Path,
15052
) -> Result<crate::repository::Kind, crate::is_git::Error> {
15153
#[derive(Eq, PartialEq)]
15254
enum Kind {
@@ -166,6 +68,8 @@ pub(crate) fn git_with_metadata(
16668
{
16769
// Fast-path: avoid doing the complete search if HEAD is already not there.
16870
// TODO(reftable): use a ref-store to lookup HEAD if ref-tables should be supported, or detect ref-tables beforehand.
71+
// Actually ref-tables still keep a specially marked `HEAD` around, so nothing might be needed here
72+
// Even though our head-check later would fail without supporting it.
16973
if !dot_git.join("HEAD").exists() {
17074
return Err(crate::is_git::Error::MissingHead);
17175
}
@@ -236,25 +140,26 @@ pub(crate) fn git_with_metadata(
236140
},
237141
Kind::MaybeRepo => {
238142
let conformed_git_dir = if git_dir == Path::new(".") {
239-
gix_path::realpath(git_dir)
143+
gix_path::realpath_opts(git_dir, cwd, gix_path::realpath::MAX_SYMLINKS)
240144
.map(Cow::Owned)
241145
.unwrap_or(Cow::Borrowed(git_dir))
242146
} else {
243-
Cow::Borrowed(git_dir)
147+
gix_path::normalize(git_dir.into(), cwd).unwrap_or(Cow::Borrowed(git_dir))
244148
};
245149
if bare(conformed_git_dir.as_ref()) || conformed_git_dir.extension() == Some(OsStr::new("git")) {
246150
crate::repository::Kind::PossiblyBare
247151
} else if submodule_git_dir(conformed_git_dir.as_ref()) {
248152
crate::repository::Kind::SubmoduleGitDir
249-
} else if conformed_git_dir.file_name() == Some(OsStr::new(DOT_GIT_DIR))
250-
|| !bare_by_config(conformed_git_dir.as_ref())
251-
.map_err(|err| crate::is_git::Error::Metadata {
252-
source: err,
253-
path: conformed_git_dir.join("config"),
254-
})?
255-
.ok_or(crate::is_git::Error::Inconclusive)?
256-
{
153+
} else if conformed_git_dir.file_name() == Some(OsStr::new(DOT_GIT_DIR)) {
257154
crate::repository::Kind::WorkTree { linked_git_dir: None }
155+
// } else if !bare_by_config(conformed_git_dir.as_ref())
156+
// .map_err(|err| crate::is_git::Error::Metadata {
157+
// source: err,
158+
// path: conformed_git_dir.join("config"),
159+
// })?
160+
// .ok_or(crate::is_git::Error::Inconclusive)?
161+
// {
162+
// crate::repository::Kind::WorktreePossiblyInConfiguration
258163
} else {
259164
crate::repository::Kind::PossiblyBare
260165
}

gix-discover/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ pub mod is_git {
3939
Metadata { source: std::io::Error, path: PathBuf },
4040
#[error("The repository's config file doesn't exist or didn't have a 'bare' configuration or contained core.worktree without value")]
4141
Inconclusive,
42+
#[error("Could not obtain current directory when conforming repository path")]
43+
CurrentDir(#[from] std::io::Error),
4244
}
4345
}
4446

gix-discover/src/repository.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ pub enum Path {
1414
/// The currently checked out or nascent work tree of a git repository
1515
WorkTree(PathBuf),
1616
/// The git repository itself, typically bare and without known worktree.
17+
/// It could also be non-bare with a worktree configured using git configuration, or no worktree at all despite
18+
/// not being bare (due to mis-configuration for example).
1719
///
18-
/// Note that it might still have linked work-trees which can be accessed later, weather bare or not, or it might be a
20+
/// Note that it might still have linked work-trees which can be accessed later, bare or not, or it might be a
1921
/// submodule git directory in the `.git/modules/**/<name>` directory of the parent repository.
2022
Repository(PathBuf),
2123
}
@@ -112,8 +114,11 @@ pub enum Kind {
112114
/// Note that this is merely a guess at this point as we didn't read the configuration yet.
113115
///
114116
/// Also note that due to optimizing for performance and *just* making an educated *guess in some situations*,
115-
/// we may consider a non-bare repository bare if it it doesn't have an index yet due to be freshly initialized.
116-
/// The caller is has to handle this, typically by reading the configuration.
117+
/// we may consider a non-bare repository bare if it doesn't have an index yet due to be freshly initialized.
118+
/// The caller has to handle this, typically by reading the configuration.
119+
///
120+
/// It could also be a directory which is non-bare by configuration, but is *not* named `.git`.
121+
/// Unusual, but it's possible that a worktree is configured via `core.worktree`.
117122
PossiblyBare,
118123
/// A `git` repository along with checked out files in a work tree.
119124
WorkTree {

gix-discover/src/upwards/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ pub(crate) mod function {
3838
// Normalize the path so that `Path::parent()` _actually_ gives
3939
// us the parent directory. (`Path::parent` just strips off the last
4040
// path component, which means it will not do what you expect when
41-
// working with paths paths that contain '..'.)
41+
// working with paths that contain '..'.)
4242
let cwd = current_dir.map_or_else(
4343
|| {
4444
// The paths we return are relevant to the repository, but at this time it's impossible to know
45-
// what `core.precomposeUnicode` is going to be. Hence the one using these paths will have to
45+
// what `core.precomposeUnicode` is going to be. Hence, the one using these paths will have to
4646
// transform the paths as needed, because we can't. `false` means to leave the obtained path as is.
4747
gix_fs::current_dir(false).map(Cow::Owned)
4848
},
@@ -130,7 +130,7 @@ pub(crate) mod function {
130130
cursor_metadata_backup = cursor_metadata.take();
131131
}
132132
if let Ok(kind) = match cursor_metadata.take() {
133-
Some(metadata) => is_git_with_metadata(&cursor, metadata),
133+
Some(metadata) => is_git_with_metadata(&cursor, metadata, &cwd),
134134
None => is_git(&cursor),
135135
} {
136136
match filter_by_trust(&cursor)? {

gix-discover/tests/discover/is_git/mod.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,30 @@ fn bare_repo_with_index_file_looks_still_looks_like_bare() -> crate::Result {
5858
Ok(())
5959
}
6060

61+
#[test]
62+
fn non_bare_repo_without_workdir() -> crate::Result {
63+
let repo = repo_path()?.join("non-bare-without-worktree");
64+
let kind = gix_discover::is_git(&repo)?;
65+
assert_eq!(
66+
kind,
67+
gix_discover::repository::Kind::PossiblyBare,
68+
"typically due to misconfiguration, but worktrees could also be configured in Git configuration"
69+
);
70+
Ok(())
71+
}
72+
73+
#[test]
74+
fn non_bare_repo_without_workdir_with_index() -> crate::Result {
75+
let repo = repo_path()?.join("non-bare-without-worktree-with-index");
76+
let kind = gix_discover::is_git(&repo)?;
77+
assert_eq!(
78+
kind,
79+
gix_discover::repository::Kind::PossiblyBare,
80+
"this means it has to be validated later"
81+
);
82+
Ok(())
83+
}
84+
6185
#[test]
6286
fn bare_repo_with_index_file_looks_still_looks_like_bare_if_it_was_renamed() -> crate::Result {
6387
for repo_name in ["bare-with-index-bare", "bare-with-index-no-config-bare"] {

gix-discover/tests/fixtures/make_basic_repo.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ mkdir -p some/very/deeply/nested/subdir
1515

1616
git clone --bare --shared . bare.git
1717

18+
git clone --bare --shared . non-bare-without-worktree
19+
(cd non-bare-without-worktree
20+
git config core.bare false
21+
)
22+
23+
git clone --bare --shared . non-bare-without-worktree-with-index
24+
(cd non-bare-without-worktree
25+
git config core.bare false
26+
cp ../.git/index .
27+
)
28+
1829
git worktree add worktrees/a
1930
git worktree add worktrees/b-private-dir-deleted
2031
rm -R .git/worktrees/b-private-dir-deleted

0 commit comments

Comments
 (0)