Skip to content
Draft
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
82 changes: 76 additions & 6 deletions gix/src/clone/fetch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ pub enum Error {
},
#[error(transparent)]
CommitterOrFallback(#[from] crate::config::time::Error),
#[error(transparent)]
RefMap(#[from] crate::remote::ref_map::Error),
}

/// Modification
Expand Down Expand Up @@ -101,14 +103,81 @@ impl PrepareFetch {
};

let mut remote = repo.remote_at(self.url.clone())?;

// For shallow clones without custom configuration, we'll use a single-branch refspec
// to match git's behavior (matching git's single-branch behavior for shallow clones).
let use_single_branch_for_shallow = self.shallow != remote::fetch::Shallow::NoChange
&& self.configure_remote.is_none()
&& remote.fetch_specs.is_empty();

let target_ref = if use_single_branch_for_shallow {
// Determine target branch from user-specified ref_name or default branch
if let Some(ref_name) = &self.ref_name {
// User specified a branch, use that
Some(format!("refs/heads/{}", ref_name.as_ref().as_bstr()))
} else {
// For shallow clones without a specified ref, we need to determine the default branch.
// We'll connect to get HEAD information. For Protocol V2, we need to explicitly list refs.
let mut connection = remote.connect(remote::Direction::Fetch).await?;

// Perform handshake and try to get HEAD from it (works for Protocol V1)
let _ = connection.ref_map_by_ref(&mut progress, Default::default()).await?;

let target = if let Some(handshake) = &connection.handshake {
// Protocol V1: refs are in handshake
handshake.refs.as_ref().and_then(|refs| {
refs.iter().find_map(|r| match r {
gix_protocol::handshake::Ref::Symbolic {
full_ref_name, target, ..
} if full_ref_name == "HEAD" => Some(target.to_string()),
_ => None,
})
})
} else {
None
};

// For Protocol V2 or if we couldn't determine HEAD, use the configured default branch
let fallback_branch = target
.or_else(|| {
repo.config
.resolved
.string(crate::config::tree::Init::DEFAULT_BRANCH)
.and_then(|name| name.to_str().ok().map(|s| format!("refs/heads/{s}")))
})
.unwrap_or_else(|| "refs/heads/main".to_string());

// Drop the connection explicitly to release the borrow on remote
drop(connection);

Some(fallback_branch)
}
} else {
None
};

// Set up refspec based on whether we're doing a single-branch shallow clone
if remote.fetch_specs.is_empty() {
remote = remote
.with_refspecs(
Some(format!("+refs/heads/*:refs/remotes/{remote_name}/*").as_str()),
remote::Direction::Fetch,
)
.expect("valid static spec");
if let Some(target_ref) = &target_ref {
// Single-branch refspec for shallow clones
let short_name = target_ref.strip_prefix("refs/heads/").unwrap_or(target_ref.as_str());
remote = remote
.with_refspecs(
Some(format!("+{target_ref}:refs/remotes/{remote_name}/{short_name}").as_str()),
remote::Direction::Fetch,
)
.expect("valid refspec");
} else {
// Wildcard refspec for non-shallow clones or when target couldn't be determined
remote = remote
.with_refspecs(
Some(format!("+refs/heads/*:refs/remotes/{remote_name}/*").as_str()),
remote::Direction::Fetch,
)
.expect("valid static spec");
}
}

let mut clone_fetch_tags = None;
if let Some(f) = self.configure_remote.as_mut() {
remote = f(remote).map_err(Error::RemoteConfiguration)?;
Expand All @@ -133,6 +202,7 @@ impl PrepareFetch {
.expect("valid")
.to_owned();
let pending_pack: remote::fetch::Prepare<'_, '_, _> = {
// For shallow clones, we already connected once, so we need to connect again
let mut connection = remote.connect(remote::Direction::Fetch).await?;
if let Some(f) = self.configure_connection.as_mut() {
f(&mut connection).map_err(Error::RemoteConnection)?;
Expand Down
29 changes: 29 additions & 0 deletions gix/tests/gix/clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,35 @@ mod blocking_io {
Ok(())
}

#[test]
fn shallow_clone_uses_single_branch_refspec() -> crate::Result {
let tmp = gix_testtools::tempfile::TempDir::new()?;
let (repo, _out) = gix::prepare_clone_bare(remote::repo("base").path(), tmp.path())?
.with_shallow(Shallow::DepthAtRemote(1.try_into()?))
.fetch_only(gix::progress::Discard, &std::sync::atomic::AtomicBool::default())?;

assert!(repo.is_shallow(), "repository should be shallow");

// Verify that only a single-branch refspec was configured
let remote = repo.find_remote("origin")?;
let refspecs: Vec<_> = remote
.refspecs(Direction::Fetch)
.iter()
.map(|spec| spec.to_ref().to_bstring())
.collect();

assert_eq!(refspecs.len(), 1, "shallow clone should have only one fetch refspec");

// The refspec should be for a single branch (main), not a wildcard
let refspec_str = refspecs[0].to_str().expect("valid utf8");
assert_eq!(
refspec_str, "+refs/heads/main:refs/remotes/origin/main",
"shallow clone refspec should not use wildcard and should be the main branch: {refspec_str}"
);

Ok(())
}

#[test]
fn from_shallow_prohibited_with_option() -> crate::Result {
let tmp = gix_testtools::tempfile::TempDir::new()?;
Expand Down
Loading