staxman-old/src/git/history.rs

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(&current),
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())
}
}