1use std::path::{Path, PathBuf};
17
18use super::super::lease::{FileWork, WorktreeLease};
19use super::super::name::WorktreeName;
20use super::super::weight::Weight;
21use super::Worktrees;
22use super::kept::restore;
23use crate::error::StorageError;
24
25pub(super) const STOCK: &str = "+spare";
30
31const STOCKING: &str = "being stocked by sprawling";
34
35enum Stock {
37 Absent,
38 Busy,
40 Broken,
43 Ready(git2::Worktree, Links),
44}
45
46struct Links {
49 tree: PathBuf,
50 admin: PathBuf,
51 dot_git: String,
53 gitdir: String,
55}
56
57impl Links {
58 fn of(&self, name: &str) -> (String, String) {
60 (
61 format!("{}worktrees/{name}/\n", self.dot_git),
62 format!("{}/{name}/.git\n", self.gitdir),
63 )
64 }
65}
66
67impl Worktrees {
68 pub fn stock(&self) -> Result<FileWork, StorageError> {
81 match self.stock_standing()? {
82 Stock::Absent => self.place_stock(),
83 Stock::Busy => Ok(FileWork::default()),
84 Stock::Broken => {
85 self.forget(STOCK)?;
86 self.place_stock()
87 }
88 Stock::Ready(tree, _) => self.refresh_stock(&tree),
89 }
90 }
91
92 pub(super) fn adopt(
101 &self,
102 name: &WorktreeName,
103 city: &Weight,
104 ) -> Result<Option<WorktreeLease>, StorageError> {
105 let Stock::Ready(_, links) = self.stock_standing()? else {
106 return Ok(None);
107 };
108 let target = self.home.join(name.as_str());
109 crate::alias::WriteTarget::within("place a worktree", &self.city_root, &target)?;
110 self.branch_at_trunk(name)?;
111 match std::fs::rename(&links.tree, &target) {
112 Ok(()) => {}
113 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
114 Err(source) => return Err(io("take the stock", &links.tree, source)),
115 }
116 let (dot_git, gitdir) = links.of(name.as_str());
117 let admin = links.admin.with_file_name(name.as_str());
118 write(&target.join(".git"), &dot_git)?;
119 std::fs::rename(&links.admin, &admin)
120 .map_err(|source| io("register the stock as a node's tree", &links.admin, source))?;
121 write(&admin.join("gitdir"), &gitdir)?;
122 let tree = self
123 .repo
124 .find_worktree(name.as_str())
125 .map_err(|err| git("find a taken stock", name.as_str(), &err))?;
126 let lease = self.reattach(name, &tree, &[])?;
127 Ok(Some(WorktreeLease {
128 work: FileWork {
129 walked: lease.work.walked.saturating_add(city.walked),
130 ..lease.work
131 },
132 ..lease
133 }))
134 }
135
136 fn stock_standing(&self) -> Result<Stock, StorageError> {
138 let tree = match self.repo.find_worktree(STOCK) {
139 Ok(tree) => tree,
140 Err(err) if err.code() == git2::ErrorCode::NotFound => return Ok(Stock::Absent),
141 Err(err) => return Err(git("find the stock", STOCK, &err)),
142 };
143 let lock = tree
144 .is_locked()
145 .map_err(|err| git("read the stock's lock", STOCK, &err))?;
146 if let git2::WorktreeLockStatus::Locked(_) = lock {
147 return Ok(Stock::Busy);
148 }
149 let admin = self.repo.commondir().join("worktrees").join(STOCK);
150 let read = |path: &Path| match std::fs::read_to_string(path) {
151 Ok(text) => Ok(Some(text)),
152 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
153 Err(source) => Err(io("read the stock's links", path, source)),
154 };
155 let dot_git = read(&tree.path().join(".git"))?;
156 let gitdir = read(&admin.join("gitdir"))?;
157 let links = dot_git
158 .as_deref()
159 .and_then(|text| text.strip_suffix(&format!("worktrees/{STOCK}/\n")))
160 .zip(
161 gitdir
162 .as_deref()
163 .and_then(|text| text.strip_suffix(&format!("/{STOCK}/.git\n"))),
164 );
165 Ok(match links {
166 Some((dot_git, gitdir)) => {
167 let links = Links {
168 tree: tree.path().to_path_buf(),
169 admin,
170 dot_git: dot_git.to_owned(),
171 gitdir: gitdir.to_owned(),
172 };
173 Stock::Ready(tree, links)
174 }
175 None => Stock::Broken,
176 })
177 }
178
179 fn place_stock(&self) -> Result<FileWork, StorageError> {
181 let city = self.refuse_oversized(STOCK)?;
182 self.clear_unregistered(STOCK)?;
183 let trunk = self
184 .repo
185 .head()
186 .and_then(|head| head.peel_to_commit())
187 .map_err(|err| git("read the city trunk", STOCK, &err))?;
188 let branch = self
189 .repo
190 .branch(STOCK, &trunk, true)
191 .map_err(|err| git("branch the stock", STOCK, &err))?;
192 let (_, written) = match self.check_out_tree(STOCK, Some(branch.get())) {
193 Ok(placed) => placed,
194 Err(refusal) => return Err(self.take_back(refusal)),
197 };
198 let tree = self
199 .repo
200 .find_worktree(STOCK)
201 .map_err(|err| git("find the stock", STOCK, &err))?;
202 cache_trees(&tree)?;
203 tree.unlock()
204 .map_err(|err| git("unlock the stock", STOCK, &err))?;
205 Ok(FileWork {
206 created: written.files,
207 walked: city.walked,
208 ..FileWork::default()
209 })
210 }
211
212 fn refresh_stock(&self, tree: &git2::Worktree) -> Result<FileWork, StorageError> {
214 tree.lock(Some(STOCKING))
215 .map_err(|err| git("lock the stock", STOCK, &err))?;
216 let trunk = self
217 .repo
218 .head()
219 .and_then(|head| head.peel_to_commit())
220 .map_err(|err| git("read the city trunk", STOCK, &err))?;
221 self.repo
222 .branch(STOCK, &trunk, true)
223 .map_err(|err| git("move the stock to the trunk", STOCK, &err))?;
224 let work = restore(tree, STOCK, &[]).and_then(|work| cache_trees(tree).map(|()| work));
228 tree.unlock()
229 .map_err(|err| git("unlock the stock", STOCK, &err))?;
230 work
231 }
232
233 fn take_back(&self, refusal: StorageError) -> StorageError {
237 match self.forget(STOCK) {
238 Ok(()) => refusal,
239 Err(left) => StorageError::Worktree {
240 op: "take back a stock that did not check out",
241 detail: format!("{left}; the checkout failed with: {refusal}"),
242 },
243 }
244 }
245
246 fn branch_at_trunk(&self, name: &WorktreeName) -> Result<(), StorageError> {
250 match self
251 .repo
252 .find_branch(name.as_str(), git2::BranchType::Local)
253 {
254 Ok(_) => Ok(()),
255 Err(err) if err.code() == git2::ErrorCode::NotFound => {
256 let trunk = self
257 .repo
258 .head()
259 .and_then(|head| head.peel_to_commit())
260 .map_err(|err| git("read the city trunk", name.as_str(), &err))?;
261 self.repo
262 .branch(name.as_str(), &trunk, false)
263 .map(drop)
264 .map_err(|err| git("branch a worktree", name.as_str(), &err))
265 }
266 Err(err) => Err(git("find a node branch", name.as_str(), &err)),
267 }
268 }
269}
270
271fn cache_trees(tree: &git2::Worktree) -> Result<(), StorageError> {
278 let repo = git2::Repository::open_from_worktree(tree)
279 .map_err(|err| git("open the stock", STOCK, &err))?;
280 let head = repo
281 .head()
282 .and_then(|head| head.peel_to_tree())
283 .map_err(|err| git("read the stock's branch", STOCK, &err))?;
284 let mut index = repo
285 .index()
286 .map_err(|err| git("read the stock's index", STOCK, &err))?;
287 index
288 .read_tree(&head)
289 .and_then(|()| index.write())
290 .map_err(|err| git("cache the stock's trees", STOCK, &err))
291}
292
293fn write(path: &Path, text: &str) -> Result<(), StorageError> {
295 std::fs::write(path, text).map_err(|source| io("rewrite a worktree link", path, source))
296}
297
298fn io(op: &'static str, path: &Path, source: std::io::Error) -> StorageError {
299 StorageError::Io {
300 op,
301 path: path.to_path_buf(),
302 source,
303 }
304}
305
306fn git(op: &'static str, id: &str, err: &git2::Error) -> StorageError {
307 StorageError::Worktree {
308 op,
309 detail: format!("{id}: {err}"),
310 }
311}
312
313#[cfg(test)]
314#[allow(
315 clippy::unwrap_used,
316 clippy::arithmetic_side_effects,
317 reason = "test code"
318)]
319mod tests {
320 use std::path::Path;
321
322 use kernel::TimeMs;
323
324 use super::super::super::lease::FileWork;
325 use super::super::Landing;
326 use super::super::Worktrees;
327 use super::super::tests::{bulk_city, entries, name, owner};
328 use crate::checkpoint::Checkpoint;
329
330 fn files_under(dir: &Path) -> u64 {
331 u64::try_from(std::fs::read_dir(dir).unwrap().count()).unwrap()
332 }
333
334 #[test]
339 fn a_placement_from_the_stock_creates_no_file_at_either_size() {
340 for files in [32, 64] {
341 let dir = tempfile::tempdir().unwrap();
342 let trees = bulk_city(dir.path(), files);
343 let city = entries(dir.path());
344 let stocked = trees.stock().unwrap();
345 let city_at_claim = entries(dir.path());
346 let placed = trees.claim(&name("node-1"), &["bulk".to_owned()]).unwrap();
347 assert_eq!(
348 (
349 stocked,
350 placed.work(),
351 files_under(&placed.path().join("bulk"))
352 ),
353 (
354 FileWork {
355 created: files,
356 walked: city,
357 ..FileWork::default()
358 },
359 FileWork {
360 walked: city_at_claim + entries(placed.path()),
361 ..FileWork::default()
362 },
363 files,
364 ),
365 "a stock and the placement that takes it at {files} files"
366 );
367 }
368 }
369
370 #[test]
375 fn a_stock_behind_the_trunk_is_placed_at_the_trunk_and_merges() {
376 let dir = tempfile::tempdir().unwrap();
377 let trees = bulk_city(dir.path(), 8);
378 trees.stock().unwrap();
379 std::fs::write(dir.path().join("bulk").join("file-0000.txt"), b"moved on").unwrap();
380 Checkpoint::open(dir.path())
381 .unwrap()
382 .land(&[], TimeMs::new(2_000), &owner(), "checkpoint: bulk")
383 .unwrap();
384
385 let bulk = ["bulk".to_owned()];
386 let placed = trees.claim(&name("node-1"), &bulk).unwrap();
387 let tree = placed.path().to_path_buf();
388 let repo = git2::Repository::open(&tree).unwrap();
389 assert_eq!(
390 (
391 placed.work().created,
392 placed.work().rewritten,
393 std::fs::read_to_string(tree.join("bulk").join("file-0000.txt")).unwrap(),
394 repo.head().unwrap().name().ok().map(str::to_owned),
395 trees.live().unwrap(),
396 ),
397 (
398 0,
399 1,
400 "moved on".to_owned(),
401 Some("refs/heads/node-1".to_owned()),
402 vec![name("node-1")],
403 ),
404 "the stock is the node's tree, at the trunk, on the node's branch"
405 );
406
407 std::fs::write(tree.join("bulk").join("file-0001.txt"), b"from the node").unwrap();
408 Checkpoint::open(&tree)
409 .unwrap()
410 .land(&bulk, TimeMs::new(3_000), &owner(), "offer: bulk")
411 .unwrap();
412 trees.release(placed).unwrap();
413 let of = owner();
414 trees
415 .plan_merge(&name("node-1"))
416 .unwrap()
417 .apply(&Landing {
418 t: TimeMs::new(4_000),
419 of: &of,
420 subject: "merge: node-1",
421 reviewed_by_person: false,
422 })
423 .unwrap();
424 assert_eq!(
425 std::fs::read_to_string(dir.path().join("bulk").join("file-0001.txt")).unwrap(),
426 "from the node"
427 );
428 }
429
430 #[test]
433 fn the_stock_is_no_nodes_tree_and_outlives_the_sweep() {
434 let dir = tempfile::tempdir().unwrap();
435 let trees = bulk_city(dir.path(), 8);
436 trees.stock().unwrap();
437 let swept = Worktrees::sweep_abandoned(dir.path(), &[]).unwrap();
438 let listed = trees.live().unwrap();
439 let placed = trees.claim(&name("node-1"), &[]).unwrap();
440 assert_eq!(
441 (swept, listed, placed.work().created),
442 (Vec::new(), Vec::new(), 0),
443 "the stock survives the sweep and the next placement creates nothing"
444 );
445 }
446}