use std::{collections::BTreeMap, path::Path, thread};
use crate::{
GitError, HistoryOptions, ObjectId, Repository, Result, TreeChange, diff, error::invalid,
};
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RepositoryId(usize);
impl RepositoryId {
#[must_use]
pub const fn index(self) -> usize {
self.0
}
}
pub struct RepositorySet {
entries: Vec<Entry>,
names: BTreeMap<String, RepositoryId>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RepositoryHistory {
pub repository: RepositoryId,
pub head: ObjectId,
pub commits: Vec<ObjectId>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SharedCommit {
pub id: ObjectId,
pub repositories: Vec<RepositoryId>,
}
struct Entry {
id: RepositoryId,
name: String,
repository: Repository,
}
impl RepositorySet {
pub fn open<I, S, P>(repositories: I) -> Result<Self>
where
I: IntoIterator<Item = (S, P)>,
S: Into<String>,
P: AsRef<Path>,
{
let mut entries = Vec::new();
let mut names = BTreeMap::new();
for (name, path) in repositories {
let name = name.into();
if name.is_empty() || name.contains(':') {
return Err(invalid("repository name is empty or contains ':'"));
}
let id = RepositoryId(entries.len());
if names.insert(name.clone(), id).is_some() {
return Err(invalid(format!("duplicate repository name {name:?}")));
}
entries.push(Entry {
id,
name,
repository: Repository::open(path)?,
});
}
Ok(Self { entries, names })
}
pub fn open_parallel<I, S, P>(repositories: I) -> Result<Self>
where
I: IntoIterator<Item = (S, P)>,
S: Into<String>,
P: AsRef<Path>,
{
let mut inputs = Vec::new();
let mut names = BTreeMap::new();
for (name, path) in repositories {
let name = name.into();
if name.is_empty() || name.contains(':') {
return Err(invalid("repository name is empty or contains ':'"));
}
let id = RepositoryId(inputs.len());
if names.insert(name.clone(), id).is_some() {
return Err(invalid(format!("duplicate repository name {name:?}")));
}
inputs.push((id, name, path.as_ref().to_path_buf()));
}
let repositories = thread::scope(|scope| {
inputs
.iter()
.map(|(_, _, path)| scope.spawn(move || Repository::open(path)))
.collect::<Vec<_>>()
.into_iter()
.map(|handle| {
handle.join().map_err(|_| {
GitError::Unsupported("repository open worker panicked".to_owned())
})?
})
.collect::<Result<Vec<_>>>()
})?;
let entries = inputs
.into_iter()
.zip(repositories)
.map(|((id, name, _), repository)| Entry {
id,
name,
repository,
})
.collect();
Ok(Self { entries, names })
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[must_use]
pub fn ids(&self) -> impl ExactSizeIterator<Item = RepositoryId> + '_ {
self.entries.iter().map(|entry| entry.id)
}
#[must_use]
pub fn id(&self, name: &str) -> Option<RepositoryId> {
self.names.get(name).copied()
}
#[must_use]
pub fn name(&self, id: RepositoryId) -> Option<&str> {
self.entries.get(id.0).map(|entry| entry.name.as_str())
}
#[must_use]
pub fn repository(&self, id: RepositoryId) -> Option<&Repository> {
self.entries.get(id.0).map(|entry| &entry.repository)
}
pub fn resolve(&self, id: RepositoryId, revision: &str) -> Result<ObjectId> {
self.get(id)?.resolve(revision)
}
#[must_use]
pub fn find_object(&self, id: ObjectId) -> Vec<RepositoryId> {
self.entries
.iter()
.filter(|entry| entry.repository.contains(id))
.map(|entry| entry.id)
.collect()
}
pub fn histories(&self, options: HistoryOptions) -> Result<Vec<RepositoryHistory>> {
self.entries
.iter()
.map(|entry| history(entry, options))
.collect()
}
pub fn histories_parallel(&self, options: HistoryOptions) -> Result<Vec<RepositoryHistory>> {
thread::scope(|scope| {
let handles = self
.entries
.iter()
.map(|entry| scope.spawn(move || history(entry, options)))
.collect::<Vec<_>>();
handles
.into_iter()
.map(|handle| {
handle
.join()
.map_err(|_| GitError::Unsupported("history worker panicked".to_owned()))?
})
.collect()
})
}
pub fn shared_commits(&self, options: HistoryOptions) -> Result<Vec<SharedCommit>> {
self.shared_commits_from("HEAD", options)
}
pub fn diff_commits(
&self,
old_repository: RepositoryId,
old_revision: &str,
new_repository: RepositoryId,
new_revision: &str,
) -> Result<Vec<TreeChange>> {
let old_repository = self.get(old_repository)?;
let new_repository = self.get(new_repository)?;
let old = old_repository.commit(old_repository.resolve(old_revision)?)?;
let new = new_repository.commit(new_repository.resolve(new_revision)?)?;
diff::across(old_repository, old.tree, new_repository, new.tree)
}
pub(crate) fn get(&self, id: RepositoryId) -> Result<&Repository> {
self.repository(id)
.ok_or_else(|| invalid(format!("unknown repository id {}", id.0)))
}
}
fn history(entry: &Entry, options: HistoryOptions) -> Result<RepositoryHistory> {
let head = entry
.repository
.head()?
.target
.ok_or_else(|| GitError::NotFound(format!("unborn HEAD in {}", entry.name)))?;
Ok(RepositoryHistory {
repository: entry.id,
head,
commits: entry.repository.history_ids(head, options)?,
})
}