weavatrix-git 0.2.0

Dependency-free, evidence-carrying Git repository reader
Documentation
use std::{
    collections::{BTreeMap, BTreeSet},
    thread,
};

use crate::{
    CommitSnapshot, HistoryOptions, ObjectId, RepositoryHistory, RepositoryId, RepositorySet,
    Result, SharedCommit, TreeChange,
};

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RepositorySnapshot {
    pub repository: RepositoryId,
    pub snapshot: CommitSnapshot,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RepositoryTimelineEntry {
    pub repository: RepositoryId,
    pub id: ObjectId,
    pub tree: ObjectId,
    pub parents: Vec<ObjectId>,
    pub committer_time: i64,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RepositoryChangeSet {
    pub repository: RepositoryId,
    pub old: ObjectId,
    pub new: ObjectId,
    pub changes: Vec<TreeChange>,
}

impl RepositorySet {
    pub fn histories_from(
        &self,
        revision: &str,
        options: HistoryOptions,
    ) -> Result<Vec<RepositoryHistory>> {
        self.ids()
            .map(|id| history(self, id, revision, options))
            .collect()
    }

    pub fn histories_from_parallel(
        &self,
        revision: &str,
        options: HistoryOptions,
    ) -> Result<Vec<RepositoryHistory>> {
        thread::scope(|scope| {
            self.ids()
                .map(|id| scope.spawn(move || history(self, id, revision, options)))
                .collect::<Vec<_>>()
                .into_iter()
                .map(join)
                .collect()
        })
    }

    pub fn snapshots(&self, revision: &str) -> Result<Vec<RepositorySnapshot>> {
        self.ids().map(|id| snapshot(self, id, revision)).collect()
    }

    pub fn snapshots_parallel(&self, revision: &str) -> Result<Vec<RepositorySnapshot>> {
        thread::scope(|scope| {
            self.ids()
                .map(|id| scope.spawn(move || snapshot(self, id, revision)))
                .collect::<Vec<_>>()
                .into_iter()
                .map(join)
                .collect()
        })
    }

    pub fn timeline(
        &self,
        revision: &str,
        options: HistoryOptions,
    ) -> Result<Vec<RepositoryTimelineEntry>> {
        let mut entries = thread::scope(|scope| {
            self.ids()
                .map(|id| scope.spawn(move || repository_timeline(self, id, revision, options)))
                .collect::<Vec<_>>()
                .into_iter()
                .map(join)
                .collect::<Result<Vec<_>>>()
        })?
        .into_iter()
        .flatten()
        .collect::<Vec<_>>();
        entries.sort_unstable_by(|left, right| {
            right
                .committer_time
                .cmp(&left.committer_time)
                .then_with(|| left.repository.cmp(&right.repository))
                .then_with(|| left.id.cmp(&right.id))
        });
        Ok(entries)
    }

    pub fn shared_commits_from(
        &self,
        revision: &str,
        options: HistoryOptions,
    ) -> Result<Vec<SharedCommit>> {
        let mut repositories = BTreeMap::<ObjectId, BTreeSet<RepositoryId>>::new();
        for history in self.histories_from_parallel(revision, options)? {
            for id in history.commits {
                repositories
                    .entry(id)
                    .or_default()
                    .insert(history.repository);
            }
        }
        Ok(repositories
            .into_iter()
            .filter_map(|(id, repositories)| {
                (repositories.len() > 1).then(|| SharedCommit {
                    id,
                    repositories: repositories.into_iter().collect(),
                })
            })
            .collect())
    }

    pub fn changes(
        &self,
        old_revision: &str,
        new_revision: &str,
    ) -> Result<Vec<RepositoryChangeSet>> {
        self.ids()
            .map(|id| changes(self, id, old_revision, new_revision))
            .collect()
    }

    pub fn changes_parallel(
        &self,
        old_revision: &str,
        new_revision: &str,
    ) -> Result<Vec<RepositoryChangeSet>> {
        thread::scope(|scope| {
            self.ids()
                .map(|id| scope.spawn(move || changes(self, id, old_revision, new_revision)))
                .collect::<Vec<_>>()
                .into_iter()
                .map(join)
                .collect()
        })
    }
}

fn history(
    set: &RepositorySet,
    id: RepositoryId,
    revision: &str,
    options: HistoryOptions,
) -> Result<RepositoryHistory> {
    let repository = set.get(id)?;
    let head = repository.resolve(revision)?;
    Ok(RepositoryHistory {
        repository: id,
        head,
        commits: repository.history_ids(head, options)?,
    })
}

fn snapshot(set: &RepositorySet, id: RepositoryId, revision: &str) -> Result<RepositorySnapshot> {
    Ok(RepositorySnapshot {
        repository: id,
        snapshot: set.get(id)?.snapshot(revision)?,
    })
}

fn repository_timeline(
    set: &RepositorySet,
    id: RepositoryId,
    revision: &str,
    options: HistoryOptions,
) -> Result<Vec<RepositoryTimelineEntry>> {
    let repository = set.get(id)?;
    let start = repository.resolve(revision)?;
    repository
        .history_ids(start, options)?
        .into_iter()
        .map(|commit_id| {
            let commit = repository.commit_metadata(commit_id)?;
            Ok(RepositoryTimelineEntry {
                repository: id,
                id: commit.id,
                tree: commit.tree,
                parents: commit.parents,
                committer_time: commit.committer_time,
            })
        })
        .collect()
}

fn changes(
    set: &RepositorySet,
    id: RepositoryId,
    old_revision: &str,
    new_revision: &str,
) -> Result<RepositoryChangeSet> {
    let repository = set.get(id)?;
    let old = repository.resolve(old_revision)?;
    let new = repository.resolve(new_revision)?;
    Ok(RepositoryChangeSet {
        repository: id,
        old,
        new,
        changes: repository.diff_commits(old, new)?,
    })
}

fn join<T>(handle: thread::ScopedJoinHandle<'_, Result<T>>) -> Result<T> {
    handle
        .join()
        .map_err(|_| crate::GitError::Unsupported("repository worker panicked".to_owned()))?
}