Skip to content

Commit 75ac1d0

Browse files
authored
Merge pull request GitoxideLabs#2403 from GitoxideLabs/improvements
fix: Differentiate between `core.bare` being known or not
2 parents ea9e2b9 + e6f62be commit 75ac1d0

File tree

21 files changed

+238
-119
lines changed

21 files changed

+238
-119
lines changed

gitoxide-core/src/organize.rs

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,50 @@ pub fn find_git_repository_workdirs(
2020
threads: Option<usize>,
2121
) -> impl Iterator<Item = (PathBuf, gix::repository::Kind)> {
2222
progress.init(None, progress::count("filesystem items"));
23-
fn is_repository(path: &Path) -> Option<gix::repository::Kind> {
23+
24+
#[derive(Debug, Clone, Copy)]
25+
struct RepoInfo {
26+
kind: gix::repository::Kind,
27+
is_bare: bool,
28+
}
29+
30+
fn is_repository(path: &Path) -> Option<RepoInfo> {
2431
// Can be git dir or worktree checkout (file)
2532
if path.file_name() != Some(OsStr::new(".git")) && path.extension() != Some(OsStr::new("git")) {
2633
return None;
2734
}
2835

2936
if path.is_dir() {
3037
if path.join("HEAD").is_file() && path.join("config").is_file() {
31-
gix::discover::is_git(path).ok().map(Into::into)
38+
gix::discover::is_git(path).ok().map(|discovered_kind| {
39+
let is_bare = discovered_kind.is_bare();
40+
let kind = match discovered_kind {
41+
gix::discover::repository::Kind::PossiblyBare => gix::repository::Kind::Common,
42+
gix::discover::repository::Kind::WorkTree { linked_git_dir: None } => {
43+
gix::repository::Kind::Common
44+
}
45+
gix::discover::repository::Kind::WorkTree {
46+
linked_git_dir: Some(_),
47+
} => gix::repository::Kind::LinkedWorkTree,
48+
gix::discover::repository::Kind::WorkTreeGitDir { .. } => gix::repository::Kind::LinkedWorkTree,
49+
gix::discover::repository::Kind::Submodule { .. } => gix::repository::Kind::Submodule,
50+
gix::discover::repository::Kind::SubmoduleGitDir => gix::repository::Kind::Submodule,
51+
};
52+
RepoInfo { kind, is_bare }
53+
})
3254
} else {
3355
None
3456
}
3557
} else {
36-
// git files are always worktrees
37-
Some(gix::repository::Kind::WorkTree { is_linked: true })
58+
// git files are always linked worktrees
59+
Some(RepoInfo {
60+
kind: gix::repository::Kind::LinkedWorkTree,
61+
is_bare: false,
62+
})
3863
}
3964
}
40-
fn into_workdir(git_dir: PathBuf, kind: &gix::repository::Kind) -> PathBuf {
41-
if matches!(kind, gix::repository::Kind::Bare) || gix::discover::is_bare(&git_dir) {
65+
fn into_workdir(git_dir: PathBuf, info: &RepoInfo) -> PathBuf {
66+
if info.is_bare {
4267
git_dir
4368
} else {
4469
git_dir.parent().expect("git is never in the root").to_owned()
@@ -47,7 +72,7 @@ pub fn find_git_repository_workdirs(
4772

4873
#[derive(Debug, Default)]
4974
struct State {
50-
kind: Option<gix::repository::Kind>,
75+
info: Option<RepoInfo>,
5176
}
5277

5378
let walk = jwalk::WalkDirGeneric::<((), State)>::new(root)
@@ -64,9 +89,9 @@ pub fn find_git_repository_workdirs(
6489
let mut found_bare_repo = false;
6590
for entry in siblings.iter_mut().flatten() {
6691
let path = entry.path();
67-
if let Some(kind) = is_repository(&path) {
68-
let is_bare = kind.is_bare();
69-
entry.client_state = State { kind: kind.into() };
92+
if let Some(info) = is_repository(&path) {
93+
let is_bare = info.is_bare;
94+
entry.client_state = State { info: info.into() };
7095
entry.read_children_path = None;
7196

7297
found_any_repo = true;
@@ -76,17 +101,17 @@ pub fn find_git_repository_workdirs(
76101
// Only return paths which are repositories are further participating in the traversal
77102
// Don't let bare repositories cause siblings to be pruned.
78103
if found_any_repo && !found_bare_repo {
79-
siblings.retain(|e| e.as_ref().map(|e| e.client_state.kind.is_some()).unwrap_or(false));
104+
siblings.retain(|e| e.as_ref().map(|e| e.client_state.info.is_some()).unwrap_or(false));
80105
}
81106
})
82107
.into_iter()
83108
.inspect(move |_| progress.inc())
84109
.filter_map(Result::ok)
85110
.filter_map(|mut e| {
86111
e.client_state
87-
.kind
112+
.info
88113
.take()
89-
.map(|kind| (into_workdir(e.path(), &kind), kind))
114+
.map(|info| (into_workdir(e.path(), &info), info.kind))
90115
})
91116
}
92117

@@ -108,7 +133,8 @@ fn handle(
108133
canonicalized_destination: &Path,
109134
progress: &mut impl Progress,
110135
) -> anyhow::Result<()> {
111-
if let gix::repository::Kind::WorkTree { is_linked: true } = kind {
136+
// Skip linked worktrees - we only handle Common and Submodule kinds
137+
if matches!(kind, gix::repository::Kind::LinkedWorkTree) {
112138
return Ok(());
113139
}
114140
fn to_relative(path: PathBuf) -> PathBuf {
@@ -173,13 +199,24 @@ fn handle(
173199
let mut path = gix_url::expand_path(None, url.path.as_bstr())?;
174200
match kind {
175201
gix::repository::Kind::Submodule => {
176-
unreachable!("BUG: We should not try to relocated submodules and not find them the first place")
202+
unreachable!("BUG: We should not try to relocate submodules and not find them the first place")
203+
}
204+
gix::repository::Kind::LinkedWorkTree => {
205+
unreachable!("BUG: LinkedWorkTree should have been skipped earlier")
177206
}
178-
gix::repository::Kind::Bare => path,
179-
gix::repository::Kind::WorkTree { .. } => {
180-
if let Some(ext) = path.extension() {
181-
if ext == "git" {
182-
path.set_extension("");
207+
gix::repository::Kind::Common => {
208+
// For Common kind, check if it's bare
209+
let git_dir = if git_workdir.join(".git").is_dir() {
210+
git_workdir.join(".git")
211+
} else {
212+
git_workdir.to_owned()
213+
};
214+
if !gix::discover::is_bare(&git_dir) {
215+
// Non-bare repository - strip .git extension if present
216+
if let Some(ext) = path.extension() {
217+
if ext == "git" {
218+
path.set_extension("");
219+
}
183220
}
184221
}
185222
path

gix-discover/src/is.rs

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::{borrow::Cow, ffi::OsStr, path::Path};
22

3-
use crate::{DOT_GIT_DIR, MODULES};
3+
use crate::path::RepositoryKind;
4+
use crate::DOT_GIT_DIR;
45

56
/// Returns true if the given `git_dir` seems to be a bare repository.
67
///
@@ -10,18 +11,9 @@ pub fn bare(git_dir_candidate: &Path) -> bool {
1011
}
1112

1213
/// Returns true if `git_dir` is located within a `.git/modules` directory, indicating it's a submodule clone.
14+
#[deprecated = "use path::repository_kind() instead"]
1315
pub fn submodule_git_dir(git_dir: &Path) -> bool {
14-
let mut last_comp = None;
15-
git_dir.file_name() != Some(OsStr::new(DOT_GIT_DIR))
16-
&& git_dir.components().rev().skip(1).any(|c| {
17-
if c.as_os_str() == OsStr::new(DOT_GIT_DIR) {
18-
true
19-
} else {
20-
last_comp = Some(c.as_os_str());
21-
false
22-
}
23-
})
24-
&& last_comp == Some(OsStr::new(MODULES))
16+
crate::path::repository_kind(git_dir).is_some_and(|kind| matches!(kind, RepositoryKind::Submodule))
2517
}
2618

2719
/// What constitutes a valid git repository, returning the guessed repository kind
@@ -157,18 +149,12 @@ pub(crate) fn git_with_metadata(
157149
};
158150
if bare(conformed_git_dir.as_ref()) || conformed_git_dir.extension() == Some(OsStr::new("git")) {
159151
crate::repository::Kind::PossiblyBare
160-
} else if submodule_git_dir(conformed_git_dir.as_ref()) {
152+
} else if crate::path::repository_kind(conformed_git_dir.as_ref())
153+
.is_some_and(|kind| matches!(kind, RepositoryKind::Submodule))
154+
{
161155
crate::repository::Kind::SubmoduleGitDir
162156
} else if conformed_git_dir.file_name() == Some(OsStr::new(DOT_GIT_DIR)) {
163157
crate::repository::Kind::WorkTree { linked_git_dir: None }
164-
// } else if !bare_by_config(conformed_git_dir.as_ref())
165-
// .map_err(|err| crate::is_git::Error::Metadata {
166-
// source: err,
167-
// path: conformed_git_dir.join("config"),
168-
// })?
169-
// .ok_or(crate::is_git::Error::Inconclusive)?
170-
// {
171-
// crate::repository::Kind::WorktreePossiblyInConfiguration
172158
} else {
173159
crate::repository::Kind::PossiblyBare
174160
}

gix-discover/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ pub mod is_git {
4545
}
4646

4747
mod is;
48-
pub use is::{bare as is_bare, git as is_git, submodule_git_dir as is_submodule_git_dir};
48+
#[allow(deprecated)]
49+
pub use is::submodule_git_dir as is_submodule_git_dir;
50+
pub use is::{bare as is_bare, git as is_git};
4951

5052
///
5153
pub mod upwards;

gix-discover/src/path.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
1+
use crate::{DOT_GIT_DIR, MODULES};
2+
use std::ffi::OsStr;
3+
use std::path::Path;
14
use std::{io::Read, path::PathBuf};
25

3-
use crate::DOT_GIT_DIR;
6+
/// The kind of repository by looking exclusively at its `git_dir`.
7+
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
8+
pub enum RepositoryKind {
9+
/// The repository resides in `.git/modules/`.
10+
Submodule,
11+
/// The repository resides in `.git/worktrees/`.
12+
LinkedWorktree,
13+
/// The repository is in a `.git` directory.
14+
Common,
15+
}
416

517
///
618
pub mod from_gitdir_file {
@@ -32,6 +44,34 @@ fn read_regular_file_content_with_size_limit(path: &std::path::Path) -> std::io:
3244
Ok(buf)
3345
}
3446

47+
/// Guess the kind of repository by looking at its `git_dir` path and return it.
48+
/// Return `None` if `git_dir` isn't called `.git` or isn't within `.git/worktrees` or `.git/modules`, or if it's
49+
/// a `.git` suffix like in `foo.git`.
50+
/// The check for markers is case-sensitive under the assumption that nobody meddles with standard directories.
51+
pub fn repository_kind(git_dir: &Path) -> Option<RepositoryKind> {
52+
if git_dir.file_name() == Some(OsStr::new(DOT_GIT_DIR)) {
53+
return Some(RepositoryKind::Common);
54+
}
55+
56+
let mut last_comp = None;
57+
git_dir.components().rev().skip(1).any(|c| {
58+
if c.as_os_str() == OsStr::new(DOT_GIT_DIR) {
59+
true
60+
} else {
61+
last_comp = Some(c.as_os_str());
62+
false
63+
}
64+
});
65+
let last_comp = last_comp?;
66+
if last_comp == OsStr::new(MODULES) {
67+
RepositoryKind::Submodule.into()
68+
} else if last_comp == OsStr::new("worktrees") {
69+
RepositoryKind::LinkedWorktree.into()
70+
} else {
71+
None
72+
}
73+
}
74+
3575
/// Reads a plain path from a file that contains it as its only content, with trailing newlines trimmed.
3676
pub fn from_plain_file(path: &std::path::Path) -> Option<std::io::Result<PathBuf>> {
3777
use bstr::ByteSlice;

gix-discover/tests/discover/path.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,25 @@ mod from_git_dir_file {
4343
Ok(file)
4444
}
4545
}
46+
47+
#[test]
48+
fn repository_kind() {
49+
use gix_discover::path::{repository_kind, RepositoryKind::*};
50+
assert_eq!(repository_kind("hello".as_ref()), None);
51+
assert_eq!(repository_kind(".git".as_ref()), Some(Common));
52+
assert_eq!(repository_kind("foo/.git".as_ref()), Some(Common));
53+
assert_eq!(
54+
repository_kind("foo/other.git".as_ref()),
55+
None,
56+
"it makes no assumption beyond the standard name, nor does it consider suffixes"
57+
);
58+
assert_eq!(repository_kind(".git/modules".as_ref()), None);
59+
assert_eq!(
60+
repository_kind(".git/modules/actual-submodule".as_ref()),
61+
Some(Submodule)
62+
);
63+
assert_eq!(
64+
repository_kind(".git/worktrees/actual-worktree".as_ref()),
65+
Some(LinkedWorktree)
66+
);
67+
}

gix/src/config/cache/access.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ use crate::{
1818

1919
/// Access
2020
impl Cache {
21+
/// Returns `true` if the configuration value isn't set, so we assume bare for safety.
22+
pub(crate) fn is_bare_but_assume_bare_if_unconfigured(&self) -> bool {
23+
self.is_bare.unwrap_or(true)
24+
}
25+
2126
#[cfg(feature = "blob-diff")]
2227
pub(crate) fn diff_algorithm(&self) -> Result<gix_diff::blob::Algorithm, config::diff::algorithm::Error> {
2328
use crate::config::{cache::util::ApplyLeniencyDefault, diff::algorithm::Error, tree::Diff};

gix/src/config/cache/incubate.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ pub(crate) struct StageOne {
1212
pub git_dir_config: gix_config::File<'static>,
1313
pub buf: Vec<u8>,
1414

15-
pub is_bare: bool,
15+
pub is_bare: Option<bool>,
1616
pub lossy: bool,
1717
pub object_hash: gix_hash::Kind,
1818
pub reflog: Option<gix_ref::store::WriteReflog>,
@@ -39,9 +39,7 @@ impl StageOne {
3939
lenient,
4040
)?;
4141

42-
// Note that we assume the repo is bare by default unless we are told otherwise. This is relevant if
43-
// the repo doesn't have a configuration file.
44-
let is_bare = util::config_bool(&config, &Core::BARE, "core.bare", true, lenient)?;
42+
let is_bare = util::config_bool_opt(&config, &Core::BARE, "core.bare", lenient)?;
4543
let repo_format_version = config
4644
.integer("core.repositoryFormatVersion")
4745
.map(|version| Core::REPOSITORY_FORMAT_VERSION.try_into_usize(version))

gix/src/config/cache/util.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub(crate) fn base_options(lossy: bool, lenient: bool) -> gix_config::file::init
2424
}
2525
}
2626

27+
/// Also sets the default if it's not set!
2728
pub(crate) fn config_bool(
2829
config: &gix_config::File<'_>,
2930
key: &'static config::tree::keys::Boolean,
@@ -44,6 +45,24 @@ pub(crate) fn config_bool(
4445
.with_lenient_default(lenient)
4546
}
4647

48+
pub(crate) fn config_bool_opt(
49+
config: &gix_config::File<'_>,
50+
key: &'static config::tree::keys::Boolean,
51+
key_str: &str,
52+
lenient: bool,
53+
) -> Result<Option<bool>, Error> {
54+
use config::tree::Key;
55+
debug_assert_eq!(
56+
key_str,
57+
key.logical_name(),
58+
"BUG: key name and hardcoded name must match"
59+
);
60+
config
61+
.boolean(key_str)
62+
.map(|res| key.enrich_error(res).map_err(Error::from).with_lenient_default(lenient))
63+
.transpose()
64+
}
65+
4766
pub(crate) fn query_refupdates(
4867
config: &gix_config::File<'static>,
4968
lenient_config: bool,

gix/src/config/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -590,8 +590,8 @@ pub(crate) struct Cache {
590590
pub resolved: crate::Config,
591591
/// The hex-length to assume when shortening object ids. If `None`, it should be computed based on the approximate object count.
592592
pub hex_len: Option<usize>,
593-
/// true if the repository is designated as 'bare', without work tree.
594-
pub is_bare: bool,
593+
/// `true` if the repository is designated as 'bare', without work tree. If `None`, the value wasn't configured.
594+
pub is_bare: Option<bool>,
595595
/// The type of hash to use.
596596
pub object_hash: gix_hash::Kind,
597597
/// If true, multi-pack indices, whether present or not, may be used by the object database.

0 commit comments

Comments
 (0)