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 = open_city(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 landing.reviewer(&self.city_root)
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 pub fn release(&self, lease: WorktreeLease) -> Result<(), StorageError> {
328 let refuse = |op: &'static str, err: git2::Error| StorageError::Worktree {
329 op,
330 detail: format!("{}: {err}", lease.name().as_str()),
331 };
332 self.repo
333 .find_worktree(lease.name().as_str())
334 .map_err(|err| refuse("find a worktree", err))?
335 .unlock()
336 .map_err(|err| refuse("unlock a worktree", err))
337 }
338
339 pub fn live(&self) -> Result<Vec<WorktreeName>, StorageError> {
345 let names = self
346 .repo
347 .worktrees()
348 .map_err(|err| StorageError::Worktree {
349 op: "list worktrees",
350 detail: err.to_string(),
351 })?;
352 let mut out = Vec::new();
353 for name in names.iter().flatten().flatten() {
356 if name != stock::STOCK {
357 out.push(WorktreeName::parse(name)?);
358 }
359 }
360 out.sort();
361 Ok(out)
362 }
363}
364
365pub(super) fn open_city(city_root: &Path) -> Result<git2::Repository, git2::Error> {
367 own_config(git2::Repository::open(city_root)?)
368}
369
370pub(super) fn open_tree(tree: &git2::Worktree) -> Result<git2::Repository, git2::Error> {
372 own_config(git2::Repository::open_from_worktree(tree)?)
373}
374
375fn own_config(repo: git2::Repository) -> Result<git2::Repository, git2::Error> {
379 let mut config = git2::Config::new()?;
380 config.add_file(
381 &repo.commondir().join("config"),
382 git2::ConfigLevel::Local,
383 false,
384 )?;
385 repo.set_config(&config)?;
386 Ok(repo)
387}
388
389#[cfg(test)]
390#[allow(
391 clippy::unwrap_used,
392 clippy::expect_used,
393 clippy::panic,
394 clippy::indexing_slicing,
395 clippy::arithmetic_side_effects,
396 reason = "test code"
397)]
398pub(crate) mod tests;