weavatrix_git/
repository_set.rs1use 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}