git-slop 0.16.0

A deterministic repository token-defragmenter for humans and AI agents.
Documentation
fn apply_rename_aliases(aliases: &mut BTreeMap<String, String>, commit: Option<&StatusCommit>) {
    let Some(commit) = commit else {
        return;
    };
    for change in &commit.changes {
        let StatusChange::Rename { old_path, new_path } = change else {
            continue;
        };
        if let Some(current_path) = aliases.get(new_path).cloned() {
            aliases.insert(old_path.clone(), current_path);
        }
    }
}

fn mapped_paths_for_status_change(
    change: &StatusChange,
    aliases: &BTreeMap<String, String>,
) -> BTreeSet<String> {
    let mut result = BTreeSet::new();
    match change {
        StatusChange::Rename { old_path, new_path } => {
            if let Some(current) = aliases.get(new_path).or_else(|| aliases.get(old_path)) {
                result.insert(current.clone());
            }
        }
        StatusChange::Copy { new_path, .. } => {
            if let Some(current) = aliases.get(new_path) {
                result.insert(current.clone());
            }
        }
        StatusChange::Path { path, .. } => {
            if let Some(current) = aliases.get(path) {
                result.insert(current.clone());
            }
        }
    }
    result
}

fn first_seen_exact(
    tracked_paths: &BTreeSet<String>,
    commits: &[StatusCommit],
) -> BTreeMap<String, Option<i64>> {
    let mut appearances = BTreeMap::new();
    let mut fallbacks = BTreeMap::new();
    for commit in commits {
        for change in &commit.changes {
            match change {
                StatusChange::Rename { new_path, .. } if tracked_paths.contains(new_path) => {
                    appearances.insert(new_path.clone(), commit.timestamp);
                    fallbacks.insert(new_path.clone(), commit.timestamp);
                }
                StatusChange::Path { status, path } if tracked_paths.contains(path) => {
                    fallbacks.insert(path.clone(), commit.timestamp);
                    if status.starts_with('A') {
                        appearances.insert(path.clone(), commit.timestamp);
                    }
                }
                _ => {}
            }
        }
    }
    tracked_paths
        .iter()
        .map(|path| {
            (
                path.clone(),
                appearances
                    .get(path)
                    .copied()
                    .or_else(|| fallbacks.get(path).copied()),
            )
        })
        .collect()
}

fn first_seen_with_lineage(
    tracked_paths: &BTreeSet<String>,
    commits: &[StatusCommit],
) -> BTreeMap<String, Option<i64>> {
    let mut aliases: BTreeMap<String, String> = tracked_paths
        .iter()
        .map(|path| (path.clone(), path.clone()))
        .collect();
    let mut result: BTreeMap<String, Option<i64>> = tracked_paths
        .iter()
        .map(|path| (path.clone(), None))
        .collect();
    for commit in commits {
        let mut touched = BTreeSet::new();
        for change in &commit.changes {
            touched.extend(mapped_paths_for_status_change(change, &aliases));
        }
        for path in touched {
            result.insert(path, Some(commit.timestamp));
        }
        apply_rename_aliases(&mut aliases, Some(commit));
    }
    result
}

fn map_numstat_exact(entry: &NumstatEntry, tracked_paths: &BTreeSet<String>) -> Option<String> {
    match entry.paths.as_slice() {
        [old_path, new_path, ..] => {
            if tracked_paths.contains(new_path) {
                Some(new_path.clone())
            } else if tracked_paths.contains(old_path) {
                Some(old_path.clone())
            } else {
                None
            }
        }
        [path] if tracked_paths.contains(path) => Some(path.clone()),
        _ => None,
    }
}

fn map_numstat_with_lineage(
    entry: &NumstatEntry,
    aliases: &BTreeMap<String, String>,
) -> Option<String> {
    match entry.paths.as_slice() {
        [old_path, new_path, ..] => aliases
            .get(new_path)
            .or_else(|| aliases.get(old_path))
            .cloned(),
        [path] => aliases.get(path).cloned(),
        _ => None,
    }
}