use super::{RhaiResult, io_err, git_err};
use crate::paint::{Style, Green, Red, Cyan};
use crate::ref_cell::{RefCell, Ref};
use std::{ops, fs};
use std::rc::Rc;
use std::str::from_utf8;
use std::path::{Path, PathBuf};
use git2::{
Repository, DiffFormat, DiffLine, ApplyLocation,
IndexAddOption, build::CheckoutBuilder,
Oid, ResetType
};
use rhai::{Engine};
struct Inner {
repo: RefCell<Repository>,
path: PathBuf
}
#[derive(Clone)]
pub struct Git {
inner: Rc<Inner>
}
impl Git {
pub fn new(path: &str) -> RhaiResult<Self> {
Ok(Self::from_repo(
path,
Repository::discover(path)
.map_err(git_err)?
))
}
fn from_repo(path: &str, repo: Repository) -> Self {
Self {
inner: Rc::new(Inner {
repo: repo.into(),
path: path.into()
})
}
}
#[allow(dead_code)]
fn root(&self) -> PathBuf {
self.inner.path.clone()
}
pub fn clone(url: &str, path: &str) -> RhaiResult<Self> {
paint_act!("git clone: {:?} into: {:?}", url, path);
if Path::is_dir(path.as_ref()) {
return Self::new(path)
}
Ok(Self::from_repo(
path,
Repository::clone(url, path)
.map_err(git_err)?
))
}
pub fn diff(&mut self) -> Diff {
Diff { inner: self.clone() }
}
fn apply_diff<D>(&mut self, diff: D) -> RhaiResult<()>
where D: RawDiff {
let diff = diff.raw_diff()?;
self.inner.repo.borrow()
.apply(&*diff, ApplyLocation::WorkDir, None)
.map_err(git_err)?;
Ok(())
}
pub fn force_head(&mut self) -> RhaiResult<()> {
let repo = self.inner.repo.borrow();
let mut ops = CheckoutBuilder::new();
ops.force()
.remove_untracked(true);
repo.checkout_head(Some(&mut ops))
.map_err(git_err)
}
fn find_tag(&mut self, tag: &str) -> RhaiResult<Oid> {
let tag = format!("refs/tags/{}", tag);
let mut id = None;
self.inner.repo.borrow()
.tag_foreach(|tag_id, name| {
let name = from_utf8(name).ok();
if matches!(name, Some(n) if n == tag) {
id = Some(tag_id);
false
} else {
true
}
}).map_err(git_err)?;
id.ok_or(err!("tag not found {}", tag))
}
pub fn checkout_tag(&mut self, tag: &str) -> RhaiResult<()> {
paint_act!("checkout tag {:?}", tag);
let tag_id = self.find_tag(tag)?;
let repo = self.inner.repo.borrow();
let obj = repo.find_object(tag_id, None)
.map_err(git_err)?;
let mut opts = CheckoutBuilder::new();
opts.force()
.remove_untracked(true);
repo.reset(&obj, ResetType::Hard, Some(&mut opts))
.map_err(git_err)
}
}
#[derive(Clone)]
pub struct Diff {
inner: Git
}
impl Diff {
fn compute<'a>(&'a self) -> RhaiResult<Ref<'a, git2::Diff<'a>>> {
let repo = self.inner.inner.repo.borrow();
Ref::transpose(unsafe {Ref::map(repo, |repo| {
let mut index = repo.index()
.map_err(git_err)?;
index.add_all(["*"].iter(), IndexAddOption::DEFAULT, None)
.map_err(git_err)?;
let head = repo.head().map_err(git_err)?;
let tree = head.peel_to_tree().map_err(git_err)?;
repo.diff_tree_to_index(
Some(&tree),
Some(&index),
None
).map_err(git_err)
})})
}
fn print(&mut self) -> RhaiResult<()> {
let diff = self.compute()?;
diff.print(DiffFormat::Patch, |_delta, _hunk, line| {
let col = line_color(&line);
print!("{}", col.prefix());
match line.origin() {
'+' | '-' | ' ' => print!("{}", line.origin()),
_ => {}
}
print!("{}", from_utf8(line.content()).unwrap());
print!("{}", col.suffix());
true
}).map_err(git_err)?;
Ok(())
}
fn to_string(&mut self) -> RhaiResult<String> {
let diff = self.compute()?;
let mut s = String::new();
diff.print(DiffFormat::Patch, |_delta, _hunk, line| {
match line.origin() {
'+' | '-' | ' ' => s.push(line.origin()),
_ => {}
}
s.push_str(from_utf8(line.content()).unwrap());
true
}).map_err(git_err)?;
Ok(s)
}
fn to_file(&mut self, file: &str) -> RhaiResult<()> {
let s = self.to_string()?;
fs::write(file, s)
.map_err(io_err)
}
}
impl RawDiff for Diff {
fn raw_diff(&self) -> RhaiResult<MaybeRef<'_, git2::Diff<'_>>> {
let diff = self.compute()?;
Ok(MaybeRef::Has(diff))
}
}
#[derive(Clone)]
pub struct DiffInFile {
inner: Rc<git2::Diff<'static>>
}
impl DiffInFile {
fn from_file(s: &str) -> RhaiResult<Self> {
let b = fs::read_to_string(s)
.map_err(io_err)?;
let diff = git2::Diff::from_buffer(b.as_bytes())
.map_err(git_err)?;
Ok(Self {
inner: Rc::new(diff)
})
}
}
impl RawDiff for DiffInFile {
fn raw_diff(&self) -> RhaiResult<MaybeRef<'_, git2::Diff<'_>>> {
Ok(MaybeRef::Borrowed(&*self.inner))
}
}
trait RawDiff {
fn raw_diff(&self) -> RhaiResult<MaybeRef<'_, git2::Diff<'_>>>;
}
enum MaybeRef<'a, T: 'a> {
Has(Ref<'a, T>),
Borrowed(&'a T)
}
impl<'a, T> ops::Deref for MaybeRef<'a, T> {
type Target = T;
fn deref(&self) -> &T {
match self {
Self::Has(r) => &*r,
Self::Borrowed(t) => t
}
}
}
fn line_color(line: &DiffLine) -> Style {
match line.origin() {
'+' => Green.normal(),
'-' => Red.normal(),
'F' => Style::new().bold(),
'H' => Cyan.normal(),
_ => Style::new()
}
}
pub fn add(engine: &mut Engine) {
engine
.register_fn("git", Git::new)
.register_fn("git_clone", Git::clone)
.register_fn("diff", Git::diff)
.register_fn("apply_diff", Git::apply_diff::<DiffInFile>)
.register_fn("apply_diff", Git::apply_diff::<Diff>)
.register_fn("force_head", Git::force_head)
.register_fn("checkout_tag", Git::checkout_tag)
.register_fn("print", Diff::print)
.register_fn("to_file", Diff::to_file)
.register_fn("to_string", Diff::to_string)
.register_fn("diff_from_file", DiffInFile::from_file);
}