From b1dd07aed1eac098cfa65d6011ade6d297f95a18 Mon Sep 17 00:00:00 2001 From: Vladan Popovic Date: Tue, 23 Jun 2020 01:05:31 +0200 Subject: [PATCH] Transfer git implementation --- .gitignore | 2 + Cargo.toml | 13 ++++ src/errors.rs | 22 +++++++ src/git/blame.rs | 50 +++++++++++++++ src/git/diff.rs | 47 ++++++++++++++ src/git/log.rs | 75 ++++++++++++++++++++++ src/git/mod.rs | 146 ++++++++++++++++++++++++++++++++++++++++++ src/git/references.rs | 66 +++++++++++++++++++ src/git/repo.rs | 31 +++++++++ src/git/tree.rs | 82 ++++++++++++++++++++++++ src/main.rs | 8 +++ 11 files changed, 542 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/errors.rs create mode 100644 src/git/blame.rs create mode 100644 src/git/diff.rs create mode 100644 src/git/log.rs create mode 100644 src/git/mod.rs create mode 100644 src/git/references.rs create mode 100644 src/git/repo.rs create mode 100644 src/git/tree.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a1f06c4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "git-social-server" +version = "0.1.0" +authors = ["Vladan Popovic "] +edition = "2018" + +[dependencies] +chrono = { version = "*" } + +[dependencies.git2] +version = "*" +default-features = false +features = [] diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..e2e5e04 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,22 @@ +use git2::Error as GitError; + +#[derive(Debug)] +pub struct SocialError { + pub message: String, +} + +impl From for SocialError { + fn from(err: GitError) -> Self { + Self { + message: err.message().to_owned(), + } + } +} + +impl From<&str> for SocialError { + fn from(err: &str) -> Self { + Self { + message: err.to_owned(), + } + } +} diff --git a/src/git/blame.rs b/src/git/blame.rs new file mode 100644 index 0000000..caacab0 --- /dev/null +++ b/src/git/blame.rs @@ -0,0 +1,50 @@ +use crate::errors::SocialError; +use crate::git::repo::SocialRepo; +use crate::git::{SocialBlame, SocialBlameHunk}; +use git2::{Blame, BlameHunk, BlameOptions}; +use std::io::{BufRead, BufReader}; + +fn into_social_blame_hunk(bh: BlameHunk, line: String) -> SocialBlameHunk { + SocialBlameHunk { + signature: bh.final_signature().into(), + commit_id: format!("{}", bh.orig_commit_id()), + line, + } +} + +fn into_social_blame(file: String, blame: Blame<'_>, buffer: BufReader<&[u8]>) -> SocialBlame { + let mut hunks: Vec = Vec::new(); + for (i, line) in buffer.lines().enumerate() { + if let (Ok(line), Some(hunk)) = (line, blame.get_line(i + 1)) { + let shunk = into_social_blame_hunk(hunk, line); + hunks.push(shunk); + } + } + SocialBlame { file, hunks } +} + +pub trait GitBlame { + fn blame(&self, from_oid: &str, path: &str) -> Result; +} + +impl GitBlame for SocialRepo { + fn blame(&self, from_oid: &str, path: &str) -> Result { + let revspec = self.repo.revparse_single(from_oid)?; + let commit_id = revspec.id(); + + let spec = format!("{}:{}", commit_id, path); + let object = self.repo.revparse_single(&spec[..])?; + let blob = self.repo.find_blob(object.id())?; + let reader = BufReader::new(blob.content()); + + let mut opts = BlameOptions::new(); + opts.track_copies_any_commit_copies(true) + .track_copies_same_commit_copies(true) + .newest_commit(revspec.id()); + + self.repo + .blame_file(std::path::Path::new(path), Some(&mut opts)) + .map(|blame| into_social_blame(path.to_owned(), blame, reader)) + .map_err(|d| d.into()) + } +} diff --git a/src/git/diff.rs b/src/git/diff.rs new file mode 100644 index 0000000..ef7e7c2 --- /dev/null +++ b/src/git/diff.rs @@ -0,0 +1,47 @@ +use crate::errors::SocialError; +use crate::git::repo::SocialRepo; +use crate::git::SocialDiff; +use git2::{Diff, DiffOptions}; +use git2::{DiffFormat, DiffLine}; + +fn into_social_diff(d: Diff<'_>) -> SocialDiff { + let mut lines: Vec = Vec::new(); + d.print(DiffFormat::Patch, |_, _, line: DiffLine| { + if let Ok(l) = String::from_utf8(line.content().to_vec()) { + lines.push(format!("{} {}", line.origin(), l)); + } else { + lines.push("? cannot convert utf8 string on this line!".to_string()); + } + true + }) + .unwrap_or(()); + SocialDiff { lines } +} + +pub trait Show { + fn show(&self, from_oid: &str, to_oid: &str) -> Result; +} + +impl Show for SocialRepo { + fn show(&self, from_oid: &str, to_oid: &str) -> Result { + let mut opts = DiffOptions::new(); + opts.reverse(false) + .force_text(false) + .ignore_whitespace_eol(false) + .ignore_whitespace_change(false) + .ignore_whitespace(false) + .include_ignored(false) + .include_untracked(false) + .patience(true) + .context_lines(5) + .minimal(false); + + let t1 = self.get_tree(from_oid).ok(); + let t2 = self.get_tree(to_oid).ok(); + + self.repo + .diff_tree_to_tree(t1.as_ref(), t2.as_ref(), Some(&mut opts)) + .map(into_social_diff) + .map_err(|d| d.into()) + } +} diff --git a/src/git/log.rs b/src/git/log.rs new file mode 100644 index 0000000..9e03c16 --- /dev/null +++ b/src/git/log.rs @@ -0,0 +1,75 @@ +use crate::errors::SocialError; +use crate::git::repo::SocialRepo; +use crate::git::SocialCommit; + +pub trait Log { + fn log<'a>( + &'a self, + start: Option, + end: Option, + count: usize, + short: bool, + ) -> Result + 'a>, SocialError>; + + fn commit(&self, rev: &str) -> Result; + fn first_commit(&self) -> Result; +} + +impl Log for SocialRepo { + fn log<'a>( + &'a self, + start: Option, + end: Option, + count: usize, + short: bool, + ) -> Result + 'a>, SocialError> { + let mut git_revwalk = self.repo.revwalk()?; + let start = start.or_else(|| Some("HEAD".to_owned())); + + if let (Some(s), Some(e)) = (&start, &end) { + git_revwalk.push_range(&format!("{}..{}", s, e))?; + } else if let Some(s) = start { + git_revwalk + .push_ref(&s) + .or(git_revwalk.push(self.repo.revparse(&s)?.from().unwrap().id()))?; + } else { + git_revwalk.push_head()?; + } + + Ok(Box::new( + git_revwalk + .filter_map(move |idres| idres.map(|id| self.repo.find_commit(id.clone())).ok()) + .flatten() + .map(|c| c.into()) + .map(move |c: SocialCommit| if short { c.oneline() } else { c }) + .take(count), + )) + } + + fn commit(&self, rev: &str) -> Result { + self.repo + .revparse_single(rev.as_ref()) + .and_then(|o| self.repo.find_commit(o.id())) + .map(|c| c.into()) + .map_err(|e| e.into()) + } + + fn first_commit(&self) -> Result { + let mut revwalk = self.repo.revwalk()?; + revwalk.set_sorting(git2::Sort::REVERSE | git2::Sort::TOPOLOGICAL)?; + revwalk.push_head()?; + revwalk + .filter_map(move |idres| idres.map(|id| self.repo.find_commit(id.clone())).ok()) + .take(1) + .next() + .map(|r| { + r.map_err(|_| SocialError { + message: "Invalid commit reference!".to_string(), + }) + .map(|c| c.into()) + }) + .ok_or(SocialError { + message: "Repo doesn't contain any commits!".to_owned(), + })? + } +} diff --git a/src/git/mod.rs b/src/git/mod.rs new file mode 100644 index 0000000..14b8698 --- /dev/null +++ b/src/git/mod.rs @@ -0,0 +1,146 @@ +pub(crate) mod blame; +pub(crate) mod diff; +pub(crate) mod log; +pub(crate) mod references; +pub(crate) mod repo; +pub(crate) mod tree; + +use chrono::offset::{Local, TimeZone}; +use chrono::DateTime; +use git2::{Commit, Signature}; + +#[derive(Clone, Debug)] +pub struct SocialSignature { + pub when: DateTime, + pub name: String, + pub email: String, +} + +impl Default for SocialSignature { + fn default() -> Self { + Self { + when: Local.timestamp(0, 0), + name: "".to_string(), + email: "".to_string(), + } + } +} + +#[derive(Clone, Debug)] +pub struct SocialCommit { + pub id: String, + pub time: DateTime, + pub message: String, + pub author: SocialSignature, + pub committer: SocialSignature, + pub diff: Option, + pub parent_ids: Option>, // TODO: Use iterator instead of vec. +} + +impl SocialCommit { + pub fn oneline(self) -> Self { + Self { + message: self.message.lines().next().unwrap_or("").to_owned(), + parent_ids: None, + diff: None, + ..self + } + } +} + +impl Default for SocialCommit { + fn default() -> Self { + Self { + id: "".to_string(), + time: Local.timestamp(0, 0), + message: "".to_string(), + author: SocialSignature::default(), + committer: SocialSignature::default(), + diff: None, + parent_ids: None, + } + } +} + +impl From> for SocialSignature { + fn from(s: Signature<'_>) -> Self { + Self { + name: s.name().unwrap_or("").to_owned(), + email: s.email().unwrap_or("").to_owned(), + when: Local.timestamp(s.when().seconds(), 0), + } + } +} + +impl From> for SocialCommit { + fn from(c: Commit<'_>) -> Self { + Self { + id: format!("{}", c.id()), + time: Local.timestamp(c.time().seconds(), 0), + message: c.message().unwrap_or("").to_owned(), + author: c.author().into(), + committer: c.committer().into(), + diff: None, + parent_ids: None, + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct SocialDiff { + pub lines: Vec, +} + +#[derive(Clone, Debug, Default)] +pub struct SocialTreeEntry { + pub id: String, + pub name: String, + pub kind: String, + pub filemode: i32, + pub size: usize, +} + +#[derive(Clone, Debug, Default)] +pub struct SocialFileContent { + pub path: String, + pub content: String, +} + +#[derive(Clone, Debug, Default)] +pub struct SocialBranch { + pub name: String, + pub head: SocialCommit, +} + +#[derive(Clone, Debug, Default)] +pub struct SocialTag { + pub name: String, + pub head: SocialCommit, + pub annotation: Option, +} + +#[derive(Clone, Debug, Default)] +pub struct SocialRefs { + pub branches: Vec, + pub tags: Vec, +} + +#[derive(Clone, Debug, Default)] +pub struct SocialBlameHunk { + pub commit_id: String, + pub line: String, + pub signature: SocialSignature, +} + +#[derive(Clone, Debug, Default)] +pub struct SocialBlame { + pub file: String, + pub hunks: Vec, +} + +#[derive(Clone, Debug, Default)] +pub struct SocialRepoInfo { + pub name: String, + pub author: SocialSignature, + pub description: String, +} diff --git a/src/git/references.rs b/src/git/references.rs new file mode 100644 index 0000000..e821f07 --- /dev/null +++ b/src/git/references.rs @@ -0,0 +1,66 @@ +use crate::errors::SocialError; +use crate::git::repo::SocialRepo; +use crate::git::{SocialBranch, SocialCommit, SocialTag}; +use git2::{Branch, BranchType, Commit}; + +fn into_social_branch(b: Branch<'_>) -> SocialBranch { + SocialBranch { + name: b.name().unwrap_or(Some("")).unwrap_or("").to_owned(), + head: SocialCommit::default(), + } +} + +fn into_social_tag(name: Option<&str>) -> SocialTag { + SocialTag { + name: name.unwrap_or("").to_owned(), + head: SocialCommit::default(), + annotation: None, + } +} + +pub trait Refs { + fn branches(&self) -> Result, SocialError>; + fn tags(&self) -> Result, SocialError>; +} + +impl Refs for SocialRepo { + fn branches(&self) -> Result, SocialError> { + let all_branches = self.repo.branches(Some(BranchType::Local))?; + let mut resulting_brances: Vec = vec![]; + + for r in all_branches { + r.map(|(branch, _)| { + let mut sb = into_social_branch(branch); + self.repo + .revparse_single(&sb.name) + .map(|obj| { + obj.peel_to_commit() + .map(|c: Commit| { + sb.head = c.into(); + resulting_brances.push(sb); + }) + .unwrap_or(()); + }) + .unwrap_or(()); + }) + .unwrap_or(()); + } + Ok(resulting_brances) + } + + fn tags(&self) -> Result, SocialError> { + let tagnames = self.repo.tag_names(None)?; + let mut result: Vec = vec![]; + + for t in tagnames.into_iter() { + let mut st = into_social_tag(t); + let _ = self.repo.revparse_single(&st.name).map(|obj| { + obj.peel_to_commit().map(|c: Commit| { + st.head = c.into(); + result.push(st); + }) + }); + } + Ok(result) + } +} diff --git a/src/git/repo.rs b/src/git/repo.rs new file mode 100644 index 0000000..e4be036 --- /dev/null +++ b/src/git/repo.rs @@ -0,0 +1,31 @@ +use crate::errors::SocialError; +use git2::{Error as GitError, Repository, Tree}; +use std::{fs::read_to_string, path::Path}; + +pub struct SocialRepo { + pub repo: Repository, +} + +impl<'repo> SocialRepo { + pub fn new(path: &str) -> Result { + Repository::open(path) + .map(|repo| Self { repo }) + .map_err(|_| { + let err = "fatal: not a git repository"; + SocialError::from(err) + }) + } + + pub fn get_tree(&self, rev: &str) -> Result { + self.repo + .revparse_single(rev) + .and_then(|obj| obj.peel_to_tree()) + } + + pub fn get_description(&self) -> String { + // The description is in a file named "description", + // in the .git directory, or the root directory if bare. + let git_path = self.repo.path().join(Path::new("description")); + read_to_string(git_path).unwrap_or("".to_string()) + } +} diff --git a/src/git/tree.rs b/src/git/tree.rs new file mode 100644 index 0000000..0056c27 --- /dev/null +++ b/src/git/tree.rs @@ -0,0 +1,82 @@ +use crate::errors::SocialError; +use crate::git::repo::SocialRepo; +use crate::git::{SocialFileContent, SocialTreeEntry}; +use git2::{Blob, Error as GitError, Object, ObjectType, TreeEntry}; +use std::path::Path; + +fn into_social_tree_entry(te: TreeEntry<'_>, size: usize) -> SocialTreeEntry { + SocialTreeEntry { + id: format!("{}", te.id()), + name: te.name().unwrap_or("").to_owned(), + kind: te.kind().map(|ot| ot.str()).unwrap_or("any").to_owned(), + filemode: te.filemode(), + size, + } +} + +pub trait LsTree { + fn ls_tree(&self, rev: &str, path: &Path) -> Result, SocialError>; + fn show_file(&self, rev: &str, path: &Path) -> Result; +} + +impl LsTree for SocialRepo { + fn ls_tree(&self, rev: &str, path: &Path) -> Result, SocialError> { + let matched_tree = if path.to_str().unwrap_or("") == "" { + self.get_tree(rev)? + } else { + self.get_tree(rev) + .and_then(|t| t.get_path(&path)) + .and_then(|entry: TreeEntry| self.repo.find_tree(entry.id()))? + }; + + let mut result: Vec = vec![]; + let mut files: Vec = vec![]; + for entry in matched_tree.iter() { + match entry.kind() { + Some(git2::ObjectType::Blob) => { + self.repo + .revparse_single(&format!("{}", entry.id())) + .map(|obj: Object| { + obj.as_blob().map(|b: &Blob| { + files.push(into_social_tree_entry(entry, b.content().len())) + }); + }) + .unwrap_or(()); + } + Some(git2::ObjectType::Tree) => result.push(into_social_tree_entry(entry, 0)), + _ => (), + } + } + result.extend(files); + Ok(result) + } + + fn show_file(&self, rev: &str, path: &Path) -> Result { + self.get_tree(rev) + .and_then(|t| t.get_path(&path)) + .and_then(|entry: TreeEntry| match entry.kind() { + Some(ObjectType::Blob) => self + .repo + .revparse_single(&format!("{}", entry.id())) + .and_then(|obj: Object| { + obj.as_blob() + .ok_or_else(|| { + GitError::from_str(&format!("Cannot get blob for {}", obj.id())) + }) + .and_then(|blob: &Blob| { + String::from_utf8(blob.content().to_vec()).map_err(|_| { + GitError::from_str( + format!("Cannot read file {}", path.display()).as_ref(), + ) + }) + }) + .map(|content| SocialFileContent { + content, + path: format!("{}", path.display()), + }) + }), + _ => Result::Err(GitError::from_str("Object is not a Blob!")), + }) + .map_err(|e| e.into()) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..86d3328 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,8 @@ +mod errors; +mod git; + +use git::repo::SocialRepo; + +fn main() { + SocialRepo::new("/home/vladan/dev/git.social/git-social-server").ok(); +}