From 553749e92dc3f399aa6fbc899e576403bd8aa3aa Mon Sep 17 00:00:00 2001 From: Theo Paris Date: Wed, 29 Apr 2026 22:20:50 -0700 Subject: [PATCH 1/3] Add a clone option to checkout a revision Co-authored-by: gemini-cli <218195315+gemini-cli@users.noreply.github.com> Signed-off-by: Theo Paris --- gitoxide-core/src/repository/clone.rs | 4 ++++ gix/src/clone/access.rs | 11 +++++++++++ gix/src/clone/fetch/mod.rs | 24 ++++++++++++++++++++++++ gix/src/clone/fetch/util.rs | 22 ++++++++++++++++++---- gix/src/clone/mod.rs | 4 ++++ 5 files changed, 61 insertions(+), 4 deletions(-) diff --git a/gitoxide-core/src/repository/clone.rs b/gitoxide-core/src/repository/clone.rs index c5cfa530784..e5f59c43718 100644 --- a/gitoxide-core/src/repository/clone.rs +++ b/gitoxide-core/src/repository/clone.rs @@ -1,4 +1,5 @@ use crate::OutputFormat; +use gix::bstr::BString; pub struct Options { pub format: OutputFormat, @@ -7,6 +8,7 @@ pub struct Options { pub no_tags: bool, pub shallow: gix::remote::fetch::Shallow, pub ref_name: Option, + pub revision: Option, } pub const PROGRESS_RANGE: std::ops::RangeInclusive = 1..=3; @@ -33,6 +35,7 @@ pub(crate) mod function { bare, no_tags, ref_name, + revision, shallow, }: Options, ) -> anyhow::Result<()> @@ -78,6 +81,7 @@ pub(crate) mod function { let (mut checkout, fetch_outcome) = prepare .with_shallow(shallow) .with_ref_name(ref_name.as_ref())? + .with_revision(revision) .fetch_then_checkout(&mut progress, &gix::interrupt::IS_INTERRUPTED)?; let (repo, outcome) = if bare { diff --git a/gix/src/clone/access.rs b/gix/src/clone/access.rs index 8bec1aa282d..db44e5430af 100644 --- a/gix/src/clone/access.rs +++ b/gix/src/clone/access.rs @@ -60,6 +60,17 @@ impl PrepareFetch { self.ref_name = name.map(TryInto::try_into).transpose()?.map(ToOwned::to_owned); Ok(self) } + + /// Set the revision to check out after fetching. + /// + /// If `None`, the `HEAD` will be used, which is the default. + pub fn with_revision(mut self, revision: Option) -> Self + where + T: Into, + { + self.revision = revision.map(Into::into); + self + } } /// Consumption diff --git a/gix/src/clone/fetch/mod.rs b/gix/src/clone/fetch/mod.rs index a07f40b95d2..ef2fe454449 100644 --- a/gix/src/clone/fetch/mod.rs +++ b/gix/src/clone/fetch/mod.rs @@ -52,6 +52,12 @@ pub enum Error { RefMap(#[from] crate::remote::ref_map::Error), #[error(transparent)] ReferenceName(#[from] gix_validate::reference::name::Error), + #[cfg(feature = "revision")] + #[error(transparent)] + RevisionResolve(#[from] crate::revision::spec::parse::single::Error), + #[cfg(feature = "revision")] + #[error(transparent)] + RevisionParse(#[from] gix_error::Error), } /// Modification @@ -300,6 +306,24 @@ impl PrepareFetch { self.ref_name.as_ref(), )?; + #[cfg(feature = "revision")] + if let Some(rev) = &self.revision { + let id = repo.rev_parse_single(rev.as_bstr())?; + repo.edit_reference(gix_ref::transaction::RefEdit { + change: gix_ref::transaction::Change::Update { + log: gix_ref::transaction::LogChange { + mode: gix_ref::transaction::RefLog::AndReference, + force_create_reflog: false, + message: reflog_message.clone(), + }, + expected: gix_ref::transaction::PreviousValue::Any, + new: gix_ref::Target::Object(id.detach()), + }, + name: "HEAD".try_into().expect("valid"), + deref: false, + })?; + } + Ok((self.repo.take().expect("still present"), outcome)) } diff --git a/gix/src/clone/fetch/util.rs b/gix/src/clone/fetch/util.rs index 5ca6f6c6385..e831abaf37a 100644 --- a/gix/src/clone/fetch/util.rs +++ b/gix/src/clone/fetch/util.rs @@ -212,10 +212,24 @@ pub(super) fn find_custom_refname<'a>( wanted: ref_name.clone(), }), 1 => { - let item = filtered_items[res.mappings[0] - .item_index - .expect("we map by name only and have no object-id in refspec")]; - Ok((Some(item.target), Some(item.full_ref_name))) + let mapping = &res.mappings[0]; + match (mapping.item_index, &mapping.lhs) { + (Some(idx), _) => { + let item = filtered_items[idx]; + Ok((Some(item.target), Some(item.full_ref_name))) + } + (None, gix_refspec::match_group::SourceRef::ObjectId(id)) => { + let target = ref_map + .mappings + .iter() + .find_map(|m| m.remote.as_id().filter(|remote_id| *remote_id == *id)) + .expect("if it matched, it must be in the mappings"); + Ok((Some(target), None)) + } + (None, gix_refspec::match_group::SourceRef::FullName(_)) => { + unreachable!("only object ids have no item index") + } + } } _ => Err(Error::RefNameAmbiguous { wanted: ref_name.clone(), diff --git a/gix/src/clone/mod.rs b/gix/src/clone/mod.rs index c3d50ad3d9b..30754f56008 100644 --- a/gix/src/clone/mod.rs +++ b/gix/src/clone/mod.rs @@ -42,6 +42,9 @@ pub struct PrepareFetch { /// The name of the reference to fetch. If `None`, the reference pointed to by `HEAD` will be checked out. #[cfg_attr(not(feature = "blocking-network-client"), allow(dead_code))] ref_name: Option, + /// The revision to check out after fetching. If `None`, the reference pointed to by `HEAD` or `ref_name` will be checked out. + #[cfg_attr(not(feature = "blocking-network-client"), allow(dead_code))] + revision: Option, } /// The error returned by [`PrepareFetch::new()`]. @@ -122,6 +125,7 @@ impl PrepareFetch { configure_connection: None, shallow: remote::fetch::Shallow::NoChange, ref_name: None, + revision: None, }) } } From b8d1da2aff80c352ab4d1205ad9ad92b33f43554 Mon Sep 17 00:00:00 2001 From: Theo Paris Date: Wed, 29 Apr 2026 22:20:50 -0700 Subject: [PATCH 2/3] Add a test for the revision option Co-authored-by: gemini-cli <218195315+gemini-cli@users.noreply.github.com> Signed-off-by: Theo Paris --- gix/tests/gix/clone.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/gix/tests/gix/clone.rs b/gix/tests/gix/clone.rs index c24ae33f3d9..57a170f3795 100644 --- a/gix/tests/gix/clone.rs +++ b/gix/tests/gix/clone.rs @@ -172,6 +172,19 @@ mod blocking_io { Ok(()) } + #[test] + fn clone_by_object_id() -> crate::Result { + let tmp = gix_testtools::tempfile::TempDir::new()?; + let hex_id = "2d9d136fb0765f2e24c44a0f91984318d580d03b"; + let (repo, _out) = gix::prepare_clone_bare(remote::repo("base").path(), tmp.path())? + .with_ref_name(Some(hex_id))? + .fetch_only(gix::progress::Discard, &std::sync::atomic::AtomicBool::default())?; + + assert!(repo.head()?.is_detached()); + assert_eq!(repo.head_id()?.to_string(), hex_id); + Ok(()) + } + #[test] fn from_non_shallow_then_deepen_then_deepen_since_to_unshallow() -> crate::Result { let tmp = gix_testtools::tempfile::TempDir::new()?; From 9f29944f6c7851cfdafec69f35a9c43d2570bd0d Mon Sep 17 00:00:00 2001 From: Theo Paris Date: Wed, 29 Apr 2026 22:20:50 -0700 Subject: [PATCH 3/3] Add revision option to the gix CLI Co-authored-by: gemini-cli <218195315+gemini-cli@users.noreply.github.com> Signed-off-by: Theo Paris --- src/plumbing/main.rs | 2 ++ src/plumbing/options/mod.rs | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index 8912f13b837..73436bf2d70 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -608,6 +608,7 @@ pub fn main() -> Result<()> { bare, no_tags, ref_name, + revision, remote, shallow, directory, @@ -618,6 +619,7 @@ pub fn main() -> Result<()> { handshake_info, no_tags, ref_name, + revision, shallow: shallow.into(), }; prepare_and_run( diff --git a/src/plumbing/options/mod.rs b/src/plumbing/options/mod.rs index ace4af0c112..8e76e1e54e4 100644 --- a/src/plumbing/options/mod.rs +++ b/src/plumbing/options/mod.rs @@ -679,6 +679,7 @@ pub mod fetch { pub mod clone { use std::{ffi::OsString, num::NonZeroU32, path::PathBuf}; + use gix::bstr::BString; use gix::remote::fetch::Shallow; #[derive(Debug, clap::Parser)] @@ -705,6 +706,13 @@ pub mod clone { #[clap(long = "ref", value_parser = crate::shared::AsPartialRefName, value_name = "REF_NAME")] pub ref_name: Option, + /// The revision to check out after cloning. + /// + /// This is useful if you want to clone a specific commit that is not a branch tip. + /// It will fetch the default branch and then attempt to check out this revision. + #[clap(long = "revision", visible_alias = "rev", value_parser = crate::shared::AsBString, value_name = "REVISION")] + pub revision: Option, + /// The directory to initialize with the new repository and to which all data should be written. pub directory: Option, }