Transfer git implementation
This commit is contained in:
parent
035f8b2872
commit
b1dd07aed1
11 changed files with 542 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
Cargo.lock
|
13
Cargo.toml
Normal file
13
Cargo.toml
Normal 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
22
src/errors.rs
Normal 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
50
src/git/blame.rs
Normal 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
47
src/git/diff.rs
Normal 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
75
src/git/log.rs
Normal 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
146
src/git/mod.rs
Normal 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
66
src/git/references.rs
Normal 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
31
src/git/repo.rs
Normal 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
82
src/git/tree.rs
Normal 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
8
src/main.rs
Normal 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();
|
||||
}
|
Loading…
Add table
Reference in a new issue