1use super::*;
6use crate::attributes::*;
7use crate::filter::*;
8use crate::index::*;
9use crate::index_io::*;
10use crate::status::*;
11use crate::types_admin::*;
12use sley_pathspec::{PathspecElement, PathspecMatchMagic};
13
14pub fn deleted_index_entries(
15 worktree_root: impl AsRef<Path>,
16 git_dir: impl AsRef<Path>,
17 format: ObjectFormat,
18) -> Result<Vec<IndexEntry>> {
19 let worktree_root = worktree_root.as_ref();
20 let git_dir = git_dir.as_ref();
21 let index_path = repository_index_path(git_dir);
22 if !index_path.exists() {
23 return Ok(Vec::new());
24 }
25 let index = Index::parse(&fs::read(index_path)?, format)?;
26 let mut deleted = Vec::new();
27 for entry in index.entries {
28 if !worktree_path(worktree_root, entry.path.as_bytes())?.exists()
29 && !index_entry_skip_worktree(&entry)
30 {
31 deleted.push(entry);
32 }
33 }
34 Ok(deleted)
35}
36
37pub fn modified_index_entries(
38 worktree_root: impl AsRef<Path>,
39 git_dir: impl AsRef<Path>,
40 format: ObjectFormat,
41) -> Result<Vec<IndexEntry>> {
42 let worktree_root = worktree_root.as_ref();
43 let git_dir = git_dir.as_ref();
44 let index_path = repository_index_path(git_dir);
45 if !index_path.exists() {
46 return Ok(Vec::new());
47 }
48 let mut index = Index::parse(&fs::read(&index_path)?, format)?;
49 let sparse_checkout_active = sparse_checkout_active_for_status(git_dir, &index);
50 if index.entries.iter().any(IndexEntry::is_sparse_dir) {
51 let db = FileObjectDatabase::from_git_dir(git_dir, format);
52 expand_sparse_index(&mut index, &db, format)?;
53 }
54 let stat_cache = IndexStatCache::from_index(&index, &index_path);
59 let mut modified = Vec::new();
60 for entry in index.entries {
61 if index_entry_skip_worktree(&entry) && !sparse_checkout_active {
62 continue;
63 }
64 let worktree_entry = worktree_entry_for_git_path(
65 worktree_root,
66 git_dir,
67 format,
68 entry.path.as_bytes(),
69 &entry.oid,
70 entry.mode,
71 Some(&stat_cache),
72 )?;
73 let Some(worktree_entry) = worktree_entry else {
74 if !index_entry_skip_worktree(&entry) {
75 modified.push(entry);
76 }
77 continue;
78 };
79 if worktree_entry.mode != entry.mode || worktree_entry.oid != entry.oid {
80 modified.push(entry);
81 }
82 }
83 Ok(modified)
84}
85
86pub fn checkout_branch(
87 worktree_root: impl AsRef<Path>,
88 git_dir: impl AsRef<Path>,
89 format: ObjectFormat,
90 branch: &str,
91 committer: Vec<u8>,
92) -> Result<CheckoutResult> {
93 let worktree_root = worktree_root.as_ref();
94 let git_dir = git_dir.as_ref();
95 let branch_ref = branch_ref_name(branch)?;
96 let refs = FileRefStore::new(git_dir, format);
97 let target = match sley_refs::resolve_ref_peeled(&refs, &branch_ref)? {
98 Some(oid) => oid,
99 None => {
100 checkout_switch_head_symbolic(&refs, branch_ref, committer, branch, None, None)?;
101 return Ok(CheckoutResult {
102 branch: branch.into(),
103 oid: ObjectId::null(format),
104 files: 0,
105 });
106 }
107 };
108 let current_head = resolve_head_commit_oid(git_dir, format)?;
109 let files = if current_head == Some(target) {
110 0
111 } else {
112 checkout_commit_to_index_and_worktree(worktree_root, git_dir, format, &target)?
113 };
114 checkout_switch_head_symbolic(
115 &refs,
116 branch_ref,
117 committer,
118 branch,
119 Some(target),
120 Some(target),
121 )?;
122 Ok(CheckoutResult {
123 branch: branch.into(),
124 oid: target,
125 files,
126 })
127}
128
129pub fn checkout_detached(
130 worktree_root: impl AsRef<Path>,
131 git_dir: impl AsRef<Path>,
132 format: ObjectFormat,
133 target: &ObjectId,
134 committer: Vec<u8>,
135 message: Vec<u8>,
136) -> Result<CheckoutResult> {
137 let worktree_root = worktree_root.as_ref();
138 let git_dir = git_dir.as_ref();
139 let files = checkout_commit_to_index_and_worktree(worktree_root, git_dir, format, target)?;
140 let refs = FileRefStore::new(git_dir, format);
141 let zero = ObjectId::null(format);
142 let mut tx = refs.transaction();
143 tx.update(RefUpdate {
144 name: "HEAD".into(),
145 expected: None,
146 new: RefTarget::Direct(*target),
147 reflog: Some(ReflogEntry {
148 old_oid: zero,
149 new_oid: *target,
150 committer,
151 message,
152 }),
153 });
154 tx.commit()?;
155 Ok(CheckoutResult {
156 branch: target.to_string(),
157 oid: *target,
158 files,
159 })
160}
161
162pub fn checkout_branch_filtered(
167 worktree_root: impl AsRef<Path>,
168 git_dir: impl AsRef<Path>,
169 format: ObjectFormat,
170 branch: &str,
171 committer: Vec<u8>,
172 config: &GitConfig,
173) -> Result<CheckoutResult> {
174 let worktree_root = worktree_root.as_ref();
175 let git_dir = git_dir.as_ref();
176 let branch_ref = branch_ref_name(branch)?;
177 let refs = FileRefStore::new(git_dir, format);
178 let target = match sley_refs::resolve_ref_peeled(&refs, &branch_ref)? {
179 Some(oid) => oid,
180 None => {
181 checkout_switch_head_symbolic(&refs, branch_ref, committer, branch, None, None)?;
182 return Ok(CheckoutResult {
183 branch: branch.into(),
184 oid: ObjectId::null(format),
185 files: 0,
186 });
187 }
188 };
189 let current_head = resolve_head_commit_oid(git_dir, format)?;
190 let files = if current_head == Some(target) {
191 0
192 } else {
193 checkout_commit_to_index_and_worktree_filtered(
194 worktree_root,
195 git_dir,
196 format,
197 &target,
198 Some(config),
199 Some(vec![
200 ("ref".to_string(), branch_ref.clone()),
201 ("treeish".to_string(), target.to_hex()),
202 ]),
203 )?
204 };
205 checkout_switch_head_symbolic(
206 &refs,
207 branch_ref,
208 committer,
209 branch,
210 Some(target),
211 Some(target),
212 )?;
213 Ok(CheckoutResult {
214 branch: branch.into(),
215 oid: target,
216 files,
217 })
218}
219
220pub fn checkout_detached_filtered(
223 worktree_root: impl AsRef<Path>,
224 git_dir: impl AsRef<Path>,
225 format: ObjectFormat,
226 target: &ObjectId,
227 committer: Vec<u8>,
228 message: Vec<u8>,
229 config: &GitConfig,
230) -> Result<CheckoutResult> {
231 let worktree_root = worktree_root.as_ref();
232 let git_dir = git_dir.as_ref();
233 let files = checkout_commit_to_index_and_worktree_filtered(
234 worktree_root,
235 git_dir,
236 format,
237 target,
238 Some(config),
239 Some(vec![("treeish".to_string(), target.to_hex())]),
240 )?;
241 let refs = FileRefStore::new(git_dir, format);
242 let zero = ObjectId::null(format);
243 let mut tx = refs.transaction();
244 tx.update(RefUpdate {
245 name: "HEAD".into(),
246 expected: None,
247 new: RefTarget::Direct(*target),
248 reflog: Some(ReflogEntry {
249 old_oid: zero,
250 new_oid: *target,
251 committer,
252 message,
253 }),
254 });
255 tx.commit()?;
256 Ok(CheckoutResult {
257 branch: target.to_string(),
258 oid: *target,
259 files,
260 })
261}
262
263pub(crate) fn checkout_commit_to_index_and_worktree(
264 worktree_root: &Path,
265 git_dir: &Path,
266 format: ObjectFormat,
267 target: &ObjectId,
268) -> Result<usize> {
269 checkout_commit_to_index_and_worktree_filtered(
270 worktree_root,
271 git_dir,
272 format,
273 target,
274 None,
275 None,
276 )
277}
278
279pub(crate) fn checkout_commit_to_index_and_worktree_filtered(
284 worktree_root: &Path,
285 git_dir: &Path,
286 format: ObjectFormat,
287 target: &ObjectId,
288 smudge_config: Option<&GitConfig>,
289 process_metadata: Option<Vec<(String, String)>>,
290) -> Result<usize> {
291 if let Some((sparse, mode)) = active_sparse_checkout(git_dir)? {
292 return checkout_commit_to_index_and_worktree_sparse(
293 worktree_root,
294 git_dir,
295 format,
296 target,
297 Some((&sparse, mode)),
298 smudge_config,
299 process_metadata,
300 );
301 }
302 let _process_filter_metadata = set_process_filter_metadata(process_metadata);
303 let _process_filter_cwd = set_process_filter_cwd(Some(worktree_root.to_path_buf()));
304 let mut dirty = false;
305 if smudge_config.is_some() {
306 dirty = !modified_index_entries(worktree_root, git_dir, format)?.is_empty();
307 } else {
308 stream_short_status(worktree_root, git_dir, format, |entry| {
309 if !status_row_is_untracked_or_ignored(entry) {
310 dirty = true;
311 return Ok(StreamControl::Stop);
312 }
313 Ok(StreamControl::Continue)
314 })?;
315 }
316 if dirty {
317 return Err(GitError::Transaction(
318 "checkout requires a clean working tree".into(),
319 ));
320 }
321 let db = FileObjectDatabase::from_git_dir(git_dir, format);
322 let commit = read_commit(&db, format, target)?;
323 let mut target_entries = BTreeMap::new();
324 collect_tree_entries(&db, format, &commit.tree, &mut target_entries)?;
325 refuse_if_current_working_directory_becomes_file(worktree_root, &target_entries)?;
326
327 let attributes = smudge_config
328 .map(|_| build_tree_attribute_matcher(worktree_root, &db, format, &commit.tree))
329 .transpose()?;
330
331 let old_index_entries = read_index_entries(git_dir, format)?;
332 for (path, old_entry) in &old_index_entries {
333 if !target_entries.contains_key(path) {
334 remove_checkout_tracked_path(worktree_root, path, old_entry)?;
335 }
336 }
337
338 let ignore_case = checkout_should_detect_case_collisions(worktree_root, git_dir);
339 let mut materialized_paths: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
340 let mut collided_paths: BTreeSet<Vec<u8>> = BTreeSet::new();
341 let mut index_entries = Vec::new();
342 let mut delayed_checkout = DelayedCheckoutQueue::default();
343 for (path, entry) in &target_entries {
344 if ignore_case {
345 let folded = checkout_collision_key(path);
346 if let Some((_, existing_path)) = materialized_paths
347 .iter()
348 .find(|(existing, _)| checkout_paths_collide(existing, &folded))
349 {
350 collided_paths.insert(existing_path.clone());
351 collided_paths.insert(path.clone());
352 index_entries.push(unmaterialized_index_entry(path, entry));
353 continue;
354 }
355 materialized_paths.push((folded, path.clone()));
356 }
357 index_entries.push(materialize_tree_entry_with_optional_smudge(
363 &db,
364 format,
365 worktree_root,
366 path,
367 entry,
368 smudge_config,
369 attributes.as_ref(),
370 Some(&mut delayed_checkout),
371 )?);
372 }
373 let mut delayed_updates = finish_delayed_checkout(worktree_root, delayed_checkout)?;
374 for entry in &mut index_entries {
375 if let Some(updated) = delayed_updates.remove(entry.path.as_bytes()) {
376 *entry = updated;
377 }
378 }
379 warn_checkout_collisions(&collided_paths);
380 index_entries.sort_by(|left, right| left.path.cmp(&right.path));
381 let extensions = preserved_index_extensions(git_dir, format)?;
382 let mut index = Index {
383 version: 2,
384 entries: index_entries,
385 extensions,
386 checksum: None,
387 };
388 refresh_cache_tree(&mut index, &db);
389 write_repository_index_ref(git_dir, format, &index)?;
390 Ok(target_entries.len())
391}
392
393fn remove_checkout_tracked_path(
394 worktree_root: &Path,
395 path: &[u8],
396 entry: &TrackedEntry,
397) -> Result<()> {
398 if !sley_index::is_gitlink(entry.mode) {
399 return remove_worktree_file(worktree_root, path);
400 }
401 let file = worktree_path(worktree_root, path)?;
402 if !file.exists() {
403 return Ok(());
404 }
405 if !file.is_dir() {
406 return remove_worktree_file(worktree_root, path);
407 }
408 match fs::remove_dir(&file) {
409 Ok(()) => prune_empty_parents(worktree_root, file.parent())?,
410 Err(err) if err.kind() == std::io::ErrorKind::DirectoryNotEmpty => {
411 eprintln!(
412 "warning: unable to rmdir '{}': Directory not empty",
413 String::from_utf8_lossy(path)
414 );
415 }
416 Err(err) => return Err(err.into()),
417 }
418 Ok(())
419}
420
421fn checkout_should_detect_case_collisions(worktree_root: &Path, git_dir: &Path) -> bool {
422 GitConfig::read(git_dir.join("config"))
423 .ok()
424 .and_then(|config| config.get_bool("core", None, "ignorecase"))
425 .unwrap_or(false)
426 || filesystem_is_case_insensitive(worktree_root)
427}
428
429fn filesystem_is_case_insensitive(root: &Path) -> bool {
430 let probe = root.join(format!(".sley-case-probe-{}", std::process::id()));
431 let upper = root.join(format!(".SLEY-CASE-PROBE-{}", std::process::id()));
432 let result = (|| -> std::io::Result<bool> {
433 fs::write(&probe, b"lower")?;
434 Ok(upper.exists())
435 })();
436 let _ = fs::remove_file(&probe);
437 if upper != probe {
438 let _ = fs::remove_file(&upper);
439 }
440 result.unwrap_or(false)
441}
442
443fn checkout_collision_key(path: &[u8]) -> Vec<u8> {
444 path.iter().map(u8::to_ascii_lowercase).collect()
445}
446
447fn checkout_paths_collide(left: &[u8], right: &[u8]) -> bool {
448 left == right
449 || left
450 .strip_prefix(right)
451 .is_some_and(|suffix| suffix.starts_with(b"/"))
452 || right
453 .strip_prefix(left)
454 .is_some_and(|suffix| suffix.starts_with(b"/"))
455}
456
457fn unmaterialized_index_entry(path: &[u8], entry: &TrackedEntry) -> IndexEntry {
458 IndexEntry {
459 ctime_seconds: 0,
460 ctime_nanoseconds: 0,
461 mtime_seconds: 0,
462 mtime_nanoseconds: 0,
463 dev: 0,
464 ino: 0,
465 mode: entry.mode,
466 uid: 0,
467 gid: 0,
468 size: 0,
469 oid: entry.oid,
470 flags: path.len().min(0x0fff) as u16,
471 flags_extended: 0,
472 path: BString::from(path),
473 }
474}
475
476pub(crate) fn unmaterialized_index_entry_from_index(entry: &IndexEntry) -> IndexEntry {
477 let mut unmaterialized = entry.clone();
478 unmaterialized.ctime_seconds = 0;
479 unmaterialized.ctime_nanoseconds = 0;
480 unmaterialized.mtime_seconds = 0;
481 unmaterialized.mtime_nanoseconds = 0;
482 unmaterialized.dev = 0;
483 unmaterialized.ino = 0;
484 unmaterialized.uid = 0;
485 unmaterialized.gid = 0;
486 unmaterialized.size = 0;
487 unmaterialized
488}
489
490#[derive(Default)]
491pub(crate) struct DelayedCheckoutQueue {
492 filters: BTreeSet<String>,
493 pending: BTreeMap<Vec<u8>, DelayedCheckoutEntry>,
494}
495
496struct DelayedCheckoutEntry {
497 process: String,
498 entry: TrackedEntry,
499}
500
501impl DelayedCheckoutQueue {
502 pub(crate) fn is_empty(&self) -> bool {
503 self.pending.is_empty()
504 }
505
506 pub(crate) fn enqueue(&mut self, process: String, path: &[u8], entry: &TrackedEntry) {
507 self.filters.insert(process.clone());
508 self.pending.insert(
509 path.to_vec(),
510 DelayedCheckoutEntry {
511 process,
512 entry: entry.clone(),
513 },
514 );
515 }
516}
517
518pub(crate) fn finish_delayed_checkout(
519 worktree_root: &Path,
520 mut delayed: DelayedCheckoutQueue,
521) -> Result<BTreeMap<Vec<u8>, IndexEntry>> {
522 if delayed.is_empty() {
523 return Ok(BTreeMap::new());
524 }
525
526 let mut updates = BTreeMap::new();
527 let mut had_error = false;
528 let mut active_filters = delayed.filters.iter().cloned().collect::<Vec<_>>();
529 while !active_filters.is_empty() {
530 let mut next_filters = Vec::new();
531 for process in active_filters {
532 let mut available = match list_available_process_filter_blobs(&process) {
533 Ok(paths) => paths,
534 Err(err) => {
535 if err.protocol {
536 eprintln!("error: external filter '{}' failed", process);
537 }
538 had_error = true;
539 continue;
540 }
541 };
542 if available.is_empty() {
543 continue;
544 }
545 available.sort();
546 available.dedup();
547
548 let mut keep_filter = true;
549 for path in available {
550 let Some(delayed_entry) = delayed.pending.remove(path.as_slice()) else {
551 eprintln!(
552 "error: external filter '{}' signaled that '{}' is now available although it has not been delayed earlier",
553 process,
554 String::from_utf8_lossy(&path)
555 );
556 had_error = true;
557 keep_filter = false;
558 continue;
559 };
560 if delayed_entry.process != process {
561 eprintln!(
562 "error: external filter '{}' signaled that '{}' is now available although it has not been delayed earlier",
563 process,
564 String::from_utf8_lossy(&path)
565 );
566 had_error = true;
567 keep_filter = false;
568 continue;
569 }
570
571 match run_process_filter(
572 &process,
573 "smudge",
574 &path,
575 &[],
576 Some(delayed_entry.entry.oid),
577 false,
578 ) {
579 Ok(ProcessFilterOutcome::Filtered(output)) => {
580 let index_entry = write_delayed_checkout_output(
581 worktree_root,
582 &path,
583 &delayed_entry.entry,
584 &output,
585 )?;
586 if let Some(index_entry) = index_entry {
587 updates.insert(path, index_entry);
588 }
589 }
590 Ok(ProcessFilterOutcome::Unsupported) => {
591 eprintln!("error: external filter '{}' failed", process);
592 had_error = true;
593 keep_filter = false;
594 }
595 Ok(ProcessFilterOutcome::Status(status)) => {
596 eprintln!(
597 "error: external filter '{}' returned status {status}",
598 process
599 );
600 had_error = true;
601 keep_filter = false;
602 }
603 Err(err) => {
604 if err.protocol {
605 eprintln!("error: external filter '{}' failed", process);
606 }
607 had_error = true;
608 keep_filter = false;
609 }
610 }
611 }
612
613 if keep_filter {
614 next_filters.push(process);
615 }
616 }
617 active_filters = next_filters;
618 }
619
620 for path in delayed.pending.keys() {
621 eprintln!(
622 "error: '{}' was not filtered properly",
623 String::from_utf8_lossy(path)
624 );
625 had_error = true;
626 }
627
628 if had_error {
629 return Err(GitError::Exit(1));
630 }
631 Ok(updates)
632}
633
634fn write_delayed_checkout_output(
635 worktree_root: &Path,
636 path: &[u8],
637 entry: &TrackedEntry,
638 body: &[u8],
639) -> Result<Option<IndexEntry>> {
640 if checkout_path_has_symlink_parent(worktree_root, path)? {
641 return Ok(None);
642 }
643 let file_path = worktree_path(worktree_root, path)?;
644 prepare_blob_parent_dirs(worktree_root, &file_path)?;
645 remove_existing_worktree_path(&file_path)?;
646 fs::write(&file_path, body)?;
647 set_worktree_file_mode(&file_path, entry.mode)?;
648 let metadata = fs::metadata(&file_path)?;
649 let mut index_entry = index_entry_from_metadata(path.to_vec(), entry.oid, &metadata);
650 index_entry.mode = entry.mode;
651 Ok(Some(index_entry))
652}
653
654fn checkout_path_has_symlink_parent(worktree_root: &Path, path: &[u8]) -> Result<bool> {
655 let rel = std::str::from_utf8(path)
656 .map_err(|_| GitError::InvalidFormat("non-utf8 worktree path".into()))?;
657 let mut current = worktree_root.to_path_buf();
658 let mut components = Path::new(rel).components().peekable();
659 while let Some(component) = components.next() {
660 if components.peek().is_none() {
661 break;
662 }
663 let std::path::Component::Normal(name) = component else {
664 continue;
665 };
666 current.push(name);
667 match fs::symlink_metadata(¤t) {
668 Ok(metadata) if metadata.file_type().is_symlink() => return Ok(true),
669 Ok(_) => {}
670 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false),
671 Err(err) => return Err(err.into()),
672 }
673 }
674 Ok(false)
675}
676
677fn warn_checkout_collisions(collided_paths: &BTreeSet<Vec<u8>>) {
678 if collided_paths.is_empty() {
679 return;
680 }
681 eprintln!("warning: the following paths have collided:");
682 for path in collided_paths {
683 eprintln!("{}", String::from_utf8_lossy(path));
684 }
685}
686
687pub(crate) fn build_tree_attribute_matcher(
691 worktree_root: &Path,
692 db: &FileObjectDatabase,
693 format: ObjectFormat,
694 tree_oid: &ObjectId,
695) -> Result<AttributeMatcher> {
696 let mut matcher = AttributeMatcher::default();
697 let git_dir = worktree_root.join(".git");
698 matcher.configure_case_sensitivity(&git_dir);
699 if !matcher.read_configured_attributes(worktree_root, &git_dir) {
700 matcher.read_default_global_attributes();
701 }
702 collect_attribute_patterns_from_tree(db, format, tree_oid, Vec::new(), &mut matcher)?;
703 read_attribute_patterns(
704 worktree_root.join(".git").join("info").join("attributes"),
705 &mut matcher,
706 &[],
707 b".git/info/attributes",
708 false,
709 );
710 Ok(matcher)
711}
712
713pub(crate) fn materialize_tree_entry_with_optional_smudge(
714 db: &FileObjectDatabase,
715 format: ObjectFormat,
716 worktree_root: &Path,
717 path: &[u8],
718 entry: &TrackedEntry,
719 smudge_config: Option<&GitConfig>,
720 attributes: Option<&AttributeMatcher>,
721 mut delayed: Option<&mut DelayedCheckoutQueue>,
722) -> Result<IndexEntry> {
723 if smudge_config.is_none()
730 || sley_index::is_gitlink(entry.mode)
731 || (entry.mode & 0o170000) == 0o120000
732 {
733 return materialize_tree_entry(db, worktree_root, path, entry);
734 }
735 let config = smudge_config.expect("checked above");
736 let matcher = attributes.expect("attributes are built when smudge_config is set");
737 let object = read_expected_object(db, &entry.oid, ObjectType::Blob)?;
738 let checks = matcher.attributes_for_path(path, &filter_attribute_names(), false);
739 let body = match apply_smudge_filter_with_attributes_maybe_delayed(
740 config,
741 &checks,
742 path,
743 &object.body,
744 format,
745 delayed.is_some(),
746 )? {
747 SmudgeFilterResult::Content(body) => body,
748 SmudgeFilterResult::Delayed { process } => {
749 let queue = delayed
750 .as_deref_mut()
751 .expect("delay is only reported when a queue is available");
752 queue.enqueue(process, path, entry);
753 return Ok(unmaterialized_index_entry(path, entry));
754 }
755 };
756 let file_path = worktree_path(worktree_root, path)?;
757 prepare_blob_parent_dirs(worktree_root, &file_path)?;
758 remove_existing_worktree_path(&file_path)?;
759 fs::write(&file_path, &body)?;
760 set_worktree_file_mode(&file_path, entry.mode)?;
761 let metadata = fs::metadata(&file_path)?;
762 let mut index_entry = index_entry_from_metadata(path.to_vec(), entry.oid, &metadata);
763 index_entry.mode = entry.mode;
764 Ok(index_entry)
765}
766
767pub(crate) fn checkout_commit_to_index_and_worktree_sparse(
778 worktree_root: &Path,
779 git_dir: &Path,
780 format: ObjectFormat,
781 target: &ObjectId,
782 sparse: Option<(&SparseCheckout, SparseCheckoutMode)>,
783 smudge_config: Option<&GitConfig>,
784 process_metadata: Option<Vec<(String, String)>>,
785) -> Result<usize> {
786 let _process_filter_metadata = set_process_filter_metadata(process_metadata);
787 let _process_filter_cwd = set_process_filter_cwd(Some(worktree_root.to_path_buf()));
788 let previously_skipped = skip_worktree_paths(git_dir, format)?;
789 let db = FileObjectDatabase::from_git_dir(git_dir, format);
790 let commit = read_commit(&db, format, target)?;
791 let mut target_entries = BTreeMap::new();
792 collect_tree_entries(&db, format, &commit.tree, &mut target_entries)?;
793
794 let mut dirty = false;
797 stream_short_status(worktree_root, git_dir, format, |entry| {
798 if previously_skipped.contains(entry.path) {
799 return Ok(StreamControl::Continue);
800 }
801 if entry.index_mode.is_some_and(sley_index::is_gitlink)
806 || entry.worktree_mode.is_some_and(sley_index::is_gitlink)
807 {
808 return Ok(StreamControl::Continue);
809 }
810 if entry.index == b'?' && entry.worktree == b'?' {
814 let path = entry.path.strip_suffix(b"/").unwrap_or(entry.path);
815 if target_entries
816 .get(path)
817 .is_some_and(|target| sley_index::is_gitlink(target.mode))
818 {
819 return Ok(StreamControl::Continue);
820 }
821 }
822 dirty = true;
823 Ok(StreamControl::Stop)
824 })?;
825 if dirty {
826 return Err(GitError::Transaction(
827 "checkout requires a clean working tree".into(),
828 ));
829 }
830
831 let matcher = sparse.map(|(spec, mode)| SparseMatcher::new(spec, mode));
832 let attributes = smudge_config
833 .map(|_| build_tree_attribute_matcher(worktree_root, &db, format, &commit.tree))
834 .transpose()?;
835
836 for path in read_index_entries(git_dir, format)?.keys() {
837 if target_entries.contains_key(path) {
838 continue;
839 }
840 if previously_skipped.contains(path) {
842 continue;
843 }
844 remove_worktree_file(worktree_root, path)?;
845 }
846
847 let mut index_entries = Vec::new();
848 let mut delayed_checkout = DelayedCheckoutQueue::default();
849 for (path, entry) in &target_entries {
850 let in_cone = matcher.as_ref().map_or_else(
851 || !previously_skipped.contains(path),
852 |matcher| matcher.includes_file(path),
853 );
854 let index_entry = if in_cone {
855 materialize_tree_entry_with_optional_smudge(
856 &db,
857 format,
858 worktree_root,
859 path,
860 entry,
861 smudge_config,
862 attributes.as_ref(),
863 Some(&mut delayed_checkout),
864 )?
865 } else {
866 remove_worktree_file(worktree_root, path)?;
870 let mut index_entry = restored_head_index_entry(worktree_root, &db, path, entry)?;
871 set_skip_worktree(&mut index_entry);
872 index_entry
873 };
874 index_entries.push(index_entry);
875 }
876 let mut delayed_updates = finish_delayed_checkout(worktree_root, delayed_checkout)?;
877 for entry in &mut index_entries {
878 if let Some(updated) = delayed_updates.remove(entry.path.as_bytes()) {
879 *entry = updated;
880 }
881 }
882 index_entries.sort_by(|left, right| left.path.cmp(&right.path));
883 let mut index = Index {
884 version: 2,
885 entries: index_entries,
886 extensions: preserved_index_extensions(git_dir, format)?,
887 checksum: None,
888 };
889 normalize_index_version_for_extended_flags(&mut index);
890 refresh_cache_tree(&mut index, &db);
891 write_repository_index_ref(git_dir, format, &index)?;
892 Ok(target_entries.len())
893}
894
895pub(crate) fn skip_worktree_paths(
896 git_dir: &Path,
897 format: ObjectFormat,
898) -> Result<BTreeSet<Vec<u8>>> {
899 let index_path = repository_index_path(git_dir);
900 if !index_path.exists() {
901 return Ok(BTreeSet::new());
902 }
903 let index = Index::parse(&fs::read(index_path)?, format)?;
904 Ok(index
905 .entries
906 .into_iter()
907 .filter(index_entry_skip_worktree)
908 .map(|entry| entry.path.into_bytes())
909 .collect())
910}
911
912pub fn restore_worktree_paths(
913 worktree_root: impl AsRef<Path>,
914 git_dir: impl AsRef<Path>,
915 format: ObjectFormat,
916 paths: &[PathBuf],
917) -> Result<RestoreResult> {
918 restore_worktree_paths_inner(
919 worktree_root.as_ref(),
920 git_dir.as_ref(),
921 format,
922 paths,
923 None,
924 )
925}
926
927pub fn restore_worktree_paths_filtered(
930 worktree_root: impl AsRef<Path>,
931 git_dir: impl AsRef<Path>,
932 format: ObjectFormat,
933 paths: &[PathBuf],
934 config: &GitConfig,
935) -> Result<RestoreResult> {
936 restore_worktree_paths_inner(
937 worktree_root.as_ref(),
938 git_dir.as_ref(),
939 format,
940 paths,
941 Some(config),
942 )
943}
944
945pub(crate) fn restore_worktree_paths_inner(
946 worktree_root: &Path,
947 git_dir: &Path,
948 format: ObjectFormat,
949 paths: &[PathBuf],
950 smudge_config: Option<&GitConfig>,
951) -> Result<RestoreResult> {
952 let index_path = repository_index_path(git_dir);
953 if !index_path.exists() {
954 return Err(GitError::Exit(1));
955 }
956 let mut index = Index::parse(&fs::read(&index_path)?, format)?;
957 let stat_cache = IndexStatCache::from_index(&index, &index_path);
958 let db = FileObjectDatabase::from_git_dir(git_dir, format);
959 let mut restored = BTreeSet::new();
960 let selected = checkout_selected_positions(
961 worktree_root,
962 paths,
963 index
964 .entries
965 .iter()
966 .enumerate()
967 .map(|(position, entry)| (position, entry.path.as_bytes())),
968 false,
969 )?;
970 for position in selected {
971 let refreshed = restore_index_entry(
972 worktree_root,
973 git_dir,
974 format,
975 &db,
976 &index.entries[position],
977 smudge_config,
978 Some(&stat_cache),
979 )?;
980 restored.insert(index.entries[position].path.clone());
981 if let Some(refreshed) = refreshed {
982 index.entries[position] = refreshed;
983 }
984 }
985 write_repository_index_ref(git_dir, format, &index)?;
986 Ok(RestoreResult {
987 restored: restored.len(),
988 })
989}
990
991pub fn checkout_index_paths(
992 worktree_root: impl AsRef<Path>,
993 git_dir: impl AsRef<Path>,
994 format: ObjectFormat,
995 paths: &[PathBuf],
996 options: CheckoutIndexPathOptions<'_>,
997) -> Result<RestoreResult> {
998 let worktree_root = worktree_root.as_ref();
999 let git_dir = git_dir.as_ref();
1000 let index_path = repository_index_path(git_dir);
1001 if !index_path.exists() {
1002 return Err(GitError::Exit(1));
1003 }
1004 let mut index = Index::parse(&fs::read(&index_path)?, format)?;
1005 if options.merge {
1006 checkout_unmerge_resolve_undo_paths(worktree_root, &mut index, format, paths)?;
1007 }
1008 let stat_cache = IndexStatCache::from_index(&index, &index_path);
1009 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1010 let selected = checkout_selected_index_paths(worktree_root, &index, paths)?;
1011
1012 if options.stage.is_none() && !options.merge && !options.force {
1013 for path in &selected {
1014 if checkout_path_is_unmerged(&index, path) {
1015 eprintln!(
1016 "error: path '{}' is unmerged",
1017 String::from_utf8_lossy(path)
1018 );
1019 return Err(GitError::Exit(1));
1020 }
1021 }
1022 }
1023
1024 let mut refreshed = BTreeMap::new();
1025 let mut restored = BTreeSet::new();
1026 let mut delayed_checkout = DelayedCheckoutQueue::default();
1027 for path in selected {
1028 let positions = index
1029 .entries
1030 .iter()
1031 .enumerate()
1032 .filter_map(|(position, entry)| (entry.path.as_bytes() == path).then_some(position))
1033 .collect::<Vec<_>>();
1034 let stage0 = positions
1035 .iter()
1036 .copied()
1037 .find(|position| index.entries[*position].stage() == Stage::Normal);
1038 let is_unmerged = positions
1039 .iter()
1040 .any(|position| index.entries[*position].stage() != Stage::Normal);
1041
1042 if is_unmerged {
1043 if let Some(stage) = options.stage {
1044 let wanted = match stage {
1045 CheckoutStage::Ours => Stage::Ours,
1046 CheckoutStage::Theirs => Stage::Theirs,
1047 };
1048 let Some(position) = positions
1049 .iter()
1050 .copied()
1051 .find(|position| index.entries[*position].stage() == wanted)
1052 else {
1053 if !options.overlay {
1054 remove_worktree_file(worktree_root, &path)?;
1055 restored.insert(path);
1056 continue;
1057 }
1058 eprintln!(
1059 "error: path '{}' does not have {} version",
1060 String::from_utf8_lossy(&path),
1061 match stage {
1062 CheckoutStage::Ours => "our",
1063 CheckoutStage::Theirs => "their",
1064 }
1065 );
1066 return Err(GitError::Exit(1));
1067 };
1068 if restore_index_entry_maybe_delayed(
1069 worktree_root,
1070 git_dir,
1071 format,
1072 &db,
1073 &index.entries[position],
1074 options.smudge_config,
1075 Some(&stat_cache),
1076 Some(&mut delayed_checkout),
1077 )?
1078 .is_some()
1079 {
1080 restored.insert(path);
1081 }
1082 continue;
1083 }
1084 if options.merge {
1085 checkout_merge_unmerged_path(
1086 worktree_root,
1087 &db,
1088 &index,
1089 &positions,
1090 options.conflict_style,
1091 )?;
1092 restored.insert(path);
1093 continue;
1094 }
1095 if options.force {
1096 continue;
1097 }
1098 }
1099
1100 if let Some(position) = stage0 {
1101 if let Some(updated) = restore_index_entry_maybe_delayed(
1102 worktree_root,
1103 git_dir,
1104 format,
1105 &db,
1106 &index.entries[position],
1107 options.smudge_config,
1108 Some(&stat_cache),
1109 Some(&mut delayed_checkout),
1110 )? {
1111 refreshed.insert(position, updated);
1112 restored.insert(path);
1113 }
1114 }
1115 }
1116
1117 let mut delayed_updates = finish_delayed_checkout(worktree_root, delayed_checkout)?;
1118 for (position, entry) in index.entries.iter().enumerate() {
1119 if let Some(updated) = delayed_updates.remove(entry.path.as_bytes()) {
1120 refreshed.insert(position, updated);
1121 }
1122 }
1123
1124 for (position, entry) in refreshed {
1125 index.entries[position] = entry;
1126 }
1127 if !index.entries.is_empty() {
1128 write_repository_index_ref(git_dir, format, &index)?;
1129 }
1130 Ok(RestoreResult {
1131 restored: restored.len(),
1132 })
1133}
1134
1135pub fn unresolve_index_paths(
1136 worktree_root: impl AsRef<Path>,
1137 git_dir: impl AsRef<Path>,
1138 format: ObjectFormat,
1139 paths: &[PathBuf],
1140) -> Result<()> {
1141 let worktree_root = worktree_root.as_ref();
1142 let git_dir = git_dir.as_ref();
1143 let index_path = repository_index_path(git_dir);
1144 if !index_path.exists() {
1145 return Ok(());
1146 }
1147 let mut index = Index::parse(&fs::read(&index_path)?, format)?;
1148 checkout_unmerge_resolve_undo_paths(worktree_root, &mut index, format, paths)?;
1149 write_repository_index_ref(git_dir, format, &index)
1150}
1151
1152pub(crate) fn checkout_selected_index_paths(
1153 worktree_root: &Path,
1154 index: &Index,
1155 paths: &[PathBuf],
1156) -> Result<BTreeSet<Vec<u8>>> {
1157 let index_paths = index
1158 .entries
1159 .iter()
1160 .map(|entry| entry.path.as_bytes().to_vec())
1161 .collect::<BTreeSet<_>>();
1162 checkout_selected_paths(
1163 worktree_root,
1164 paths,
1165 index_paths.iter().map(Vec::as_slice),
1166 false,
1167 )
1168}
1169
1170#[derive(Debug)]
1171struct CheckoutPathspec {
1172 display: String,
1173 element: PathspecElement,
1174 matched: bool,
1175}
1176
1177#[derive(Debug)]
1178struct CheckoutPathspecs {
1179 specs: Vec<CheckoutPathspec>,
1180 have_include: bool,
1181}
1182
1183impl CheckoutPathspecs {
1184 fn parse(worktree_root: &Path, paths: &[PathBuf]) -> Result<Self> {
1185 let mut specs = Vec::with_capacity(paths.len());
1186 let mut have_include = false;
1187 for path in paths {
1188 let git_path = checkout_pathspec_pattern(worktree_root, path)?;
1189 let element = PathspecElement::parse(&git_path, PathspecMatchMagic::default())
1190 .map_err(|err| GitError::Command(format!("bad pathspec: {err}")))?;
1191 have_include |= !element.is_exclude();
1192 specs.push(CheckoutPathspec {
1193 display: path.display().to_string(),
1194 element,
1195 matched: false,
1196 });
1197 }
1198 Ok(Self {
1199 specs,
1200 have_include,
1201 })
1202 }
1203
1204 fn is_empty(&self) -> bool {
1205 self.specs.is_empty()
1206 }
1207
1208 fn matches(&mut self, candidate: &[u8]) -> bool {
1209 if self.specs.is_empty() {
1210 return true;
1211 }
1212
1213 let mut included = false;
1214 let mut excluded = false;
1215 for spec in &mut self.specs {
1216 if spec.element.matches_path(candidate) {
1217 spec.matched = true;
1218 if spec.element.is_exclude() {
1219 excluded = true;
1220 } else {
1221 included = true;
1222 }
1223 }
1224 }
1225
1226 !excluded && (!self.have_include || included)
1227 }
1228
1229 fn require_matched_includes(&self, allow_unmatched: bool) -> Result<()> {
1230 if allow_unmatched {
1231 return Ok(());
1232 }
1233 if let Some(spec) = self
1234 .specs
1235 .iter()
1236 .find(|spec| !spec.element.is_exclude() && !spec.matched)
1237 {
1238 eprintln!(
1239 "error: pathspec '{}' did not match any file(s) known to git",
1240 spec.display
1241 );
1242 return Err(GitError::Exit(1));
1243 }
1244 Ok(())
1245 }
1246}
1247
1248fn checkout_pathspec_pattern(worktree_root: &Path, path: &Path) -> Result<Vec<u8>> {
1249 let absolute = if path.is_absolute() {
1250 path.to_path_buf()
1251 } else {
1252 worktree_root.join(path)
1253 };
1254 let absolute = normalize_absolute_path_lexically(&absolute);
1255 let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
1256 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
1257 })?;
1258 git_path_bytes(relative)
1259}
1260
1261fn checkout_selected_paths<'a, I>(
1262 worktree_root: &Path,
1263 paths: &[PathBuf],
1264 candidates: I,
1265 allow_unmatched: bool,
1266) -> Result<BTreeSet<Vec<u8>>>
1267where
1268 I: IntoIterator<Item = &'a [u8]>,
1269{
1270 let mut pathspecs = CheckoutPathspecs::parse(worktree_root, paths)?;
1271 if pathspecs.is_empty() {
1272 return Ok(BTreeSet::new());
1273 }
1274
1275 let mut selected = BTreeSet::new();
1276 for candidate in candidates {
1277 if pathspecs.matches(candidate) {
1278 selected.insert(candidate.to_vec());
1279 }
1280 }
1281 pathspecs.require_matched_includes(allow_unmatched)?;
1282 Ok(selected)
1283}
1284
1285fn checkout_selected_positions<'a, I>(
1286 worktree_root: &Path,
1287 paths: &[PathBuf],
1288 candidates: I,
1289 allow_unmatched: bool,
1290) -> Result<BTreeSet<usize>>
1291where
1292 I: IntoIterator<Item = (usize, &'a [u8])>,
1293{
1294 let mut pathspecs = CheckoutPathspecs::parse(worktree_root, paths)?;
1295 if pathspecs.is_empty() {
1296 return Ok(BTreeSet::new());
1297 }
1298
1299 let mut selected = BTreeSet::new();
1300 for (position, candidate) in candidates {
1301 if pathspecs.matches(candidate) {
1302 selected.insert(position);
1303 }
1304 }
1305 pathspecs.require_matched_includes(allow_unmatched)?;
1306 Ok(selected)
1307}
1308
1309pub(crate) fn checkout_unmerge_resolve_undo_paths(
1310 worktree_root: &Path,
1311 index: &mut Index,
1312 format: ObjectFormat,
1313 paths: &[PathBuf],
1314) -> Result<()> {
1315 let records = parse_resolve_undo_records(index.extension(b"REUC")?, format)?;
1316 if records.is_empty() {
1317 return Ok(());
1318 }
1319 let mut pathspecs = CheckoutPathspecs::parse(worktree_root, paths)?;
1320 let mut remaining = Vec::new();
1321 let mut unmerged_any = false;
1322 for record in records {
1323 if pathspecs.matches(&record.path) {
1324 remove_index_entries_with_path(&mut index.entries, &record.path);
1325 for (idx, stage) in record.stages.into_iter().enumerate() {
1326 let Some((mode, oid)) = stage else {
1327 continue;
1328 };
1329 index.entries.push(resolve_undo_index_entry(
1330 record.path.clone(),
1331 mode,
1332 oid,
1333 (idx + 1) as u16,
1334 ));
1335 }
1336 unmerged_any = true;
1337 } else {
1338 remaining.push(record);
1339 }
1340 }
1341 if unmerged_any {
1342 index.entries.sort_by(compare_index_key);
1343 normalize_index_version_for_extended_flags(index);
1344 set_resolve_undo_extension(index, &remaining)?;
1345 }
1346 Ok(())
1347}
1348
1349pub(crate) fn resolve_undo_index_entry(
1350 path: Vec<u8>,
1351 mode: u32,
1352 oid: ObjectId,
1353 stage: u16,
1354) -> IndexEntry {
1355 let name_len = (path
1356 .len()
1357 .min(sley_index::INDEX_FLAG_NAME_LENGTH_MASK as usize)) as u16;
1358 IndexEntry {
1359 ctime_seconds: 0,
1360 ctime_nanoseconds: 0,
1361 mtime_seconds: 0,
1362 mtime_nanoseconds: 0,
1363 dev: 0,
1364 ino: 0,
1365 mode,
1366 uid: 0,
1367 gid: 0,
1368 size: 0,
1369 oid,
1370 flags: name_len | (stage << 12),
1371 flags_extended: 0,
1372 path: path.into(),
1373 }
1374}
1375
1376pub(crate) fn checkout_path_is_unmerged(index: &Index, path: &[u8]) -> bool {
1377 index
1378 .entries
1379 .iter()
1380 .any(|entry| entry.path.as_bytes() == path && entry.stage() != Stage::Normal)
1381}
1382
1383pub(crate) fn checkout_merge_unmerged_path(
1384 worktree_root: &Path,
1385 db: &FileObjectDatabase,
1386 index: &Index,
1387 positions: &[usize],
1388 style: CheckoutConflictStyle,
1389) -> Result<()> {
1390 let mut base = None;
1391 let mut ours = None;
1392 let mut theirs = None;
1393 for position in positions {
1394 let entry = &index.entries[*position];
1395 match entry.stage() {
1396 Stage::Base => base = Some(entry),
1397 Stage::Ours => ours = Some(entry),
1398 Stage::Theirs => theirs = Some(entry),
1399 Stage::Normal => {}
1400 }
1401 }
1402 let Some(ours) = ours else {
1403 return Ok(());
1404 };
1405 let Some(theirs) = theirs else {
1406 return Ok(());
1407 };
1408 let base_body = match base {
1409 Some(entry) => read_expected_object(db, &entry.oid, ObjectType::Blob)?
1410 .body
1411 .clone(),
1412 None => Vec::new(),
1413 };
1414 let ours_body = read_expected_object(db, &ours.oid, ObjectType::Blob)?
1415 .body
1416 .clone();
1417 let theirs_body = read_expected_object(db, &theirs.oid, ObjectType::Blob)?
1418 .body
1419 .clone();
1420 let result = sley_diff_merge::merge_blobs(
1421 &base_body,
1422 &ours_body,
1423 &theirs_body,
1424 &sley_diff_merge::MergeBlobOptions {
1425 ours_label: "ours",
1426 theirs_label: "theirs",
1427 base_label: "base",
1428 style: match style {
1429 CheckoutConflictStyle::Merge => sley_diff_merge::ConflictStyle::Merge,
1430 CheckoutConflictStyle::Diff3 => sley_diff_merge::ConflictStyle::Diff3,
1431 },
1432 favor: sley_diff_merge::MergeFavor::None,
1433 ws_ignore: sley_diff_merge::WsIgnore::EMPTY,
1434 marker_size: 7,
1435 },
1436 );
1437 let file_path = worktree_path(worktree_root, ours.path.as_bytes())?;
1438 prepare_blob_parent_dirs(worktree_root, &file_path)?;
1439 remove_existing_worktree_path(&file_path)?;
1440 fs::write(&file_path, result.content)?;
1441 set_worktree_file_mode(&file_path, ours.mode)?;
1442 Ok(())
1443}
1444
1445pub fn restore_index_paths_from_head(
1446 worktree_root: impl AsRef<Path>,
1447 git_dir: impl AsRef<Path>,
1448 format: ObjectFormat,
1449 paths: &[PathBuf],
1450) -> Result<RestoreResult> {
1451 let worktree_root = worktree_root.as_ref();
1452 let git_dir = git_dir.as_ref();
1453 let index_path = repository_index_path(git_dir);
1454 let index = if index_path.exists() {
1455 Index::parse(&fs::read(&index_path)?, format)?
1456 } else {
1457 Index {
1458 version: 2,
1459 entries: Vec::new(),
1460 extensions: Vec::new(),
1461 checksum: None,
1462 }
1463 };
1464 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1465 let head_entries = head_tree_entries(git_dir, format, &db)?;
1466 restore_index_paths_from_entries(
1467 worktree_root,
1468 git_dir,
1469 format,
1470 &db,
1471 index,
1472 &head_entries,
1473 paths,
1474 false,
1475 )
1476}
1477
1478pub fn restore_index_paths_from_tree(
1479 worktree_root: impl AsRef<Path>,
1480 git_dir: impl AsRef<Path>,
1481 format: ObjectFormat,
1482 tree_oid: &ObjectId,
1483 paths: &[PathBuf],
1484) -> Result<RestoreResult> {
1485 let worktree_root = worktree_root.as_ref();
1486 let git_dir = git_dir.as_ref();
1487 let index_path = repository_index_path(git_dir);
1488 let index = if index_path.exists() {
1489 Index::parse(&fs::read(&index_path)?, format)?
1490 } else {
1491 Index {
1492 version: 2,
1493 entries: Vec::new(),
1494 extensions: Vec::new(),
1495 checksum: None,
1496 }
1497 };
1498 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1499 let source_entries = tree_entries(&db, format, tree_oid)?;
1500 restore_index_paths_from_entries(
1501 worktree_root,
1502 git_dir,
1503 format,
1504 &db,
1505 index,
1506 &source_entries,
1507 paths,
1508 false,
1509 )
1510}
1511
1512pub fn restore_index_paths_from_tree_allow_unmatched(
1513 worktree_root: impl AsRef<Path>,
1514 git_dir: impl AsRef<Path>,
1515 format: ObjectFormat,
1516 tree_oid: &ObjectId,
1517 paths: &[PathBuf],
1518) -> Result<RestoreResult> {
1519 let worktree_root = worktree_root.as_ref();
1520 let git_dir = git_dir.as_ref();
1521 let index_path = repository_index_path(git_dir);
1522 let index = if index_path.exists() {
1523 Index::parse(&fs::read(&index_path)?, format)?
1524 } else {
1525 Index {
1526 version: 2,
1527 entries: Vec::new(),
1528 extensions: Vec::new(),
1529 checksum: None,
1530 }
1531 };
1532 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1533 let source_entries = tree_entries(&db, format, tree_oid)?;
1534 restore_index_paths_from_entries(
1535 worktree_root,
1536 git_dir,
1537 format,
1538 &db,
1539 index,
1540 &source_entries,
1541 paths,
1542 true,
1543 )
1544}
1545
1546pub(crate) fn restore_index_paths_from_entries(
1547 worktree_root: &Path,
1548 git_dir: &Path,
1549 format: ObjectFormat,
1550 db: &FileObjectDatabase,
1551 mut index: Index,
1552 source_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1553 paths: &[PathBuf],
1554 allow_unmatched: bool,
1555) -> Result<RestoreResult> {
1556 let sparse = active_sparse_checkout(git_dir)?;
1557 if index.is_sparse() {
1558 expand_sparse_index(&mut index, db, format)?;
1559 }
1560 let index_version = index.version;
1561 let extensions = index_extensions_without_cache_tree(&index.extensions);
1562 let mut index_entries = index
1563 .entries
1564 .into_iter()
1565 .map(|entry| (entry.path.as_bytes().to_vec(), entry))
1566 .collect::<BTreeMap<_, _>>();
1567 let prior_skip_worktree = index_entries
1568 .iter()
1569 .filter(|(_, entry)| entry.is_skip_worktree())
1570 .map(|(path, _)| path.clone())
1571 .collect::<BTreeSet<_>>();
1572 let mut restored = BTreeSet::new();
1573 let matched_paths = checkout_selected_paths(
1574 worktree_root,
1575 paths,
1576 index_entries
1577 .keys()
1578 .chain(source_entries.keys())
1579 .map(Vec::as_slice),
1580 allow_unmatched,
1581 )?;
1582 for path in matched_paths {
1583 if let Some(entry) = source_entries.get(&path) {
1584 let unchanged = index_entries.get(&path).is_some_and(|existing| {
1591 existing.oid == entry.oid
1592 && existing.mode == entry.mode
1593 && !existing.is_intent_to_add()
1594 });
1595 if !unchanged {
1596 let mut restored = restored_head_index_entry(worktree_root, db, &path, entry)?;
1597 if prior_skip_worktree.contains(&path) {
1598 restored.set_skip_worktree(true);
1599 }
1600 index_entries.insert(path.clone(), restored);
1601 }
1602 } else {
1603 index_entries.remove(&path);
1604 }
1605 restored.insert(path);
1606 }
1607 let mut entries = index_entries.into_values().collect::<Vec<_>>();
1608 entries.sort_by(|left, right| left.path.cmp(&right.path));
1609 let restored_paths = restored.iter().cloned().collect::<Vec<_>>();
1610 let mut index = Index {
1611 version: index_version,
1612 entries,
1613 extensions,
1614 checksum: None,
1615 };
1616 invalidate_untracked_cache_for_git_paths(&mut index, format, &restored_paths)?;
1617 if let Some((sparse, mode)) = sparse
1618 && sparse.sparse_index
1619 {
1620 let matcher = SparseMatcher::new(&sparse, mode);
1621 collapse_to_sparse_index(&mut index, &matcher, db, format)?;
1622 }
1623 write_repository_index_ref(git_dir, format, &index)?;
1624 Ok(RestoreResult {
1625 restored: restored.len(),
1626 })
1627}
1628
1629pub fn restore_index_and_worktree_paths_from_head(
1630 worktree_root: impl AsRef<Path>,
1631 git_dir: impl AsRef<Path>,
1632 format: ObjectFormat,
1633 paths: &[PathBuf],
1634 overlay: bool,
1635) -> Result<RestoreResult> {
1636 let worktree_root = worktree_root.as_ref();
1637 let git_dir = git_dir.as_ref();
1638 let index_path = repository_index_path(git_dir);
1639 let index = if index_path.exists() {
1640 Index::parse(&fs::read(&index_path)?, format)?
1641 } else {
1642 Index {
1643 version: 2,
1644 entries: Vec::new(),
1645 extensions: Vec::new(),
1646 checksum: None,
1647 }
1648 };
1649 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1650 let head_entries = head_tree_entries(git_dir, format, &db)?;
1651 restore_index_and_worktree_paths_from_entries(
1652 worktree_root,
1653 git_dir,
1654 format,
1655 &db,
1656 index,
1657 &head_entries,
1658 paths,
1659 overlay,
1660 )
1661}
1662
1663pub fn restore_index_and_worktree_paths_from_tree(
1664 worktree_root: impl AsRef<Path>,
1665 git_dir: impl AsRef<Path>,
1666 format: ObjectFormat,
1667 tree_oid: &ObjectId,
1668 paths: &[PathBuf],
1669 overlay: bool,
1670) -> Result<RestoreResult> {
1671 let worktree_root = worktree_root.as_ref();
1672 let git_dir = git_dir.as_ref();
1673 let index_path = repository_index_path(git_dir);
1674 let index = if index_path.exists() {
1675 Index::parse(&fs::read(&index_path)?, format)?
1676 } else {
1677 Index {
1678 version: 2,
1679 entries: Vec::new(),
1680 extensions: Vec::new(),
1681 checksum: None,
1682 }
1683 };
1684 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1685 let source_entries = tree_entries(&db, format, tree_oid)?;
1686 restore_index_and_worktree_paths_from_entries(
1687 worktree_root,
1688 git_dir,
1689 format,
1690 &db,
1691 index,
1692 &source_entries,
1693 paths,
1694 overlay,
1695 )
1696}
1697
1698pub(crate) fn restore_index_and_worktree_paths_from_entries(
1699 worktree_root: &Path,
1700 git_dir: &Path,
1701 format: ObjectFormat,
1702 db: &FileObjectDatabase,
1703 index: Index,
1704 source_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1705 paths: &[PathBuf],
1706 overlay: bool,
1707) -> Result<RestoreResult> {
1708 let index_version = index.version;
1709 let extensions = index_extensions_without_cache_tree(&index.extensions);
1710 let config = sley_config::read_repo_config(git_dir, None).unwrap_or_default();
1711 let mut index_entries = index
1712 .entries
1713 .into_iter()
1714 .map(|entry| (entry.path.as_bytes().to_vec(), entry))
1715 .collect::<BTreeMap<_, _>>();
1716 let mut restored = BTreeSet::new();
1717 let matched_paths = checkout_selected_paths(
1718 worktree_root,
1719 paths,
1720 index_entries
1721 .keys()
1722 .chain(source_entries.keys())
1723 .map(Vec::as_slice),
1724 false,
1725 )?;
1726 for path in matched_paths {
1727 if let Some(entry) = source_entries.get(&path) {
1728 index_entries.insert(
1729 path.clone(),
1730 materialize_path_restore_entry_filtered(
1731 db,
1732 format,
1733 worktree_root,
1734 git_dir,
1735 &path,
1736 entry,
1737 &config,
1738 )?,
1739 );
1740 } else if overlay {
1741 continue;
1745 } else {
1746 index_entries.remove(&path);
1749 remove_worktree_file(worktree_root, &path)?;
1750 }
1751 restored.insert(path);
1752 }
1753 let mut entries = index_entries.into_values().collect::<Vec<_>>();
1754 entries.sort_by(|left, right| left.path.cmp(&right.path));
1755 let restored_paths = restored.iter().cloned().collect::<Vec<_>>();
1756 let mut index = Index {
1757 version: index_version,
1758 entries,
1759 extensions,
1760 checksum: None,
1761 };
1762 invalidate_untracked_cache_for_git_paths(&mut index, format, &restored_paths)?;
1763 write_repository_index_ref(git_dir, format, &index)?;
1764 Ok(RestoreResult {
1765 restored: restored.len(),
1766 })
1767}
1768
1769pub fn reset_index_and_worktree_to_commit(
1770 worktree_root: impl AsRef<Path>,
1771 git_dir: impl AsRef<Path>,
1772 format: ObjectFormat,
1773 commit_oid: &ObjectId,
1774) -> Result<RestoreResult> {
1775 let worktree_root = worktree_root.as_ref();
1776 let git_dir = git_dir.as_ref();
1777 let _process_filter_cwd = set_process_filter_cwd(Some(worktree_root.to_path_buf()));
1778 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1779 let commit = read_commit(&db, format, commit_oid)?;
1780 let mut target_entries = BTreeMap::new();
1781 collect_tree_entries(&db, format, &commit.tree, &mut target_entries)?;
1782 refuse_if_current_working_directory_becomes_file(worktree_root, &target_entries)?;
1783 let config = sley_config::read_repo_config(git_dir, None).unwrap_or_default();
1784 let attributes = build_tree_attribute_matcher(worktree_root, &db, format, &commit.tree)?;
1785
1786 for path in current_index_paths(git_dir, format, &db)? {
1793 if !target_entries.contains_key(&path) {
1794 remove_worktree_file(worktree_root, &path)?;
1795 }
1796 }
1797
1798 let mut index_entries = Vec::new();
1799 let mut delayed_checkout = DelayedCheckoutQueue::default();
1800 for (path, entry) in &target_entries {
1801 index_entries.push(materialize_tree_entry_with_optional_smudge(
1802 &db,
1803 format,
1804 worktree_root,
1805 path,
1806 entry,
1807 Some(&config),
1808 Some(&attributes),
1809 Some(&mut delayed_checkout),
1810 )?);
1811 }
1812 let mut delayed_updates = finish_delayed_checkout(worktree_root, delayed_checkout)?;
1813 for entry in &mut index_entries {
1814 if let Some(updated) = delayed_updates.remove(entry.path.as_bytes()) {
1815 *entry = updated;
1816 }
1817 }
1818 index_entries.sort_by(|left, right| left.path.cmp(&right.path));
1819 let extensions = preserved_index_extensions(git_dir, format)?;
1820 let mut index = Index {
1821 version: 2,
1822 entries: index_entries,
1823 extensions,
1824 checksum: None,
1825 };
1826 refresh_cache_tree(&mut index, &db);
1827 write_repository_index_ref(git_dir, format, &index)?;
1828 Ok(RestoreResult {
1829 restored: target_entries.len(),
1830 })
1831}
1832
1833pub fn reset_index_and_worktree_to_commit_with_process_filter_metadata(
1834 worktree_root: impl AsRef<Path>,
1835 git_dir: impl AsRef<Path>,
1836 format: ObjectFormat,
1837 commit_oid: &ObjectId,
1838 process_metadata: Option<ProcessFilterMetadata>,
1839) -> Result<RestoreResult> {
1840 let _process_filter_metadata = set_process_filter_metadata(process_metadata);
1841 reset_index_and_worktree_to_commit(worktree_root, git_dir, format, commit_oid)
1842}
1843
1844pub(crate) fn current_index_paths(
1850 git_dir: &Path,
1851 format: ObjectFormat,
1852 db: &FileObjectDatabase,
1853) -> Result<BTreeSet<Vec<u8>>> {
1854 let (index, _stat_cache, _head_matches) = read_index_with_stat_cache(git_dir, format, db)?;
1855 Ok(index
1856 .entries
1857 .into_iter()
1858 .map(|entry| entry.path.into_bytes())
1859 .collect())
1860}
1861
1862pub(crate) fn materialize_tree_entry(
1872 db: &FileObjectDatabase,
1873 worktree_root: &Path,
1874 path: &[u8],
1875 entry: &TrackedEntry,
1876) -> Result<IndexEntry> {
1877 if sley_index::is_gitlink(entry.mode) {
1878 let dir_path = worktree_path(worktree_root, path)?;
1879 materialize_gitlink_dir(worktree_root, &dir_path)?;
1880 return Ok(IndexEntry {
1881 ctime_seconds: 0,
1882 ctime_nanoseconds: 0,
1883 mtime_seconds: 0,
1884 mtime_nanoseconds: 0,
1885 dev: 0,
1886 ino: 0,
1887 mode: entry.mode,
1888 uid: 0,
1889 gid: 0,
1890 size: 0,
1891 oid: entry.oid,
1892 flags: path.len().min(0x0fff) as u16,
1893 flags_extended: 0,
1894 path: BString::from(path),
1895 });
1896 }
1897 let file_path = write_worktree_blob_entry(db, worktree_root, path, entry)?;
1898 let metadata = fs::symlink_metadata(&file_path)?;
1899 let mut index_entry = index_entry_from_metadata(path.to_vec(), entry.oid, &metadata);
1900 index_entry.mode = entry.mode;
1901 Ok(index_entry)
1902}
1903
1904pub(crate) fn materialize_gitlink_dir(worktree_root: &Path, dir_path: &Path) -> Result<()> {
1905 prepare_blob_parent_dirs(worktree_root, dir_path)?;
1906 if fs::symlink_metadata(dir_path).is_ok_and(|metadata| !metadata.is_dir()) {
1907 remove_existing_worktree_path(dir_path)?;
1908 }
1909 fs::create_dir_all(dir_path)?;
1910 Ok(())
1911}
1912
1913pub(crate) fn materialize_path_restore_entry_filtered(
1914 db: &FileObjectDatabase,
1915 format: ObjectFormat,
1916 worktree_root: &Path,
1917 git_dir: &Path,
1918 path: &[u8],
1919 entry: &TrackedEntry,
1920 config: &GitConfig,
1921) -> Result<IndexEntry> {
1922 if sley_index::is_gitlink(entry.mode) || (entry.mode & 0o170000) == 0o120000 {
1923 return materialize_tree_entry(db, worktree_root, path, entry);
1924 }
1925 let object = read_expected_object(db, &entry.oid, ObjectType::Blob)?;
1926 let checks = smudge_attribute_checks_from_index(worktree_root, git_dir, format, path)?;
1927 let body = apply_smudge_filter_with_attributes_cow_format(
1928 config,
1929 &checks,
1930 path,
1931 &object.body,
1932 format,
1933 )?;
1934 let file_path = worktree_path(worktree_root, path)?;
1935 prepare_blob_parent_dirs(worktree_root, &file_path)?;
1936 remove_existing_worktree_path(&file_path)?;
1937 fs::write(&file_path, &body)?;
1938 set_worktree_file_mode(&file_path, entry.mode)?;
1939 let metadata = fs::symlink_metadata(&file_path)?;
1940 let mut index_entry = index_entry_from_metadata(path.to_vec(), entry.oid, &metadata);
1941 index_entry.mode = entry.mode;
1942 Ok(index_entry)
1943}
1944
1945pub(crate) fn write_worktree_blob_entry(
1956 db: &FileObjectDatabase,
1957 worktree_root: &Path,
1958 path: &[u8],
1959 entry: &TrackedEntry,
1960) -> Result<PathBuf> {
1961 let object = read_expected_object(db, &entry.oid, ObjectType::Blob)?;
1962 let file_path = worktree_path(worktree_root, path)?;
1963 prepare_blob_parent_dirs(worktree_root, &file_path)?;
1966 remove_existing_worktree_path(&file_path)?;
1969 write_blob_body_or_symlink(&file_path, entry.mode, &object.body, &object.body)?;
1970 Ok(file_path)
1971}
1972
1973pub fn write_blob_body_or_symlink(
1992 file_path: &Path,
1993 mode: u32,
1994 body: &[u8],
1995 link_target: &[u8],
1996) -> Result<()> {
1997 if (mode & 0o170000) == 0o120000 {
1998 #[cfg(unix)]
1999 {
2000 use std::os::unix::ffi::OsStringExt;
2001 let target =
2002 std::path::PathBuf::from(std::ffi::OsString::from_vec(link_target.to_vec()));
2003 std::os::unix::fs::symlink(&target, file_path)?;
2004 }
2005 #[cfg(not(unix))]
2006 {
2007 let _ = link_target;
2008 fs::write(file_path, body)?;
2009 }
2010 } else {
2011 fs::write(file_path, body)?;
2012 set_worktree_file_mode(file_path, mode)?;
2013 }
2014 Ok(())
2015}
2016
2017pub(crate) fn prepare_blob_parent_dirs(worktree_root: &Path, file_path: &Path) -> Result<()> {
2031 let parent = match file_path.parent() {
2032 Some(parent) => parent,
2033 None => return Ok(()),
2034 };
2035 if parent.is_dir() {
2037 return Ok(());
2038 }
2039 let mut components: Vec<&Path> = Vec::new();
2043 let mut cursor = Some(parent);
2044 while let Some(dir) = cursor {
2045 if dir == worktree_root {
2046 break;
2047 }
2048 components.push(dir);
2049 cursor = dir.parent();
2050 if cursor.is_none() {
2051 break;
2052 }
2053 }
2054 for dir in components.iter().rev() {
2056 match fs::symlink_metadata(dir) {
2057 Ok(metadata) if metadata.is_dir() => {}
2058 Ok(_) => {
2059 fs::remove_file(dir)?;
2062 fs::create_dir(dir)?;
2063 }
2064 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
2065 fs::create_dir(dir)?;
2066 }
2067 Err(err) => return Err(err.into()),
2068 }
2069 }
2070 Ok(())
2071}
2072
2073pub(crate) fn remove_existing_worktree_path(file_path: &Path) -> Result<()> {
2078 let metadata = match fs::symlink_metadata(file_path) {
2079 Ok(metadata) => metadata,
2080 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
2081 Err(err) => return Err(err.into()),
2082 };
2083 if metadata.is_dir() {
2084 if path_is_original_cwd(file_path) {
2085 return refuse_remove_current_working_directory(file_path);
2086 }
2087 match fs::remove_dir_all(file_path) {
2090 Ok(()) => {}
2091 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
2092 Err(err) => return Err(err.into()),
2093 }
2094 } else {
2095 fs::remove_file(file_path)?;
2096 }
2097 Ok(())
2098}
2099
2100#[cfg(unix)]
2120pub(crate) fn set_worktree_file_mode(file_path: &Path, entry_mode: u32) -> Result<()> {
2121 use std::os::unix::fs::PermissionsExt;
2122 let perms = match entry_mode {
2123 0o100755 => 0o755,
2124 0o100644 => 0o644,
2125 _ => return Ok(()),
2126 };
2127 fs::set_permissions(file_path, fs::Permissions::from_mode(perms))?;
2128 Ok(())
2129}
2130
2131#[cfg(not(unix))]
2132pub(crate) fn set_worktree_file_mode(_file_path: &Path, _entry_mode: u32) -> Result<()> {
2133 Ok(())
2134}
2135
2136pub fn checkout_tree_to_index_and_worktree(
2138 worktree_root: impl AsRef<Path>,
2139 git_dir: impl AsRef<Path>,
2140 format: ObjectFormat,
2141 tree_oid: &ObjectId,
2142) -> Result<RestoreResult> {
2143 let worktree_root = worktree_root.as_ref();
2144 let git_dir = git_dir.as_ref();
2145 let db = FileObjectDatabase::from_git_dir(git_dir, format);
2146 let mut target_entries = BTreeMap::new();
2147 collect_tree_entries(&db, format, tree_oid, &mut target_entries)?;
2148
2149 for path in read_index_entries(git_dir, format)?.keys() {
2150 if !target_entries.contains_key(path) {
2151 remove_worktree_file(worktree_root, path)?;
2152 }
2153 }
2154
2155 let mut index_entries = Vec::new();
2156 for (path, entry) in &target_entries {
2157 index_entries.push(materialize_tree_entry(&db, worktree_root, path, entry)?);
2158 }
2159 index_entries.sort_by(|left, right| left.path.cmp(&right.path));
2160 let extensions = preserved_index_extensions(git_dir, format)?;
2161 let mut index = Index {
2162 version: 2,
2163 entries: index_entries,
2164 extensions,
2165 checksum: None,
2166 };
2167 refresh_cache_tree(&mut index, &db);
2168 write_repository_index_ref(git_dir, format, &index)?;
2169 Ok(RestoreResult {
2170 restored: target_entries.len(),
2171 })
2172}
2173
2174pub fn reset_index_to_commit(
2175 worktree_root: impl AsRef<Path>,
2176 git_dir: impl AsRef<Path>,
2177 format: ObjectFormat,
2178 commit_oid: &ObjectId,
2179) -> Result<RestoreResult> {
2180 let worktree_root = worktree_root.as_ref();
2181 let git_dir = git_dir.as_ref();
2182 let db = FileObjectDatabase::from_git_dir(git_dir, format);
2183 let commit = read_commit(&db, format, commit_oid)?;
2184 let mut target_entries = BTreeMap::new();
2185 collect_tree_entries(&db, format, &commit.tree, &mut target_entries)?;
2186 let index_path = repository_index_path(git_dir);
2190 let prior_skip_worktree: BTreeSet<Vec<u8>> = match fs::read(&index_path) {
2191 Ok(bytes) => Index::parse(&bytes, format)?
2192 .entries
2193 .iter()
2194 .filter(|entry| entry.is_skip_worktree())
2195 .map(|entry| entry.path.as_bytes().to_vec())
2196 .collect(),
2197 Err(err) if err.kind() == std::io::ErrorKind::NotFound => BTreeSet::new(),
2198 Err(err) => return Err(err.into()),
2199 };
2200 let mut index_entries = Vec::new();
2201 for (path, entry) in &target_entries {
2202 let mut restored = restored_head_index_entry(worktree_root, &db, path, entry)?;
2203 if prior_skip_worktree.contains(path) {
2204 restored.set_skip_worktree(true);
2205 }
2206 index_entries.push(restored);
2207 }
2208 index_entries.sort_by(|left, right| left.path.cmp(&right.path));
2209 let mut index = Index {
2210 version: 2,
2211 entries: index_entries,
2212 extensions: preserved_index_extensions(git_dir, format)?,
2213 checksum: None,
2214 };
2215 index.upgrade_version_for_flags();
2216 refresh_cache_tree(&mut index, &db);
2217 write_repository_index_ref(git_dir, format, &index)?;
2218 Ok(RestoreResult {
2219 restored: target_entries.len(),
2220 })
2221}
2222
2223pub fn index_from_tree(
2233 db: &FileObjectDatabase,
2234 format: ObjectFormat,
2235 tree_oid: &ObjectId,
2236) -> Result<Index> {
2237 let mut entries: Vec<IndexEntry> = Vec::new();
2238 if *tree_oid != ObjectId::empty_tree(format) {
2239 let mut tree_entries = BTreeMap::new();
2240 collect_tree_entries(db, format, tree_oid, &mut tree_entries)?;
2241 entries.reserve(tree_entries.len());
2242 for (path, entry) in tree_entries {
2243 let name_len = (path.len().min(0x0fff)) as u16;
2244 entries.push(IndexEntry {
2245 ctime_seconds: 0,
2246 ctime_nanoseconds: 0,
2247 mtime_seconds: 0,
2248 mtime_nanoseconds: 0,
2249 dev: 0,
2250 ino: 0,
2251 mode: entry.mode,
2252 uid: 0,
2253 gid: 0,
2254 size: 0,
2255 oid: entry.oid,
2256 flags: name_len,
2257 flags_extended: 0,
2258 path: path.into(),
2259 });
2260 }
2261 }
2262 entries.sort_by(|left, right| left.path.cmp(&right.path));
2265 Ok(Index {
2266 version: 2,
2267 entries,
2268 extensions: Vec::new(),
2269 checksum: None,
2270 })
2271}
2272
2273pub fn path_in_sparse_checkout(
2292 path: &[u8],
2293 sparse: &SparseCheckout,
2294 mode: SparseCheckoutMode,
2295) -> bool {
2296 SparseMatcher::new(sparse, mode).includes_file(path)
2297}
2298
2299pub(crate) fn active_sparse_checkout(
2300 git_dir: &Path,
2301) -> Result<Option<(SparseCheckout, SparseCheckoutMode)>> {
2302 let worktree_config = GitConfig::read(git_dir.join("config.worktree")).unwrap_or_default();
2303 let repo_config = GitConfig::read(git_dir.join("config")).unwrap_or_default();
2304 let sparse_enabled = worktree_config
2305 .get_bool("core", None, "sparseCheckout")
2306 .or_else(|| repo_config.get_bool("core", None, "sparseCheckout"))
2307 .unwrap_or(false);
2308 if !sparse_enabled {
2309 return Ok(None);
2310 }
2311 let sparse_file = git_dir.join("info").join("sparse-checkout");
2312 if !sparse_file.exists() {
2313 return Ok(None);
2314 }
2315 let cone = worktree_config
2316 .get_bool("core", None, "sparseCheckoutCone")
2317 .or_else(|| repo_config.get_bool("core", None, "sparseCheckoutCone"))
2318 .unwrap_or(false);
2319 let sparse_index = cone
2320 && worktree_config
2321 .get_bool("index", None, "sparse")
2322 .or_else(|| repo_config.get_bool("index", None, "sparse"))
2323 .unwrap_or(false);
2324 let bytes = fs::read(sparse_file)?;
2325 let mut patterns = bytes
2326 .split(|byte| *byte == b'\n')
2327 .map(<[u8]>::to_vec)
2328 .collect::<Vec<_>>();
2329 if patterns.last().map(Vec::is_empty) == Some(true) {
2330 patterns.pop();
2331 }
2332 let mode = if cone {
2333 SparseCheckoutMode::Cone
2334 } else {
2335 SparseCheckoutMode::Full
2336 };
2337 Ok(Some((
2338 SparseCheckout {
2339 patterns,
2340 sparse_index,
2341 },
2342 mode,
2343 )))
2344}
2345
2346pub fn apply_sparse_checkout(
2349 worktree_root: impl AsRef<Path>,
2350 git_dir: impl AsRef<Path>,
2351 format: ObjectFormat,
2352 sparse: &SparseCheckout,
2353) -> Result<ApplySparseResult> {
2354 apply_sparse_checkout_with_mode(
2355 worktree_root,
2356 git_dir,
2357 format,
2358 sparse,
2359 SparseCheckoutMode::Auto,
2360 )
2361}
2362
2363pub fn apply_sparse_checkout_with_mode(
2366 worktree_root: impl AsRef<Path>,
2367 git_dir: impl AsRef<Path>,
2368 format: ObjectFormat,
2369 sparse: &SparseCheckout,
2370 mode: SparseCheckoutMode,
2371) -> Result<ApplySparseResult> {
2372 let worktree_root = worktree_root.as_ref();
2373 let git_dir = git_dir.as_ref();
2374 let index_path = repository_index_path(git_dir);
2375 let mut index = if index_path.exists() {
2376 Index::parse(&fs::read(&index_path)?, format)?
2377 } else {
2378 return Ok(ApplySparseResult {
2379 materialized: Vec::new(),
2380 skipped: Vec::new(),
2381 not_up_to_date: Vec::new(),
2382 });
2383 };
2384 let matcher = SparseMatcher::new(sparse, mode);
2385 let db = FileObjectDatabase::from_git_dir(git_dir, format);
2386 if index.entries.iter().any(IndexEntry::is_sparse_dir) {
2391 expand_sparse_index(&mut index, &db, format)?;
2392 }
2393 let mut materialized = Vec::new();
2394 let mut skipped = Vec::new();
2395 let mut not_up_to_date = Vec::new();
2396 for entry in &mut index.entries {
2397 if index_entry_stage(entry) != 0 {
2399 continue;
2400 }
2401 if matcher.includes_file(entry.path.as_bytes()) {
2402 clear_skip_worktree(entry);
2403 let file_path = worktree_path(worktree_root, entry.path.as_bytes())?;
2404 if !file_path.exists() {
2405 materialize_index_entry_file(&db, worktree_root, &file_path, entry)?;
2406 let metadata = fs::symlink_metadata(&file_path)?;
2407 *entry = index_entry_with_refreshed_stat(entry, &metadata);
2408 }
2409 materialized.push(entry.path.as_bytes().to_vec());
2410 } else {
2411 let file_path = worktree_path(worktree_root, entry.path.as_bytes())?;
2418 match fs::symlink_metadata(&file_path) {
2419 Ok(metadata) if !worktree_entry_is_uptodate(entry, &metadata) => {
2420 clear_skip_worktree(entry);
2421 not_up_to_date.push(entry.path.as_bytes().to_vec());
2422 }
2423 _ => {
2424 set_skip_worktree(entry);
2425 remove_worktree_file(worktree_root, entry.path.as_bytes())?;
2426 skipped.push(entry.path.as_bytes().to_vec());
2427 }
2428 }
2429 }
2430 }
2431 not_up_to_date.sort();
2432 normalize_index_version_for_extended_flags(&mut index);
2433 if sparse.sparse_index {
2438 collapse_to_sparse_index(&mut index, &matcher, &db, format)?;
2439 } else {
2440 index.clear_sparse_extension()?;
2441 }
2442 write_repository_index_ref(git_dir, format, &index)?;
2443 Ok(ApplySparseResult {
2444 materialized,
2445 skipped,
2446 not_up_to_date,
2447 })
2448}
2449
2450pub fn expand_sparse_index(
2460 index: &mut Index,
2461 db: &FileObjectDatabase,
2462 format: ObjectFormat,
2463) -> Result<bool> {
2464 if !index.entries.iter().any(IndexEntry::is_sparse_dir) {
2465 let had_marker = index.is_sparse();
2467 index.clear_sparse_extension()?;
2468 if had_marker {
2469 sley_core::trace2::region("index", "ensure_full_index");
2470 }
2471 return Ok(had_marker);
2472 }
2473 let mut expanded: Vec<IndexEntry> = Vec::with_capacity(index.entries.len());
2474 for entry in std::mem::take(&mut index.entries) {
2475 if !entry.is_sparse_dir() {
2476 expanded.push(entry);
2477 continue;
2478 }
2479 let dir = entry.path.as_bytes();
2481 let dir_prefix = dir; for (rel, (mode, oid)) in sley_diff_merge::flatten_tree(db, format, &entry.oid)? {
2483 let mut full_path = dir_prefix.to_vec();
2484 full_path.extend_from_slice(&rel);
2485 let mut blob = blank_sparse_blob_entry(format, &full_path, mode, oid);
2486 blob.set_skip_worktree(true);
2488 expanded.push(blob);
2489 }
2490 }
2491 expanded.sort_by(|a, b| a.path.as_bytes().cmp(b.path.as_bytes()));
2492 index.entries = expanded;
2493 index.clear_sparse_extension()?;
2494 normalize_index_version_for_extended_flags(index);
2495 sley_core::trace2::region("index", "ensure_full_index");
2496 Ok(true)
2497}
2498
2499pub(crate) fn index_sparse_dir_contains_path(index: &Index, git_path: &[u8]) -> bool {
2500 index.entries.iter().any(|entry| {
2501 entry.is_sparse_dir()
2502 && git_path.starts_with(entry.path.as_bytes())
2503 && git_path.len() > entry.path.len()
2504 })
2505}
2506
2507pub(crate) fn blank_sparse_blob_entry(
2512 format: ObjectFormat,
2513 path: &[u8],
2514 mode: u32,
2515 oid: ObjectId,
2516) -> IndexEntry {
2517 let _ = format;
2518 let mut entry = IndexEntry {
2519 ctime_seconds: 0,
2520 ctime_nanoseconds: 0,
2521 mtime_seconds: 0,
2522 mtime_nanoseconds: 0,
2523 dev: 0,
2524 ino: 0,
2525 mode,
2526 uid: 0,
2527 gid: 0,
2528 size: 0,
2529 oid,
2530 flags: 0,
2531 flags_extended: 0,
2532 path: path.into(),
2533 };
2534 entry.refresh_name_length();
2535 entry
2536}
2537
2538pub(crate) fn collapse_to_sparse_index(
2545 index: &mut Index,
2546 matcher: &SparseMatcher,
2547 db: &FileObjectDatabase,
2548 format: ObjectFormat,
2549) -> Result<()> {
2550 if index.entries.iter().any(IndexEntry::is_sparse_dir) {
2553 expand_sparse_index(index, db, format)?;
2554 }
2555
2556 if index.entries.iter().any(|e| index_entry_stage(e) != 0) {
2559 index.clear_sparse_extension()?;
2560 return Ok(());
2561 }
2562
2563 index
2564 .entries
2565 .sort_by(|a, b| a.path.as_bytes().cmp(b.path.as_bytes()));
2566
2567 use std::collections::BTreeMap;
2570 let mut dir_has_in_cone: BTreeMap<Vec<u8>, bool> = BTreeMap::new();
2571 for entry in &index.entries {
2572 let path = entry.path.as_bytes();
2573 let in_cone = matcher.includes_file(path);
2574 let mut start = 0usize;
2575 while let Some(rel) = path
2576 .get(start..)
2577 .and_then(|s| s.iter().position(|b| *b == b'/'))
2578 {
2579 let end = start + rel;
2580 let dir = path[..end].to_vec();
2581 let flag = dir_has_in_cone.entry(dir).or_insert(false);
2582 *flag = *flag || in_cone;
2583 start = end + 1;
2584 }
2585 }
2586
2587 let collapsible: Vec<Vec<u8>> = {
2590 let all: Vec<Vec<u8>> = dir_has_in_cone
2591 .iter()
2592 .filter(|(_, has)| !**has)
2593 .map(|(dir, _)| dir.clone())
2594 .collect();
2595 all.iter()
2596 .filter(|dir| {
2597 !all.iter().any(|other| {
2598 other != *dir
2599 && dir
2600 .strip_prefix(other.as_slice())
2601 .is_some_and(|rest| rest.first() == Some(&b'/'))
2602 })
2603 })
2604 .cloned()
2605 .collect()
2606 };
2607 if collapsible.is_empty() {
2608 index.clear_sparse_extension()?;
2609 return Ok(());
2610 }
2611
2612 let mut checker = db.presence_checker();
2613 let mut new_entries: Vec<IndexEntry> = Vec::with_capacity(index.entries.len());
2614 let mut consumed: std::collections::HashSet<Vec<u8>> = std::collections::HashSet::new();
2615 for dir in &collapsible {
2616 let mut subtree: Vec<&IndexEntry> = index
2618 .entries
2619 .iter()
2620 .filter(|e| {
2621 e.path
2622 .as_bytes()
2623 .strip_prefix(dir.as_slice())
2624 .is_some_and(|rest| rest.first() == Some(&b'/'))
2625 })
2626 .collect();
2627 if subtree.is_empty() {
2628 continue;
2629 }
2630 subtree.sort_by(|a, b| a.path.as_bytes().cmp(b.path.as_bytes()));
2631 let mut prefix = dir.clone();
2633 prefix.push(b'/');
2634 let tree_entries: Vec<WriteTreeEntry<'_>> = subtree
2635 .iter()
2636 .map(|e| WriteTreeEntry {
2637 path: e.path.as_bytes(),
2638 mode: e.mode,
2639 oid: e.oid.clone(),
2640 })
2641 .collect();
2642 let tree_oid =
2643 write_tree_entries_stream(&tree_entries, &prefix, None, db, &mut checker, false)?;
2644 for e in &subtree {
2646 consumed.insert(e.path.as_bytes().to_vec());
2647 }
2648 let mut sparse_path = dir.clone();
2650 sparse_path.push(b'/');
2651 let mut sparse_entry =
2652 blank_sparse_blob_entry(format, &sparse_path, SPARSE_DIR_MODE, tree_oid);
2653 sparse_entry.set_skip_worktree(true);
2654 new_entries.push(sparse_entry);
2655 }
2656 for entry in &index.entries {
2658 if consumed.contains(entry.path.as_bytes()) {
2659 continue;
2660 }
2661 new_entries.push(entry.clone());
2662 }
2663 new_entries.sort_by(|a, b| a.path.as_bytes().cmp(b.path.as_bytes()));
2664 index.entries = new_entries;
2665 index.set_sparse_extension();
2666 normalize_index_version_for_extended_flags(index);
2667 sley_core::trace2::region("index", "convert_to_sparse");
2668 Ok(())
2669}
2670
2671pub(crate) fn worktree_entry_is_uptodate(entry: &IndexEntry, metadata: &fs::Metadata) -> bool {
2678 if u64::from(entry.size) != metadata.len() {
2679 return false;
2680 }
2681 let Some((mtime_seconds, mtime_nanoseconds)) = file_mtime_parts(metadata) else {
2682 return false;
2685 };
2686 u64::from(entry.mtime_seconds) == mtime_seconds
2687 && u64::from(entry.mtime_nanoseconds) == mtime_nanoseconds
2688}
2689
2690pub(crate) fn worktree_entry_ref_is_uptodate(
2691 entry: &IndexEntryRef<'_>,
2692 metadata: &fs::Metadata,
2693) -> bool {
2694 if u64::from(entry.size) != metadata.len() {
2695 return false;
2696 }
2697 let Some((mtime_seconds, mtime_nanoseconds)) = file_mtime_parts(metadata) else {
2698 return false;
2699 };
2700 u64::from(entry.mtime_seconds) == mtime_seconds
2701 && u64::from(entry.mtime_nanoseconds) == mtime_nanoseconds
2702}
2703
2704pub(crate) fn file_mtime_parts(metadata: &fs::Metadata) -> Option<(u64, u64)> {
2707 let modified = metadata.modified().ok()?;
2708 let duration = modified.duration_since(UNIX_EPOCH).ok()?;
2709 Some((duration.as_secs(), u64::from(duration.subsec_nanos())))
2710}
2711
2712pub fn write_metadata_file_atomic(
2719 path: impl AsRef<Path>,
2720 bytes: &[u8],
2721 options: AtomicMetadataWriteOptions,
2722) -> Result<AtomicMetadataWriteResult> {
2723 let path = path.as_ref();
2724 let parent = path.parent().ok_or_else(|| {
2725 GitError::InvalidPath(format!("metadata path has no parent: {}", path.display()))
2726 })?;
2727 if !parent.as_os_str().is_empty() {
2728 fs::create_dir_all(parent)?;
2729 }
2730 let lock_path = metadata_lock_path(path)?;
2731 let mut lock = match fs::OpenOptions::new()
2732 .write(true)
2733 .create_new(true)
2734 .open(&lock_path)
2735 {
2736 Ok(lock) => lock,
2737 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
2738 return Err(GitError::Transaction(format!(
2739 "metadata lock already exists: {}",
2740 lock_path.display()
2741 )));
2742 }
2743 Err(err) => return Err(err.into()),
2744 };
2745 if let Err(err) = lock.write_all(bytes) {
2746 let _ = fs::remove_file(&lock_path);
2747 return Err(err.into());
2748 }
2749 if options.fsync_file
2750 && let Err(err) = lock.sync_all()
2751 {
2752 let _ = fs::remove_file(&lock_path);
2753 return Err(err.into());
2754 }
2755 drop(lock);
2756 if let Err(err) = fs::rename(&lock_path, path) {
2757 let _ = fs::remove_file(&lock_path);
2758 return Err(err.into());
2759 }
2760 if options.fsync_dir
2761 && let Ok(dir) = fs::File::open(parent)
2762 {
2763 dir.sync_all()?;
2764 }
2765 let metadata = fs::metadata(path)?;
2766 Ok(AtomicMetadataWriteResult {
2767 path: path.to_path_buf(),
2768 len: metadata.len(),
2769 mtime: file_mtime_parts(&metadata),
2770 })
2771}
2772
2773pub(crate) fn metadata_lock_path(path: &Path) -> Result<PathBuf> {
2774 let file_name = path.file_name().ok_or_else(|| {
2775 GitError::InvalidPath(format!("metadata path has no filename: {}", path.display()))
2776 })?;
2777 let mut lock_name = file_name.to_os_string();
2778 lock_name.push(".lock");
2779 Ok(path.with_file_name(lock_name))
2780}
2781
2782pub fn checkout_detached_sparse(
2792 worktree_root: impl AsRef<Path>,
2793 git_dir: impl AsRef<Path>,
2794 format: ObjectFormat,
2795 target: &ObjectId,
2796 committer: Vec<u8>,
2797 message: Vec<u8>,
2798 sparse: &SparseCheckout,
2799) -> Result<CheckoutResult> {
2800 let worktree_root = worktree_root.as_ref();
2801 let git_dir = git_dir.as_ref();
2802 let files = checkout_commit_to_index_and_worktree_sparse(
2803 worktree_root,
2804 git_dir,
2805 format,
2806 target,
2807 Some((sparse, SparseCheckoutMode::Auto)),
2808 None,
2809 None,
2810 )?;
2811 let refs = FileRefStore::new(git_dir, format);
2812 let zero = ObjectId::null(format);
2813 let mut tx = refs.transaction();
2814 tx.update(RefUpdate {
2815 name: "HEAD".into(),
2816 expected: None,
2817 new: RefTarget::Direct(*target),
2818 reflog: Some(ReflogEntry {
2819 old_oid: zero,
2820 new_oid: *target,
2821 committer,
2822 message,
2823 }),
2824 });
2825 tx.commit()?;
2826 Ok(CheckoutResult {
2827 branch: target.to_string(),
2828 oid: *target,
2829 files,
2830 })
2831}
2832
2833pub(crate) fn materialize_index_entry_file(
2834 db: &FileObjectDatabase,
2835 worktree_root: &Path,
2836 file_path: &Path,
2837 entry: &IndexEntry,
2838) -> Result<()> {
2839 if sley_index::is_gitlink(entry.mode) {
2845 materialize_gitlink_dir(worktree_root, file_path)?;
2846 return Ok(());
2847 }
2848 let object = read_expected_object(db, &entry.oid, ObjectType::Blob)?;
2849 prepare_blob_parent_dirs(worktree_root, file_path)?;
2850 remove_existing_worktree_path(file_path)?;
2851 write_blob_body_or_symlink(file_path, entry.mode, &object.body, &object.body)?;
2852 Ok(())
2853}
2854
2855pub(crate) fn set_skip_worktree(entry: &mut IndexEntry) {
2856 entry.flags |= INDEX_FLAG_EXTENDED;
2857 entry.flags_extended |= INDEX_EXTENDED_FLAG_SKIP_WORKTREE;
2858}
2859
2860pub(crate) fn clear_skip_worktree(entry: &mut IndexEntry) {
2861 entry.flags_extended &= !INDEX_EXTENDED_FLAG_SKIP_WORKTREE;
2862 if entry.flags_extended == 0 {
2863 entry.flags &= !INDEX_FLAG_EXTENDED;
2864 }
2865}
2866
2867pub fn restore_worktree_paths_from_head(
2868 worktree_root: impl AsRef<Path>,
2869 git_dir: impl AsRef<Path>,
2870 format: ObjectFormat,
2871 paths: &[PathBuf],
2872) -> Result<RestoreResult> {
2873 let worktree_root = worktree_root.as_ref();
2874 let git_dir = git_dir.as_ref();
2875 let index_path = repository_index_path(git_dir);
2876 let index = if index_path.exists() {
2877 Index::parse(&fs::read(&index_path)?, format)?
2878 } else {
2879 Index {
2880 version: 2,
2881 entries: Vec::new(),
2882 extensions: Vec::new(),
2883 checksum: None,
2884 }
2885 };
2886 let db = FileObjectDatabase::from_git_dir(git_dir, format);
2887 let head_entries = head_tree_entries(git_dir, format, &db)?;
2888 restore_worktree_paths_from_entries(
2889 worktree_root,
2890 git_dir,
2891 format,
2892 &db,
2893 index,
2894 &head_entries,
2895 paths,
2896 )
2897}
2898
2899pub fn restore_worktree_paths_from_tree(
2900 worktree_root: impl AsRef<Path>,
2901 git_dir: impl AsRef<Path>,
2902 format: ObjectFormat,
2903 tree_oid: &ObjectId,
2904 paths: &[PathBuf],
2905) -> Result<RestoreResult> {
2906 let worktree_root = worktree_root.as_ref();
2907 let git_dir = git_dir.as_ref();
2908 let index_path = repository_index_path(git_dir);
2909 let index = if index_path.exists() {
2910 Index::parse(&fs::read(&index_path)?, format)?
2911 } else {
2912 Index {
2913 version: 2,
2914 entries: Vec::new(),
2915 extensions: Vec::new(),
2916 checksum: None,
2917 }
2918 };
2919 let db = FileObjectDatabase::from_git_dir(git_dir, format);
2920 let source_entries = tree_entries(&db, format, tree_oid)?;
2921 restore_worktree_paths_from_entries(
2922 worktree_root,
2923 git_dir,
2924 format,
2925 &db,
2926 index,
2927 &source_entries,
2928 paths,
2929 )
2930}
2931
2932pub(crate) fn restore_worktree_paths_from_entries(
2933 worktree_root: &Path,
2934 git_dir: &Path,
2935 format: ObjectFormat,
2936 db: &FileObjectDatabase,
2937 index: Index,
2938 source_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
2939 paths: &[PathBuf],
2940) -> Result<RestoreResult> {
2941 let index_entries = index
2942 .entries
2943 .into_iter()
2944 .map(|entry| entry.path.into_bytes())
2945 .collect::<BTreeSet<_>>();
2946 let config = sley_config::read_repo_config(git_dir, None).unwrap_or_default();
2947 let mut restored = BTreeSet::new();
2948 let matched_paths = checkout_selected_paths(
2949 worktree_root,
2950 paths,
2951 index_entries
2952 .iter()
2953 .chain(source_entries.keys())
2954 .map(Vec::as_slice),
2955 false,
2956 )?;
2957 for path in matched_paths {
2958 if let Some(entry) = source_entries.get(&path) {
2959 materialize_path_restore_entry_filtered(
2960 db,
2961 format,
2962 worktree_root,
2963 git_dir,
2964 &path,
2965 entry,
2966 &config,
2967 )?;
2968 } else {
2969 remove_worktree_file(worktree_root, &path)?;
2970 }
2971 restored.insert(path);
2972 }
2973 Ok(RestoreResult {
2974 restored: restored.len(),
2975 })
2976}