Skip to main content

weavatrix_git/
repository_set.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    path::Path,
4    thread,
5};
6
7use crate::{
8    GitError, HistoryOptions, ObjectId, Repository, Result, TreeChange, diff, error::invalid,
9};
10
11#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
12pub struct RepositoryId(usize);
13
14impl RepositoryId {
15    #[must_use]
16    pub const fn index(self) -> usize {
17        self.0
18    }
19}
20
21pub struct RepositorySet {
22    entries: Vec<Entry>,
23    names: BTreeMap<String, RepositoryId>,
24}
25
26pub struct RepositoryHistory {
27    pub repository: RepositoryId,
28    pub head: ObjectId,
29    pub commits: Vec<ObjectId>,
30}
31
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct SharedCommit {
34    pub id: ObjectId,
35    pub repositories: Vec<RepositoryId>,
36}
37
38struct Entry {
39    id: RepositoryId,
40    name: String,
41    repository: Repository,
42}
43
44impl RepositorySet {
45    pub fn open<I, S, P>(repositories: I) -> Result<Self>
46    where
47        I: IntoIterator<Item = (S, P)>,
48        S: Into<String>,
49        P: AsRef<Path>,
50    {
51        let mut entries = Vec::new();
52        let mut names = BTreeMap::new();
53        for (name, path) in repositories {
54            let name = name.into();
55            if name.is_empty() || name.contains(':') {
56                return Err(invalid("repository name is empty or contains ':'"));
57            }
58            let id = RepositoryId(entries.len());
59            if names.insert(name.clone(), id).is_some() {
60                return Err(invalid(format!("duplicate repository name {name:?}")));
61            }
62            entries.push(Entry {
63                id,
64                name,
65                repository: Repository::open(path)?,
66            });
67        }
68        Ok(Self { entries, names })
69    }
70
71    #[must_use]
72    pub fn len(&self) -> usize {
73        self.entries.len()
74    }
75
76    #[must_use]
77    pub fn is_empty(&self) -> bool {
78        self.entries.is_empty()
79    }
80
81    #[must_use]
82    pub fn id(&self, name: &str) -> Option<RepositoryId> {
83        self.names.get(name).copied()
84    }
85
86    #[must_use]
87    pub fn name(&self, id: RepositoryId) -> Option<&str> {
88        self.entries.get(id.0).map(|entry| entry.name.as_str())
89    }
90
91    #[must_use]
92    pub fn repository(&self, id: RepositoryId) -> Option<&Repository> {
93        self.entries.get(id.0).map(|entry| &entry.repository)
94    }
95
96    pub fn resolve(&self, id: RepositoryId, revision: &str) -> Result<ObjectId> {
97        self.get(id)?.resolve(revision)
98    }
99
100    #[must_use]
101    pub fn find_object(&self, id: ObjectId) -> Vec<RepositoryId> {
102        self.entries
103            .iter()
104            .filter(|entry| entry.repository.contains(id))
105            .map(|entry| entry.id)
106            .collect()
107    }
108
109    pub fn histories(&self, options: HistoryOptions) -> Result<Vec<RepositoryHistory>> {
110        self.entries
111            .iter()
112            .map(|entry| history(entry, options))
113            .collect()
114    }
115
116    pub fn histories_parallel(&self, options: HistoryOptions) -> Result<Vec<RepositoryHistory>> {
117        thread::scope(|scope| {
118            let handles = self
119                .entries
120                .iter()
121                .map(|entry| scope.spawn(move || history(entry, options)))
122                .collect::<Vec<_>>();
123            handles
124                .into_iter()
125                .map(|handle| {
126                    handle
127                        .join()
128                        .map_err(|_| GitError::Unsupported("history worker panicked".to_owned()))?
129                })
130                .collect()
131        })
132    }
133
134    pub fn shared_commits(&self, options: HistoryOptions) -> Result<Vec<SharedCommit>> {
135        let mut repositories = BTreeMap::<ObjectId, BTreeSet<RepositoryId>>::new();
136        for history in self.histories_parallel(options)? {
137            for id in history.commits {
138                repositories
139                    .entry(id)
140                    .or_default()
141                    .insert(history.repository);
142            }
143        }
144        Ok(repositories
145            .into_iter()
146            .filter_map(|(id, repositories)| {
147                (repositories.len() > 1).then(|| SharedCommit {
148                    id,
149                    repositories: repositories.into_iter().collect(),
150                })
151            })
152            .collect())
153    }
154
155    pub fn diff_commits(
156        &self,
157        old_repository: RepositoryId,
158        old_revision: &str,
159        new_repository: RepositoryId,
160        new_revision: &str,
161    ) -> Result<Vec<TreeChange>> {
162        let old_repository = self.get(old_repository)?;
163        let new_repository = self.get(new_repository)?;
164        let old = old_repository.commit(old_repository.resolve(old_revision)?)?;
165        let new = new_repository.commit(new_repository.resolve(new_revision)?)?;
166        diff::across(old_repository, old.tree, new_repository, new.tree)
167    }
168
169    fn get(&self, id: RepositoryId) -> Result<&Repository> {
170        self.repository(id)
171            .ok_or_else(|| invalid(format!("unknown repository id {}", id.0)))
172    }
173}
174
175fn history(entry: &Entry, options: HistoryOptions) -> Result<RepositoryHistory> {
176    let head = entry
177        .repository
178        .head()?
179        .target
180        .ok_or_else(|| GitError::NotFound(format!("unborn HEAD in {}", entry.name)))?;
181    Ok(RepositoryHistory {
182        repository: entry.id,
183        head,
184        commits: entry.repository.history_ids(head, options)?,
185    })
186}