1use std::path::{Path, PathBuf};
9
10use kernel::{ByteLen, consts_policy::WORKTREE_MAX_BYTES};
11
12use crate::error::StorageError;
13
14use super::landing::{CheckoutRun, Landing, PlannedMerge, check_out};
15use super::lease::{FileWork, WorktreeLease};
16use super::name::WorktreeName;
17use super::weight::{Weight, Written, measure, written};
18use kept::Standing;
19
20mod kept;
21mod stock;
22
23pub(super) const WORKTREE_DIR: &str = "worktrees";
27
28pub struct Worktrees {
30 pub(super) repo: git2::Repository,
31 pub(super) home: PathBuf,
32 pub(super) city_root: PathBuf,
34 ceiling: ByteLen,
35}
36
37impl std::fmt::Debug for Worktrees {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.debug_struct("Worktrees")
43 .field("home", &self.home)
44 .field("ceiling", &self.ceiling)
45 .finish()
46 }
47}
48
49impl Worktrees {
50 pub fn open(city_root: &Path) -> Result<Worktrees, StorageError> {
57 let repo = git2::Repository::open(city_root).map_err(|err| StorageError::Worktree {
58 op: "open the city repository",
59 detail: format!("{}: {err}", city_root.display()),
60 })?;
61 Ok(Worktrees::over(repo, city_root))
62 }
63
64 pub(super) fn over(repo: git2::Repository, city_root: &Path) -> Worktrees {
66 Worktrees {
67 repo,
68 home: city_root.join(kernel::RESERVED_PREFIX).join(WORKTREE_DIR),
69 city_root: city_root.to_path_buf(),
70 ceiling: ByteLen::new(WORKTREE_MAX_BYTES),
71 }
72 }
73
74 pub fn claim(
82 &self,
83 name: &WorktreeName,
84 scopes: &[String],
85 ) -> Result<WorktreeLease, StorageError> {
86 match self.standing(name)? {
87 Standing::Held => Err(StorageError::WorktreeBusy {
88 name: name.as_str().to_owned(),
89 detail: "another node holds this tree".to_owned(),
90 }),
91 Standing::Kept(tree) => self.reattach(name, &tree, scopes),
92 Standing::Absent => self.place(name),
93 }
94 }
95
96 fn place(&self, name: &WorktreeName) -> Result<WorktreeLease, StorageError> {
100 let city = self.refuse_oversized(name.as_str())?;
101 if self.repo.head().is_err() {
102 return Err(StorageError::Worktree {
103 op: "branch a worktree",
104 detail: "the city has no checkpoint yet, and a tree branches from a commit"
105 .to_owned(),
106 });
107 }
108 self.clear_unregistered(name.as_str())?;
111 if let Some(lease) = self.adopt(name, &city)? {
112 return Ok(lease);
113 }
114 let branch = match self
119 .repo
120 .find_branch(name.as_str(), git2::BranchType::Local)
121 {
122 Ok(branch) => Some(branch),
123 Err(err) if err.code() == git2::ErrorCode::NotFound => None,
124 Err(err) => {
125 return Err(StorageError::Worktree {
126 op: "find a node branch",
127 detail: format!("{}: {err}", name.as_str()),
128 });
129 }
130 };
131 self.add_tree(name, branch.as_ref().map(git2::Branch::get), &city)
132 }
133
134 pub(super) fn refuse_oversized(&self, id: &str) -> Result<Weight, StorageError> {
138 let source = self.repo.workdir().ok_or_else(|| StorageError::Worktree {
139 op: "find the city working tree",
140 detail: "the repository is bare".to_owned(),
141 })?;
142 let city = measure(source)?;
143 if city.bytes.get() > self.ceiling.get() {
144 return Err(StorageError::WorktreeBusy {
145 name: id.to_owned(),
146 detail: format!(
147 "the city working tree is {} bytes and the ceiling is {}",
148 city.bytes.get(),
149 self.ceiling.get()
150 ),
151 });
152 }
153 Ok(city)
154 }
155
156 pub(super) fn add_tree(
160 &self,
161 name: &WorktreeName,
162 reference: Option<&git2::Reference<'_>>,
163 city: &Weight,
164 ) -> Result<WorktreeLease, StorageError> {
165 let (path, checked_out) = self.check_out_tree(name.as_str(), reference)?;
166 Ok(WorktreeLease {
167 name: name.clone(),
168 disk: checked_out.bytes,
169 path,
170 work: FileWork {
171 created: checked_out.files,
172 walked: city.walked,
173 ..FileWork::default()
174 },
175 })
176 }
177
178 pub(super) fn check_out_tree(
181 &self,
182 id: &str,
183 reference: Option<&git2::Reference<'_>>,
184 ) -> Result<(PathBuf, Written), StorageError> {
185 std::fs::create_dir_all(&self.home).map_err(|source| StorageError::Io {
186 op: "create the worktree home",
187 path: self.home.clone(),
188 source,
189 })?;
190 let path = self.home.join(id);
191 crate::alias::WriteTarget::within("place a worktree", &self.city_root, &path)?;
194 let mut opts = git2::WorktreeAddOptions::new();
195 opts.lock(true);
196 opts.reference(reference);
197 let tree =
198 self.repo
199 .worktree(id, &path, Some(&opts))
200 .map_err(|err| StorageError::Worktree {
201 op: "add a worktree",
202 detail: format!("{id}: {err}"),
203 })?;
204 Ok((path, written(&tree)?))
207 }
208
209 pub fn plan_merge(&self, name: &WorktreeName) -> Result<PlannedMerge<'_>, StorageError> {
227 let refuse = |op: &'static str, detail: String| StorageError::Worktree { op, detail };
228 let branch = self
229 .repo
230 .find_branch(name.as_str(), git2::BranchType::Local)
231 .map_err(|err| refuse("find a node branch", format!("{}: {err}", name.as_str())))?;
232 let theirs = branch
233 .get()
234 .peel_to_commit()
235 .map_err(|err| refuse("read a node branch", err.to_string()))?;
236 let head = self
237 .repo
238 .head()
239 .map_err(|err| refuse("read the city trunk", err.to_string()))?;
240 let ours = head
241 .peel_to_commit()
242 .map_err(|err| refuse("read the city trunk", err.to_string()))?;
243 let descends = theirs.id() == ours.id()
248 || self
249 .repo
250 .graph_descendant_of(theirs.id(), ours.id())
251 .map_err(|err| refuse("judge a fast-forward", err.to_string()))?;
252 if !descends {
253 return Err(StorageError::MergeStale {
254 name: name.as_str().to_owned(),
255 detail: format!("the trunk moved to {} after this node branched", ours.id()),
256 });
257 }
258 let tree = theirs
259 .tree()
260 .map_err(|err| refuse("read the node tree", err.to_string()))?;
261 check_out(&self.repo, &tree, CheckoutRun::DryRun)?;
262 Ok(PlannedMerge {
263 trees: self,
264 target: theirs.id(),
265 })
266 }
267
268 pub(super) fn land_merge(
271 &self,
272 target: git2::Oid,
273 landing: &Landing<'_>,
274 ) -> Result<(), StorageError> {
275 let refuse = |op: &'static str, detail: String| StorageError::Worktree { op, detail };
276 let head = self
277 .repo
278 .head()
279 .map_err(|err| refuse("read the city trunk", err.to_string()))?;
280 let ours = head
281 .peel_to_commit()
282 .map_err(|err| refuse("read the city trunk", err.to_string()))?;
283 let theirs = self
284 .repo
285 .find_commit(target)
286 .map_err(|err| refuse("read the node commit", err.to_string()))?;
287 let tree = theirs
290 .tree()
291 .map_err(|err| refuse("read the node tree", err.to_string()))?;
292 let seconds = i64::try_from(landing.t.value().saturating_div(1000)).unwrap_or(0);
293 let when = git2::Time::new(seconds, 0);
294 let email = landing.of.email();
295 let signature = git2::Signature::new(landing.of.actor().as_str(), &email, &when)
296 .map_err(|err| refuse("build the merge signature", err.to_string()))?;
297 let message = format!(
298 "{}\n\n{}{}",
299 landing.subject,
300 landing.of.trailers(),
301 self.reviewer(landing.reviewed_by_person)
302 );
303 let merge = self
304 .repo
305 .commit(
306 None,
307 &signature,
308 &signature,
309 &message,
310 &tree,
311 &[&ours, &theirs],
312 )
313 .map_err(|err| refuse("commit the merge", err.to_string()))?;
314 check_out(&self.repo, &tree, CheckoutRun::Write)?;
315 let trunk = head.name().unwrap_or("HEAD");
316 self.repo
317 .reference_matching(trunk, merge, true, ours.id(), landing.subject)
318 .map_err(|err| refuse("move the city trunk", err.to_string()))?;
319 Ok(())
320 }
321
322 fn reviewer(&self, reviewed_by_person: bool) -> String {
327 if !reviewed_by_person {
328 return String::new();
329 }
330 let Ok(config) = self.repo.config() else {
331 return String::new();
332 };
333 match (
334 config.get_string("user.name"),
335 config.get_string("user.email"),
336 ) {
337 (Ok(name), Ok(email)) => format!("Reviewed-by: {name} <{email}>\n"),
338 _ => String::new(),
339 }
340 }
341
342 pub fn release(&self, lease: WorktreeLease) -> Result<(), StorageError> {
348 let refuse = |op: &'static str, err: git2::Error| StorageError::Worktree {
349 op,
350 detail: format!("{}: {err}", lease.name().as_str()),
351 };
352 self.repo
353 .find_worktree(lease.name().as_str())
354 .map_err(|err| refuse("find a worktree", err))?
355 .unlock()
356 .map_err(|err| refuse("unlock a worktree", err))
357 }
358
359 pub fn live(&self) -> Result<Vec<WorktreeName>, StorageError> {
365 let names = self
366 .repo
367 .worktrees()
368 .map_err(|err| StorageError::Worktree {
369 op: "list worktrees",
370 detail: err.to_string(),
371 })?;
372 let mut out = Vec::new();
373 for name in names.iter().flatten().flatten() {
376 if name != stock::STOCK {
377 out.push(WorktreeName::parse(name)?);
378 }
379 }
380 out.sort();
381 Ok(out)
382 }
383}
384
385#[cfg(test)]
386#[allow(
387 clippy::unwrap_used,
388 clippy::expect_used,
389 clippy::panic,
390 clippy::indexing_slicing,
391 clippy::arithmetic_side_effects,
392 reason = "test code"
393)]
394pub(crate) mod tests;