use crate::{CommitEvent, FileChange, Hunk, Options, Result};
pub mod types;
pub use types::{TagInfo, WorktreeChange, WorktreeChangeKind};
pub trait Repo: Send + Sync {
fn walk_commits<'a>(
&'a self,
opts: &'a Options,
) -> Result<Box<dyn Iterator<Item = Result<CommitEvent>> + Send + 'a>>;
fn changed_files(&self, rev: &str) -> Result<Vec<FileChange>>;
fn diff_hunks(&self, rev: &str, path: &str) -> Result<Vec<Hunk>>;
fn resolve_alias(&self, name: &str, email: &str) -> String;
fn head_sha(&self) -> Result<String>;
fn is_worktree_dirty(&self) -> bool {
false
}
fn merge_or_rebase_in_progress(&self) -> bool {
false
}
fn is_shallow(&self) -> bool {
false
}
fn read_blob_at(&self, _rev: &str, _path: &str) -> Result<Option<Vec<u8>>> {
Ok(None)
}
fn read_blob_at_head(&self, path: &str) -> Result<Option<Vec<u8>>> {
self.read_blob_at("HEAD", path)
}
fn blob_reader_at<'a>(&'a self, rev: &str) -> Box<dyn BlobReader + 'a> {
Box::new(PerCallBlobReader {
repo: self,
rev: rev.to_string(),
})
}
fn worktree_changes(&self) -> Result<Vec<WorktreeChange>> {
Ok(Vec::new())
}
fn tracked_paths_at_head(&self) -> Result<Vec<String>>;
fn tags(&self) -> Result<Vec<TagInfo>>;
}
pub trait BlobReader {
fn read(&mut self, path: &str) -> Result<Option<Vec<u8>>>;
}
struct PerCallBlobReader<'a, R: Repo + ?Sized> {
repo: &'a R,
rev: String,
}
impl<R: Repo + ?Sized> BlobReader for PerCallBlobReader<'_, R> {
fn read(&mut self, path: &str) -> Result<Option<Vec<u8>>> {
self.repo.read_blob_at(&self.rev, path)
}
}
pub(crate) const WORKTREE_CONFLICT_MESSAGE: &str =
"unmerged paths in working tree; resolve conflicts before gating";
fn add_worktree_candidate(
candidates: &mut std::collections::BTreeMap<String, Option<String>>,
path: String,
rename_from: Option<String>,
) {
let slot = candidates.entry(path).or_insert(None);
if slot.is_none() {
*slot = rename_from;
}
}
fn net_classify_candidates<R: Repo>(
repo: &R,
worktree_root: &std::path::Path,
candidates: std::collections::BTreeMap<String, Option<String>>,
) -> Result<Vec<WorktreeChange>> {
let mut changes = Vec::with_capacity(candidates.len());
for (path, rename_from) in candidates {
let at_head = repo.read_blob_at_head(&path)?.is_some();
let on_disk = std::fs::metadata(worktree_root.join(&path)).is_ok_and(|m| m.is_file());
let kind = match (at_head, on_disk) {
(false, true) => WorktreeChangeKind::Added,
(true, false) => WorktreeChangeKind::Deleted,
(true, true) => WorktreeChangeKind::Modified,
(false, false) => continue,
};
changes.push(WorktreeChange {
path,
kind,
rename_from,
});
}
Ok(changes)
}
fn parse_semver_prefix(name: &str) -> Option<(u64, u64, u64)> {
fn leading_number(segment: &str) -> Option<u64> {
let digits: String = segment.chars().take_while(char::is_ascii_digit).collect();
if digits.is_empty() {
None
} else {
digits.parse().ok()
}
}
let rest = name.strip_prefix(['v', 'V']).unwrap_or(name);
let mut parts = rest.split('.');
let major = leading_number(parts.next()?)?;
let minor = parts.next().and_then(leading_number).unwrap_or(0);
let patch = parts.next().and_then(leading_number).unwrap_or(0);
Some((major, minor, patch))
}
pub(crate) fn tag_tiebreak_cmp(a: &str, b: &str) -> std::cmp::Ordering {
match (parse_semver_prefix(a), parse_semver_prefix(b)) {
(Some(pa), Some(pb)) => pa.cmp(&pb).then_with(|| a.cmp(b)),
_ => a.cmp(b),
}
}
#[cfg(test)]
mod tag_tiebreak_tests {
use super::tag_tiebreak_cmp;
use std::cmp::Ordering;
#[test]
fn semver_minor_version_orders_numerically_not_lexically() {
assert_eq!(tag_tiebreak_cmp("v1.9.0", "v1.10.0"), Ordering::Less);
assert_eq!(tag_tiebreak_cmp("v1.10.0", "v1.9.0"), Ordering::Greater);
}
#[test]
fn semver_major_version_orders_numerically() {
assert_eq!(tag_tiebreak_cmp("v2.0.0", "v10.0.0"), Ordering::Less);
}
#[test]
fn non_semver_names_fall_back_to_lexical() {
assert_eq!(
tag_tiebreak_cmp("nightly-1", "nightly-2"),
Ordering::Less,
"neither name is semver-shaped; must fall back to lexical order"
);
assert_eq!(tag_tiebreak_cmp("release-a", "release-b"), Ordering::Less);
}
#[test]
fn mixed_semver_and_non_semver_falls_back_to_lexical() {
assert_eq!(tag_tiebreak_cmp("v1.0.0", "nightly-1"), Ordering::Greater);
assert_eq!(tag_tiebreak_cmp("nightly-1", "v1.0.0"), Ordering::Less);
}
#[test]
fn missing_patch_defaults_to_zero() {
assert_eq!(tag_tiebreak_cmp("v1.9", "v1.9.0"), Ordering::Less);
assert_eq!(tag_tiebreak_cmp("v1.9", "v1.10.0"), Ordering::Less);
}
#[test]
fn uppercase_v_prefix_is_stripped() {
assert_eq!(tag_tiebreak_cmp("V1.9.0", "V1.10.0"), Ordering::Less);
}
#[test]
fn equal_names_are_equal() {
assert_eq!(tag_tiebreak_cmp("v1.2.3", "v1.2.3"), Ordering::Equal);
}
}
pub mod gix_repo;
pub use gix_repo::GixRepo;
pub mod git_cli_repo;
pub use git_cli_repo::GitCliRepo;