use std::io::Write; use gix_ref::{ Category, FullNameRef, PartialName, transaction::{LogChange, RefLog},}; use super::Error;use crate::{ Repository, bstr::{BStr, BString, ByteSlice},}; enum WriteMode { Overwrite, Append,} #[expect( clippy::result_large_err, reason = "will be removed once `gix-error` is used consistently")]pub fn append_remote_to_local_config_file( remote: &mut crate::Remote<'_>, remote_name: BString,) -> Result<gix_config::File, Error> { let mut config = gix_config::File::new(local_config_meta(remote.repo)); remote.save_as_to(remote_name, &mut config)?; write_to_local_config(&config, WriteMode::Append)?; Ok(config)} /// Reconfigure the freshly-initialized, still-empty repository `repo` to use `object_hash`/// by rewriting the object-format related entries in its local configuration file on disk,/// and reload the repository handle.////// This relies on the initial reference database not having persisted any hash-format-dependent/// state. That is true for the current file-based ref store, but a future reftable backend must/// not be initialized with the wrong hash and then reused. If clone learns the remote hash only/// after repository creation, initialize a non-reftable reference database first, then convert it/// to reftable once the remote hash is known.////// Existing local configuration, including the remote section written during clone setup,/// is preserved. Only local sections are written back to `.git/config`,////// The returned repository is reopened from disk so the object hash change affects all/// hash-dependent state. Callers that need in-memory configuration from `repo` must/// transfer it to the returned handle.#[cfg(feature = "sha256")]pub(super) fn reinitialize_with_object_hash( repo: &crate::Repository, object_hash: gix_hash::Kind,) -> Result<crate::Repository, Error> { let git_dir = repo.git_dir(); let config_path = git_dir.join("config"); let mut config = gix_config::File::from_path_no_includes(config_path.clone(), gix_config::Source::Local)?; // Mirror what `crate::create` writes at init time: only SHA-256 repositories get // `repositoryformatversion = 1` along with the `objectformat` extension. let is_sha256 = object_hash == gix_hash::Kind::Sha256; config .section_mut("core", None) .expect("freshly initialized repository has a core section") .set("repositoryformatversion", if is_sha256 { "1" } else { "0" })?; if is_sha256 { config .section_mut_or_create_new("extensions", None) .expect("valid section name") .set("objectformat", object_hash.to_string())?; } else { // In a freshly initialized repository, this section exists solely to carry `objectformat`. config.remove_section("extensions", None); } let mut lock = gix_lock::File::acquire_to_update_resource(&config_path, gix_lock::acquire::Fail::Immediately, None)?; config.write_to_filter(&mut lock, |section| section.meta().source == gix_config::Source::Local)?; lock.commit()?; Ok(crate::ThreadSafeRepository::open_opts(git_dir, repo.options.clone())?.to_thread_local())} fn local_config_meta(repo: &Repository) -> gix_config::file::Metadata { let meta = repo.config.resolved.meta().clone(); assert_eq!( meta.source, gix_config::Source::Local, "local path is the default for new sections" ); meta} fn write_to_local_config(config: &gix_config::File, mode: WriteMode) -> std::io::Result<()> { assert_eq!( config.meta().source, gix_config::Source::Local, "made for appending to local configuration file" ); let mut local_config = std::fs::OpenOptions::new() .create(false) .write(matches!(mode, WriteMode::Overwrite)) .append(matches!(mode, WriteMode::Append)) .open(config.meta().path.as_deref().expect("local config with path set"))?; local_config.write_all(config.detect_newline_style())?; config.write_to_filter(&mut local_config, |s| s.meta().source == gix_config::Source::Local)} /// Append `config` to `repo`'s in-memory resolved configuration.////// This is used after writing clone-specific local configuration to `.git/config`,/// as the `repo` handle was opened before that write and won't observe it until/// it is either updated in memory or reopened.pub fn append_config_to_repo_config( repo: &mut Repository, config: gix_config::File,) -> Result<(), gix_config::parse::span::Error> { let repo_config = gix_features::threading::OwnShared::make_mut(&mut repo.config.resolved); repo_config.append(config)?; Ok(())} /// HEAD cannot be written by means of refspec by design, so we have to do it manually here. Also create the pointed-to ref/// if we have to, as it might not have been naturally included in the ref-specs./// Lastly, use `ref_name` if it was provided instead, and let `HEAD` point to it.pub fn update_head( repo: &mut Repository, ref_map: &crate::remote::fetch::RefMap, reflog_message: &BStr, remote_name: &BStr, ref_name: Option<&PartialName>,) -> Result<(), Error> { use gix_ref::{ Target, transaction::{PreviousValue, RefEdit}, }; let head_info = match ref_name { Some(ref_name) => { let (target, full_ref_name) = find_custom_refname(ref_map, ref_name)?; Some((Some(target), Some(full_ref_name))) } None => ref_map.remote_refs.iter().find_map(|r| { Some(match r { gix_protocol::handshake::Ref::Symbolic { full_ref_name, target, tag: _, object, } if full_ref_name == "HEAD" => (Some(object.as_ref()), Some(target.as_bstr())), gix_protocol::handshake::Ref::Direct { full_ref_name, object } if full_ref_name == "HEAD" => { (Some(object.as_ref()), None) } gix_protocol::handshake::Ref::Unborn { full_ref_name, target } if full_ref_name == "HEAD" => { (None, Some(target.as_bstr())) } _ => return None, }) }), }; let Some((head_peeled_id, head_ref)) = head_info else { return Ok(()); }; let head: gix_ref::FullName = "HEAD".try_into().expect("valid"); let reflog_message = || LogChange { mode: RefLog::AndReference, force_create_reflog: false, message: reflog_message.to_owned(), }; match head_ref { Some(referent) => { let referent: gix_ref::FullName = referent.try_into().map_err(|err| Error::InvalidHeadRef { head_ref_name: referent.to_owned(), source: err, })?; repo.refs .transaction() .packed_refs(gix_ref::file::transaction::PackedRefs::DeletionsAndNonSymbolicUpdates( Box::new(&repo.objects), )) .prepare( { let mut edits = vec![RefEdit { change: gix_ref::transaction::Change::Update { log: reflog_message(), expected: PreviousValue::Any, new: Target::Symbolic(referent.clone()), }, name: head.clone(), deref: false, }]; if let Some(head_peeled_id) = head_peeled_id { edits.push(RefEdit { change: gix_ref::transaction::Change::Update { log: reflog_message(), expected: PreviousValue::Any, new: Target::Object(head_peeled_id.to_owned()), }, name: referent.clone(), deref: false, }); } edits }, gix_lock::acquire::Fail::Immediately, gix_lock::acquire::Fail::Immediately, ) .map_err(crate::reference::edit::Error::from)? .commit( repo.committer() .transpose() .map_err(|err| Error::HeadUpdate(crate::reference::edit::Error::ParseCommitterTime(err)))?, ) .map_err(crate::reference::edit::Error::from)?; if let Some(head_peeled_id) = head_peeled_id { let mut log = reflog_message(); log.mode = RefLog::Only; repo.edit_reference(RefEdit { change: gix_ref::transaction::Change::Update { log, expected: PreviousValue::Any, new: Target::Object(head_peeled_id.to_owned()), }, name: head, deref: false, })?; } setup_branch_config(repo, referent.as_ref(), head_peeled_id, remote_name)?; } None => { repo.edit_reference(RefEdit { change: gix_ref::transaction::Change::Update { log: reflog_message(), expected: PreviousValue::Any, new: Target::Object( head_peeled_id .expect("detached heads always point to something") .to_owned(), ), }, name: head, deref: false, })?; } } Ok(())} pub(super) fn find_custom_refname<'a>( ref_map: &'a crate::remote::fetch::RefMap, ref_name: &PartialName,) -> Result<(&'a gix_hash::oid, &'a BStr), Error> { let group = gix_refspec::MatchGroup::from_fetch_specs(Some( gix_refspec::parse(ref_name.as_ref().as_bstr(), gix_refspec::parse::Operation::Fetch) .expect("partial names are valid refs"), )); let filtered_items: Vec<_> = ref_map .mappings .iter() .filter_map(|m| m.remote.as_name().zip(m.remote.as_id())) .map(|(full_ref_name, target)| gix_refspec::match_group::Item { full_ref_name, target, object: None, }) .collect(); let requested_name = ref_name.as_ref().as_bstr(); let find_item = |name: &BStr| filtered_items.iter().find(|item| item.full_ref_name == name).copied(); // Preserve gix's documented full-ref support, then match git clone --branch by trying heads before tags. if let Some(item) = find_item(requested_name) { return Ok((item.target, item.full_ref_name)); } if !requested_name.starts_with(b"refs/") { let branch_name = Category::LocalBranch.to_full_name(requested_name)?; if let Some(item) = find_item(branch_name.as_bstr()) { return Ok((item.target, item.full_ref_name)); } let tag_name = Category::Tag.to_full_name(requested_name)?; if let Some(item) = find_item(tag_name.as_bstr()) { return Ok((item.target, item.full_ref_name)); } } let res = group.match_lhs(filtered_items.iter().copied()); match res.mappings.len() { 0 => Err(Error::RefNameMissing { 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((item.target, item.full_ref_name)) } _ => Err(Error::RefNameAmbiguous { wanted: ref_name.clone(), candidates: res .mappings .into_iter() .filter_map(|m| match m.lhs { gix_refspec::match_group::SourceRef::FullName(name) => Some(name.into_owned()), gix_refspec::match_group::SourceRef::ObjectId(_) => None, }) .collect(), }), }} /// Set up the remote configuration for `branch` so that it points to itself, but on the remote, if and only if currently/// saved refspecs are able to match it./// For that we reload the remote of `remote_name` and use its `ref_specs` for match.fn setup_branch_config( repo: &mut Repository, branch: &FullNameRef, branch_id: Option<&gix_hash::oid>, remote_name: &BStr,) -> Result<(), Error> { let short_name = match branch.category_and_short_name() { Some((gix_ref::Category::LocalBranch, shortened)) => match shortened.to_str() { Ok(s) => s, Err(_) => return Ok(()), }, _ => return Ok(()), }; let remote = repo .find_remote(remote_name) .expect("remote was just created and must be visible in config"); let group = gix_refspec::MatchGroup::from_fetch_specs(remote.fetch_specs.iter().map(gix_refspec::RefSpec::to_ref)); let null = gix_hash::ObjectId::null(repo.object_hash()); let res = group.match_lhs( Some(gix_refspec::match_group::Item { full_ref_name: branch.as_bstr(), target: branch_id.unwrap_or(&null), object: None, }) .into_iter(), ); if !res.mappings.is_empty() { let mut config = repo.config_snapshot_mut(); let mut section = config .new_section("branch", short_name) .expect("section header name is always valid per naming rules, our input branch name is valid"); section.push("remote", remote_name)?; section.push("merge", branch.as_bstr())?; write_to_local_config(&config, WriteMode::Overwrite)?; config.commit().expect("configuration we set is valid"); } Ok(())}
use std::io::Write;
use gix_ref::{
Category, FullNameRef, PartialName,
transaction::{LogChange, RefLog},
};
use super::Error;
use crate::{
Repository,
bstr::{BStr, BString, ByteSlice},
};
enum WriteMode {
Overwrite,
Append,
}
#[expect(
clippy::result_large_err,
reason = "will be removed once `gix-error` is used consistently"
)]
pub fn append_remote_to_local_config_file(
remote: &mut crate::Remote<'_>,
remote_name: BString,
) -> Result<gix_config::File, Error> {
let mut config = gix_config::File::new(local_config_meta(remote.repo));
remote.save_as_to(remote_name, &mut config)?;
write_to_local_config(&config, WriteMode::Append)?;
Ok(config)
}
/// Reconfigure the freshly-initialized, still-empty repository `repo` to use `object_hash`
/// by rewriting the object-format related entries in its local configuration file on disk,
/// and reload the repository handle.
///
/// This relies on the initial reference database not having persisted any hash-format-dependent
/// state. That is true for the current file-based ref store, but a future reftable backend must
/// not be initialized with the wrong hash and then reused. If clone learns the remote hash only
/// after repository creation, initialize a non-reftable reference database first, then convert it
/// to reftable once the remote hash is known.
///
/// Existing local configuration, including the remote section written during clone setup,
/// is preserved. Only local sections are written back to `.git/config`,
///
/// The returned repository is reopened from disk so the object hash change affects all
/// hash-dependent state. Callers that need in-memory configuration from `repo` must
/// transfer it to the returned handle.
#[cfg(feature = "sha256")]
pub(super) fn reinitialize_with_object_hash(
repo: &crate::Repository,
object_hash: gix_hash::Kind,
) -> Result<crate::Repository, Error> {
let git_dir = repo.git_dir();
let config_path = git_dir.join("config");
let mut config = gix_config::File::from_path_no_includes(config_path.clone(), gix_config::Source::Local)?;
// Mirror what `crate::create` writes at init time: only SHA-256 repositories get
// `repositoryformatversion = 1` along with the `objectformat` extension.
let is_sha256 = object_hash == gix_hash::Kind::Sha256;
config
.section_mut("core", None)
.expect("freshly initialized repository has a core section")
.set("repositoryformatversion", if is_sha256 { "1" } else { "0" })?;
if is_sha256 {
config
.section_mut_or_create_new("extensions", None)
.expect("valid section name")
.set("objectformat", object_hash.to_string())?;
} else {
// In a freshly initialized repository, this section exists solely to carry `objectformat`.
config.remove_section("extensions", None);
}
let mut lock =
gix_lock::File::acquire_to_update_resource(&config_path, gix_lock::acquire::Fail::Immediately, None)?;
config.write_to_filter(&mut lock, |section| section.meta().source == gix_config::Source::Local)?;
lock.commit()?;
Ok(crate::ThreadSafeRepository::open_opts(git_dir, repo.options.clone())?.to_thread_local())
}
fn local_config_meta(repo: &Repository) -> gix_config::file::Metadata {
let meta = repo.config.resolved.meta().clone();
assert_eq!(
meta.source,
gix_config::Source::Local,
"local path is the default for new sections"
);
meta
}
fn write_to_local_config(config: &gix_config::File, mode: WriteMode) -> std::io::Result<()> {
assert_eq!(
config.meta().source,
gix_config::Source::Local,
"made for appending to local configuration file"
);
let mut local_config = std::fs::OpenOptions::new()
.create(false)
.write(matches!(mode, WriteMode::Overwrite))
.append(matches!(mode, WriteMode::Append))
.open(config.meta().path.as_deref().expect("local config with path set"))?;
local_config.write_all(config.detect_newline_style())?;
config.write_to_filter(&mut local_config, |s| s.meta().source == gix_config::Source::Local)
}
/// Append `config` to `repo`'s in-memory resolved configuration.
///
/// This is used after writing clone-specific local configuration to `.git/config`,
/// as the `repo` handle was opened before that write and won't observe it until
/// it is either updated in memory or reopened.
pub fn append_config_to_repo_config(
repo: &mut Repository,
config: gix_config::File,
) -> Result<(), gix_config::parse::span::Error> {
let repo_config = gix_features::threading::OwnShared::make_mut(&mut repo.config.resolved);
repo_config.append(config)?;
Ok(())
}
/// HEAD cannot be written by means of refspec by design, so we have to do it manually here. Also create the pointed-to ref
/// if we have to, as it might not have been naturally included in the ref-specs.
/// Lastly, use `ref_name` if it was provided instead, and let `HEAD` point to it.
pub fn update_head(
repo: &mut Repository,
ref_map: &crate::remote::fetch::RefMap,
reflog_message: &BStr,
remote_name: &BStr,
ref_name: Option<&PartialName>,
) -> Result<(), Error> {
use gix_ref::{
Target,
transaction::{PreviousValue, RefEdit},
};
let head_info = match ref_name {
Some(ref_name) => {
let (target, full_ref_name) = find_custom_refname(ref_map, ref_name)?;
Some((Some(target), Some(full_ref_name)))
}
None => ref_map.remote_refs.iter().find_map(|r| {
Some(match r {
gix_protocol::handshake::Ref::Symbolic {
full_ref_name,
target,
tag: _,
object,
} if full_ref_name == "HEAD" => (Some(object.as_ref()), Some(target.as_bstr())),
gix_protocol::handshake::Ref::Direct { full_ref_name, object } if full_ref_name == "HEAD" => {
(Some(object.as_ref()), None)
}
gix_protocol::handshake::Ref::Unborn { full_ref_name, target } if full_ref_name == "HEAD" => {
(None, Some(target.as_bstr()))
}
_ => return None,
})
}),
};
let Some((head_peeled_id, head_ref)) = head_info else {
return Ok(());
};
let head: gix_ref::FullName = "HEAD".try_into().expect("valid");
let reflog_message = || LogChange {
mode: RefLog::AndReference,
force_create_reflog: false,
message: reflog_message.to_owned(),
};
match head_ref {
Some(referent) => {
let referent: gix_ref::FullName = referent.try_into().map_err(|err| Error::InvalidHeadRef {
head_ref_name: referent.to_owned(),
source: err,
})?;
repo.refs
.transaction()
.packed_refs(gix_ref::file::transaction::PackedRefs::DeletionsAndNonSymbolicUpdates(
Box::new(&repo.objects),
))
.prepare(
{
let mut edits = vec![RefEdit {
change: gix_ref::transaction::Change::Update {
log: reflog_message(),
expected: PreviousValue::Any,
new: Target::Symbolic(referent.clone()),
},
name: head.clone(),
deref: false,
}];
if let Some(head_peeled_id) = head_peeled_id {
edits.push(RefEdit {
change: gix_ref::transaction::Change::Update {
log: reflog_message(),
expected: PreviousValue::Any,
new: Target::Object(head_peeled_id.to_owned()),
},
name: referent.clone(),
deref: false,
});
}
edits
},
gix_lock::acquire::Fail::Immediately,
gix_lock::acquire::Fail::Immediately,
)
.map_err(crate::reference::edit::Error::from)?
.commit(
repo.committer()
.transpose()
.map_err(|err| Error::HeadUpdate(crate::reference::edit::Error::ParseCommitterTime(err)))?,
)
.map_err(crate::reference::edit::Error::from)?;
if let Some(head_peeled_id) = head_peeled_id {
let mut log = reflog_message();
log.mode = RefLog::Only;
repo.edit_reference(RefEdit {
change: gix_ref::transaction::Change::Update {
log,
expected: PreviousValue::Any,
new: Target::Object(head_peeled_id.to_owned()),
},
name: head,
deref: false,
})?;
}
setup_branch_config(repo, referent.as_ref(), head_peeled_id, remote_name)?;
}
None => {
repo.edit_reference(RefEdit {
change: gix_ref::transaction::Change::Update {
log: reflog_message(),
expected: PreviousValue::Any,
new: Target::Object(
head_peeled_id
.expect("detached heads always point to something")
.to_owned(),
),
},
name: head,
deref: false,
})?;
}
}
Ok(())
}
pub(super) fn find_custom_refname<'a>(
ref_map: &'a crate::remote::fetch::RefMap,
ref_name: &PartialName,
) -> Result<(&'a gix_hash::oid, &'a BStr), Error> {
let group = gix_refspec::MatchGroup::from_fetch_specs(Some(
gix_refspec::parse(ref_name.as_ref().as_bstr(), gix_refspec::parse::Operation::Fetch)
.expect("partial names are valid refs"),
));
let filtered_items: Vec<_> = ref_map
.mappings
.iter()
.filter_map(|m| m.remote.as_name().zip(m.remote.as_id()))
.map(|(full_ref_name, target)| gix_refspec::match_group::Item {
full_ref_name,
target,
object: None,
})
.collect();
let requested_name = ref_name.as_ref().as_bstr();
let find_item = |name: &BStr| filtered_items.iter().find(|item| item.full_ref_name == name).copied();
// Preserve gix's documented full-ref support, then match git clone --branch by trying heads before tags.
if let Some(item) = find_item(requested_name) {
return Ok((item.target, item.full_ref_name));
}
if !requested_name.starts_with(b"refs/") {
let branch_name = Category::LocalBranch.to_full_name(requested_name)?;
if let Some(item) = find_item(branch_name.as_bstr()) {
return Ok((item.target, item.full_ref_name));
}
let tag_name = Category::Tag.to_full_name(requested_name)?;
if let Some(item) = find_item(tag_name.as_bstr()) {
return Ok((item.target, item.full_ref_name));
}
}
let res = group.match_lhs(filtered_items.iter().copied());
match res.mappings.len() {
0 => Err(Error::RefNameMissing {
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((item.target, item.full_ref_name))
}
_ => Err(Error::RefNameAmbiguous {
wanted: ref_name.clone(),
candidates: res
.mappings
.into_iter()
.filter_map(|m| match m.lhs {
gix_refspec::match_group::SourceRef::FullName(name) => Some(name.into_owned()),
gix_refspec::match_group::SourceRef::ObjectId(_) => None,
})
.collect(),
}),
}
}
/// Set up the remote configuration for `branch` so that it points to itself, but on the remote, if and only if currently
/// saved refspecs are able to match it.
/// For that we reload the remote of `remote_name` and use its `ref_specs` for match.
fn setup_branch_config(
repo: &mut Repository,
branch: &FullNameRef,
branch_id: Option<&gix_hash::oid>,
remote_name: &BStr,
) -> Result<(), Error> {
let short_name = match branch.category_and_short_name() {
Some((gix_ref::Category::LocalBranch, shortened)) => match shortened.to_str() {
Ok(s) => s,
Err(_) => return Ok(()),
},
_ => return Ok(()),
};
let remote = repo
.find_remote(remote_name)
.expect("remote was just created and must be visible in config");
let group = gix_refspec::MatchGroup::from_fetch_specs(remote.fetch_specs.iter().map(gix_refspec::RefSpec::to_ref));
let null = gix_hash::ObjectId::null(repo.object_hash());
let res = group.match_lhs(
Some(gix_refspec::match_group::Item {
full_ref_name: branch.as_bstr(),
target: branch_id.unwrap_or(&null),
object: None,
})
.into_iter(),
);
if !res.mappings.is_empty() {
let mut config = repo.config_snapshot_mut();
let mut section = config
.new_section("branch", short_name)
.expect("section header name is always valid per naming rules, our input branch name is valid");
section.push("remote", remote_name)?;
section.push("merge", branch.as_bstr())?;
write_to_local_config(&config, WriteMode::Overwrite)?;
config.commit().expect("configuration we set is valid");
}
Ok(())
}