chore: refactor git module #27

Merged
reya merged 2 commits from refactor-git into master 2026-10-07 02:40:13 +00:00
8 changed files with 180 additions and 109 deletions
Showing only changes of commit 73431c55eb - Show all commits
Generated
+1
View File
@@ -8143,6 +8143,7 @@ dependencies = [
"gix-worktree",
"gix-worktree-state",
"ignore",
"log",
"nostr",
"serde",
"serde_json",
+2 -1
View File
@@ -10,10 +10,11 @@ signed_core = { path = "../signed_core" }
nostr.workspace = true
serde.workspace = true
serde_json.workspace = true
log.workspace = true
anyhow.workspace = true
gix = { workspace = true, features = ["revision", "blob-diff"] }
gix-worktree = "0.57"
gix-worktree-state = "0.35"
anyhow.workspace = true
diffy = "0.5"
ignore = "0.4"
+3 -1
View File
@@ -43,7 +43,9 @@ impl GitCache {
let path = self.repo_path(addr);
if let Some(repo) = self.open(addr)? {
repo.fetch().ok();
if let Err(error) = repo.fetch() {
log::warn!("failed to refresh the cached repository: {error:#}");
}
return Ok(repo);
}
+84 -33
View File
@@ -110,6 +110,74 @@ impl CommitDiff {
}
}
pub(crate) struct HunkBuilder {
old_start: u32,
old_lines: u32,
new_start: u32,
new_lines: u32,
old: u32,
new: u32,
lines: Vec<DiffLine>,
insertions: usize,
deletions: usize,
}
impl HunkBuilder {
/// Starts a hunk spanning the given old and new ranges.
pub(crate) fn new(old_start: u32, old_lines: u32, new_start: u32, new_lines: u32) -> Self {
Self {
old_start,
old_lines,
new_start,
new_lines,
old: old_start,
new: new_start,
lines: Vec::new(),
insertions: 0,
deletions: 0,
}
}
/// Appends a line, numbering it and counting insertions and deletions.
pub(crate) fn push(&mut self, kind: DiffLineKind, text: impl Into<String>) {
let (old_no, new_no) = match kind {
DiffLineKind::Context => {
let numbers = (Some(self.old), Some(self.new));
self.old += 1;
self.new += 1;
numbers
}
DiffLineKind::Addition => {
self.insertions += 1;
let number = Some(self.new);
self.new += 1;
(None, number)
}
DiffLineKind::Deletion => {
self.deletions += 1;
let number = Some(self.old);
self.old += 1;
(number, None)
}
};
self.lines
.push(DiffLine::new(kind, old_no, new_no, text.into()));
}
/// Finishes the hunk and returns it with its insertion and deletion counts.
pub(crate) fn finish(self) -> (DiffHunk, usize, usize) {
let hunk = DiffHunk::new(
self.old_start,
self.old_lines,
self.new_start,
self.new_lines,
self.lines,
);
(hunk, self.insertions, self.deletions)
}
}
pub(crate) struct HunkCollector<'a> {
hunks: &'a mut Vec<DiffHunk>,
insertions: &'a mut usize,
@@ -140,43 +208,26 @@ impl ConsumeHunk for HunkCollector<'_> {
header: HunkHeader,
lines: &[(GixLineKind, &[u8])],
) -> std::io::Result<()> {
let mut old_ln = header.before_hunk_start;
let mut new_ln = header.after_hunk_start;
let mut out = Vec::with_capacity(lines.len());
for (kind, content) in lines {
let text = String::from_utf8_lossy(content).into_owned();
let line = match kind {
GixLineKind::Context => {
let line =
DiffLine::new(DiffLineKind::Context, Some(old_ln), Some(new_ln), text);
old_ln += 1;
new_ln += 1;
line
}
GixLineKind::Remove => {
*self.deletions += 1;
let line = DiffLine::new(DiffLineKind::Deletion, Some(old_ln), None, text);
old_ln += 1;
line
}
GixLineKind::Add => {
*self.insertions += 1;
let line = DiffLine::new(DiffLineKind::Addition, None, Some(new_ln), text);
new_ln += 1;
line
}
};
out.push(line);
}
self.hunks.push(DiffHunk::new(
let mut builder = HunkBuilder::new(
header.before_hunk_start,
header.before_hunk_len,
header.after_hunk_start,
header.after_hunk_len,
out,
));
);
for (kind, content) in lines {
let kind = match kind {
GixLineKind::Context => DiffLineKind::Context,
GixLineKind::Remove => DiffLineKind::Deletion,
GixLineKind::Add => DiffLineKind::Addition,
};
builder.push(kind, String::from_utf8_lossy(content).into_owned());
}
let (hunk, insertions, deletions) = builder.finish();
*self.insertions += insertions;
*self.deletions += deletions;
self.hunks.push(hunk);
Ok(())
}
+16 -50
View File
@@ -2,7 +2,7 @@ use anyhow::Result;
use diffy::patch_set::{FileOperation, FilePatch, ParseOptions, PatchSet};
use diffy::{Hunk, Line};
use crate::diff::{CommitDiff, DiffHunk, DiffLine, DiffLineKind, DiffStatus, FileDiff};
use crate::diff::{CommitDiff, DiffHunk, DiffLineKind, DiffStatus, FileDiff, HunkBuilder};
use crate::history::FileCommit;
pub struct PatchParser;
@@ -169,17 +169,9 @@ impl PatchParser {
if let Some(text) = patch.as_text() {
for hunk in text.hunks() {
let hunk = Self::hunk_diff(hunk);
insertions += hunk
.lines
.iter()
.filter(|line| line.kind == DiffLineKind::Addition)
.count();
deletions += hunk
.lines
.iter()
.filter(|line| line.kind == DiffLineKind::Deletion)
.count();
let (hunk, hunk_insertions, hunk_deletions) = Self::hunk_diff(hunk);
insertions += hunk_insertions;
deletions += hunk_deletions;
hunks.push(hunk);
}
}
@@ -195,14 +187,17 @@ impl PatchParser {
)
}
/// Converts a diffy hunk into a `DiffHunk` with per-line old and new numbers.
fn hunk_diff(hunk: &Hunk<'_, str>) -> DiffHunk {
/// Converts a diffy hunk into a `DiffHunk` with its insertion and deletion counts.
fn hunk_diff(hunk: &Hunk<'_, str>) -> (DiffHunk, usize, usize) {
let old_range = hunk.old_range();
let new_range = hunk.new_range();
let mut old = old_range.start() as u32;
let mut new = new_range.start() as u32;
let mut lines = Vec::with_capacity(hunk.lines().len());
let mut builder = HunkBuilder::new(
old_range.start() as u32,
old_range.len() as u32,
new_range.start() as u32,
new_range.len() as u32,
);
for line in hunk.lines() {
let (kind, text) = match line {
@@ -211,41 +206,12 @@ impl PatchParser {
Line::Insert(text) => (DiffLineKind::Addition, *text),
};
let (old_no, new_no) = match kind {
DiffLineKind::Context => {
let numbers = (Some(old), Some(new));
old += 1;
new += 1;
numbers
}
DiffLineKind::Addition => {
let number = Some(new);
new += 1;
(None, number)
}
DiffLineKind::Deletion => {
let number = Some(old);
old += 1;
(number, None)
}
};
let text = text
.strip_suffix('\n')
.unwrap_or(text)
.strip_suffix('\r')
.unwrap_or(text);
lines.push(DiffLine::new(kind, old_no, new_no, text.to_owned()));
let text = text.strip_suffix('\n').unwrap_or(text);
let text = text.strip_suffix('\r').unwrap_or(text);
builder.push(kind, text.to_owned());
}
DiffHunk::new(
old_range.start() as u32,
old_range.len() as u32,
new_range.start() as u32,
new_range.len() as u32,
lines,
)
builder.finish()
}
}
+6 -4
View File
@@ -140,7 +140,9 @@ impl Repo {
for url in clone_urls {
match Self::clone_from(url.as_ref(), path) {
Ok(repo) => {
repo.fetch().ok();
if let Err(error) = repo.fetch() {
log::warn!("failed to fetch NIP-34 refs after cloning: {error:#}");
}
return Ok(repo);
}
Err(error) => last_error = Some(error),
@@ -871,9 +873,9 @@ impl Repo {
readme_path,
readme,
self.current_branch(),
self.head_commit().unwrap_or(None),
self.branches().unwrap_or_default(),
self.tags().unwrap_or_default(),
self.head_commit()?,
self.branches()?,
self.tags()?,
))
}
+40 -20
View File
@@ -1,4 +1,5 @@
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use ignore::WalkBuilder;
@@ -20,39 +21,58 @@ impl LocalRepo {
}
}
/// Finds the outermost git repositories under `root`, up to `SCAN_MAX_DEPTH` deep.
/// Finds git repositories under `root` up to `SCAN_MAX_DEPTH` deep, without descending into them.
pub fn find_git_repos(root: &Path) -> Vec<LocalRepo> {
let Ok(root) = root.canonicalize() else {
return Vec::new();
};
let found = Arc::new(Mutex::new(Vec::<PathBuf>::new()));
let walker = WalkBuilder::new(&root)
.max_depth(Some(SCAN_MAX_DEPTH))
.require_git(false)
.filter_entry({
let found = found.clone();
move |entry| {
let Some(kind) = entry.file_type() else {
return true;
};
if !kind.is_dir() {
return true;
}
let dir = entry.path();
let Ok(repo) = gix::discover(dir) else {
return true;
};
let Some(workdir) = repo.workdir() else {
return false;
};
if workdir != dir {
return true;
}
match found.lock() {
Ok(mut repos) => repos.push(dir.to_path_buf()),
Err(poisoned) => poisoned.into_inner().push(dir.to_path_buf()),
}
false
}
})
.build();
let mut repos: Vec<PathBuf> = walker
.flatten()
.filter(|entry| entry.file_type().is_some_and(|kind| kind.is_dir()))
.map(ignore::DirEntry::into_path)
.filter_map(|dir| {
let repo = gix::discover(&dir).ok()?;
let workdir = repo.workdir()?.to_path_buf();
workdir.starts_with(&root).then_some(workdir)
})
.collect();
for _ in walker {}
let mut repos = match found.lock() {
Ok(mut repos) => std::mem::take(&mut *repos),
Err(poisoned) => std::mem::take(&mut *poisoned.into_inner()),
};
repos.sort();
repos.dedup();
let mut roots: Vec<PathBuf> = Vec::with_capacity(repos.len());
for repo in repos {
if !roots.iter().any(|kept| repo.starts_with(kept)) {
roots.push(repo);
}
}
roots
repos
.into_iter()
.map(|path| {
let nip34 = Repo::open(&path).ok().and_then(|repo| repo.nip34_binding());
+28
View File
@@ -450,6 +450,34 @@ fn find_git_repos_reports_the_outermost_repository() {
assert!(repos[0].nip34.is_none());
}
#[test]
fn find_git_repos_skips_repositories_nested_in_repositories() {
let root = tempfile::tempdir().expect("tempdir");
let outer = root.path().join("outer");
std::fs::create_dir_all(&outer).expect("mkdir");
git_run(&outer, &["init", "-q"]);
let nested = outer.join("nested");
std::fs::create_dir_all(&nested).expect("mkdir");
git_run(&nested, &["init", "-q"]);
let repos = find_git_repos(root.path());
assert_eq!(repos.len(), 1);
assert_eq!(repos[0].path, outer.canonicalize().expect("canonical"));
}
#[test]
fn find_git_repos_ignores_a_repository_above_the_scan_root() {
let outer = tempfile::tempdir().expect("tempdir");
git_run(outer.path(), &["init", "-q"]);
let root = outer.path().join("scan");
std::fs::create_dir_all(&root).expect("mkdir");
assert!(find_git_repos(&root).is_empty());
}
/// Creates an empty bare server repository for push tests.
fn bare_server(base: &Path, owner: &str, name: &str) -> PathBuf {
let repo = base.join(owner).join(format!("{name}.git"));