use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::config::InvalidValue;
use crate::refs::{Expected, RefName, ReferenceError, Target};
use crate::{
AheadBehind, Commit, Config, HistoryError, HistoryLimits, IdMap, Object, ObjectFormat,
ObjectId, ObjectKind, ObjectReadError, Objects, ReadCommitError, ReadLimits, Repository,
};
const MAX_LINKS: usize = 4;
const MAX_GRAFT_BYTES: u64 = 64 << 20;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Settings {
use_replace_refs: bool,
base: String,
}
impl Default for Settings {
fn default() -> Self {
Self {
use_replace_refs: true,
base: "refs/replace/".to_owned(),
}
}
}
impl Settings {
pub fn from_config(config: &Config) -> Result<Self, InvalidValue> {
let enabled = config.boolean("core", None, "useReplaceRefs")?;
Ok(Self::default().with_replace_refs(enabled.unwrap_or(true)))
}
pub fn with_environment(
self,
environment: &crate::workflow::Environment,
) -> Result<Self, SettingsError> {
let mut settings = self;
if environment.get("GIT_NO_REPLACE_OBJECTS").is_some() {
settings.use_replace_refs = false;
}
if let Some(base) = environment.get("GIT_REPLACE_REF_BASE") {
let base = base
.to_str()
.ok_or_else(|| SettingsError::RefBase(base.to_string_lossy().into_owned()))?;
settings = settings.with_ref_base(base)?;
}
Ok(settings)
}
#[must_use]
pub fn with_replace_refs(mut self, enabled: bool) -> Self {
self.use_replace_refs = enabled;
self
}
pub fn with_ref_base(mut self, base: &str) -> Result<Self, SettingsError> {
let invalid = || SettingsError::RefBase(base.to_owned());
if !base.ends_with('/') || !base.starts_with("refs/") {
return Err(invalid());
}
RefName::new(format!("{base}0")).map_err(|_| invalid())?;
self.base = base.to_owned();
Ok(self)
}
pub fn uses_replace_refs(&self) -> bool {
self.use_replace_refs
}
pub fn ref_base(&self) -> &[u8] {
self.base.as_bytes()
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SettingsError {
#[error("invalid replace reference base {0:?}")]
RefBase(String),
}
#[derive(Clone, Debug, Default)]
pub struct Replacements {
objects: IdMap<ObjectId>,
grafts: IdMap<Vec<ObjectId>>,
ignored_refs: Vec<RefName>,
ignored_graft_lines: Vec<usize>,
}
impl Replacements {
pub fn get(&self, original: ObjectId) -> Option<ObjectId> {
self.objects.get(&original).copied()
}
pub fn graft(&self, commit: ObjectId) -> Option<&[ObjectId]> {
self.grafts.get(&commit).map(Vec::as_slice)
}
pub fn is_empty(&self) -> bool {
self.objects.is_empty() && self.grafts.is_empty()
}
pub fn replaced_count(&self) -> usize {
self.objects.len()
}
pub fn graft_count(&self) -> usize {
self.grafts.len()
}
pub fn ignored_refs(&self) -> &[RefName] {
&self.ignored_refs
}
pub fn ignored_graft_lines(&self) -> &[usize] {
&self.ignored_graft_lines
}
pub fn resolve(&self, id: ObjectId) -> Result<ObjectId, ObjectReadError> {
let mut current = id;
for _ in 0..=MAX_LINKS {
match self.get(current) {
None => return Ok(current),
Some(next) => current = next,
}
}
Err(ObjectReadError::ReplaceDepth(id))
}
fn load(repository: &Repository, settings: &Settings) -> Result<Self, LoadError> {
let format = repository.object_format();
let mut replacements = Self::default();
if settings.use_replace_refs {
let base = settings.base.as_bytes();
for reference in repository.references()?.list_prefix(base)? {
let suffix = &reference.name.as_bytes()[base.len()..];
let original = std::str::from_utf8(suffix)
.ok()
.and_then(|hex| ObjectId::from_hex(format, hex).ok());
match (original, reference.target) {
(Some(original), Target::Direct(replacement)) => {
if replacements.objects.insert(original, replacement).is_some() {
return Err(LoadError::Duplicate(reference.name));
}
}
_ => replacements.ignored_refs.push(reference.name),
}
}
}
let path = repository.common_dir().join("info").join("grafts");
if let Some(bytes) = read_grafts(&path)? {
replacements.parse_grafts(format, &bytes);
}
Ok(replacements)
}
fn parse_grafts(&mut self, format: ObjectFormat, bytes: &[u8]) {
for (index, line) in bytes.split(|&byte| byte == b'\n').enumerate() {
let line = line.trim_ascii_end();
if line.is_empty() || line.starts_with(b"#") {
continue;
}
match parse_graft(format, line) {
Some((commit, parents)) if !self.grafts.contains_key(&commit) => {
self.grafts.insert(commit, parents);
}
_ => self.ignored_graft_lines.push(index + 1),
}
}
}
}
fn parse_graft(format: ObjectFormat, line: &[u8]) -> Option<(ObjectId, Vec<ObjectId>)> {
let width = format.digest_len() * 2;
let parse = |hex: &[u8]| {
std::str::from_utf8(hex)
.ok()
.and_then(|hex| ObjectId::from_hex(format, hex).ok())
};
let commit = parse(line.get(..width)?)?;
let mut rest = &line[width..];
let mut parents = Vec::new();
while !rest.is_empty() {
if !matches!(rest[0], b' ' | b'\t') {
return None;
}
parents.push(parse(rest.get(1..=width)?)?);
rest = &rest[width + 1..];
}
Some((commit, parents))
}
fn read_grafts(path: &Path) -> Result<Option<Vec<u8>>, LoadError> {
use std::io::Read as _;
let io = |source| LoadError::Grafts {
path: path.to_owned(),
source,
};
let file = match std::fs::File::open(path) {
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(io(error)),
};
let mut bytes = Vec::new();
file.take(MAX_GRAFT_BYTES + 1)
.read_to_end(&mut bytes)
.map_err(io)?;
if bytes.len() as u64 > MAX_GRAFT_BYTES {
return Err(LoadError::GraftsTooLarge(path.to_owned()));
}
Ok(Some(bytes))
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum LoadError {
#[error("cannot list replace references")]
References(#[from] ReferenceError),
#[error("duplicate replace reference {}", String::from_utf8_lossy(.0.as_bytes()))]
Duplicate(RefName),
#[error("cannot read {path}")]
Grafts {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("grafts file {0} exceeds 64 MiB")]
GraftsTooLarge(PathBuf),
}
impl Repository {
pub fn replacements(&self, settings: &Settings) -> Result<Replacements, LoadError> {
Replacements::load(self, settings)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum Force {
#[default]
No,
Yes,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum WriteError {
#[error("object {0} not found")]
Missing(ObjectId),
#[error(
"{original} is a {}, but replacement {replacement} is a {}",
original_kind.as_str(),
replacement_kind.as_str()
)]
KindMismatch {
original: ObjectId,
original_kind: ObjectKind,
replacement: ObjectId,
replacement_kind: ObjectKind,
},
#[error("{0} cannot replace itself")]
SelfReplacement(ObjectId),
#[error("replace reference for {0} already exists")]
Exists(ObjectId),
#[error("replace reference for {0} not found")]
NotFound(ObjectId),
#[error("{id} is a {}, not a commit", kind.as_str())]
NotCommit {
id: ObjectId,
kind: ObjectKind,
},
#[error("new commit is the same as the old one: {0}")]
Unchanged(ObjectId),
#[error("commit {0} has a mergetag header; grafting it is unsupported")]
MergeTag(ObjectId),
#[error("malformed commit {id}")]
Commit {
id: ObjectId,
#[source]
source: crate::CommitError,
},
#[error("cannot read objects")]
Read(#[from] ObjectReadError),
#[error("cannot write replacement commit")]
Write(#[from] crate::loose::Error),
#[error("cannot update replace reference")]
Reference(#[from] ReferenceError),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct Grafted {
pub replacement: ObjectId,
pub dropped_signature: bool,
}
impl Repository {
pub fn replace(
&self,
original: ObjectId,
replacement: ObjectId,
force: Force,
) -> Result<(), WriteError> {
if original == replacement {
return Err(WriteError::SelfReplacement(original));
}
let objects = self.objects()?;
let original_kind = kind(&objects, original)?;
let replacement_kind = kind(&objects, replacement)?;
if original_kind != replacement_kind && force == Force::No {
return Err(WriteError::KindMismatch {
original,
original_kind,
replacement,
replacement_kind,
});
}
self.write_replace_ref(original, replacement, force)
}
pub fn replace_graft(
&self,
commit: ObjectId,
parents: &[ObjectId],
force: Force,
) -> Result<Grafted, WriteError> {
let objects = self.objects()?;
for &id in std::iter::once(&commit).chain(parents) {
let kind = kind(&objects, id)?;
if kind != ObjectKind::Commit {
return Err(WriteError::NotCommit { id, kind });
}
}
if force == Force::No && self.replace_ref_target(commit)?.is_some() {
return Err(WriteError::Exists(commit));
}
let object = objects
.read_with(commit, ReadLimits::default())?
.ok_or(WriteError::Missing(commit))?;
Commit::parse(self.object_format(), object.data())
.map_err(|source| WriteError::Commit { id: commit, source })?;
let (bytes, dropped_signature) = graft_bytes(commit, object.data(), parents)?;
if self.object_format().hash_object(ObjectKind::Commit, &bytes) == commit {
return Err(WriteError::Unchanged(commit));
}
let replacement =
self.loose_objects()
.write_object(self.object_format(), ObjectKind::Commit, &bytes)?;
self.write_replace_ref(commit, replacement, force)?;
Ok(Grafted {
replacement,
dropped_signature,
})
}
pub fn delete_replacement(&self, original: ObjectId) -> Result<ObjectId, WriteError> {
let name = replace_ref_name(original);
let target = self
.replace_ref_target(original)?
.ok_or(WriteError::NotFound(original))?;
self.references()?
.delete_without_reflog(&name, Expected::Value(Target::Direct(target)))?;
Ok(target)
}
fn replace_ref_target(&self, original: ObjectId) -> Result<Option<ObjectId>, WriteError> {
Ok(
match self.references()?.read(&replace_ref_name(original))? {
Some(Target::Direct(id)) => Some(id),
Some(Target::Symbolic(_)) | None => None,
},
)
}
fn write_replace_ref(
&self,
original: ObjectId,
replacement: ObjectId,
force: Force,
) -> Result<(), WriteError> {
let expected = match force {
Force::No => Expected::Absent,
Force::Yes => Expected::Any,
};
let result = self.references()?.update_without_reflog(
&replace_ref_name(original),
Target::Direct(replacement),
expected,
);
match result {
Err(ReferenceError::Mismatch { .. }) => Err(WriteError::Exists(original)),
other => Ok(other?),
}
}
}
fn replace_ref_name(original: ObjectId) -> RefName {
RefName::new(format!("refs/replace/{original}")).expect("hex IDs form valid names")
}
fn kind(objects: &Objects, id: ObjectId) -> Result<ObjectKind, WriteError> {
Ok(objects
.header(id, ReadLimits::default())?
.ok_or(WriteError::Missing(id))?
.kind())
}
fn graft_bytes(
id: ObjectId,
data: &[u8],
parents: &[ObjectId],
) -> Result<(Vec<u8>, bool), WriteError> {
let end = data
.windows(2)
.position(|pair| pair == b"\n\n")
.map_or(data.len(), |position| position + 1);
let (header, body) = data.split_at(end);
let mut lines = header.split_inclusive(|&byte| byte == b'\n').peekable();
let mut out = Vec::with_capacity(data.len());
out.extend_from_slice(lines.next().unwrap_or_default());
while lines.next_if(|line| line.starts_with(b"parent ")).is_some() {}
for parent in parents {
out.extend_from_slice(format!("parent {parent}\n").as_bytes());
}
let mut dropped_signature = false;
let mut skipping = false;
for line in lines {
if line.starts_with(b" ") {
if !skipping {
out.extend_from_slice(line);
}
continue;
}
skipping = false;
let name = line
.split(|&byte| byte == b' ' || byte == b'\n')
.next()
.unwrap_or_default();
match name {
b"gpgsig" | b"gpgsig-sha256" => {
dropped_signature = true;
skipping = true;
}
b"mergetag" => return Err(WriteError::MergeTag(id)),
_ => out.extend_from_slice(line),
}
}
out.extend_from_slice(body);
Ok((out, dropped_signature))
}
#[derive(Clone, Debug)]
pub struct ReplacedObjects {
raw: Objects,
view: Objects,
replacements: Arc<Replacements>,
}
impl ReplacedObjects {
pub fn new(objects: Objects, replacements: Replacements) -> Self {
let replacements = Arc::new(replacements);
let mut view = objects.clone();
if !replacements.is_empty() {
view.commit_graph = None;
view.replacements = Some(Arc::clone(&replacements));
}
Self {
raw: objects,
view,
replacements,
}
}
pub fn raw(&self) -> &Objects {
&self.raw
}
pub(crate) fn view(&self) -> &Objects {
&self.view
}
pub fn replacements(&self) -> &Replacements {
&self.replacements
}
pub fn read(&self, id: ObjectId) -> Result<Option<Object>, ObjectReadError> {
self.read_with(id, ReadLimits::default())
}
pub fn read_with(
&self,
id: ObjectId,
limits: ReadLimits,
) -> Result<Option<Object>, ObjectReadError> {
self.view.read_with(id, limits)
}
pub fn read_commit_with(
&self,
id: ObjectId,
limits: ReadLimits,
) -> Result<Commit, ReadCommitError> {
let object = self
.read_with(id, limits)?
.ok_or(ReadCommitError::Missing(id))?;
if object.kind() != ObjectKind::Commit {
return Err(ReadCommitError::Kind {
id,
kind: object.kind(),
});
}
Ok(Commit::parse(object.object_format(), object.data())?)
}
pub fn parents<'a>(&'a self, id: ObjectId, commit: &'a Commit) -> &'a [ObjectId] {
self.replacements.graft(id).unwrap_or(commit.parents())
}
pub fn walk(&self, roots: &[ObjectId]) -> Result<Vec<ObjectId>, HistoryError> {
self.view.walk(roots)
}
pub fn walk_with(
&self,
roots: &[ObjectId],
limits: HistoryLimits,
) -> Result<Vec<ObjectId>, HistoryError> {
self.view.walk_with(roots, limits)
}
pub fn walk_range(
&self,
include: &[ObjectId],
exclude: &[ObjectId],
) -> Result<Vec<ObjectId>, HistoryError> {
self.view.walk_range(include, exclude)
}
pub fn walk_range_with(
&self,
include: &[ObjectId],
exclude: &[ObjectId],
limits: HistoryLimits,
) -> Result<Vec<ObjectId>, HistoryError> {
self.view.walk_range_with(include, exclude, limits)
}
pub fn is_ancestor(
&self,
ancestor: ObjectId,
descendant: ObjectId,
) -> Result<bool, HistoryError> {
self.view.is_ancestor(ancestor, descendant)
}
pub fn is_ancestor_with(
&self,
ancestor: ObjectId,
descendant: ObjectId,
limits: HistoryLimits,
) -> Result<bool, HistoryError> {
self.view.is_ancestor_with(ancestor, descendant, limits)
}
pub fn merge_bases(
&self,
left: ObjectId,
right: ObjectId,
) -> Result<Vec<ObjectId>, HistoryError> {
self.view.merge_bases(left, right)
}
pub fn merge_bases_with(
&self,
left: ObjectId,
right: ObjectId,
limits: HistoryLimits,
) -> Result<Vec<ObjectId>, HistoryError> {
self.view.merge_bases_with(left, right, limits)
}
pub fn ahead_behind(
&self,
left: ObjectId,
right: ObjectId,
) -> Result<AheadBehind, HistoryError> {
self.view.ahead_behind(left, right)
}
}