use std::io;
use std::ops::ControlFlow;
use std::path::PathBuf;
use std::sync::Arc;
use crate::error::{Error, Result};
use crate::reflog::ReflogEntry;
use crate::refs::{read_ref_file, Ref};
use super::{
PreparedRefTransaction, RawRef, RefEntry, RefStorageFormat, RefStore, RefStoreError,
RefTransaction, StoreResult,
};
#[derive(Debug)]
pub(crate) struct LinkedWorktreeRefStore {
admin_dir: PathBuf,
shared: Arc<dyn RefStore>,
}
#[derive(Debug, thiserror::Error)]
#[error("HEAD of the linked worktree at {} cannot be written through an injected ref store", .admin_dir.display())]
pub(crate) struct LinkedWorktreeHeadReadOnly {
admin_dir: PathBuf,
}
impl LinkedWorktreeRefStore {
pub(crate) fn new(admin_dir: PathBuf, shared: Arc<dyn RefStore>) -> Self {
Self { admin_dir, shared }
}
fn head_read_only(&self) -> RefStoreError {
RefStoreError::backend(LinkedWorktreeHeadReadOnly {
admin_dir: self.admin_dir.clone(),
})
}
fn read_head(&self) -> Result<Option<RawRef>> {
match read_ref_file(&self.admin_dir.join("HEAD")) {
Ok(Ref::Direct(oid)) => Ok(Some(RawRef::Direct(oid))),
Ok(Ref::Symbolic(target)) => Ok(Some(RawRef::Symbolic(target))),
Err(Error::Io(err)) if err.kind() == io::ErrorKind::NotFound => Ok(None),
Err(err) => Err(err),
}
}
}
impl RefStore for LinkedWorktreeRefStore {
fn format(&self) -> RefStorageFormat {
self.shared.format()
}
fn read_raw(&self, name: &str) -> Result<Option<RawRef>> {
if name == "HEAD" {
return self.read_head();
}
self.shared.read_raw(name)
}
fn for_each_ref(
&self,
prefix: &str,
f: &mut dyn FnMut(&RefEntry) -> ControlFlow<()>,
) -> Result<()> {
self.shared.for_each_ref(prefix, &mut |entry| {
if entry.name == "HEAD" {
ControlFlow::Continue(())
} else {
f(entry)
}
})
}
fn prepare(
&self,
txn: RefTransaction,
) -> StoreResult<Box<dyn PreparedRefTransaction + Send + '_>> {
if txn.updates().iter().any(|update| update.name == "HEAD") {
return Err(self.head_read_only());
}
self.shared.prepare(txn)
}
fn reflog_exists(&self, name: &str) -> Result<bool> {
if name == "HEAD" {
return Ok(false);
}
self.shared.reflog_exists(name)
}
fn for_each_reflog_entry(
&self,
name: &str,
reverse: bool,
f: &mut dyn FnMut(&ReflogEntry) -> ControlFlow<()>,
) -> Result<()> {
if name == "HEAD" {
return Ok(());
}
self.shared.for_each_reflog_entry(name, reverse, f)
}
fn create_reflog(&self, name: &str) -> Result<()> {
if name == "HEAD" {
return Err(self.head_read_only().into());
}
self.shared.create_reflog(name)
}
fn delete_reflog(&self, name: &str) -> Result<()> {
if name == "HEAD" {
return Err(self.head_read_only().into());
}
self.shared.delete_reflog(name)
}
fn replace_reflog(&self, name: &str, entries: Vec<ReflogEntry>) -> Result<()> {
if name == "HEAD" {
return Err(self.head_read_only().into());
}
self.shared.replace_reflog(name, entries)
}
fn for_each_reflog_ref(&self, f: &mut dyn FnMut(&str) -> ControlFlow<()>) -> Result<()> {
self.shared.for_each_reflog_ref(&mut |name| {
if name == "HEAD" {
ControlFlow::Continue(())
} else {
f(name)
}
})
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use std::fs;
use super::*;
use crate::objects::ObjectId;
use crate::refs::store::{Expected, MemoryRefStore, RefUpdate, RefUpdateFlags};
fn oid(byte: u8) -> ObjectId {
ObjectId::from_bytes(&[byte; 20]).expect("oid")
}
fn setup(
head: Option<&str>,
) -> (
tempfile::TempDir,
Arc<MemoryRefStore>,
LinkedWorktreeRefStore,
) {
let dir = tempfile::tempdir().expect("tempdir");
if let Some(head) = head {
fs::write(dir.path().join("HEAD"), head).expect("HEAD");
}
let shared = Arc::new(MemoryRefStore::new());
shared
.set_ref("HEAD", RawRef::Symbolic("refs/heads/main".into()))
.expect("main HEAD");
shared
.set_ref("refs/heads/main", RawRef::Direct(oid(1)))
.expect("main");
shared
.set_ref("refs/heads/linked", RawRef::Direct(oid(2)))
.expect("linked");
let view = LinkedWorktreeRefStore::new(dir.path().to_path_buf(), shared.clone());
(dir, shared, view)
}
#[test]
fn head_comes_from_the_admin_dir_and_branches_from_the_shared_store() {
let (_dir, _shared, view) = setup(Some("ref: refs/heads/linked\n"));
assert_eq!(
view.read_raw("HEAD").expect("HEAD"),
Some(RawRef::Symbolic("refs/heads/linked".into()))
);
assert_eq!(view.resolve("HEAD").expect("resolve"), oid(2));
assert_eq!(view.resolve("refs/heads/main").expect("main"), oid(1));
assert_eq!(view.format(), RefStorageFormat::Memory);
let mut names = Vec::new();
view.for_each_ref("", &mut |entry| {
names.push(entry.name.clone());
ControlFlow::Continue(())
})
.expect("list");
assert_eq!(names, vec!["refs/heads/linked", "refs/heads/main"]);
}
#[test]
fn detached_and_missing_head() {
let (_dir, _shared, view) = setup(Some(&format!("{}\n", oid(3))));
assert_eq!(
view.read_raw("HEAD").expect("HEAD"),
Some(RawRef::Direct(oid(3)))
);
let (_dir, _shared, view) = setup(None);
assert_eq!(view.read_raw("HEAD").expect("HEAD"), None);
}
#[test]
fn head_writes_are_refused_and_branch_writes_reach_the_shared_store() {
let (_dir, shared, view) = setup(Some("ref: refs/heads/linked\n"));
let head = RefTransaction::new()
.update(RefUpdate {
name: "HEAD".into(),
new_value: Some(RawRef::Direct(oid(4))),
expected: Expected::Any,
reflog: None,
flags: RefUpdateFlags::default(),
})
.expect("txn");
let err = view.prepare(head).err().expect("HEAD is read-only");
let RefStoreError::Backend(backend) = err else {
panic!("expected a backend error, got {err:?}");
};
assert!(backend
.downcast_ref::<LinkedWorktreeHeadReadOnly>()
.is_some());
assert!(view.create_reflog("HEAD").is_err());
assert!(!view.reflog_exists("HEAD").expect("exists"));
let branch = RefTransaction::new()
.update(RefUpdate {
name: "refs/heads/linked".into(),
new_value: Some(RawRef::Direct(oid(5))),
expected: Expected::Any,
reflog: None,
flags: RefUpdateFlags::default(),
})
.expect("txn");
view.prepare(branch)
.expect("prepare")
.commit()
.expect("commit");
assert_eq!(
shared.read_raw("refs/heads/linked").expect("linked"),
Some(RawRef::Direct(oid(5)))
);
assert_eq!(
shared.read_raw("HEAD").expect("main HEAD"),
Some(RawRef::Symbolic("refs/heads/main".into()))
);
}
}