use crate::config::ConfigSet;
use crate::diff::zero_oid;
use crate::error::{Error, Result};
use crate::objects::ObjectId;
use super::store::{Expected, RawRef, RefStore, RefUpdate, RefUpdateFlags, ReflogUpdate};
use super::{
append_reflog_with_config, commit_ref_update_on_store, commit_ref_updates_on_store,
ensure_refname_safe_for_storage, should_autocreate_reflog_for_mode, store_write_value_on_store,
BranchCommitRefUpdate,
};
fn reflog_wanted(store: &dyn RefStore, config: &ConfigSet, refname: &str) -> Result<bool> {
if should_autocreate_reflog_for_mode(refname, config.effective_log_refs_config()) {
return Ok(true);
}
store.reflog_exists(refname)
}
fn explicit_reflog(identity: &str, message: &str, old: ObjectId, new: ObjectId) -> ReflogUpdate {
ReflogUpdate {
identity: identity.to_owned(),
message: message.to_owned(),
time: time::OffsetDateTime::UNIX_EPOCH,
old_oid: Some(old),
new_oid: Some(new),
}
}
fn expected_tip(expected_old: Option<ObjectId>) -> Expected {
expected_old.map_or(Expected::Missing, Expected::Oid)
}
pub fn update_branch_for_commit_on_store(
store: &dyn RefStore,
update: &BranchCommitRefUpdate<'_>,
config: &ConfigSet,
) -> Result<()> {
match store.read_raw("HEAD")? {
Some(RawRef::Symbolic(target)) if target == update.branch_ref => {}
_ => {
return Err(Error::Message(format!(
"HEAD does not point at branch '{}'",
update.branch_ref
)))
}
}
let reflog_old = update.expected_old.unwrap_or_else(zero_oid);
if let Some(files) = store.as_files() {
return super::update_branch_for_commit_files(files.git_dir(), update, &reflog_old, config);
}
ensure_refname_safe_for_storage(update.branch_ref)?;
let reflog = || {
explicit_reflog(
update.identity,
update.reflog_message,
reflog_old,
update.new_oid,
)
};
let mut updates = Vec::with_capacity(2);
if reflog_wanted(store, config, "HEAD")? {
updates.push(RefUpdate {
name: "HEAD".to_owned(),
new_value: Some(RawRef::Direct(update.new_oid)),
expected: Expected::Any,
reflog: Some(reflog()),
flags: RefUpdateFlags {
no_deref: true,
log_only: true,
},
});
}
updates.push(RefUpdate {
name: update.branch_ref.to_owned(),
new_value: Some(RawRef::Direct(update.new_oid)),
expected: expected_tip(update.expected_old),
reflog: reflog_wanted(store, config, update.branch_ref)?.then(reflog),
flags: RefUpdateFlags::default(),
});
commit_ref_updates_on_store(store, &updates, &[])
}
pub(crate) fn update_detached_head_for_commit_on_store(
store: &dyn RefStore,
expected_old: Option<ObjectId>,
new_oid: ObjectId,
identity: &str,
reflog_message: &str,
config: &ConfigSet,
) -> Result<()> {
if matches!(store.read_raw("HEAD")?, Some(RawRef::Symbolic(_))) {
return Err(Error::Message(
"HEAD points at a branch; use update_branch_for_commit".into(),
));
}
if let Some(files) = store.as_files() {
return super::update_detached_head_for_commit_files(
files.git_dir(),
expected_old,
new_oid,
identity,
reflog_message,
config,
);
}
let reflog_old = expected_old.unwrap_or_else(zero_oid);
let reflog = reflog_wanted(store, config, "HEAD")?
.then(|| explicit_reflog(identity, reflog_message, reflog_old, new_oid));
commit_ref_update_on_store(
store,
RefUpdate {
name: "HEAD".to_owned(),
new_value: Some(RawRef::Direct(new_oid)),
expected: expected_tip(expected_old),
reflog,
flags: RefUpdateFlags {
no_deref: true,
log_only: false,
},
},
)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn append_reflog_on_store(
store: &dyn RefStore,
refname: &str,
old_oid: &ObjectId,
new_oid: &ObjectId,
identity: &str,
message: &str,
force_create: bool,
config: &ConfigSet,
) -> Result<()> {
if let Some(files) = store.as_files() {
return append_reflog_with_config(
files.git_dir(),
refname,
old_oid,
new_oid,
identity,
message,
force_create,
Some(config),
);
}
ensure_refname_safe_for_storage(refname)?;
if !force_create && !reflog_wanted(store, config, refname)? {
return Ok(());
}
commit_ref_update_on_store(
store,
RefUpdate {
name: refname.to_owned(),
new_value: Some(RawRef::Direct(*new_oid)),
expected: Expected::Any,
reflog: Some(explicit_reflog(identity, message, *old_oid, *new_oid)),
flags: RefUpdateFlags {
no_deref: true,
log_only: true,
},
},
)
}
pub(crate) fn write_symbolic_ref_on_store(
store: &dyn RefStore,
git_dir: &std::path::Path,
refname: &str,
target: &str,
) -> Result<()> {
store_write_value_on_store(
store,
git_dir,
refname,
Some(RawRef::Symbolic(target.to_owned())),
Expected::Any,
None,
)
}
pub(crate) fn delete_ref_on_store(store: &dyn RefStore, refname: &str) -> Result<()> {
if let Some(files) = store.as_files() {
return super::delete_ref(files.git_dir(), refname);
}
ensure_refname_safe_for_storage(refname)?;
commit_ref_update_on_store(
store,
RefUpdate {
name: refname.to_owned(),
new_value: None,
expected: Expected::Exists,
reflog: None,
flags: RefUpdateFlags {
no_deref: true,
log_only: false,
},
},
)?;
store.delete_reflog(refname)
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use std::ops::ControlFlow;
use std::path::Path;
use super::*;
use crate::config::{ConfigFile, ConfigScope};
use crate::refs::store::{MemoryRefStore, RefStoreError};
const IDENT: &str = "T <t@e.x> 1700000000 +0000";
fn oid(byte: u8) -> ObjectId {
let mut bytes = [0u8; 20];
bytes[19] = byte;
ObjectId::from_bytes(&bytes).expect("oid")
}
fn config(log_all_ref_updates: Option<&str>) -> ConfigSet {
let text = log_all_ref_updates
.map(|value| format!("[core]\n\tlogAllRefUpdates = {value}\n"))
.unwrap_or_default();
let file =
ConfigFile::parse(Path::new("config"), &text, ConfigScope::Local).expect("parse");
let mut set = ConfigSet::new();
set.merge(&file);
set
}
fn store_on(branch: &str, tip: Option<ObjectId>) -> MemoryRefStore {
let store = MemoryRefStore::new();
store
.set_ref("HEAD", RawRef::Symbolic(branch.to_owned()))
.expect("HEAD");
if let Some(tip) = tip {
store.set_ref(branch, RawRef::Direct(tip)).expect("tip");
}
store
}
fn log(store: &dyn RefStore, name: &str) -> Vec<(ObjectId, ObjectId, String, String)> {
let mut out = Vec::new();
store
.for_each_reflog_entry(name, false, &mut |e| {
out.push((e.old_oid, e.new_oid, e.identity.clone(), e.message.clone()));
ControlFlow::Continue(())
})
.expect("reflog");
out
}
fn update<'a>(expected_old: Option<ObjectId>, new_oid: ObjectId) -> BranchCommitRefUpdate<'a> {
BranchCommitRefUpdate {
branch_ref: "refs/heads/main",
expected_old,
new_oid,
identity: IDENT,
reflog_message: "commit: two",
}
}
#[test]
fn branch_commit_moves_branch_and_logs_branch_and_head_in_the_store() {
let store = store_on("refs/heads/main", Some(oid(1)));
update_branch_for_commit_on_store(&store, &update(Some(oid(1)), oid(2)), &config(None))
.expect("update");
assert_eq!(
store.read_raw("refs/heads/main").expect("read"),
Some(RawRef::Direct(oid(2)))
);
assert_eq!(
store.read_raw("HEAD").expect("read"),
Some(RawRef::Symbolic("refs/heads/main".into()))
);
let expected = vec![(oid(1), oid(2), IDENT.to_owned(), "commit: two".to_owned())];
assert_eq!(log(&store, "refs/heads/main"), expected);
assert_eq!(log(&store, "HEAD"), expected);
}
#[test]
fn unborn_branch_commit_creates_the_branch_with_a_null_old_id() {
let store = store_on("refs/heads/main", None);
update_branch_for_commit_on_store(&store, &update(None, oid(1)), &config(None))
.expect("update");
assert_eq!(store.resolve("HEAD").expect("HEAD"), oid(1));
assert_eq!(log(&store, "refs/heads/main")[0].0, zero_oid());
}
#[test]
fn stale_expected_tip_is_a_typed_mismatch_and_changes_nothing() {
let store = store_on("refs/heads/main", Some(oid(3)));
let err =
update_branch_for_commit_on_store(&store, &update(Some(oid(1)), oid(2)), &config(None))
.expect_err("stale tip");
assert!(matches!(
err,
Error::RefStore(RefStoreError::ExpectedMismatch { ref name, .. }) if name == "refs/heads/main"
));
assert_eq!(store.resolve("refs/heads/main").expect("tip"), oid(3));
assert!(log(&store, "HEAD").is_empty());
let err = update_branch_for_commit_on_store(&store, &update(None, oid(2)), &config(None))
.expect_err("branch exists");
assert!(matches!(
err,
Error::RefStore(RefStoreError::ExpectedMismatch { .. })
));
}
#[test]
fn branch_commit_refuses_when_head_is_elsewhere() {
let store = store_on("refs/heads/other", Some(oid(1)));
store
.set_ref("refs/heads/main", RawRef::Direct(oid(1)))
.expect("main");
let err =
update_branch_for_commit_on_store(&store, &update(Some(oid(1)), oid(2)), &config(None))
.expect_err("HEAD elsewhere");
assert!(matches!(err, Error::Message(_)));
assert_eq!(store.resolve("refs/heads/main").expect("tip"), oid(1));
}
#[test]
fn disabled_ref_logging_skips_new_reflogs_but_appends_to_existing_ones() {
let store = store_on("refs/heads/main", Some(oid(1)));
update_branch_for_commit_on_store(
&store,
&update(Some(oid(1)), oid(2)),
&config(Some("false")),
)
.expect("update");
assert!(!store.reflog_exists("refs/heads/main").expect("exists"));
assert!(!store.reflog_exists("HEAD").expect("exists"));
store.create_reflog("HEAD").expect("create");
update_branch_for_commit_on_store(
&store,
&update(Some(oid(2)), oid(3)),
&config(Some("false")),
)
.expect("update");
assert_eq!(log(&store, "HEAD").len(), 1);
assert!(!store.reflog_exists("refs/heads/main").expect("exists"));
}
#[test]
fn detached_commit_moves_head_with_cas_and_logs_only_head() {
let store = MemoryRefStore::new();
store.set_ref("HEAD", RawRef::Direct(oid(1))).expect("HEAD");
update_detached_head_for_commit_on_store(
&store,
Some(oid(1)),
oid(2),
IDENT,
"commit: d",
&config(None),
)
.expect("detached");
assert_eq!(
store.read_raw("HEAD").expect("HEAD"),
Some(RawRef::Direct(oid(2)))
);
assert_eq!(log(&store, "HEAD").len(), 1);
let err = update_detached_head_for_commit_on_store(
&store,
Some(oid(1)),
oid(3),
IDENT,
"commit: d",
&config(None),
)
.expect_err("stale");
assert!(matches!(
err,
Error::RefStore(RefStoreError::ExpectedMismatch { .. })
));
let attached = store_on("refs/heads/main", Some(oid(1)));
assert!(matches!(
update_detached_head_for_commit_on_store(
&attached,
Some(oid(1)),
oid(2),
IDENT,
"commit: d",
&config(None),
),
Err(Error::Message(_))
));
}
#[test]
fn append_reflog_honours_policy_and_force_without_moving_the_ref() {
let store = store_on("refs/heads/main", Some(oid(1)));
append_reflog_on_store(
&store,
"refs/tags/v1",
&oid(0),
&oid(1),
IDENT,
"tag",
false,
&config(None),
)
.expect("skip");
assert!(!store.reflog_exists("refs/tags/v1").expect("exists"));
append_reflog_on_store(
&store,
"refs/stash",
&zero_oid(),
&oid(4),
IDENT,
"WIP",
true,
&config(None),
)
.expect("forced");
assert_eq!(
log(&store, "refs/stash"),
vec![(zero_oid(), oid(4), IDENT.to_owned(), "WIP".to_owned())]
);
assert_eq!(store.read_raw("refs/stash").expect("read"), None);
append_reflog_on_store(
&store,
"HEAD",
&oid(1),
&oid(1),
IDENT,
"checkout: moving from main to main",
false,
&config(None),
)
.expect("HEAD");
assert_eq!(log(&store, "HEAD").len(), 1);
assert!(log(&store, "refs/heads/main").is_empty());
assert_eq!(store.resolve("refs/heads/main").expect("tip"), oid(1));
}
#[test]
fn symbolic_write_and_delete_go_through_the_store() {
let store = store_on("refs/heads/main", Some(oid(1)));
write_symbolic_ref_on_store(&store, Path::new("unused"), "HEAD", "refs/heads/topic")
.expect("symref");
assert_eq!(
store.read_raw("HEAD").expect("HEAD"),
Some(RawRef::Symbolic("refs/heads/topic".into()))
);
store
.set_ref("refs/stash", RawRef::Direct(oid(5)))
.expect("stash");
store.create_reflog("refs/stash").expect("log");
delete_ref_on_store(&store, "refs/stash").expect("delete");
assert_eq!(store.read_raw("refs/stash").expect("read"), None);
assert!(!store.reflog_exists("refs/stash").expect("exists"));
assert!(delete_ref_on_store(&store, "refs/stash").is_err());
}
}