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}