Transfer git implementation

This commit is contained in:
Vladan Popovic 2020-06-23 01:05:31 +02:00
parent 035f8b2872
commit b1dd07aed1
11 changed files with 542 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
Cargo.lock

13
Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "git-social-server"
version = "0.1.0"
authors = ["Vladan Popovic <vladanovic@gmail.com>"]
edition = "2018"
[dependencies]
chrono = { version = "*" }
[dependencies.git2]
version = "*"
default-features = false
features = []

22
src/errors.rs Normal file
View File

@ -0,0 +1,22 @@
use git2::Error as GitError;
#[derive(Debug)]
pub struct SocialError {
pub message: String,
}
impl From<GitError> 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(),
}
}
}

50
src/git/blame.rs Normal file
View File

@ -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<SocialBlameHunk> = 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<SocialBlame, SocialError>;
}
impl GitBlame for SocialRepo {
fn blame(&self, from_oid: &str, path: &str) -> Result<SocialBlame, SocialError> {
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())
}
}

47
src/git/diff.rs Normal file
View File

@ -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<String> = 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<SocialDiff, SocialError>;
}
impl Show for SocialRepo {
fn show(&self, from_oid: &str, to_oid: &str) -> Result<SocialDiff, SocialError> {
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())
}
}

75
src/git/log.rs Normal file
View File

@ -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<String>,
end: Option<String>,
count: usize,
short: bool,
) -> Result<Box<dyn Iterator<Item = SocialCommit> + 'a>, SocialError>;
fn commit(&self, rev: &str) -> Result<SocialCommit, SocialError>;
fn first_commit(&self) -> Result<SocialCommit, SocialError>;
}
impl Log for SocialRepo {
fn log<'a>(
&'a self,
start: Option<String>,
end: Option<String>,
count: usize,
short: bool,
) -> Result<Box<dyn Iterator<Item = SocialCommit> + '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<SocialCommit, SocialError> {
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<SocialCommit, SocialError> {
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(),
})?
}
}

146
src/git/mod.rs Normal file
View File

@ -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<Local>,
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<Local>,
pub message: String,
pub author: SocialSignature,
pub committer: SocialSignature,
pub diff: Option<SocialDiff>,
pub parent_ids: Option<Vec<String>>, // 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<Signature<'_>> 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<Commit<'_>> 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<String>,
}
#[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<String>,
}
#[derive(Clone, Debug, Default)]
pub struct SocialRefs {
pub branches: Vec<SocialBranch>,
pub tags: Vec<SocialTag>,
}
#[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<SocialBlameHunk>,
}
#[derive(Clone, Debug, Default)]
pub struct SocialRepoInfo {
pub name: String,
pub author: SocialSignature,
pub description: String,
}

66
src/git/references.rs Normal file
View File

@ -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<Vec<SocialBranch>, SocialError>;
fn tags(&self) -> Result<Vec<SocialTag>, SocialError>;
}
impl Refs for SocialRepo {
fn branches(&self) -> Result<Vec<SocialBranch>, SocialError> {
let all_branches = self.repo.branches(Some(BranchType::Local))?;
let mut resulting_brances: Vec<SocialBranch> = 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<Vec<SocialTag>, SocialError> {
let tagnames = self.repo.tag_names(None)?;
let mut result: Vec<SocialTag> = 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)
}
}

31
src/git/repo.rs Normal file
View File

@ -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<Self, SocialError> {
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<Tree, GitError> {
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())
}
}

82
src/git/tree.rs Normal file
View File

@ -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<Vec<SocialTreeEntry>, SocialError>;
fn show_file(&self, rev: &str, path: &Path) -> Result<SocialFileContent, SocialError>;
}
impl LsTree for SocialRepo {
fn ls_tree(&self, rev: &str, path: &Path) -> Result<Vec<SocialTreeEntry>, 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<SocialTreeEntry> = vec![];
let mut files: Vec<SocialTreeEntry> = 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<SocialFileContent, SocialError> {
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())
}
}

8
src/main.rs Normal file
View File

@ -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();
}