girt 0.17.0

An incremental, idiomatic Rust implementation of Git
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
use std::sync::atomic::{AtomicBool, Ordering};

use super::memory::Memory;
use super::{
    ContentHunk, ContentMerge, MarkerOptions, RenameError, TreeError, TreeMerge, TreeMergeOptions,
};
use crate::{EntryMode, ObjectId, Objects, Repository, Tree, index};

/// Bounds for preparing marker objects and a staged index, without writing them.
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct PreparationLimits {
    /// Tree traversal bounds; rename inference is not performed during preparation.
    pub trees: TreeMergeOptions,
    /// Index entry/encoding bounds.
    pub index: index::Limits,
    /// Conservative aggregate rendered conflict-payload bound (default 64 MiB).
    ///
    /// Counts all retained sides and marker lines before rendering, so it can exceed actual
    /// output when common boundaries are removed. Labels and widths are included.
    pub max_marker_bytes: usize,
}

impl Default for PreparationLimits {
    fn default() -> Self {
        Self {
            trees: TreeMergeOptions::default(),
            index: index::Limits::default(),
            max_marker_bytes: 64 * 1024 * 1024,
        }
    }
}

/// Read-only marker-tree and staged-index plan, prepared without external mutation.
///
/// [`Repository::checkout_merge`] installs its generated objects and materializes it through the
/// guarded checkout engine. The index retains original conflict IDs while working files receive
/// marker bytes. The plan does not authorize overwriting a dirty or changed baseline.
///
/// ```no_run
/// use girt::merge::MarkerOptions;
/// use girt::{ObjectId, Repository};
/// fn combine(repo: &Repository, ours: ObjectId, theirs: ObjectId) -> girt::Result<()> {
///     let objects = repo.objects()?;
///     let merged = objects.merge_commits(ours, theirs)?;
///     let baseline = objects.read_commit(ours)?.tree();
///     let plan = merged
///         .result
///         .prepare_checkout(&objects, &MarkerOptions::default())?;
///     // Requires an exclusively owned, clean checkout of ours.
///     let report = repo.checkout_merge(Some(baseline), &plan)?;
///     assert!(report.index_published);
///     Ok(())
/// }
/// ```
#[derive(Clone, Debug)]
pub struct MergeCheckout {
    tree: ObjectId,
    index: index::Index,
    blobs: Vec<(ObjectId, Vec<u8>)>,
    trees: Vec<Tree>,
}

impl MergeCheckout {
    // Stash application can restore working files while preserving a different staged snapshot.
    pub(crate) fn with_index(mut self, index: index::Index) -> Result<Self, PublicationError> {
        if index.object_format() != self.index.object_format() {
            return Err(PublicationError::ObjectFormat);
        }
        self.index = index;
        Ok(self)
    }

    /// Identity of the marker-bearing working-tree snapshot; not proof of object installation.
    pub fn tree_id(&self) -> ObjectId {
        self.tree
    }

    /// Validated index with resolved stage-zero entries and original unresolved stages 1–3.
    ///
    /// Clone this draft when the caller needs an index-only publication through
    /// [`crate::index::IndexEdit::replace_index`]. It does not contain marker blob IDs in stages.
    pub fn index(&self) -> &index::Index {
        &self.index
    }

    /// Installs generated blobs and trees without publishing an index, working files or refs.
    ///
    /// Existing objects are retained. A failure can leave some generated objects installed;
    /// installation is additive and can be retried. Index-only callers must complete this step
    /// before publishing [`Self::index`].
    ///
    /// # Errors
    ///
    /// Returns format or object-write errors; no previously stored object is removed.
    pub fn install_objects(&self, loose: &crate::LooseObjects) -> Result<(), PublicationError> {
        self.install_objects_controlled(loose, &AtomicBool::new(false))
    }

    /// Installs generated objects while checking cancellation between individual writes.
    ///
    /// # Errors
    ///
    /// Returns [`Self::install_objects`]'s errors or cancellation. Completed additive writes
    /// remain.
    pub fn install_objects_controlled(
        &self,
        loose: &crate::LooseObjects,
        cancel: &AtomicBool,
    ) -> Result<(), PublicationError> {
        check(cancel)?;
        if loose.object_format() != self.index.object_format() {
            return Err(PublicationError::ObjectFormat);
        }
        for (_, bytes) in &self.blobs {
            check(cancel)?;
            loose.write_blob(bytes)?;
        }
        for tree in &self.trees {
            check(cancel)?;
            loose.write_tree(tree)?;
        }
        Ok(())
    }
}

/// Preparation or publication failed; working-file failure retains the checkout partial report.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum PublicationError {
    /// Candidate tree traversal could not complete.
    #[error("preparing merge paths")]
    Paths(#[from] RenameError),
    /// A directory conflict or generated tree could not be represented.
    #[error("preparing merge conflict objects")]
    Tree(#[from] TreeError),
    /// The staged index failed validation.
    #[error("preparing merge index")]
    Index(#[from] index::Error),
    /// Additive object installation failed; already installed objects may remain.
    #[error("installing merge objects")]
    Objects(#[from] crate::loose::Error),
    /// Checkout failed; inspect completed mutations, publication and cleanup before recovery.
    #[error("publishing merge checkout")]
    Checkout(#[source] Box<crate::checkout::Failure>),
    /// The plan and selected repository/reader use different object formats.
    #[error("merge publication object format mismatch")]
    ObjectFormat,
    /// Caller-edited generated bytes do not have the identity claimed by the merge result.
    #[error("generated merge blob identity mismatch")]
    ObjectIdentity,
    /// A configured preparation/output bound was reached before publication.
    #[error("merge preparation limit: {0}")]
    Limit(&'static str),
    /// Cancelled before mutable checkout; some additive object writes may already have completed.
    #[error("merge publication cancelled")]
    Cancelled,
}

impl TreeMerge {
    /// Prepares conflict working bytes and original index stages with default bounds.
    ///
    /// See [`Self::prepare_checkout_with`] for byte, object and failure contracts.
    ///
    /// # Errors
    ///
    /// Returns [`PublicationError`] without writing objects, index or working files.
    pub fn prepare_checkout(
        &self,
        objects: &Objects,
        markers: &MarkerOptions,
    ) -> Result<MergeCheckout, PublicationError> {
        self.prepare_checkout_with(
            objects,
            markers,
            PreparationLimits::default(),
            &AtomicBool::new(false),
        )
    }

    /// Prepares a marker-tree snapshot and a staged index without changing external state.
    ///
    /// Content conflicts render with the supplied marker policy; binary/structural conflicts
    /// retain provisional working content. Original leaf entries become stages 1–3, including
    /// distinct rename/rename paths. Resolved entries remain stage zero. Directory conflicts and
    /// stored empty subdirectories are refused. Generated input objects are copied into the plan.
    /// Labels are emitted verbatim; their conservative output cost is bounded before rendering.
    /// The resulting plan is immutable; baseline/worktree validation occurs during publication.
    ///
    /// # Errors
    ///
    /// Returns [`PublicationError`] for input/traversal failures, directory stage conversion,
    /// invalid index paths, mixed formats, output bounds or cancellation. No partial plan escapes.
    pub fn prepare_checkout_with(
        &self,
        objects: &Objects,
        markers: &MarkerOptions,
        limits: PreparationLimits,
        cancel: &AtomicBool,
    ) -> Result<MergeCheckout, PublicationError> {
        check(cancel)?;
        if objects.object_format() != self.tree.object_format() {
            return Err(PublicationError::ObjectFormat);
        }
        let mut memory = Memory::default();
        for tree in &self.trees {
            if tree.object_format() != objects.object_format() {
                return Err(PublicationError::ObjectFormat);
            }
            if !memory.trees.contains_key(&tree.id()) {
                memory.order.push(tree.id());
            }
            memory.trees.insert(tree.id(), tree.clone());
        }
        for (id, bytes) in &self.blobs {
            check(cancel)?;
            if *id != ObjectId::for_blob(objects.object_format(), bytes) {
                return Err(PublicationError::ObjectIdentity);
            }
            memory.blobs.insert(*id, bytes.clone());
        }
        if !memory.trees.contains_key(&self.tree.id()) {
            memory.order.push(self.tree.id());
        }
        memory.trees.insert(self.tree.id(), self.tree.clone());
        let mut leaves =
            super::renames::flatten(objects, self.tree.id(), limits.trees, cancel, &memory)?;
        let mut staged = Vec::new();
        let mut conflict_paths = std::collections::BTreeSet::new();
        let mut remaining = limits.max_marker_bytes;
        for conflict in &self.conflicts {
            check(cancel)?;
            let entries = conflict.index_entries()?;
            for entry in &entries {
                check(cancel)?;
                if entry.mode != index::Mode::Gitlink {
                    memory.read_blob(objects, entry.id)?;
                }
            }
            conflict_paths.extend(entries.iter().map(|entry| entry.path.clone()));
            staged.extend(entries);
            if let Some(content) = &conflict.content {
                remaining = remaining
                    .checked_sub(render_bound(content, markers)?)
                    .ok_or(PublicationError::Limit("marker bytes"))?;
                if let Some(leaf) = leaves.get_mut(&conflict.path) {
                    let bytes = content.render(markers);
                    let id = ObjectId::for_blob(objects.object_format(), &bytes);
                    memory.blobs.insert(id, bytes);
                    leaf.id = id;
                }
            }
        }
        for (path, value) in &leaves {
            if !conflict_paths.contains(path) {
                staged.push(index::Entry::new(path.clone(), mode(value.mode)?, value.id));
            }
        }
        let index = index::Index::new(objects.object_format(), staged, limits.index)?;
        let tree = memory.build(objects, leaves)?;
        check(cancel)?;
        let trees = memory
            .order
            .iter()
            .map(|id| memory.trees[id].clone())
            .collect();
        Ok(MergeCheckout {
            tree,
            index,
            blobs: memory.blobs.into_iter().collect(),
            trees,
        })
    }
}

impl Repository {
    /// Installs generated merge objects, writes conflict working files, then publishes stages.
    ///
    /// Uses default checkout bounds and no cancellation. See [`Self::checkout_merge_with`] for
    /// baseline, platform, concurrency and partial-effect contracts.
    ///
    /// # Errors
    ///
    /// Returns [`PublicationError`]; checkout failures retain their full partial report.
    pub fn checkout_merge(
        &self,
        baseline: Option<ObjectId>,
        plan: &MergeCheckout,
    ) -> Result<crate::checkout::Report, PublicationError> {
        self.checkout_merge_with(
            baseline,
            plan,
            crate::checkout::Limits::default(),
            &AtomicBool::new(false),
        )
    }

    /// Materializes a prepared merge and publishes its staged index under one index guard.
    ///
    /// Requires the same clean baseline and caller-owned exclusion as
    /// [`Self::checkout_tree_with`]. Baseline preparation, untracked/metadata/path protections,
    /// working-file verification, final index publication and partial reports use that engine.
    /// The replacement index is validated before any working-file mutation and stays under the
    /// same guard; there is no temporary stage-zero index representing conflict resolution.
    /// Linux/macOS checkout restrictions apply, including gitlink and unsupported path refusals.
    /// Other platforms return checkout's unsupported failure without installing objects.
    ///
    /// Generated object installation is additive and precedes working-file preparation. A later
    /// failure can leave unreachable objects. Working files are not rolled back; inspect
    /// [`PublicationError::Checkout`] reports before recovery. HEAD and references never move.
    ///
    /// # Errors
    ///
    /// Returns [`PublicationError`] for format mismatch, object installation, cancellation or
    /// guarded checkout failure. Only a successful report proves the staged index was published.
    pub fn checkout_merge_with(
        &self,
        baseline: Option<ObjectId>,
        plan: &MergeCheckout,
        limits: crate::checkout::Limits,
        cancel: &AtomicBool,
    ) -> Result<crate::checkout::Report, PublicationError> {
        let _operation = crate::locks::operation();
        check(cancel)?;
        if self.object_format() != plan.index.object_format()
            || baseline.is_some_and(|id| id.format() != self.object_format())
        {
            return Err(PublicationError::ObjectFormat);
        }
        #[cfg(any(target_os = "linux", target_os = "macos"))]
        {
            plan.install_objects_controlled(&self.loose_objects(), cancel)?;
            crate::checkout::run_with_index(
                self,
                baseline,
                plan.tree,
                plan.index.entries(),
                limits,
                cancel,
            )
            .map_err(|failure| PublicationError::Checkout(Box::new(failure)))
        }
        #[cfg(not(any(target_os = "linux", target_os = "macos")))]
        {
            self.checkout_tree_with(baseline, Some(plan.tree), limits, cancel)
                .map_err(|failure| PublicationError::Checkout(Box::new(failure)))
        }
    }
}

fn check(cancel: &AtomicBool) -> Result<(), PublicationError> {
    if cancel.load(Ordering::Relaxed) {
        Err(PublicationError::Cancelled)
    } else {
        Ok(())
    }
}

fn mode(mode: EntryMode) -> Result<index::Mode, PublicationError> {
    Ok(match mode {
        EntryMode::Blob => index::Mode::Regular,
        EntryMode::Executable => index::Mode::Executable,
        EntryMode::Symlink => index::Mode::Symlink,
        EntryMode::Gitlink => index::Mode::Gitlink,
        EntryMode::Tree => return Err(TreeError::DirectoryStages.into()),
    })
}

fn render_bound(
    content: &ContentMerge,
    markers: &MarkerOptions,
) -> Result<usize, PublicationError> {
    let limit = || PublicationError::Limit("marker bytes");
    let mut count = 0usize;
    for hunk in content.hunks() {
        let bytes = match hunk {
            ContentHunk::Resolved(bytes) => bytes.len(),
            ContentHunk::Conflict { ours, .. } if content.is_binary() => ours.len(),
            ContentHunk::Conflict { base, ours, theirs } => {
                let label = markers
                    .base
                    .len()
                    .max(markers.ours.len())
                    .max(markers.theirs.len());
                let lines = markers
                    .width
                    .get()
                    .checked_add(label)
                    .and_then(|n| n.checked_add(2))
                    .and_then(|n| n.checked_mul(4))
                    .ok_or_else(limit)?;
                base.len()
                    .checked_add(ours.len())
                    .and_then(|n| n.checked_add(theirs.len()))
                    .and_then(|n| n.checked_add(lines))
                    .and_then(|n| n.checked_add(3))
                    .ok_or_else(limit)?
            }
        };
        count = count.checked_add(bytes).ok_or_else(limit)?;
    }
    Ok(count)
}