mod graph_history;
use std::{
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
use crate::{
Commit, CommitMetadata, GitError, HashKind, Head, HistoryOptions, HistoryRecord, Object,
ObjectId, ObjectKind, Reference, Result, Tag, Tree, TreeChange,
commit_graph::CommitGraph,
diff,
error::invalid,
history, index, layout,
refs::{self, RefTarget},
store::ObjectStore,
};
#[derive(Clone, Copy, Debug)]
pub struct Limits {
pub max_object_bytes: usize,
pub max_delta_depth: usize,
pub max_ref_depth: usize,
pub max_tree_depth: usize,
pub max_tree_entries: usize,
pub max_history_commits: usize,
pub max_parents: usize,
pub object_cache_bytes: usize,
pub delta_cache_bytes: usize,
pub max_bitmap_objects: usize,
pub max_reflog_entries: usize,
pub max_index_entries: usize,
}
impl Default for Limits {
fn default() -> Self {
Self {
max_object_bytes: 512 * 1024 * 1024,
max_delta_depth: 64,
max_ref_depth: 16,
max_tree_depth: 256,
max_tree_entries: 5_000_000,
max_history_commits: 1_000_000,
max_parents: 256,
object_cache_bytes: 32 * 1024 * 1024,
delta_cache_bytes: 16 * 1024 * 1024,
max_bitmap_objects: 10_000_000,
max_reflog_entries: 1_000_000,
max_index_entries: 10_000_000,
}
}
}
pub struct Repository {
work_dir: Option<PathBuf>,
git_dir: PathBuf,
common_dir: PathBuf,
hash: HashKind,
pub(crate) limits: Limits,
pub(crate) graph: Option<CommitGraph>,
pub(crate) store: ObjectStore,
pub(crate) backends: Vec<Arc<dyn crate::ObjectBackend>>,
pub(crate) index_cache: Mutex<Option<index::CachedIndex>>,
}
impl Repository {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
Self::open_with_limits(path, Limits::default())
}
pub fn open_with_limits(path: impl AsRef<Path>, limits: Limits) -> Result<Self> {
Self::open_with_backends(path, limits, Vec::new())
}
pub fn open_with_backends(
path: impl AsRef<Path>,
limits: Limits,
backends: Vec<Arc<dyn crate::ObjectBackend>>,
) -> Result<Self> {
let (work_dir, git_dir) = layout::discover(path.as_ref())?;
let common_dir = layout::common_dir(&git_dir)?;
let hash = layout::hash_kind(&common_dir)?;
let graph = CommitGraph::open(&common_dir, hash)?;
let store = ObjectStore::open(
common_dir.join("objects"),
hash,
limits.object_cache_bytes,
limits.delta_cache_bytes,
)?;
Ok(Self {
work_dir,
git_dir,
common_dir,
hash,
limits,
graph,
store,
backends,
index_cache: Mutex::new(None),
})
}
#[must_use]
pub fn work_dir(&self) -> Option<&Path> {
self.work_dir.as_deref()
}
#[must_use]
pub fn git_dir(&self) -> &Path {
&self.git_dir
}
#[must_use]
pub fn common_dir(&self) -> &Path {
&self.common_dir
}
#[must_use]
pub const fn hash_kind(&self) -> HashKind {
self.hash
}
#[must_use]
pub const fn limits(&self) -> &Limits {
&self.limits
}
#[must_use]
pub fn pack_count(&self) -> usize {
self.store.pack_count()
}
pub fn head(&self) -> Result<Head> {
let value = refs::read_text(&self.git_dir.join("HEAD"))?
.ok_or_else(|| GitError::NotRepository("HEAD is missing".to_owned()))?;
match refs::parse_target(&value, self.hash)? {
RefTarget::Direct(id) => Ok(Head {
symbolic: None,
target: Some(id),
}),
RefTarget::Symbolic(name) => Ok(Head {
target: self.resolve_ref_optional(&name, 0)?,
symbolic: Some(name),
}),
}
}
pub fn reference(&self, name: &str) -> Result<Reference> {
refs::validate_name(name)?;
Ok(Reference {
name: name.to_owned(),
target: self
.resolve_ref_optional(name, 0)?
.ok_or_else(|| refs::missing_ref(name))?,
})
}
pub fn resolve(&self, value: &str) -> Result<ObjectId> {
if value.len() == self.hash.hex_len() && value.bytes().all(|byte| byte.is_ascii_hexdigit())
{
return ObjectId::from_hex_for(value, self.hash);
}
if value == "HEAD" {
return self
.head()?
.target
.ok_or_else(|| GitError::NotFound("unborn HEAD".to_owned()));
}
for candidate in [
value.to_owned(),
format!("refs/heads/{value}"),
format!("refs/tags/{value}"),
] {
if let Some(id) = self.resolve_ref_optional(&candidate, 0)? {
return Ok(id);
}
}
Err(refs::missing_ref(value))
}
pub fn object(&self, id: ObjectId) -> Result<Object> {
Ok((*self.object_shared(id)?).clone())
}
#[must_use]
pub fn contains(&self, id: ObjectId) -> bool {
self.contains_checked(id).unwrap_or(false)
}
pub fn contains_checked(&self, id: ObjectId) -> Result<bool> {
if id.kind() != self.hash {
return Ok(false);
}
Ok(crate::backend::contains(&self.backends, id)? || self.store.contains(id))
}
pub fn commit(&self, id: ObjectId) -> Result<Commit> {
let object = self.object(id)?;
expect_kind(object.kind, ObjectKind::Commit)?;
Commit::parse(id, &object.data, self.limits.max_parents)
}
pub fn commit_metadata(&self, id: ObjectId) -> Result<CommitMetadata> {
if let Some(graph) = self
.graph
.as_ref()
.map_or(Ok(None), |graph| graph.find(id))?
{
return Ok(CommitMetadata {
id: graph.id,
tree: graph.tree,
parents: graph.parents,
committer_time: graph.time,
});
}
let commit = self.commit(id)?;
let committer_time = commit
.committer
.as_ref()
.or(commit.author.as_ref())
.map_or(0, |signature| signature.timestamp);
Ok(CommitMetadata {
id,
tree: commit.tree,
parents: commit.parents,
committer_time,
})
}
pub fn tree(&self, id: ObjectId) -> Result<Tree> {
let object = self.object(id)?;
expect_kind(object.kind, ObjectKind::Tree)?;
Tree::parse(&object.data, self.hash, self.limits.max_tree_entries)
}
pub fn tag(&self, id: ObjectId) -> Result<Tag> {
let object = self.object(id)?;
expect_kind(object.kind, ObjectKind::Tag)?;
Tag::parse(id, &object.data)
}
pub fn history(&self, start: ObjectId, options: HistoryOptions) -> Result<Vec<HistoryRecord>> {
history::walk(self, start, options)
}
pub fn history_ids(&self, start: ObjectId, options: HistoryOptions) -> Result<Vec<ObjectId>> {
history::walk_ids(self, start, options)
}
pub fn diff_trees(&self, old: ObjectId, new: ObjectId) -> Result<Vec<TreeChange>> {
diff::between(self, old, new)
}
pub fn diff_commits(&self, old: ObjectId, new: ObjectId) -> Result<Vec<TreeChange>> {
self.diff_trees(self.commit(old)?.tree, self.commit(new)?.tree)
}
fn resolve_ref_optional(&self, name: &str, depth: usize) -> Result<Option<ObjectId>> {
refs::validate_name(name)?;
if depth >= self.limits.max_ref_depth {
return Err(GitError::LimitExceeded {
resource: "symbolic ref depth",
limit: self.limits.max_ref_depth,
});
}
for root in [&self.git_dir, &self.common_dir] {
if let Some(value) = refs::read_text(&root.join(name))? {
return match refs::parse_target(&value, self.hash)? {
RefTarget::Direct(id) => Ok(Some(id)),
RefTarget::Symbolic(next) => self.resolve_ref_optional(&next, depth + 1),
};
}
}
for root in [&self.git_dir, &self.common_dir] {
if let Some(id) = refs::packed_target(&root.join("packed-refs"), name, self.hash)? {
return Ok(Some(id));
}
}
Ok(None)
}
}
fn expect_kind(actual: ObjectKind, expected: ObjectKind) -> Result<()> {
if actual != expected {
return Err(invalid(format!(
"expected {} object, found {}",
expected.as_str(),
actual.as_str()
)));
}
Ok(())
}