use std::{fmt, path::Path};
use crate::{
diff,
file::{self, File},
Error, Result,
};
#[allow(unused)]
use tracing::{debug, error, info, instrument, span, warn};
pub struct Repo {
pub(crate) internal: Internal,
history: undo::History<Change>,
}
impl Repo {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let internal = Internal::open(path)?;
let history = undo::History::new();
Ok(Self { internal, history })
}
pub fn can_undo(&self) -> bool {
self.history.can_undo()
}
pub fn can_redo(&self) -> bool {
self.history.can_redo()
}
pub fn undo(&mut self) -> Result<()> {
self.history
.undo(&mut self.internal)
.ok_or(Error::UndoEmpty)
.flatten()
}
pub fn redo(&mut self) -> Result<()> {
self.history
.redo(&mut self.internal)
.ok_or(Error::RedoEmpty)
.flatten()
}
pub fn path(&self) -> &Path {
self.internal.path()
}
pub fn uncommitted_files(&self) -> Result<Vec<diff::Meta>> {
self.internal.uncommitted_files()
}
pub fn diff_details(&self, diff: &diff::Meta) -> Result<diff::Details> {
self.internal.diff_details(diff)
}
pub fn stage_file(&mut self, file: File) -> Result<()> {
self.apply(Change::StageFile(file))
}
pub fn unstage_file(&mut self, file: File) -> Result<()> {
self.apply(Change::UnstageFile(file))
}
pub fn stage_contents(&mut self, file: File, contents: file::Contents) -> Result<()> {
self.apply(Change::StageContents(file, contents))
}
fn apply(&mut self, change: Change) -> Result<()> {
self.history.apply(&mut self.internal, change)
}
}
impl fmt::Debug for Repo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let history = format!("{}", self.history.display());
f.debug_struct("Repo")
.field("internal", &self.internal)
.field("history", &history)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone)]
enum Change {
StageFile(File),
UnstageFile(File),
StageContents(File, file::Contents),
}
impl undo::Action for Change {
type Target = Internal;
type Output = ();
type Error = Error;
fn apply(&mut self, target: &mut Self::Target) -> undo::Result<Self> {
match self {
Change::StageFile(file) => target.stage_file(file),
Change::UnstageFile(file) => target.unstage_file(file),
Change::StageContents(file, contents) => target.stage_contents(file, contents),
}
}
fn undo(&mut self, target: &mut Self::Target) -> undo::Result<Self> {
match self {
Change::StageFile(file) => target.unstage_file(file),
Change::UnstageFile(file) => target.stage_file(file),
Change::StageContents(file, contents) => todo!(),
}
}
}
impl fmt::Display for Change {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(self, f)
}
}
pub(crate) struct Internal {
git: git2::Repository,
}
impl Internal {
fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let git = git2::Repository::open(path)?;
Ok(Self { git })
}
pub(crate) fn path(&self) -> &Path {
let path = self.git.path();
if path.ends_with(".git") {
path.parent().expect("If non-bare has parent")
} else {
path
}
}
fn head_assuming_born(&self) -> std::result::Result<git2::Tree, git2::Error> {
self.git.head()?.peel_to_commit()?.tree()
}
fn head(&self) -> Result<Option<git2::Tree>> {
match self.head_assuming_born() {
Ok(head) => Ok(Some(head)),
Err(err) if err.code() == git2::ErrorCode::UnbornBranch => Ok(None),
Err(err) => Err(err.into()),
}
}
fn uncommitted_files(&self) -> Result<Vec<diff::Meta>> {
let head = self.head()?;
let mut opts = Self::uncommitted_opts();
opts.include_unmodified(true); opts.recurse_untracked_dirs(true);
let mut diff = self
.git
.diff_tree_to_workdir_with_index(head.as_ref(), Some(&mut opts))?;
let mut opts = git2::DiffFindOptions::new();
opts.renames(true)
.copies(true)
.copies_from_unmodified(true)
.for_untracked(true)
.ignore_whitespace(true)
.break_rewrites_for_renames_only(true)
.remove_unmodified(true);
diff.find_similar(Some(&mut opts))?;
diff.deltas()
.map(|delta| diff::Meta::from_git2(&delta))
.collect()
}
fn diff_details(&self, meta: &diff::Meta) -> Result<diff::Details> {
match meta {
crate::Meta::Added(f)
| crate::Meta::Deleted(f)
| crate::Meta::Modified { new: f, .. }
| crate::Meta::Renamed { new: f, .. }
| crate::Meta::Copied { new: f, .. }
| crate::Meta::Ignored(f)
| crate::Meta::Untracked(f)
| crate::Meta::Typechange { new: f, .. }
| crate::Meta::Unreadable(f)
| crate::Meta::Conflicted { new: f, .. } => self._diff_details(f),
}
}
fn _diff_details(&self, file: &File) -> Result<diff::Details> {
let head = self.head()?;
let mut opts = Self::uncommitted_opts();
opts.pathspec(file.rel_path());
let mut meta: Option<Result<diff::Meta>> = None;
let mut file_cb = |delta: git2::DiffDelta<'_>, _progress| {
if let Some(delta_path) = Self::delta_path(&delta) {
if delta_path == file.rel_path() {
meta = Some(diff::Meta::from_git2(&delta));
return true;
}
}
meta.is_none()
};
let mut lines = vec![];
let mut line_cb = |delta: git2::DiffDelta<'_>,
_hunk: Option<git2::DiffHunk<'_>>,
line: git2::DiffLine<'_>| {
if let Some(delta_path) = Self::delta_path(&delta) {
if delta_path == file.rel_path() {
let line = diff::Line::from_git2(&line);
lines.push(line);
}
}
true
};
match self
.git
.diff_tree_to_workdir_with_index(head.as_ref(), Some(&mut opts))?
.foreach(&mut file_cb, None, None, Some(&mut line_cb))
{
Ok(()) => (),
Err(err) if err.code() == git2::ErrorCode::User => (),
Err(err) => return Err(err.into()),
}
let meta = meta
.ok_or_else(|| Error::PathNotFound(file.clone()))
.flatten()?;
Ok(diff::Details::new(meta, lines))
}
fn delta_path<'a, 'b>(delta: &'a git2::DiffDelta<'b>) -> Option<&'b Path> {
delta
.new_file()
.path()
.map_or_else(|| delta.old_file().path(), |delta_path| Some(delta_path))
}
fn uncommitted_opts() -> git2::DiffOptions {
let mut opts = git2::DiffOptions::new();
opts.include_untracked(true)
.include_typechange(true)
.include_unmodified(false)
.include_unreadable(true)
.include_untracked(true)
.include_ignored(true);
opts
}
fn stage_file(&mut self, file: &File) -> Result<()> {
self.ensure_not_ignored(file)?;
self.git.index()?.add_path(file.rel_path())?;
Ok(())
}
fn unstage_file(&mut self, file: &File) -> Result<()> {
self.ensure_not_ignored(file)?;
self.git.index()?.remove_path(file.rel_path())?;
Ok(())
}
fn stage_contents(&self, file: &File, contents: &file::Contents) -> Result<()> {
self.ensure_not_ignored(file)?;
let entry = git2::IndexEntry {
ctime: git2::IndexTime::new(0, 0),
mtime: git2::IndexTime::new(0, 0),
dev: 0,
ino: 0,
mode: libgit2_sys::GIT_FILEMODE_BLOB,
uid: 0,
gid: 0,
file_size: 0,
id: git2::Oid::zero(),
flags: 0,
flags_extended: 0,
path: file.rel_path_bytes()?.into_bytes(),
};
let mut index = self.git.index()?;
index.add_frombuffer(&entry, contents.buffer())?;
index.write()?;
Ok(())
}
fn ensure_not_ignored(&self, file: &File) -> Result<()> {
if self.git.status_should_ignore(file.rel_path())? {
return Err(Error::Ignored(file.clone()));
}
Ok(())
}
}
impl fmt::Debug for Internal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RepoInternal")
.field("path", &self.git.path())
.finish_non_exhaustive()
}
}