Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

handle discovery of split worktrees #1068

Merged
merged 4 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions gitoxide-core/src/discover.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use std::path::Path;

pub fn discover(repo: &Path, mut out: impl std::io::Write) -> anyhow::Result<()> {
let mut has_err = false;
writeln!(out, "open (strict) {}:", repo.display())?;
has_err |= print_result(
&mut out,
gix::open_opts(repo, gix::open::Options::default().strict_config(true)),
)?;

if has_err {
writeln!(out, "open (lenient) {}:", repo.display())?;
has_err |= print_result(
&mut out,
gix::open_opts(repo, gix::open::Options::default().strict_config(false)),
)?;
}

writeln!(out)?;
writeln!(out, "discover from {}:", repo.display())?;
has_err |= print_result(&mut out, gix::discover(repo))?;

writeln!(out)?;
writeln!(out, "discover (plumbing) from {}:", repo.display())?;
has_err |= print_result(&mut out, gix::discover::upwards(repo))?;

if has_err {
writeln!(out)?;
anyhow::bail!("At least one operation failed")
}

Ok(())
}

fn print_result<T, E>(mut out: impl std::io::Write, res: Result<T, E>) -> std::io::Result<bool>
where
T: std::fmt::Debug,
E: std::error::Error + Send + Sync + 'static,
{
let mut has_err = false;
let to_print = match res {
Ok(good) => {
format!("{good:#?}")
}
Err(err) => {
has_err = true;
format!("{:?}", anyhow::Error::from(err))
}
};
indent(&mut out, to_print)?;
Ok(has_err)
}

fn indent(mut out: impl std::io::Write, msg: impl Into<String>) -> std::io::Result<()> {
for line in msg.into().lines() {
writeln!(out, "\t{line}")?;
}
Ok(())
}
3 changes: 3 additions & 0 deletions gitoxide-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,8 @@ pub mod pack;
pub mod query;
pub mod repository;

mod discover;
pub use discover::discover;

#[cfg(all(feature = "async-client", feature = "blocking-client"))]
compile_error!("Cannot set both 'blocking-client' and 'async-client' features as they are mutually exclusive");
41 changes: 28 additions & 13 deletions gix-discover/src/is.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,34 @@ fn bare_by_config(git_dir_candidate: &Path) -> std::io::Result<Option<bool>> {
mod config {
use bstr::{BStr, ByteSlice};

/// Note that we intentionally turn repositories that have a worktree configuration into bare repos,
/// as we don't actually parse the worktree from the config file and expect the caller to do the right
/// think when seemingly seeing bare repository.
/// The reason we do this is to not incorrectly pretend this is a worktree.
pub(crate) fn parse_bare(buf: &[u8]) -> Option<bool> {
buf.lines().find_map(|line| {
let line = line.trim().strip_prefix(b"bare")?;
match line.first() {
None => Some(true),
Some(c) if *c == b'=' => parse_bool(line.get(1..)?.trim_start().as_bstr()),
Some(c) if c.is_ascii_whitespace() => match line.split_once_str(b"=") {
Some((_left, right)) => parse_bool(right.trim_start().as_bstr()),
None => Some(true),
},
Some(_other_char_) => None,
let mut is_bare = None;
let mut has_worktree_configuration = false;
for line in buf.lines() {
if is_bare.is_none() {
if let Some(line) = line.trim().strip_prefix(b"bare") {
is_bare = match line.first() {
None => Some(true),
Some(c) if *c == b'=' => parse_bool(line.get(1..)?.trim_start().as_bstr()),
Some(c) if c.is_ascii_whitespace() => match line.split_once_str(b"=") {
Some((_left, right)) => parse_bool(right.trim_start().as_bstr()),
None => Some(true),
},
Some(_other_char_) => None,
};
continue;
}
}
})
if line.trim().strip_prefix(b"worktree").is_some() {
has_worktree_configuration = true;
break;
}
}
is_bare.map(|bare| bare || has_worktree_configuration)
}

fn parse_bool(value: &BStr) -> Option<bool> {
Expand Down Expand Up @@ -233,7 +248,7 @@ pub(crate) fn git_with_metadata(
Cow::Borrowed(git_dir)
};
if bare(conformed_git_dir.as_ref()) || conformed_git_dir.extension() == Some(OsStr::new("git")) {
crate::repository::Kind::Bare
crate::repository::Kind::PossiblyBare
} else if submodule_git_dir(conformed_git_dir.as_ref()) {
crate::repository::Kind::SubmoduleGitDir
} else if conformed_git_dir.file_name() == Some(OsStr::new(DOT_GIT_DIR))
Expand All @@ -246,7 +261,7 @@ pub(crate) fn git_with_metadata(
{
crate::repository::Kind::WorkTree { linked_git_dir: None }
} else {
crate::repository::Kind::Bare
crate::repository::Kind::PossiblyBare
}
}
})
Expand Down
2 changes: 1 addition & 1 deletion gix-discover/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub mod is_git {
GitFile(#[from] crate::path::from_gitdir_file::Error),
#[error("Could not retrieve metadata of \"{path}\"")]
Metadata { source: std::io::Error, path: PathBuf },
#[error("The repository's config file doesn't exist or didn't have a 'bare' configuration")]
#[error("The repository's config file doesn't exist or didn't have a 'bare' configuration or contained core.worktree without value")]
Inconclusive,
}
}
Expand Down
12 changes: 8 additions & 4 deletions gix-discover/src/repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ mod path {
Path::WorkTree(work_dir)
}
},
Kind::Bare => Path::Repository(dir),
Kind::PossiblyBare => Path::Repository(dir),
}
.into()
}
Expand All @@ -89,7 +89,7 @@ mod path {
linked_git_dir: Some(git_dir.to_owned()),
},
Path::WorkTree(_) => Kind::WorkTree { linked_git_dir: None },
Path::Repository(_) => Kind::Bare,
Path::Repository(_) => Kind::PossiblyBare,
}
}

Expand All @@ -110,7 +110,11 @@ 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,
///
/// Also note that due to optimizing for performance and *just* making an educated *guess in some situations*,
/// we may consider a non-bare repository bare if it it doesn't have an index yet due to be freshly initialized.
/// The caller is has to handle this, typically by reading the configuration.
PossiblyBare,
/// A `git` repository along with checked out files in a work tree.
WorkTree {
/// If set, this is the git dir associated with this _linked_ worktree.
Expand All @@ -135,6 +139,6 @@ pub enum Kind {
impl Kind {
/// Returns true if this is a bare repository, one without a work tree.
pub fn is_bare(&self) -> bool {
matches!(self, Kind::Bare)
matches!(self, Kind::PossiblyBare)
}
}
22 changes: 22 additions & 0 deletions gix-discover/tests/fixtures/make_basic_repo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,25 @@ git init non-bare-without-index
git commit -m "init"
rm .git/index
)

git --git-dir=repo-with-worktree-in-config-unborn-no-worktreedir --work-tree=does-not-exist-yet init
worktree=repo-with-worktree-in-config-unborn-worktree
git --git-dir=repo-with-worktree-in-config-unborn --work-tree=$worktree init && mkdir $worktree

repo=repo-with-worktree-in-config-unborn-empty-worktreedir
git --git-dir=$repo --work-tree="." init
touch $repo/index
git -C $repo config core.worktree ''

repo=repo-with-worktree-in-config-unborn-worktreedir-missing-value
git --git-dir=$repo init
touch $repo/index
echo " worktree" >> $repo/config

worktree=repo-with-worktree-in-config-worktree
git --git-dir=repo-with-worktree-in-config --work-tree=$worktree init
mkdir $worktree && touch $worktree/file
(cd repo-with-worktree-in-config
git add file
git commit -m "make sure na index exists"
)
27 changes: 24 additions & 3 deletions gix-discover/tests/is_git/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ fn missing_configuration_file_is_not_a_dealbreaker_in_bare_repo() -> crate::Resu
for name in ["bare-no-config-after-init.git", "bare-no-config.git"] {
let repo = repo_path()?.join(name);
let kind = gix_discover::is_git(&repo)?;
assert_eq!(kind, gix_discover::repository::Kind::Bare);
assert_eq!(kind, gix_discover::repository::Kind::PossiblyBare);
}
Ok(())
}
Expand All @@ -54,7 +54,7 @@ fn missing_configuration_file_is_not_a_dealbreaker_in_bare_repo() -> crate::Resu
fn bare_repo_with_index_file_looks_still_looks_like_bare() -> crate::Result {
let repo = repo_path()?.join("bare-with-index.git");
let kind = gix_discover::is_git(&repo)?;
assert_eq!(kind, gix_discover::repository::Kind::Bare);
assert_eq!(kind, gix_discover::repository::Kind::PossiblyBare);
Ok(())
}

Expand All @@ -63,7 +63,7 @@ fn bare_repo_with_index_file_looks_still_looks_like_bare_if_it_was_renamed() ->
for repo_name in ["bare-with-index-bare", "bare-with-index-no-config-bare"] {
let repo = repo_path()?.join(repo_name);
let kind = gix_discover::is_git(&repo)?;
assert_eq!(kind, gix_discover::repository::Kind::Bare);
assert_eq!(kind, gix_discover::repository::Kind::PossiblyBare);
}
Ok(())
}
Expand All @@ -85,3 +85,24 @@ fn missing_configuration_file_is_not_a_dealbreaker_in_nonbare_repo() -> crate::R
}
Ok(())
}

#[test]
fn split_worktree_using_configuration() -> crate::Result {
for name in [
"repo-with-worktree-in-config",
"repo-with-worktree-in-config-unborn",
"repo-with-worktree-in-config-unborn-no-worktreedir",
"repo-with-worktree-in-config-unborn-empty-worktreedir",
"repo-with-worktree-in-config-unborn-worktreedir-missing-value",
] {
let repo = repo_path()?.join(name);
let kind = gix_discover::is_git(&repo)?;
assert_eq!(
kind,
gix_discover::repository::Kind::PossiblyBare,
"{name}: we think these are bare as we don't read the configuration in this case - \
a shortcoming to favor performance which still comes out correct in `gix`"
);
}
Ok(())
}
4 changes: 2 additions & 2 deletions gix-discover/tests/isolated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ fn upwards_bare_repo_with_index() -> gix_testtools::Result {
let (repo_path, _trust) = gix_discover::upwards(".".as_ref())?;
assert_eq!(
repo_path.kind(),
gix_discover::repository::Kind::Bare,
gix_discover::repository::Kind::PossiblyBare,
"bare stays bare, even with index, as it resolves the path as needed in this special case"
);
Ok(())
Expand All @@ -25,7 +25,7 @@ fn in_cwd_upwards_bare_repo_without_index() -> gix_testtools::Result {

let _keep = gix_testtools::set_current_dir(repo.join("bare.git"))?;
let (repo_path, _trust) = gix_discover::upwards(".".as_ref())?;
assert_eq!(repo_path.kind(), gix_discover::repository::Kind::Bare);
assert_eq!(repo_path.kind(), gix_discover::repository::Kind::PossiblyBare);
Ok(())
}

Expand Down
8 changes: 4 additions & 4 deletions gix-discover/tests/upwards/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ fn from_bare_git_dir() -> crate::Result {
let dir = repo_path()?.join("bare.git");
let (path, trust) = gix_discover::upwards(&dir)?;
assert_eq!(path.as_ref(), dir, "the bare .git dir is directly returned");
assert_eq!(path.kind(), Kind::Bare);
assert_eq!(path.kind(), Kind::PossiblyBare);
assert_eq!(trust, expected_trust());
Ok(())
}
Expand All @@ -27,7 +27,7 @@ fn from_bare_with_index() -> crate::Result {
let dir = repo_path()?.join("bare-with-index.git");
let (path, trust) = gix_discover::upwards(&dir)?;
assert_eq!(path.as_ref(), dir, "the bare .git dir is directly returned");
assert_eq!(path.kind(), Kind::Bare);
assert_eq!(path.kind(), Kind::PossiblyBare);
assert_eq!(trust, expected_trust());
Ok(())
}
Expand All @@ -48,7 +48,7 @@ fn from_bare_git_dir_without_config_file() -> crate::Result {
let dir = repo_path()?.join(name);
let (path, trust) = gix_discover::upwards(&dir)?;
assert_eq!(path.as_ref(), dir, "the bare .git dir is directly returned");
assert_eq!(path.kind(), Kind::Bare);
assert_eq!(path.kind(), Kind::PossiblyBare);
assert_eq!(trust, expected_trust());
}
Ok(())
Expand All @@ -64,7 +64,7 @@ fn from_inside_bare_git_dir() -> crate::Result {
git_dir,
"the bare .git dir is found while traversing upwards"
);
assert_eq!(path.kind(), Kind::Bare);
assert_eq!(path.kind(), Kind::PossiblyBare);
assert_eq!(trust, expected_trust());
Ok(())
}
Expand Down
7 changes: 5 additions & 2 deletions gix/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,11 @@ pub enum Error {
ResolveIncludes(#[from] gix_config::file::includes::Error),
#[error(transparent)]
FromEnv(#[from] gix_config::file::init::from_env::Error),
#[error(transparent)]
PathInterpolation(#[from] gix_config::path::interpolate::Error),
#[error("The path {path:?} at the 'core.worktree' configuration could not be interpolated")]
PathInterpolation {
path: BString,
source: gix_config::path::interpolate::Error,
},
#[error("{source:?} configuration overrides at open or init time could not be applied.")]
ConfigOverrides {
#[source]
Expand Down
2 changes: 1 addition & 1 deletion gix/src/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ pub fn into(
Ok(gix_discover::repository::Path::from_dot_git_dir(
dot_git,
if bare {
gix_discover::repository::Kind::Bare
gix_discover::repository::Kind::PossiblyBare
} else {
gix_discover::repository::Kind::WorkTree { linked_git_dir: None }
},
Expand Down
22 changes: 18 additions & 4 deletions gix/src/open/repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,13 +237,27 @@ impl ThreadSafeRepository {
.resolved
.path_filter("core", None, Core::WORKTREE.name, &mut filter_config_section)
{
let wt_clone = wt.clone();
let wt_path = wt
.interpolate(interpolate_context(git_install_dir.as_deref(), home.as_deref()))
.map_err(config::Error::PathInterpolation)?;
worktree_dir = {
gix_path::normalize(git_dir.join(wt_path).into(), current_dir)
.and_then(|wt| wt.as_ref().is_dir().then(|| wt.into_owned()))
.map_err(|err| config::Error::PathInterpolation {
path: wt_clone.value.into_owned(),
source: err,
})?;
worktree_dir = gix_path::normalize(git_dir.join(wt_path).into(), current_dir).map(Cow::into_owned);
#[allow(unused_variables)]
if let Some(worktree_path) = worktree_dir.as_deref().filter(|wtd| !wtd.is_dir()) {
gix_trace::warn!("The configured worktree path '{}' is not a directory or doesn't exist - `core.worktree` may be misleading", worktree_path.display());
}
} else if !config.lenient_config
&& config
.resolved
.boolean_filter("core", None, Core::WORKTREE.name, &mut filter_config_section)
.is_some()
{
return Err(Error::from(config::Error::ConfigTypedString(
config::key::GenericErrorWithValue::from(&Core::WORKTREE),
)));
}
}

Expand Down
2 changes: 1 addition & 1 deletion gix/src/repository/kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ impl From<gix_discover::repository::Kind> for Kind {
gix_discover::repository::Kind::Submodule { .. } | gix_discover::repository::Kind::SubmoduleGitDir => {
Kind::WorkTree { is_linked: false }
}
gix_discover::repository::Kind::Bare => Kind::Bare,
gix_discover::repository::Kind::PossiblyBare => Kind::Bare,
gix_discover::repository::Kind::WorkTreeGitDir { .. } => Kind::WorkTree { is_linked: true },
gix_discover::repository::Kind::WorkTree { linked_git_dir } => Kind::WorkTree {
is_linked: linked_git_dir.is_some(),
Expand Down
Loading
Loading