use std::{borrow::Cow, convert::TryInto};
use git_odb::Find;
use git_ref::{
transaction::{LogChange, RefLog},
FullNameRef,
};
use super::Error;
use crate::{
bstr::{BStr, BString, ByteSlice},
Repository,
};
#[allow(clippy::result_large_err)]
pub fn write_remote_to_local_config_file(
remote: &mut crate::Remote<'_>,
remote_name: BString,
) -> Result<git_config::File<'static>, Error> {
let mut metadata = git_config::file::Metadata::from(git_config::Source::Local);
let config_path = remote.repo.git_dir().join("config");
metadata.path = Some(config_path.clone());
let mut config =
git_config::File::from_paths_metadata(Some(metadata), Default::default())?.expect("one file to load");
remote.save_as_to(remote_name, &mut config)?;
std::fs::write(config_path, config.to_bstring())?;
Ok(config)
}
pub fn replace_changed_local_config_file(repo: &mut Repository, mut config: git_config::File<'static>) {
let repo_config = git_features::threading::OwnShared::make_mut(&mut repo.config.resolved);
let ids_to_remove: Vec<_> = repo_config
.sections_and_ids()
.filter_map(|(s, id)| {
matches!(s.meta().source, git_config::Source::Local | git_config::Source::Api).then(|| id)
})
.collect();
for id in ids_to_remove {
repo_config.remove_section_by_id(id);
}
crate::config::overrides::append(
&mut config,
&repo.options.api_config_overrides,
git_config::Source::Api,
|_| None,
)
.expect("applied once and can be applied again");
repo_config.append(config);
repo.reread_values_and_clear_caches()
.expect("values could be read once and can be read again");
}
pub fn update_head(
repo: &mut Repository,
remote_refs: &[git_protocol::handshake::Ref],
reflog_message: &BStr,
remote_name: &BStr,
) -> Result<(), Error> {
use git_ref::{
transaction::{PreviousValue, RefEdit},
Target,
};
let (head_peeled_id, head_ref) = match remote_refs.iter().find_map(|r| {
Some(match r {
git_protocol::handshake::Ref::Symbolic {
full_ref_name,
target,
object,
} if full_ref_name == "HEAD" => (Some(object.as_ref()), Some(target)),
git_protocol::handshake::Ref::Direct { full_ref_name, object } if full_ref_name == "HEAD" => {
(Some(object.as_ref()), None)
}
git_protocol::handshake::Ref::Unborn { full_ref_name, target } if full_ref_name == "HEAD" => {
(None, Some(target))
}
_ => return None,
})
}) {
Some(t) => t,
None => return Ok(()),
};
let head: git_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: git_ref::FullName = referent.try_into().map_err(|err| Error::InvalidHeadRef {
head_ref_name: referent.to_owned(),
source: err,
})?;
repo.refs
.transaction()
.packed_refs(git_ref::file::transaction::PackedRefs::DeletionsAndNonSymbolicUpdates(
Box::new(|oid, buf| {
repo.objects
.try_find(oid, buf)
.map(|obj| obj.map(|obj| obj.kind))
.map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync + 'static>)
}),
))
.prepare(
{
let mut edits = vec![RefEdit {
change: git_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: git_ref::transaction::Change::Update {
log: reflog_message(),
expected: PreviousValue::Any,
new: Target::Peeled(head_peeled_id.to_owned()),
},
name: referent.clone(),
deref: false,
});
};
edits
},
git_lock::acquire::Fail::Immediately,
git_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: git_ref::transaction::Change::Update {
log,
expected: PreviousValue::Any,
new: Target::Peeled(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: git_ref::transaction::Change::Update {
log: reflog_message(),
expected: PreviousValue::Any,
new: Target::Peeled(
head_peeled_id
.expect("detached heads always point to something")
.to_owned(),
),
},
name: head,
deref: false,
})?;
}
};
Ok(())
}
fn setup_branch_config(
repo: &mut Repository,
branch: &FullNameRef,
branch_id: Option<&git_hash::oid>,
remote_name: &BStr,
) -> Result<(), Error> {
let short_name = match branch.category_and_short_name() {
Some((cat, shortened)) if cat == git_ref::Category::LocalBranch => 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 = git_refspec::MatchGroup::from_fetch_specs(remote.fetch_specs.iter().map(|s| s.to_ref()));
let null = git_hash::ObjectId::null(repo.object_hash());
let res = group.match_remotes(
Some(git_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 metadata = git_config::file::Metadata::from(git_config::Source::Local);
let config_path = remote.repo.git_dir().join("config");
metadata.path = Some(config_path.clone());
let mut config =
git_config::File::from_paths_metadata(Some(metadata), Default::default())?.expect("one file to load");
let mut section = config
.new_section("branch", Some(Cow::Owned(short_name.into())))
.expect("section header name is always valid per naming rules, our input branch name is valid");
section.push("remote".try_into().expect("valid at compile time"), Some(remote_name));
section.push(
"merge".try_into().expect("valid at compile time"),
Some(branch.as_bstr()),
);
std::fs::write(config_path, config.to_bstring())?;
replace_changed_local_config_file(repo, config);
}
Ok(())
}