Skip to content
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
2 changes: 1 addition & 1 deletion gix-discover/src/is.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ pub(crate) fn git_with_metadata(
let worktree_and_common_dir = crate::path::from_plain_file(&common_dir)
.and_then(Result::ok)
.and_then(|cd| {
crate::path::from_plain_file(&dot_git.join("gitdir"))
crate::path::from_plain_file_relative_to_file(&dot_git.join("gitdir"))
.and_then(Result::ok)
.map(|worktree_gitfile| (crate::path::without_dot_git_dir(worktree_gitfile), cd))
});
Expand Down
48 changes: 39 additions & 9 deletions gix-discover/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,30 @@ fn read_regular_file_content_with_size_limit(path: &std::path::Path) -> std::io:
Ok(buf)
}

/// Read a plain path file, returning `None` if the file is missing.
///
/// Linked-worktree `gitdir` files are plain path files in Git, not `gitdir:`
/// files. Match Git's `get_linked_worktree()` behavior by trimming trailing
/// whitespace before interpreting the content. Empty or whitespace-only path
/// files are invalid.
fn read_plain_file_content(path: &std::path::Path) -> Option<std::io::Result<Vec<u8>>> {
use bstr::ByteSlice;
let mut buf = match read_regular_file_content_with_size_limit(path) {
Ok(buf) => buf,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return None,
Err(err) => return Some(Err(err)),
};
let trimmed_len = buf.trim_end().len();
buf.truncate(trimmed_len);
if buf.is_empty() {
return Some(Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Refusing to read an empty path from '{}'", path.display()),
)));
}
Some(Ok(buf))
}

/// Guess the kind of repository by looking at its `git_dir` path and return it.
/// Return `None` if `git_dir` isn't called `.git` or isn't within `.git/worktrees` or `.git/modules`, or if it's
/// a `.git` suffix like in `foo.git`.
Expand Down Expand Up @@ -74,15 +98,21 @@ pub fn repository_kind(git_dir: &Path) -> Option<RepositoryKind> {

/// Reads a plain path from a file that contains it as its only content, with trailing newlines trimmed.
pub fn from_plain_file(path: &std::path::Path) -> Option<std::io::Result<PathBuf>> {
use bstr::ByteSlice;
let mut buf = match read_regular_file_content_with_size_limit(path) {
Ok(buf) => buf,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return None,
Err(err) => return Some(Err(err)),
};
let trimmed_len = buf.trim_end().len();
buf.truncate(trimmed_len);
Some(Ok(gix_path::from_bstring(buf)))
read_plain_file_content(path).map(|res| res.map(gix_path::from_bstring))
}

/// Reads a plain path from a file like [`from_plain_file()`], resolving relative paths against
/// the file's containing directory as needed.
pub fn from_plain_file_relative_to_file(path: &std::path::Path) -> Option<std::io::Result<PathBuf>> {
read_plain_file_content(path).map(|res| {
res.map(|buf| {
let plain_path = gix_path::from_bstring(buf);
match (plain_path.is_relative(), path.parent()) {
(true, Some(parent)) => parent.join(plain_path),
_ => plain_path,
}
})
})
}
Comment on lines +104 to 116

/// Reads typical `gitdir: ` files from disk as used by worktrees and submodules.
Expand Down
64 changes: 64 additions & 0 deletions gix-discover/tests/discover/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,70 @@ mod from_git_dir_file {
}
}

mod from_plain_file_relative_to_file {
use crate::path::plain_file_with_content;
use std::path::{Path, PathBuf};

#[test]
fn relative_path_is_made_absolute_relative_to_containing_dir() -> crate::Result {
let (path, plain_file) = write_and_read(b"relative/path\n")?;
assert_eq!(path, plain_file.parent().unwrap().join(Path::new("relative/path")));
Ok(())
}

#[test]
fn empty_or_whitespace_only_path_is_invalid() -> crate::Result {
for content in [b"".as_slice(), b" \n".as_slice()] {
let file = plain_file_with_content(content)?;
let err = gix_discover::path::from_plain_file_relative_to_file(file.path())
.expect("file exists")
.expect_err("empty paths must be rejected");
assert_eq!(
err.kind(),
std::io::ErrorKind::InvalidData,
"empty plain path files are malformed, just like in Git"
);
}
Ok(())
}

fn write_and_read(content: &[u8]) -> crate::Result<(PathBuf, PathBuf)> {
let file = plain_file_with_content(content)?;
Ok((
gix_discover::path::from_plain_file_relative_to_file(file.path())
.expect("file exists")
.expect("valid plain path"),
file.path().into(),
))
}
}

mod from_plain_file {
use crate::path::plain_file_with_content;

#[test]
fn empty_or_whitespace_only_path_is_invalid() -> crate::Result {
for content in [b"".as_slice(), b" \n".as_slice()] {
let file = plain_file_with_content(content)?;
let err = gix_discover::path::from_plain_file(file.path())
.expect("file exists")
.expect_err("empty paths must be rejected");
assert_eq!(
err.kind(),
std::io::ErrorKind::InvalidData,
"empty plain path files are malformed, just like in Git"
);
}
Ok(())
}
}

fn plain_file_with_content(content: &[u8]) -> std::io::Result<tempfile::NamedTempFile> {
let mut file = tempfile::NamedTempFile::new()?;
std::io::Write::write_all(&mut file, content)?;
Ok(file)
}

#[test]
fn repository_kind() {
use gix_discover::path::{RepositoryKind::*, repository_kind};
Expand Down
60 changes: 60 additions & 0 deletions gix-discover/tests/discover/upwards/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,66 @@ fn from_existing_worktree() -> crate::Result {
Ok(())
}

#[test]
fn from_existing_worktree_with_relative_linking_files() -> crate::Result {
let fixture = gix_testtools::scripted_fixture_read_only_needs_archive("make_worktree_relative_linking.sh")?;
let main = fixture.join("main");
let linked = fixture.join("linked");
Comment thread
Byron marked this conversation as resolved.
let private_git_dir = main.join(".git/worktrees/linked");
assert_eq!(
std::fs::read_to_string(linked.join(".git"))?,
"gitdir: ../main/.git/worktrees/linked\n",
"the linked checkout uses a relative gitdir file"
);
let backlink = std::fs::read_to_string(private_git_dir.join("gitdir"))?;
assert_eq!(
backlink, "../../../../linked/.git\n",
"the private git dir points back to the checkout with a relative path"
);

for discover_path in [&linked, &private_git_dir] {
let (path, trust) = gix_discover::upwards(discover_path)?;
assert_eq!(trust, expected_trust());
let (actual_git_dir, actual_worktree) = path.into_repository_and_work_tree_directories();
assert_eq!(
gix_path::realpath(&actual_git_dir)?,
gix_path::realpath(&private_git_dir)?,
"discovery resolves the private git dir from relative worktree metadata"
);
assert_eq!(
actual_worktree.as_deref().map(gix_path::realpath).transpose()?,
Some(gix_path::realpath(&linked)?),
"discovery resolves the linked worktree from relative worktree metadata"
);
}

Ok(())
}

#[test]
#[cfg(unix)]
fn from_symlinked_worktree_with_relative_linking_files() -> crate::Result {
let fixture = gix_testtools::scripted_fixture_read_only_needs_archive("make_worktree_relative_linking.sh")?;
let main = fixture.join("actual/main");
let linked_symlink = fixture.join("linked-symlink");
Comment thread
Byron marked this conversation as resolved.

let (path, trust) = gix_discover::upwards(&linked_symlink)?;
assert_eq!(trust, expected_trust());
let (actual_git_dir, actual_worktree) = path.into_repository_and_work_tree_directories();
assert_eq!(
gix_path::realpath(&actual_git_dir)?,
gix_path::realpath(main.join(".git/worktrees/linked"))?,
"the private git dir is found through a relative gitdir file reached via a symlinked checkout"
);
assert_eq!(
actual_worktree.as_deref(),
Some(linked_symlink.as_path()),
"the discovered worktree remains the user-provided symlinked checkout"
);

Ok(())
}

#[cfg(target_os = "macos")]
#[test]
fn cross_fs() -> crate::Result {
Expand Down
Binary file not shown.
21 changes: 21 additions & 0 deletions gix-discover/tests/fixtures/make_worktree_relative_linking.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -eu -o pipefail

make_repo_with_relative_worktree_links() {
local base=$1

mkdir -p "$base"
git -C "$base" init -q main
(
cd "$base/main"
git commit -q --allow-empty -m init
git worktree add -q --detach --relative-paths ../linked HEAD
)
}

make_repo_with_relative_worktree_links .
make_repo_with_relative_worktree_links actual

# The symlinked checkout variant is only used by Unix tests. On platforms where
# creating symlinks is unavailable, keep the rest of the fixture useful.
ln -s actual/linked linked-symlink 2>/dev/null || true
Comment thread
Byron marked this conversation as resolved.
2 changes: 1 addition & 1 deletion gix/src/worktree/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ impl Proxy<'_> {
/// Note that the location might not exist.
pub fn base(&self) -> std::io::Result<PathBuf> {
let git_dir = self.git_dir.join("gitdir");
let base_dot_git = gix_discover::path::from_plain_file(&git_dir).ok_or_else(|| {
let base_dot_git = gix_discover::path::from_plain_file_relative_to_file(&git_dir).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Required file '{}' does not exist", git_dir.display()),
Expand Down
Binary file not shown.
21 changes: 21 additions & 0 deletions gix/tests/fixtures/make_worktree_relative_linking.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -eu -o pipefail

make_repo_with_relative_worktree_links() {
local base=$1

mkdir -p "$base"
git -C "$base" init -q main
(
cd "$base/main"
git commit -q --allow-empty -m init
git worktree add -q --detach --relative-paths ../linked HEAD
Comment thread
Byron marked this conversation as resolved.
)
}

make_repo_with_relative_worktree_links .
make_repo_with_relative_worktree_links actual

# The symlinked main-repository variant is only used by Unix tests. On platforms
# where creating symlinks is unavailable, keep the rest of the fixture useful.
ln -s actual/main main-symlink 2>/dev/null || true
Comment thread
Byron marked this conversation as resolved.
52 changes: 52 additions & 0 deletions gix/tests/gix/repository/worktree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,58 @@ fn from_nonbare_parent_repo() {
run_assertions(repo, false /* bare */);
}

#[test]
fn linked_worktree_proxy_base_with_relative_linking_files() -> crate::Result {
let fixture = gix_testtools::scripted_fixture_read_only_needs_archive("make_worktree_relative_linking.sh")?;
let main = fixture.join("main");
let linked = fixture.join("linked");
Comment thread
Byron marked this conversation as resolved.
let private_git_dir = main.join(".git/worktrees/linked");
let repo = gix::open(&main)?;
let worktrees = repo.worktrees()?;
assert_eq!(worktrees.len(), 1, "the relative-path fixture has one linked worktree");
let proxy = worktrees.into_iter().next().expect("one worktree");

assert_eq!(
gix_path::realpath(proxy.base()?)?,
gix_path::realpath(&linked)?,
"proxy bases resolve relative worktrees/<id>/gitdir paths against the private git dir"
);
let linked_repo = proxy.into_repo()?;
assert_eq!(
linked_repo.workdir().map(gix_path::realpath).transpose()?,
Some(gix_path::realpath(&linked)?)
);
assert_eq!(linked_repo.git_dir(), private_git_dir);

Ok(())
}

#[test]
#[cfg(unix)]
fn linked_worktree_proxy_base_with_symlinked_main_repo() -> crate::Result {
let fixture = gix_testtools::scripted_fixture_read_only_needs_archive("make_worktree_relative_linking.sh")?;
let linked = fixture.join("actual/linked");
let main_symlink = fixture.join("main-symlink");
Comment thread
Byron marked this conversation as resolved.

let repo = gix::open(&main_symlink)?;
let worktrees = repo.worktrees()?;
assert_eq!(worktrees.len(), 1, "the relative-path fixture has one linked worktree");
let proxy = worktrees.into_iter().next().expect("one worktree");

assert_eq!(
gix_path::realpath(proxy.base()?)?,
gix_path::realpath(&linked)?,
"proxy bases preserve symlink semantics when resolving relative worktrees/<id>/gitdir paths"
);
let repo = proxy.into_repo()?;
assert_eq!(
repo.workdir().map(gix_path::realpath).transpose()?,
Some(gix_path::realpath(&linked)?)
);

Ok(())
}

#[test]
fn from_nonbare_parent_repo_set_workdir() -> gix_testtools::Result {
if gix_testtools::should_skip_as_git_version_is_smaller_than(2, 31, 0) {
Expand Down