#![cfg_attr(all(target_arch = "wasm32", feature = "bare", not(test)), no_std)]
extern crate alloc;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
#[cfg(all(target_arch = "wasm32", feature = "bare", not(test)))]
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
core::arch::wasm32::unreachable()
}
#[cfg(all(target_arch = "wasm32", feature = "bare", not(test)))]
#[global_allocator]
static ALLOC: dlmalloc::GlobalDlmalloc = dlmalloc::GlobalDlmalloc;
#[cfg(feature = "bindgen")]
use wasm_bindgen::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum State {
Added,
Modified,
Deleted,
Renamed,
Untracked,
Conflicted,
}
impl State {
fn from_code(code: u8) -> Option<Self> {
Some(match code {
0 => State::Added,
1 => State::Modified,
2 => State::Deleted,
3 => State::Renamed,
4 => State::Untracked,
5 => State::Conflicted,
_ => return None,
})
}
fn is_conflicted(self) -> bool {
matches!(self, State::Conflicted)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Entry {
pub index: u8,
pub worktree: u8,
}
#[derive(Debug, Clone, Copy)]
pub struct Counts {
pub staged: u32,
pub unstaged: u32,
pub conflicted: u32,
pub untracked: u32,
}
impl Counts {
fn as_slice(self) -> [u32; 4] {
[self.staged, self.unstaged, self.conflicted, self.untracked]
}
}
pub fn summarize_status(entries: &[Entry]) -> Counts {
let mut counts = Counts { staged: 0, unstaged: 0, conflicted: 0, untracked: 0 };
for entry in entries {
let index = State::from_code(entry.index);
let worktree = State::from_code(entry.worktree);
if index.is_some_and(State::is_conflicted) || worktree.is_some_and(State::is_conflicted) {
counts.conflicted += 1;
}
if index.is_some() {
counts.staged += 1;
}
if let Some(worktree) = worktree {
counts.unstaged += 1;
if matches!(worktree, State::Untracked) {
counts.untracked += 1;
}
}
}
counts
}
fn rank(entry: &Entry) -> u8 {
let index = State::from_code(entry.index);
let worktree = State::from_code(entry.worktree);
if index.is_some_and(State::is_conflicted) || worktree.is_some_and(State::is_conflicted) {
0
} else if index.is_some() && worktree.is_some() {
1
} else {
2
}
}
pub fn order_entries(entries: &[Entry]) -> Vec<u32> {
let mut order: Vec<u32> = (0..entries.len() as u32).collect();
order.sort_by_key(|&i| rank(&entries[i as usize]));
order
}
pub fn diff_counts(before: Option<&str>, after: Option<&str>) -> (u32, u32) {
let before = before.unwrap_or("");
let after = after.unwrap_or("");
let before_lines: Vec<&str> = before.split('\n').collect();
let after_lines: Vec<&str> = after.split('\n').collect();
let mut head = 0;
while head < before_lines.len() && head < after_lines.len()
&& before_lines[head] == after_lines[head]
{
head += 1;
}
let mut tail = 0;
while tail < before_lines.len() - head
&& tail < after_lines.len() - head
&& before_lines[before_lines.len() - 1 - tail] == after_lines[after_lines.len() - 1 - tail]
{
tail += 1;
}
let before_remaining = before_lines.len().saturating_sub(head + tail);
let after_remaining = after_lines.len().saturating_sub(head + tail);
(after_remaining as u32, before_remaining as u32)
}
const NONE: u8 = 255;
#[cfg_attr(feature = "bindgen", wasm_bindgen)]
pub fn summarize_status_js(codes: &[i32]) -> Vec<u32> {
let entries: Vec<Entry> = codes
.chunks_exact(2)
.map(|pair| Entry {
index: normalize(pair[0]),
worktree: normalize(pair[1]),
})
.collect();
summarize_status(&entries).as_slice().to_vec()
}
#[cfg_attr(feature = "bindgen", wasm_bindgen)]
pub fn order_entries_js(codes: &[i32]) -> Vec<u32> {
let entries: Vec<Entry> = codes
.chunks_exact(2)
.map(|pair| Entry {
index: normalize(pair[0]),
worktree: normalize(pair[1]),
})
.collect();
order_entries(&entries)
}
#[cfg_attr(feature = "bindgen", wasm_bindgen)]
pub fn diff_counts_js(before: Option<String>, after: Option<String>) -> Vec<u32> {
let (added, removed) = diff_counts(before.as_deref(), after.as_deref());
vec![added, removed]
}
fn normalize(code: i32) -> u8 {
if !(0..=5).contains(&code) { NONE } else { code as u8 }
}
#[cfg(test)]
mod tests {
use super::*;
const ADDED: u8 = 0;
const MODIFIED: u8 = 1;
const UNTRACKED: u8 = 4;
const CONFLICTED: u8 = 5;
fn entry(index: u8, worktree: u8) -> Entry {
Entry { index, worktree }
}
#[test]
fn counts_a_clean_tree_as_zero() {
let counts = summarize_status(&[]);
assert_eq!(counts.staged, 0);
assert_eq!(counts.unstaged, 0);
assert_eq!(counts.conflicted, 0);
assert_eq!(counts.untracked, 0);
}
#[test]
fn counts_staged_and_worktree_separately() {
let entries = [entry(MODIFIED, NONE), entry(NONE, MODIFIED)];
let counts = summarize_status(&entries);
assert_eq!(counts.staged, 1);
assert_eq!(counts.unstaged, 1);
}
#[test]
fn counts_a_file_changed_in_both_under_both_counts() {
let counts = summarize_status(&[entry(MODIFIED, MODIFIED)]);
assert_eq!(counts.staged, 1, "it is in the commit");
assert_eq!(counts.unstaged, 1, "and it is not fully staged");
}
#[test]
fn counts_untracked_as_both_unstaged_and_untracked() {
let counts = summarize_status(&[entry(NONE, UNTRACKED), entry(NONE, UNTRACKED)]);
assert_eq!(counts.unstaged, 2);
assert_eq!(counts.untracked, 2);
assert_eq!(counts.staged, 0);
}
#[test]
fn counts_a_conflicted_file_as_conflicted_and_not_as_staged() {
let counts = summarize_status(&[entry(CONFLICTED, CONFLICTED)]);
assert_eq!(counts.conflicted, 1);
assert_eq!(counts.staged, 1, "conflicted in the index, so it is in the index");
assert_eq!(counts.unstaged, 1);
}
#[test]
fn treats_an_unknown_state_code_as_absent() {
let counts = summarize_status(&[entry(99, 99)]);
assert_eq!(counts.staged, 0);
assert_eq!(counts.unstaged, 0);
}
#[test]
fn orders_conflicts_first_then_both_sided_then_the_rest() {
let entries = [
entry(MODIFIED, NONE), entry(MODIFIED, MODIFIED), entry(CONFLICTED, CONFLICTED), ];
let order = order_entries(&entries);
assert_eq!(order, vec![2, 1, 0]);
}
#[test]
fn ordering_is_stable_within_a_rank() {
let entries = [entry(MODIFIED, NONE), entry(MODIFIED, NONE), entry(MODIFIED, NONE)];
assert_eq!(order_entries(&entries), vec![0, 1, 2]);
}
#[test]
fn ordering_breaks_ties_by_caller_order_not_by_path() {
let entries = [entry(MODIFIED, NONE), entry(MODIFIED, NONE)];
assert_eq!(order_entries(&entries), vec![0, 1]);
}
#[test]
fn counts_added_and_removed_lines() {
let (added, removed) = diff_counts(Some("a\nb\n"), Some("a\nc\nd\n"));
assert_eq!(added, 2, "c and d");
assert_eq!(removed, 1, "b");
}
#[test]
fn treats_a_missing_revision_as_empty() {
let (added, removed) = diff_counts(None, Some("a\nb\n"));
assert_eq!(added, 2);
assert_eq!(removed, 0);
let (added, removed) = diff_counts(Some("a\nb\n"), None);
assert_eq!(added, 0);
assert_eq!(removed, 2);
}
#[test]
fn reports_zero_for_identical_text() {
assert_eq!(diff_counts(Some("a\nb\n"), Some("a\nb\n")), (0, 0));
assert_eq!(diff_counts(None, None), (0, 0));
}
#[test]
fn counts_a_whole_replacement() {
let (added, removed) = diff_counts(Some("a\nb\n"), Some("x\n"));
assert_eq!(added, 1);
assert_eq!(removed, 2);
}
#[test]
fn counts_a_trailing_newline_difference() {
let (added, removed) = diff_counts(Some("a\n"), Some("a"));
assert_eq!(added, 0);
assert_eq!(removed, 1);
}
#[test]
fn handles_empty_strings_without_underflowing() {
assert_eq!(diff_counts(Some(""), Some("")), (0, 0));
assert_eq!(diff_counts(Some(""), Some("a\n")), (1, 0));
assert_eq!(diff_counts(Some("a\n"), Some("")), (0, 1));
assert_eq!(diff_counts(Some("\n"), Some("\n")), (0, 0));
assert_eq!(diff_counts(Some("\n"), Some("a\n\n")), (1, 0));
}
#[test]
fn counts_a_whitespace_only_change() {
assert_eq!(diff_counts(Some("a\n b\n"), Some("a\nb\n")), (1, 1));
}
#[test]
fn the_js_boundary_normalizes_unknown_codes() {
let counts = summarize_status_js(&[99, 99, 0, 255]);
assert_eq!(counts, vec![1, 0, 0, 0], "only the known code counted");
}
}