Skip to main content

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}