use std::fmt::Display;
use std::path::Path;
use std::sync::Arc;
use super::reader::ObjectView;
use crate::config::ConfigInputs;
use crate::refs::{Dereference, Expected, RefEdit, RefName, Reflog, Target};
use crate::workflow::{self, Environment, FetchOptions, NoCallbacks, PushOptions};
use crate::{Commit, LooseObjects, ObjectId, ObjectKind, Repository, Signature};
#[derive(Debug)]
pub struct Repo {
pub(super) reader: Arc<ObjectView>,
loose: LooseObjects,
environment: Environment,
filter_commands: crate::filter::Commands,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("invalid repository-relative path {path:?}")]
InvalidPath {
path: Vec<u8>,
},
#[error("path {path:?} is absent from tree {tree}")]
PathAbsent {
path: Vec<u8>,
tree: ObjectId,
},
#[error("path {path:?} requires {wanted:?}, found {found:?}")]
PathKind {
path: Vec<u8>,
wanted: ObjectKind,
found: ObjectKind,
},
#[error("remote {name:?} is not configured")]
RemoteNotConfigured {
name: String,
},
#[error("remote {name:?} has no fetch refspecs; use workflow::fetch with explicit options")]
NoFetchRefspecs {
name: String,
},
#[error("finish or abort the current repository operation first")]
OperationInProgress,
#[error("commit {id} has no committer date")]
MissingCommitter {
id: ObjectId,
},
#[error("checkout completed but HEAD publication failed")]
SwitchHead {
checkout: Box<crate::checkout::Report>,
#[source]
source: Box<crate::refs::TransactionError>,
},
#[error("diff driver {driver:?} of {path:?} configures textconv, which girt does not run")]
Textconv {
path: Vec<u8>,
driver: Vec<u8>,
},
}
impl Repo {
pub fn new(repository: Repository) -> Self {
let loose = repository.loose_objects();
Self {
reader: Arc::new(ObjectView::new(repository)),
loose,
environment: Environment::from_process(),
filter_commands: crate::filter::Commands::Run,
}
}
pub fn discover(path: impl AsRef<Path>) -> crate::Result<Self> {
let environment = Environment::from_process();
let inputs = ConfigInputs::git_default(&environment)?;
let repository = Repository::discover_with_config(path, &inputs)?;
Ok(Self::new(repository).with_environment(environment))
}
pub fn clone(url: &str, destination: impl AsRef<Path>) -> crate::Result<Self> {
let outcome = workflow::clone(url, destination)?;
let repository = outcome
.clone
.repository
.ok_or_else(|| std::io::Error::other("completed clone has no repository"))?;
Ok(Self::new(repository))
}
#[must_use]
pub fn with_environment(mut self, environment: Environment) -> Self {
self.environment = environment;
self
}
#[must_use]
pub fn with_filter_commands(mut self, commands: crate::filter::Commands) -> Self {
self.filter_commands = commands;
self
}
pub fn repository(&self) -> &Repository {
&self.reader.repository
}
pub fn rev_parse(&self, revision: impl Display) -> crate::Result<ObjectId> {
self.reader.resolve(revision, None)
}
pub fn blob_at(
&self,
revision: impl Display,
path: impl AsRef<[u8]>,
) -> crate::Result<Vec<u8>> {
let path = path.as_ref();
if path.is_empty()
|| path
.split(|b| *b == b'/')
.any(|p| p.is_empty() || p == b"." || p == b".." || p.contains(&0))
{
return Err(Error::InvalidPath {
path: path.to_vec(),
}
.into());
}
let mut id = self.reader.resolve(revision, Some(ObjectKind::Tree))?;
let mut components = path.split(|b| *b == b'/').peekable();
while let Some(component) = components.next() {
let tree = self.reader.read_tree(id)?;
let entry = tree
.entries()
.iter()
.find(|e| e.name == component)
.ok_or_else(|| Error::PathAbsent {
path: path.to_vec(),
tree: id,
})?;
let wanted = if components.peek().is_some() {
ObjectKind::Tree
} else {
ObjectKind::Blob
};
if entry.mode.object_kind() != wanted {
return Err(Error::PathKind {
path: path.to_vec(),
wanted,
found: entry.mode.object_kind(),
}
.into());
}
id = entry.id;
}
self.reader.read_blob(id)
}
pub fn blame(
&self,
revision: impl Display,
path: impl AsRef<[u8]>,
) -> crate::Result<Vec<crate::blame::Hunk>> {
self.blame_with(
revision,
path,
Default::default(),
&std::sync::atomic::AtomicBool::new(false),
)
}
pub fn blame_with(
&self,
revision: impl Display,
path: impl AsRef<[u8]>,
limits: crate::blame::Limits,
cancel: &std::sync::atomic::AtomicBool,
) -> crate::Result<Vec<crate::blame::Hunk>> {
let commit = self.reader.resolve(revision, Some(ObjectKind::Commit))?;
let path = path.as_ref();
let mut attributes = self.reader.repository.diff_attributes(&self.environment)?;
if crate::blame::valid_path(path)
&& let Some(driver) = attributes.textconv_driver(path)?
{
return Err(Error::Textconv {
path: path.to_vec(),
driver,
}
.into());
}
Ok(self
.reader
.snapshot()?
.blame_with(commit, path, limits, cancel)?)
}
pub fn status(&self) -> crate::Result<crate::status::Report> {
let repository = &self.reader.repository;
let rules = repository.ignore_rules(&self.environment)?;
let mut filters = repository.filters(&self.environment, self.filter_commands)?;
Ok(repository.status_with_filters(
crate::status::Baseline::Head,
crate::status::UntrackedFiles::Collapsed,
&rules,
&crate::pathspec::Pathspec::all(),
&mut filters,
crate::status::Limits::default(),
&std::sync::atomic::AtomicBool::new(false),
)?)
}
pub fn write_patch(
&self,
out: impl std::io::Write,
old: impl Display,
new: impl Display,
) -> crate::Result<()> {
let old = self.reader.resolve(old, Some(ObjectKind::Tree))?;
let new = self.reader.resolve(new, Some(ObjectKind::Tree))?;
let objects = self.reader.snapshot()?;
let options = crate::rewrites::Options::default();
let comparison = objects.diff_trees_with(
Some(old),
Some(new),
options,
Default::default(),
&std::sync::atomic::AtomicBool::new(false),
)?;
let mut attributes = self.reader.repository.diff_attributes(&self.environment)?;
Ok(objects.write_patch_with(
out,
&comparison.entries,
crate::patch::Options::default(),
Some(&mut attributes),
crate::patch::Limits::default(),
&std::sync::atomic::AtomicBool::new(false),
)?)
}
pub fn tree_editor(&self) -> crate::tree::TreeEditor<'_> {
crate::tree::TreeEditor::new(&self.loose)
}
pub fn commit(&mut self, tree: ObjectId, message: impl AsRef<[u8]>) -> crate::Result<ObjectId> {
self.require_idle()?;
self.reader.read_tree(tree)?;
let author = Signature::author(self.reader.repository.config(), &self.environment)?;
let committer = Signature::committer(self.reader.repository.config(), &self.environment)?;
let refs = self.reader.repository.references()?;
let parent = refs.resolve(&RefName::HEAD)?.id;
let expected = parent.map_or(Expected::Absent, |id| Expected::Value(Target::Direct(id)));
let commit = Commit::builder(tree, author, message.as_ref())
.committer(committer.clone())
.parents(parent)
.build()?;
let prefix = if parent.is_some() {
b"commit: ".as_slice()
} else {
b"commit (initial): ".as_slice()
};
let message = [prefix, &commit.subject()].concat();
let id = self.loose.write_commit(&commit)?;
let edit = RefEdit::new(
RefName::HEAD,
Some(Target::Direct(id)),
expected,
Reflog::Append { committer, message },
);
let edit = edit.with_dereference(Dereference::Follow);
refs.transaction(&[edit])?;
Ok(id)
}
pub fn switch(&mut self, branch: &str) -> crate::Result<crate::checkout::Report> {
self.switch_with(branch, || {}, || {})
}
fn switch_with(
&mut self,
branch: &str,
after_prepare: impl FnOnce(),
after_checkout: impl FnOnce(),
) -> crate::Result<crate::checkout::Report> {
self.require_idle()?;
let branch_name = branch;
let branch = RefName::branch(branch_name)?;
let repository = &self.reader.repository;
let refs = repository.references()?;
let stored = refs.read(&RefName::HEAD)?;
let from = match &stored {
Some(Target::Symbolic(name)) => name
.as_bytes()
.strip_prefix(b"refs/heads/")
.unwrap_or(name.as_bytes())
.to_vec(),
Some(Target::Direct(id)) => id.to_string().into_bytes(),
None => Vec::new(),
};
let message = [
b"checkout: moving from ".as_slice(),
&from,
b" to ",
branch_name.as_bytes(),
]
.concat();
let committer = Signature::committer(repository.config(), &self.environment)?;
let expected = stored.map_or(Expected::Absent, Expected::Value);
let edit = RefEdit::new(
RefName::HEAD,
Some(Target::Symbolic(branch)),
expected,
Reflog::Append { committer, message },
)
.with_dereference(Dereference::Stored);
let prepared = refs.prepare_transaction(&[edit])?;
after_prepare();
let old = refs.resolve(&RefName::HEAD)?.id;
let baseline = old
.map(|id| self.reader.read_commit(id).map(|commit| commit.tree()))
.transpose()?;
let revision = format!("refs/heads/{branch_name}");
let target = self.reader.resolve(&revision, Some(ObjectKind::Commit))?;
let target_tree = self.reader.read_commit(target)?.tree();
let mut current = repository.filters(&self.environment, self.filter_commands)?;
let mut next = repository.filters_for_checkout(
&self.environment,
target_tree,
self.filter_commands,
)?;
let checkout = repository.checkout_tree_with_filters(
baseline,
Some(target_tree),
&mut current,
&mut next,
crate::checkout::Limits::default(),
&std::sync::atomic::AtomicBool::new(false),
)?;
after_checkout();
if let Err(source) = prepared.publish() {
return Err(Error::SwitchHead {
checkout: Box::new(checkout),
source: Box::new(source),
}
.into());
}
Ok(checkout)
}
fn require_idle(&self) -> crate::Result<()> {
if !self
.reader
.repository
.operation_state(Default::default())?
.is_empty()
{
return Err(Error::OperationInProgress.into());
}
Ok(())
}
pub fn fetch(&self, remote: &str) -> crate::Result<workflow::FetchOutcome> {
let configured = crate::remote::ConfiguredRemote::find(
self.reader.repository.config(),
remote.as_bytes(),
)?
.ok_or_else(|| Error::RemoteNotConfigured {
name: remote.to_owned(),
})?;
if configured.fetch_refspecs().is_empty() {
return Err(Error::NoFetchRefspecs {
name: remote.to_owned(),
}
.into());
}
let options = FetchOptions::new(configured.fetch_refspecs().iter().map(|r| r.to_bytes()));
Ok(workflow::fetch(
&self.reader.repository,
remote,
&options,
&self.environment,
&mut NoCallbacks,
)?)
}
pub fn push(
&self,
remote: &str,
commands: Vec<crate::push::PushCommand>,
) -> crate::Result<crate::push::PushReport> {
let identity = Signature::committer(self.reader.repository.config(), &self.environment)?;
let options = PushOptions::default().with_identity(identity);
Ok(workflow::push(
&self.reader.repository,
remote,
commands,
&options,
&self.environment,
&mut NoCallbacks,
)?)
}
}
#[cfg(all(test, any(target_os = "linux", target_os = "macos")))]
mod tests {
use super::*;
use crate::{InitKind, ObjectFormat};
#[test]
fn switch_locks_target_before_reading_and_retains_checkout_on_publication_failure() {
let dir = tempfile::tempdir().unwrap();
let repository =
Repository::init(ObjectFormat::Sha1, dir.path(), InitKind::Worktree).unwrap();
let environment = Environment::default()
.with("GIT_AUTHOR_NAME", "A")
.with("GIT_AUTHOR_EMAIL", "a@example.com")
.with("GIT_COMMITTER_NAME", "A")
.with("GIT_COMMITTER_EMAIL", "a@example.com");
let mut handle = Repo::new(repository).with_environment(environment);
let tree = handle.tree_editor().write().unwrap();
let root = handle.commit(tree, "root").unwrap();
handle
.repository()
.references()
.unwrap()
.transaction(&[RefEdit::create(
RefName::branch("topic").unwrap(),
root,
Reflog::Preserve,
)])
.unwrap();
let git_dir = handle.repository().git_dir().to_path_buf();
let error = handle
.switch_with(
"topic",
|| {
assert!(git_dir.join("HEAD.lock").exists());
assert!(git_dir.join("refs/heads/topic.lock").exists());
},
|| std::fs::remove_file(git_dir.join("HEAD.lock")).unwrap(),
)
.unwrap_err();
let Error::SwitchHead {
checkout,
source: _,
} = error.downcast::<Error>().unwrap()
else {
panic!("expected a post-checkout publication failure");
};
assert!(checkout.index_published);
assert!(handle.repository().git_dir().join("index").exists());
assert_eq!(
handle.repository().head().unwrap().name,
RefName::branch("main").unwrap()
);
}
}