175 lines
5.7 KiB
Rust
175 lines
5.7 KiB
Rust
use anyhow::Result;
|
|
use git2::{DiffLine, TreeWalkMode, TreeWalkResult};
|
|
use serde::Serialize;
|
|
use std::{collections::HashMap, path::Path};
|
|
use time::{
|
|
format_description::well_known::{Iso8601, Rfc2822},
|
|
OffsetDateTime,
|
|
};
|
|
|
|
use super::ThreadSafeRepository;
|
|
|
|
#[derive(Serialize, Clone)]
|
|
pub struct LineChange {
|
|
pub op: char,
|
|
pub old_line: Option<u32>,
|
|
pub new_line: Option<u32>,
|
|
pub content: String,
|
|
}
|
|
|
|
#[derive(Serialize, Clone)]
|
|
pub struct CommitInfo {
|
|
pub oid: String,
|
|
pub message: String,
|
|
pub author: String,
|
|
pub date: String,
|
|
pub changes: HashMap<String, Vec<LineChange>>,
|
|
}
|
|
|
|
impl ThreadSafeRepository {
|
|
pub fn get_history(&self, path: &str) -> Result<Vec<CommitInfo>> {
|
|
let repository = self.repository()?;
|
|
|
|
let mut revwalk = repository.revwalk()?;
|
|
revwalk.push_head()?;
|
|
revwalk.set_sorting(git2::Sort::TIME | git2::Sort::REVERSE)?;
|
|
|
|
let mut history = Vec::new();
|
|
let mut last_blobs = HashMap::new();
|
|
for oid in revwalk {
|
|
// Get commit
|
|
let oid = oid?;
|
|
let commit = repository.find_commit(oid)?;
|
|
let tree = commit.tree()?;
|
|
|
|
let mut changed = vec![];
|
|
|
|
// Check if any file we care about was modified
|
|
tree.walk(TreeWalkMode::PreOrder, |val, entry| {
|
|
// We're at root, only traverse the path we care about
|
|
if val.is_empty() {
|
|
if entry.name().unwrap_or_default() == path {
|
|
TreeWalkResult::Ok
|
|
} else {
|
|
TreeWalkResult::Skip
|
|
}
|
|
} else {
|
|
// Track files, skip directories
|
|
if entry.kind() == Some(git2::ObjectType::Blob) {
|
|
let file_path = entry.name().unwrap_or_default().to_string();
|
|
|
|
// Check if blob exists
|
|
match last_blobs.get(&file_path) {
|
|
Some(blob_id) => {
|
|
if blob_id != &entry.id() {
|
|
changed.push((file_path, entry.id()))
|
|
}
|
|
}
|
|
None => changed.push((file_path, entry.id())),
|
|
}
|
|
}
|
|
TreeWalkResult::Skip
|
|
}
|
|
})?;
|
|
|
|
if changed.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
// Get changes for each file
|
|
let mut file_changes = HashMap::new();
|
|
for (file_path, id) in changed {
|
|
let current = repository.find_blob(id)?;
|
|
let old = last_blobs
|
|
.get(&file_path)
|
|
.and_then(|id| repository.find_blob(*id).ok());
|
|
|
|
let mut changes = vec![];
|
|
repository.diff_blobs(
|
|
old.as_ref(),
|
|
None,
|
|
Some(¤t),
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
Some(&mut |_, _, line| {
|
|
changes.push(line.into());
|
|
true
|
|
}),
|
|
)?;
|
|
|
|
// Write new blob id to compare against
|
|
last_blobs.insert(file_path.clone(), id);
|
|
|
|
// Add changes to file changes list
|
|
file_changes.insert(file_path, changes);
|
|
}
|
|
|
|
history.push(CommitInfo {
|
|
oid: commit.id().to_string(),
|
|
message: commit.message().unwrap_or_default().to_string(),
|
|
author: commit.author().to_string(),
|
|
date: OffsetDateTime::from_unix_timestamp(commit.time().seconds())
|
|
.unwrap_or_else(|_| OffsetDateTime::now_utc())
|
|
.format(&Iso8601::DEFAULT)
|
|
.unwrap_or_default(),
|
|
changes: file_changes,
|
|
});
|
|
}
|
|
|
|
Ok(history)
|
|
}
|
|
|
|
pub fn get_file_at_commit(&self, path: &Path, oid: &str) -> Result<String> {
|
|
// lol codewhisperer autocompleted all this, have fun
|
|
let repository = self.repository()?;
|
|
let commit = repository.find_commit(oid.parse()?)?;
|
|
let tree = commit.tree()?;
|
|
let entry = tree.get_path(path)?;
|
|
let blob = repository.find_blob(entry.id())?;
|
|
Ok(String::from_utf8_lossy(blob.content()).into_owned())
|
|
}
|
|
}
|
|
|
|
impl From<DiffLine<'_>> for LineChange {
|
|
fn from(value: DiffLine<'_>) -> Self {
|
|
Self {
|
|
op: value.origin(),
|
|
old_line: value.old_lineno(),
|
|
new_line: value.new_lineno(),
|
|
content: String::from_utf8_lossy(value.content()).into_owned(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CommitInfo {
|
|
pub fn diff(&self) -> HashMap<String, Vec<String>> {
|
|
let mut files = HashMap::new();
|
|
for (file, changes) in &self.changes {
|
|
let mut ordered = changes.clone();
|
|
ordered.sort_by(|a, b| {
|
|
let line_a = a.old_line.or(a.new_line).unwrap_or_default();
|
|
let line_b = b.old_line.or(b.new_line).unwrap_or_default();
|
|
line_a.cmp(&line_b)
|
|
});
|
|
files.insert(
|
|
file.clone(),
|
|
ordered
|
|
.iter()
|
|
.map(|change| format!("{} {}", change.op, change.content))
|
|
.collect(),
|
|
);
|
|
}
|
|
files
|
|
}
|
|
|
|
pub fn date_human(&self) -> String {
|
|
OffsetDateTime::parse(&self.date, &Iso8601::DEFAULT)
|
|
.ok()
|
|
.and_then(|date| date.format(&Rfc2822).ok())
|
|
.unwrap_or_else(|| self.date.clone())
|
|
}
|
|
}
|