pub mod diff;
pub mod ignores;
pub mod status;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::mpsc::Sender;
use std::thread;
use crate::events::Event;
pub use ignores::Ignores;
pub fn git_dir(root: &Path) -> Option<PathBuf> {
gix::discover(root)
.ok()
.map(|repo| repo.git_dir().to_path_buf())
}
pub fn spawn(root: PathBuf, events: Sender<Event>) {
thread::spawn(move || {
let info = status::compute(&root);
let _ = events.send(Event::GitDone(Box::new(info)));
});
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Change {
Added,
Modified,
Deleted,
Renamed,
Type,
}
impl Change {
fn letter(self) -> char {
match self {
Change::Added => 'A',
Change::Modified => 'M',
Change::Deleted => 'D',
Change::Renamed => 'R',
Change::Type => 'T',
}
}
}
#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
pub struct DiffStat {
pub added: u32,
pub removed: u32,
}
impl DiffStat {
pub fn is_empty(self) -> bool {
self.added == 0 && self.removed == 0
}
fn merge(self, other: Self) -> Self {
Self {
added: self.added + other.added,
removed: self.removed + other.removed,
}
}
}
#[derive(Clone, Copy, Default, Debug)]
pub struct FileStatus {
pub staged: Option<Change>,
pub unstaged: Option<Change>,
pub untracked: bool,
pub conflicted: bool,
pub staged_stat: DiffStat,
pub unstaged_stat: DiffStat,
}
impl FileStatus {
pub fn code(&self) -> String {
if self.conflicted {
return "UU".to_owned();
}
if self.untracked {
return "??".to_owned();
}
let staged = self.staged.map_or(' ', Change::letter);
let unstaged = self.unstaged.map_or(' ', Change::letter);
format!("{staged}{unstaged}")
}
pub fn stat(&self) -> DiffStat {
self.staged_stat.merge(self.unstaged_stat)
}
}
#[derive(Clone, Copy, Default, Debug)]
pub struct Totals {
pub staged: DiffStat,
pub unstaged: DiffStat,
pub modified_files: usize,
pub staged_files: usize,
pub untracked_files: usize,
}
#[derive(Default)]
pub struct GitInfo {
pub branch: Option<String>,
pub totals: Totals,
files: HashMap<PathBuf, FileStatus>,
dirs: HashMap<PathBuf, DiffStat>,
is_repo: bool,
}
impl GitInfo {
pub fn none() -> Self {
Self::default()
}
pub fn is_repo(&self) -> bool {
self.is_repo
}
pub fn file(&self, path: &Path) -> Option<&FileStatus> {
self.files.get(path)
}
pub fn directory(&self, path: &Path) -> Option<DiffStat> {
self.dirs.get(path).copied()
}
pub fn has_changes(&self, path: &Path) -> bool {
self.files.contains_key(path) || self.dirs.contains_key(path)
}
fn record(&mut self, path: PathBuf, update: impl FnOnce(&mut FileStatus)) {
update(self.files.entry(path).or_default());
}
fn summarise(&mut self, workdir: &Path) {
let mut totals = Totals::default();
let mut dirs: HashMap<PathBuf, DiffStat> = HashMap::new();
for (path, status) in &self.files {
totals.staged = totals.staged.merge(status.staged_stat);
totals.unstaged = totals.unstaged.merge(status.unstaged_stat);
if status.untracked {
totals.untracked_files += 1;
} else {
if status.staged.is_some() {
totals.staged_files += 1;
}
if status.unstaged.is_some() || status.conflicted {
totals.modified_files += 1;
}
}
let stat = status.stat();
for ancestor in path.ancestors().skip(1) {
let entry = dirs.entry(ancestor.to_path_buf()).or_default();
*entry = entry.merge(stat);
if ancestor == workdir {
break;
}
}
}
self.totals = totals;
self.dirs = dirs;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn codes_match_git_status_short() {
let staged = FileStatus {
staged: Some(Change::Modified),
..FileStatus::default()
};
assert_eq!(staged.code(), "M ");
let unstaged = FileStatus {
unstaged: Some(Change::Modified),
..FileStatus::default()
};
assert_eq!(unstaged.code(), " M");
let both = FileStatus {
staged: Some(Change::Added),
unstaged: Some(Change::Modified),
..FileStatus::default()
};
assert_eq!(both.code(), "AM");
let untracked = FileStatus {
untracked: true,
..FileStatus::default()
};
assert_eq!(untracked.code(), "??");
let conflicted = FileStatus {
conflicted: true,
unstaged: Some(Change::Modified),
..FileStatus::default()
};
assert_eq!(conflicted.code(), "UU");
}
#[test]
fn ancestors_aggregate_up_to_the_worktree_root() {
let workdir = PathBuf::from("/repo");
let mut info = GitInfo {
is_repo: true,
..GitInfo::default()
};
info.record(PathBuf::from("/repo/src/ui/mod.rs"), |status| {
status.unstaged = Some(Change::Modified);
status.unstaged_stat = DiffStat {
added: 3,
removed: 1,
};
});
info.record(PathBuf::from("/repo/src/main.rs"), |status| {
status.staged = Some(Change::Modified);
status.staged_stat = DiffStat {
added: 5,
removed: 0,
};
});
info.summarise(&workdir);
assert_eq!(
info.directory(Path::new("/repo/src")),
Some(DiffStat {
added: 8,
removed: 1
})
);
assert_eq!(
info.directory(Path::new("/repo/src/ui")),
Some(DiffStat {
added: 3,
removed: 1
})
);
assert_eq!(info.directory(Path::new("/")), None);
assert_eq!(info.totals.modified_files, 1);
assert_eq!(info.totals.staged_files, 1);
assert_eq!(info.totals.staged.added, 5);
assert_eq!(info.totals.unstaged.added, 3);
}
}