storage/worktree/landing.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! A merge that has been decided, and what it writes down when it is
7//! made.
8//!
9//! Specified by `crates/storage/spec/Worktree.lean` ยง8-9.
10
11use kernel::TimeMs;
12
13use crate::checkpoint::Provenance;
14use crate::error::StorageError;
15
16use super::trees::Worktrees;
17
18/// What one merge writes down: four values that arrive together and are
19/// meaningless apart - a subject with nobody's provenance under it says
20/// who merged nothing. `reviewed_by_person` adds one `Reviewed-by: Name
21/// <email>` trailer, and only when this repository's git config also
22/// carries `user.name` and `user.email`: the city does not invent a
23/// person's name.
24pub struct Landing<'a> {
25 /// Determinism rule 2: the signature carries the injected instant.
26 pub t: TimeMs,
27 pub of: &'a Provenance,
28 pub subject: &'a str,
29 pub reviewed_by_person: bool,
30}
31
32/// A merge that has been decided and not yet made.
33///
34/// The commit the trunk will land on is settled at construction and
35/// every refusal has already happened, so the line announcing this merge
36/// can be written before the trunk moves. [`PlannedMerge::apply`] is the
37/// only way to move it, and [`Worktrees::plan_merge`] is this value's
38/// only source.
39pub struct PlannedMerge<'a> {
40 /// Reachable from `trees` alone: [`Worktrees::plan_merge`] is this
41 /// value's only source, and the refusals it made are why holding
42 /// one means the merge was allowed.
43 pub(super) trees: &'a Worktrees,
44 pub(super) target: git2::Oid,
45}
46
47/// Names the decision, not the repository holding it: a `Worktrees` has
48/// no useful `Debug` and printing one would say nothing about which
49/// merge this is.
50impl std::fmt::Debug for PlannedMerge<'_> {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 f.debug_struct("PlannedMerge")
53 .field("commit", &self.target)
54 .finish()
55 }
56}
57
58impl PlannedMerge<'_> {
59 /// The commit the city trunk will point at, for the line that says so.
60 pub fn commit(&self) -> String {
61 self.target.to_string()
62 }
63
64 /// Brings a node's committed work into the city's own trunk, as a
65 /// merge commit carrying the merging run's trailers. The judgement
66 /// stays fast-forward only; what the history keeps is the merge
67 /// commit, because a pointer move leaves nothing to read and
68 /// no place to say who verified it.
69 ///
70 /// # Errors
71 /// Propagates a trunk that cannot be read, committed onto or checked
72 /// out. The fast-forward judgement is not repeated: it was made, and
73 /// refused if it had to be, before this value existed.
74 pub fn apply(self, landing: &Landing<'_>) -> Result<(), StorageError> {
75 self.trees.land_merge(self.target, landing)
76 }
77}
78
79/// Whether a checkout only judges the city folder or also writes it.
80#[derive(Clone, Copy)]
81pub(super) enum CheckoutRun {
82 DryRun,
83 Write,
84}
85
86/// Makes the city folder match `tree` without discarding anything:
87/// libgit2's safe strategy judges every path against the trunk as it
88/// stands, and one the person changed and did not commit is a
89/// conflict. Every conflict is named in the refusal, and a refused
90/// checkout has written nothing, because libgit2 finds all conflicts
91/// before it writes a file.
92pub(super) fn check_out(
93 repo: &git2::Repository,
94 tree: &git2::Tree<'_>,
95 run: CheckoutRun,
96) -> Result<(), StorageError> {
97 let mut paths = Vec::new();
98 let mut checkout = git2::build::CheckoutBuilder::new();
99 checkout.safe();
100 match run {
101 CheckoutRun::DryRun => checkout.dry_run(),
102 CheckoutRun::Write => &mut checkout,
103 };
104 checkout.notify_on(git2::CheckoutNotificationType::CONFLICT);
105 checkout.notify(|_, path, _, _, _| {
106 paths.extend(path.map(|p| p.to_string_lossy().into_owned()));
107 true
108 });
109 let outcome = repo.checkout_tree(tree.as_object(), Some(&mut checkout));
110 drop(checkout);
111 if !paths.is_empty() {
112 return Err(StorageError::MergeWouldDiscard { paths });
113 }
114 outcome.map_err(|err| StorageError::Worktree {
115 op: "check out the merged trunk",
116 detail: err.to_string(),
117 })
118}