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
use std::sync::atomic::AtomicBool;
use super::{Failure, Limits, Report};
use crate::{ObjectId, Repository};
impl Repository {
/// Materializes raw bytes and POSIX modes, then publishes the index from a verified clean
/// baseline.
///
/// `None` means an empty tree for either input. For a no-checkout clone, use an empty baseline
/// and the desired commit's tree as target. For updates, supply the tree represented by the
/// current index. HEAD and references remain unchanged, so Git may report staged changes
/// relative to HEAD after success. See [`crate::checkout`] for raw semantics, caller exclusion,
/// platform limits and failure recovery. No previous status report authorizes this operation.
///
/// # Errors
///
/// Returns [`Failure`] on dirty/staged/conflicted state, obstruction, unsupported input,
/// cancellation, exceeded limits or I/O. Its report records prior mutations and publication;
/// cleanup failures are separate. Preparation errors leave worktree content unchanged.
///
/// # Replace a clean checkout, then remove its tracked content
///
/// This example owns the disposable repository exclusively. The second call supplies the tree
/// represented by the index as its baseline; supplying `None` again would lose that contract.
///
/// ```
/// use std::sync::atomic::AtomicBool;
///
/// use girt::{InitKind, ObjectFormat, Repository};
/// # if cfg!(not(any(target_os = "linux", target_os = "macos"))) { return Ok(()); }
/// let directory = tempfile::tempdir()?;
/// let repo = Repository::init(
/// ObjectFormat::Sha1,
/// directory.path().join("r"),
/// InitKind::Worktree,
/// )?;
///
/// use girt::{EntryMode, Tree, TreeEntry};
///
/// let loose = repo.loose_objects();
/// let blob = loose.write_blob(b"hello\n")?;
/// let tree = loose.write_tree(&Tree::new(
/// ObjectFormat::Sha1,
/// vec![TreeEntry {
/// name: b"hello.txt".to_vec(),
/// mode: EntryMode::Blob,
/// id: blob,
/// }],
/// )?)?;
///
/// let cancel = AtomicBool::new(false);
/// repo.checkout_tree_with(None, Some(tree), Default::default(), &cancel)?;
/// let report = repo.checkout_tree_with(Some(tree), None, Default::default(), &cancel)?;
///
/// assert!(report.index_published);
/// assert!(!repo.worktree().unwrap().join("hello.txt").exists());
/// assert!(loose.read_blob(blob, 1024).is_ok()); // Removing a file does not delete its object.
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
pub fn checkout_tree_with(
&self,
baseline: Option<ObjectId>,
target: Option<ObjectId>,
limits: Limits,
cancel: &AtomicBool,
) -> Result<Report, Failure> {
self.checkout_tree_with_progress(baseline, target, limits, cancel, |_, _| {})
}
/// Materializes a tree with synchronous completed-operation counts `(done, total)`.
///
/// Reports zero after preparation, then each applied file or directory operation. The total
/// includes directories, not just files. The final notification follows index publication;
/// reaching the operation total alone does not establish publication. Observers must return
/// promptly and must not access the repository. A panic may leave the partial mutations
/// described by [`crate::checkout`]'s recovery contract.
///
/// # Errors
///
/// Returns [`Self::checkout_tree_with`]'s failures and partial mutation reports.
pub fn checkout_tree_with_progress(
&self,
baseline: Option<ObjectId>,
target: Option<ObjectId>,
limits: Limits,
cancel: &AtomicBool,
progress: impl FnMut(u64, u64),
) -> Result<Report, Failure> {
let _lock_operation = crate::locks::operation();
for id in baseline.into_iter().chain(target) {
id.require_format(self.object_format())
.map_err(|error| Failure {
cause: Box::new(error.into()),
report: Report::default(),
cleanup: vec![],
})?;
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
let mut progress = progress;
super::worktree::run_observed(
self,
baseline,
target,
None,
limits,
cancel,
&mut |_, _| Ok(()),
&mut progress,
)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
let _ = (baseline, target, limits, progress);
let cause = if cancel.load(std::sync::atomic::Ordering::Relaxed) {
super::Error::Cancelled
} else {
super::types::refused(b"", "checkout requires Linux/macOS")
};
Err(Failure {
cause: Box::new(cause),
report: Report::default(),
cleanup: vec![],
})
}
}
/// Checks out a tree as [`Self::checkout_tree_with`] does, converting files through `filters`.
///
/// Each regular file of the target is written as `target_filters` converts its blob with
/// [`Pipeline::to_worktree`]: line endings, `ident` and filter drivers such as Git LFS. Each
/// baseline file must equal what `baseline_filters` converts its blob to, to count as clean.
/// Load `baseline_filters` with [`Self::filters`], which reads the `.gitattributes` files
/// currently in the working tree, and `target_filters` with [`Self::filters_for_checkout`],
/// which reads the target's own attributes as Git reads those of the index it checks out.
/// Publication verifies the converted bytes. `core.symlinks=false` writes symlink target
/// placeholders; native links and placeholders bypass filters. All conversion
/// happens during preparation, before any working-tree change, so a failing required filter
/// changes nothing; converted contents of the target are held in memory until publication.
/// Paths written unconverted because a non-required driver failed are listed in
/// [`Report::unfiltered`](super::Report::unfiltered).
///
/// Index entries keep zero cached stat words, so a later status reads and converts each file.
///
/// # Errors
///
/// Returns the failures of [`Self::checkout_tree_with`], plus
/// [`Error::Filter`](super::Error::Filter) when converting a blob fails, including a command
/// refused under [`Commands::Refuse`](crate::filter::Commands::Refuse).
///
/// [`Pipeline::to_worktree`]: crate::filter::Pipeline::to_worktree
pub fn checkout_tree_with_filters(
&self,
baseline: Option<ObjectId>,
target: Option<ObjectId>,
baseline_filters: &mut crate::filter::Pipeline,
target_filters: &mut crate::filter::Pipeline,
limits: Limits,
cancel: &AtomicBool,
) -> Result<Report, Failure> {
self.checkout_filtered_with_progress(
baseline,
target,
baseline_filters,
target_filters,
limits,
cancel,
&mut |_, _| {},
)
}
/// [`Self::checkout_tree_with_filters`], reporting progress as
/// [`Self::checkout_tree_with_progress`] does.
#[allow(
clippy::too_many_arguments,
reason = "the public call's inputs plus progress"
)]
pub(crate) fn checkout_filtered_with_progress(
&self,
baseline: Option<ObjectId>,
target: Option<ObjectId>,
baseline_filters: &mut crate::filter::Pipeline,
target_filters: &mut crate::filter::Pipeline,
limits: Limits,
cancel: &AtomicBool,
progress: &mut dyn FnMut(u64, u64),
) -> Result<Report, Failure> {
let _lock_operation = crate::locks::operation();
for id in baseline.into_iter().chain(target) {
id.require_format(self.object_format())
.map_err(|error| Failure {
cause: Box::new(error.into()),
report: Report::default(),
cleanup: vec![],
})?;
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
super::worktree::run_observed(
self,
baseline,
target,
Some(super::worktree::Filters {
baseline: Some(baseline_filters),
target: target_filters,
}),
limits,
cancel,
&mut |_, _| Ok(()),
&mut |done, total| progress(done, total),
)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
let _ = (
baseline,
target,
baseline_filters,
target_filters,
limits,
progress,
);
let cause = if cancel.load(std::sync::atomic::Ordering::Relaxed) {
super::Error::Cancelled
} else {
super::types::refused(b"", "checkout requires Linux/macOS")
};
Err(Failure {
cause: Box::new(cause),
report: Report::default(),
cleanup: vec![],
})
}
}
/// Checks out a tree using the repository's configured symlink representation.
///
/// With `core.symlinks=false`, symlink blobs are written as regular files containing the
/// target bytes, while index entries retain symlink mode. Existing placeholders and native
/// symlinks must match their baseline target bytes before any mutation. Index promises such
/// as assume-unchanged never bypass this verification. Other baseline, exclusion and recovery
/// requirements are those of [`Self::checkout_tree_with`]. Attributes and filters are not
/// applied. Use [`Self::checkout_tree_with`] for literal native-symlink materialization.
/// Configuration comes from this repository handle, including explicitly loaded layers.
///
/// # Errors
///
/// Returns the failures of [`Self::checkout_tree_with`], including invalid `core.symlinks`
/// configuration. Preparation failures leave working-tree content unchanged.
pub fn checkout_tree_configured_with(
&self,
baseline: Option<ObjectId>,
target: Option<ObjectId>,
limits: Limits,
cancel: &AtomicBool,
) -> Result<Report, Failure> {
let _lock_operation = crate::locks::operation();
for id in baseline.into_iter().chain(target) {
id.require_format(self.object_format())
.map_err(|error| Failure {
cause: Box::new(error.into()),
report: Report::default(),
cleanup: vec![],
})?;
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
return super::worktree::run_configured(self, baseline, target, limits, cancel);
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
self.checkout_tree_with(baseline, target, limits, cancel)
}
/// Checks out a tree with configured symlinks, default limits and no cancellation.
///
/// ```
/// use girt::config::{Config, ConfigInputs};
/// use girt::{EntryMode, InitKind, ObjectFormat, Repository, Tree, TreeEntry};
///
/// # if cfg!(not(any(target_os = "linux", target_os = "macos"))) { return Ok(()); }
/// let directory = tempfile::tempdir()?;
/// let root = directory.path().join("repo");
/// Repository::init(ObjectFormat::Sha1, &root, InitKind::Worktree)?;
/// let inputs =
/// ConfigInputs::default().with_command(Some(Config::parse(b"[core]\nsymlinks=false\n")?));
/// let repo = Repository::open_with_config(&root, &inputs)?;
/// let objects = repo.loose_objects();
/// let target = objects.write_blob(b"destination")?;
/// let tree = Tree::new(
/// ObjectFormat::Sha1,
/// vec![TreeEntry {
/// name: b"link".to_vec(),
/// mode: EntryMode::Symlink,
/// id: target,
/// }],
/// )?;
/// let tree = objects.write_tree(&tree)?;
///
/// repo.checkout_tree_configured(None, Some(tree))?;
/// assert_eq!(std::fs::read(root.join("link"))?, b"destination");
/// assert!(!std::fs::symlink_metadata(root.join("link"))?.is_symlink());
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// See [`Self::checkout_tree_configured_with`] for representation, exclusion and recovery.
///
/// # Errors
///
/// Returns the failures of [`Self::checkout_tree_configured_with`] except cancellation.
pub fn checkout_tree_configured(
&self,
baseline: Option<ObjectId>,
target: Option<ObjectId>,
) -> Result<Report, Failure> {
self.checkout_tree_configured_with(
baseline,
target,
Limits::default(),
&AtomicBool::new(false),
)
}
/// Materializes a tree with [`Limits::default`] and no cancellation.
///
/// Equivalent to `self.checkout_tree_with(baseline, target, Limits::default(), &never)` with a
/// flag that is never set; see [`Self::checkout_tree_with`] for the baseline contract, recovery
/// and platform limits. Use [`Self::checkout_tree_with_progress`] to observe progress or
/// cancel.
///
/// # Errors
///
/// Returns the failures of [`Self::checkout_tree_with`] except cancellation.
pub fn checkout_tree(
&self,
baseline: Option<ObjectId>,
target: Option<ObjectId>,
) -> Result<Report, Failure> {
self.checkout_tree_with(baseline, target, Limits::default(), &AtomicBool::new(false))
}
}