Skip to main content

weavatrix_git/
repository_set.rs

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