pub mod fixture;
mod git2_backend;
pub mod patch;
pub use fixture::FixtureBackend;
pub use git2_backend::Git2Backend;
pub use patch::{PartialMode, build_partial_patch, is_change_line};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RefKind {
Head,
DetachedHead,
LocalBranch,
RemoteBranch,
Tag,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RefLabel {
pub name: String,
pub kind: RefKind,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ChangeStatus {
Added,
Modified,
Deleted,
Renamed,
Copied,
TypeChange,
Untracked,
Other,
}
impl ChangeStatus {
pub fn badge(self) -> char {
match self {
ChangeStatus::Added => 'A',
ChangeStatus::Modified => 'M',
ChangeStatus::Deleted => 'D',
ChangeStatus::Renamed => 'R',
ChangeStatus::Copied => 'C',
ChangeStatus::TypeChange => 'T',
ChangeStatus::Untracked => '?',
ChangeStatus::Other => '?',
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FileChange {
pub path: String,
pub old_path: Option<String>,
pub status: ChangeStatus,
}
impl FileChange {
pub fn display(&self) -> String {
match (&self.old_path, self.status) {
(Some(old), ChangeStatus::Renamed | ChangeStatus::Copied) if old != &self.path => {
format!("{old} -> {}", self.path)
}
_ => self.path.clone(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DiffLineKind {
CommitHeader,
FileHeader,
HunkHeader,
Context,
Addition,
Deletion,
Meta,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DiffLine {
pub kind: DiffLineKind,
pub text: String,
}
impl DiffLine {
pub fn new(kind: DiffLineKind, text: impl Into<String>) -> Self {
Self {
kind,
text: text.into(),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Diff {
pub lines: Vec<DiffLine>,
}
impl Diff {
pub fn is_empty(&self) -> bool {
self.lines.is_empty()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct WorkingStatus {
pub unstaged: Vec<FileChange>,
pub staged: Vec<FileChange>,
}
impl WorkingStatus {
pub fn is_clean(&self) -> bool {
self.unstaged.is_empty() && self.staged.is_empty()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CommitInfo {
pub id: String,
pub short_id: String,
pub summary: String,
pub message: String,
pub author_name: String,
pub author_email: String,
pub committer_name: String,
pub committer_email: String,
pub time_seconds: i64,
pub time_offset_minutes: i32,
pub parents: Vec<String>,
pub refs: Vec<RefLabel>,
}
impl CommitInfo {
pub fn date_string(&self) -> String {
format_git_time(self.time_seconds, self.time_offset_minutes)
}
pub fn short_date_string(&self) -> String {
let full = self.date_string();
full.get(..16).unwrap_or(&full).to_string()
}
pub fn is_merge(&self) -> bool {
self.parents.len() > 1
}
}
pub trait RepoBackend {
fn path(&self) -> &str;
fn commits(&self) -> &[CommitInfo];
fn changed_files(&self, index: usize) -> Vec<FileChange>;
fn commit_diff(&self, index: usize) -> Diff;
fn file_diff(&self, index: usize, path: &str) -> Diff;
fn working_status(&self, amend: bool) -> WorkingStatus;
fn working_diff(&self, path: &str, staged: bool, amend: bool) -> Diff;
fn stage(&self, path: &str) -> Result<(), String>;
fn unstage(&self, path: &str, amend: bool) -> Result<(), String>;
fn revert(&self, path: &str) -> Result<(), String>;
fn delete_untracked(&self, path: &str) -> Result<(), String>;
fn apply_to_index(&self, patch: &str) -> Result<(), String> {
let _ = patch;
Err("Partial staging is not supported by this backend.".into())
}
fn commit(&self, message: &str, amend: bool) -> Result<(), String>;
fn head_message(&self) -> Option<String>;
fn signature(&self) -> Option<(String, String)> {
None
}
}
pub fn format_git_time(seconds: i64, offset_minutes: i32) -> String {
let local = seconds + offset_minutes as i64 * 60;
let days = local.div_euclid(86_400);
let secs_of_day = local.rem_euclid(86_400);
let (y, m, d) = civil_from_days(days);
let hh = secs_of_day / 3600;
let mm = (secs_of_day % 3600) / 60;
let ss = secs_of_day % 60;
let sign = if offset_minutes < 0 { '-' } else { '+' };
let off = offset_minutes.abs();
format!(
"{y:04}-{m:02}-{d:02} {hh:02}:{mm:02}:{ss:02} {sign}{:02}{:02}",
off / 60,
off % 60,
)
}
fn civil_from_days(z: i64) -> (i64, u32, u32) {
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = (doy - (153 * mp + 2) / 5 + 1) as u32; let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32; (if m <= 2 { y + 1 } else { y }, m, d)
}
#[cfg(test)]
mod time_tests {
use super::format_git_time;
#[test]
fn formats_known_timestamps() {
assert_eq!(
format_git_time(1_609_459_200, 0),
"2021-01-01 00:00:00 +0000"
);
assert_eq!(format_git_time(0, 0), "1970-01-01 00:00:00 +0000");
assert_eq!(
format_git_time(1_609_459_200, 120),
"2021-01-01 02:00:00 +0200"
);
assert_eq!(
format_git_time(1_609_459_200, -120),
"2020-12-31 22:00:00 -0200"
);
}
}