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 Some(config) = smudge_config else {
736 return materialize_tree_entry(db, worktree_root, path, entry);
737 };
738 let Some(matcher) = attributes else {
739 return materialize_tree_entry(db, worktree_root, path, entry);
740 };
741 let object = read_expected_object(db, &entry.oid, ObjectType::Blob)?;
742 let checks = matcher.attributes_for_path(path, &filter_attribute_names(), false);
743 let body = match apply_smudge_filter_with_attributes_maybe_delayed(
744 config,
745 &checks,
746 path,
747 &object.body,
748 format,
749 delayed.is_some(),
750 )? {
751 SmudgeFilterResult::Content(body) => body,
752 SmudgeFilterResult::Delayed { process } => {
753 if let Some(queue) = delayed.as_deref_mut() {
754 queue.enqueue(process, path, entry);
755 return Ok(unmaterialized_index_entry(path, entry));
756 }
757 return Err(GitError::InvalidFormat(
758 "smudge filter requested delay without a checkout queue".into(),
759 ));
760 }
761 };
762 let file_path = worktree_path(worktree_root, path)?;
763 prepare_blob_parent_dirs(worktree_root, &file_path)?;
764 remove_existing_worktree_path(&file_path)?;
765 fs::write(&file_path, &body)?;
766 set_worktree_file_mode(&file_path, entry.mode)?;
767 let metadata = fs::metadata(&file_path)?;
768 let mut index_entry = index_entry_from_metadata(path.to_vec(), entry.oid, &metadata);
769 index_entry.mode = entry.mode;
770 Ok(index_entry)
771}
772
773pub(crate) fn checkout_commit_to_index_and_worktree_sparse(
784 worktree_root: &Path,
785 git_dir: &Path,
786 format: ObjectFormat,
787 target: &ObjectId,
788 sparse: Option<(&SparseCheckout, SparseCheckoutMode)>,
789 smudge_config: Option<&GitConfig>,
790 process_metadata: Option<Vec<(String, String)>>,
791) -> Result<usize> {
792 let _process_filter_metadata = set_process_filter_metadata(process_metadata);
793 let _process_filter_cwd = set_process_filter_cwd(Some(worktree_root.to_path_buf()));
794 let previously_skipped = skip_worktree_paths(git_dir, format)?;
795 let db = FileObjectDatabase::from_git_dir(git_dir, format);
796 let commit = read_commit(&db, format, target)?;
797 let mut target_entries = BTreeMap::new();
798 collect_tree_entries(&db, format, &commit.tree, &mut target_entries)?;
799
800 let mut dirty = false;
803 stream_short_status(worktree_root, git_dir, format, |entry| {
804 if previously_skipped.contains(entry.path) {
805 return Ok(StreamControl::Continue);
806 }
807 if entry.index_mode.is_some_and(sley_index::is_gitlink)
812 || entry.worktree_mode.is_some_and(sley_index::is_gitlink)
813 {
814 return Ok(StreamControl::Continue);
815 }
816 if entry.index == b'?' && entry.worktree == b'?' {
820 let path = entry.path.strip_suffix(b"/").unwrap_or(entry.path);
821 if target_entries
822 .get(path)
823 .is_some_and(|target| sley_index::is_gitlink(target.mode))
824 {
825 return Ok(StreamControl::Continue);
826 }
827 }
828 dirty = true;
829 Ok(StreamControl::Stop)
830 })?;
831 if dirty {
832 return Err(GitError::Transaction(
833 "checkout requires a clean working tree".into(),
834 ));
835 }
836
837 let matcher = sparse.map(|(spec, mode)| SparseMatcher::new(spec, mode));
838 let attributes = smudge_config
839 .map(|_| build_tree_attribute_matcher(worktree_root, &db, format, &commit.tree))
840 .transpose()?;
841
842 for path in read_index_entries(git_dir, format)?.keys() {
843 if target_entries.contains_key(path) {
844 continue;
845 }
846 if previously_skipped.contains(path) {
848 continue;
849 }
850 remove_worktree_file(worktree_root, path)?;
851 }
852
853 let mut index_entries = Vec::new();
854 let mut delayed_checkout = DelayedCheckoutQueue::default();
855 for (path, entry) in &target_entries {
856 let in_cone = matcher.as_ref().map_or_else(
857 || !previously_skipped.contains(path),
858 |matcher| matcher.includes_file(path),
859 );
860 let index_entry = if in_cone {
861 materialize_tree_entry_with_optional_smudge(
862 &db,
863 format,
864 worktree_root,
865 path,
866 entry,
867 smudge_config,
868 attributes.as_ref(),
869 Some(&mut delayed_checkout),
870 )?
871 } else {
872 remove_worktree_file(worktree_root, path)?;
876 let mut index_entry = restored_head_index_entry(worktree_root, &db, path, entry)?;
877 set_skip_worktree(&mut index_entry);
878 index_entry
879 };
880 index_entries.push(index_entry);
881 }
882 let mut delayed_updates = finish_delayed_checkout(worktree_root, delayed_checkout)?;
883 for entry in &mut index_entries {
884 if let Some(updated) = delayed_updates.remove(entry.path.as_bytes()) {
885 *entry = updated;
886 }
887 }
888 index_entries.sort_by(|left, right| left.path.cmp(&right.path));
889 let mut index = Index {
890 version: 2,
891 entries: index_entries,
892 extensions: preserved_index_extensions(git_dir, format)?,
893 checksum: None,
894 };
895 normalize_index_version_for_extended_flags(&mut index);
896 refresh_cache_tree(&mut index, &db);
897 write_repository_index_ref(git_dir, format, &index)?;
898 Ok(target_entries.len())
899}
900
901pub(crate) fn skip_worktree_paths(
902 git_dir: &Path,
903 format: ObjectFormat,
904) -> Result<BTreeSet<Vec<u8>>> {
905 let index_path = repository_index_path(git_dir);
906 if !index_path.exists() {
907 return Ok(BTreeSet::new());
908 }
909 let index = Index::parse(&fs::read(index_path)?, format)?;
910 Ok(index
911 .entries
912 .into_iter()
913 .filter(index_entry_skip_worktree)
914 .map(|entry| entry.path.into_bytes())
915 .collect())
916}
917
918pub fn restore_worktree_paths(
919 worktree_root: impl AsRef<Path>,
920 git_dir: impl AsRef<Path>,
921 format: ObjectFormat,
922 paths: &[PathBuf],
923) -> Result<RestoreResult> {
924 restore_worktree_paths_inner(
925 worktree_root.as_ref(),
926 git_dir.as_ref(),
927 format,
928 paths,
929 None,
930 )
931}
932
933pub fn restore_worktree_paths_filtered(
936 worktree_root: impl AsRef<Path>,
937 git_dir: impl AsRef<Path>,
938 format: ObjectFormat,
939 paths: &[PathBuf],
940 config: &GitConfig,
941) -> Result<RestoreResult> {
942 restore_worktree_paths_inner(
943 worktree_root.as_ref(),
944 git_dir.as_ref(),
945 format,
946 paths,
947 Some(config),
948 )
949}
950
951pub(crate) fn restore_worktree_paths_inner(
952 worktree_root: &Path,
953 git_dir: &Path,
954 format: ObjectFormat,
955 paths: &[PathBuf],
956 smudge_config: Option<&GitConfig>,
957) -> Result<RestoreResult> {
958 let index_path = repository_index_path(git_dir);
959 if !index_path.exists() {
960 return Err(GitError::Exit(1));
961 }
962 let mut index = Index::parse(&fs::read(&index_path)?, format)?;
963 let stat_cache = IndexStatCache::from_index(&index, &index_path);
964 let db = FileObjectDatabase::from_git_dir(git_dir, format);
965 let mut restored = BTreeSet::new();
966 let selected = checkout_selected_positions(
967 worktree_root,
968 paths,
969 index
970 .entries
971 .iter()
972 .enumerate()
973 .map(|(position, entry)| (position, entry.path.as_bytes())),
974 false,
975 )?;
976 for position in selected {
977 let refreshed = restore_index_entry(
978 worktree_root,
979 git_dir,
980 format,
981 &db,
982 &index.entries[position],
983 smudge_config,
984 Some(&stat_cache),
985 )?;
986 restored.insert(index.entries[position].path.clone());
987 if let Some(refreshed) = refreshed {
988 index.entries[position] = refreshed;
989 }
990 }
991 write_repository_index_ref(git_dir, format, &index)?;
992 Ok(RestoreResult {
993 restored: restored.len(),
994 })
995}
996
997pub fn checkout_index_paths(
998 worktree_root: impl AsRef<Path>,
999 git_dir: impl AsRef<Path>,
1000 format: ObjectFormat,
1001 paths: &[PathBuf],
1002 options: CheckoutIndexPathOptions<'_>,
1003) -> Result<RestoreResult> {
1004 let worktree_root = worktree_root.as_ref();
1005 let git_dir = git_dir.as_ref();
1006 let index_path = repository_index_path(git_dir);
1007 if !index_path.exists() {
1008 return Err(GitError::Exit(1));
1009 }
1010 let mut index = Index::parse(&fs::read(&index_path)?, format)?;
1011 if options.merge {
1012 checkout_unmerge_resolve_undo_paths(worktree_root, &mut index, format, paths)?;
1013 }
1014 let stat_cache = IndexStatCache::from_index(&index, &index_path);
1015 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1016 let selected = checkout_selected_index_paths(worktree_root, &index, paths)?;
1017
1018 if options.stage.is_none() && !options.merge && !options.force {
1019 for path in &selected {
1020 if checkout_path_is_unmerged(&index, path) {
1021 eprintln!(
1022 "error: path '{}' is unmerged",
1023 String::from_utf8_lossy(path)
1024 );
1025 return Err(GitError::Exit(1));
1026 }
1027 }
1028 }
1029
1030 let mut refreshed = BTreeMap::new();
1031 let mut restored = BTreeSet::new();
1032 let mut delayed_checkout = DelayedCheckoutQueue::default();
1033 for path in selected {
1034 let positions = index
1035 .entries
1036 .iter()
1037 .enumerate()
1038 .filter_map(|(position, entry)| (entry.path.as_bytes() == path).then_some(position))
1039 .collect::<Vec<_>>();
1040 let stage0 = positions
1041 .iter()
1042 .copied()
1043 .find(|position| index.entries[*position].stage() == Stage::Normal);
1044 let is_unmerged = positions
1045 .iter()
1046 .any(|position| index.entries[*position].stage() != Stage::Normal);
1047
1048 if is_unmerged {
1049 if let Some(stage) = options.stage {
1050 let wanted = match stage {
1051 CheckoutStage::Ours => Stage::Ours,
1052 CheckoutStage::Theirs => Stage::Theirs,
1053 };
1054 let Some(position) = positions
1055 .iter()
1056 .copied()
1057 .find(|position| index.entries[*position].stage() == wanted)
1058 else {
1059 if !options.overlay {
1060 remove_worktree_file(worktree_root, &path)?;
1061 restored.insert(path);
1062 continue;
1063 }
1064 eprintln!(
1065 "error: path '{}' does not have {} version",
1066 String::from_utf8_lossy(&path),
1067 match stage {
1068 CheckoutStage::Ours => "our",
1069 CheckoutStage::Theirs => "their",
1070 }
1071 );
1072 return Err(GitError::Exit(1));
1073 };
1074 if restore_index_entry_maybe_delayed(
1075 worktree_root,
1076 git_dir,
1077 format,
1078 &db,
1079 &index.entries[position],
1080 options.smudge_config,
1081 Some(&stat_cache),
1082 Some(&mut delayed_checkout),
1083 )?
1084 .is_some()
1085 {
1086 restored.insert(path);
1087 }
1088 continue;
1089 }
1090 if options.merge {
1091 checkout_merge_unmerged_path(
1092 worktree_root,
1093 &db,
1094 &index,
1095 &positions,
1096 options.conflict_style,
1097 )?;
1098 restored.insert(path);
1099 continue;
1100 }
1101 if options.force {
1102 continue;
1103 }
1104 }
1105
1106 if let Some(position) = stage0 {
1107 if let Some(updated) = restore_index_entry_maybe_delayed(
1108 worktree_root,
1109 git_dir,
1110 format,
1111 &db,
1112 &index.entries[position],
1113 options.smudge_config,
1114 Some(&stat_cache),
1115 Some(&mut delayed_checkout),
1116 )? {
1117 refreshed.insert(position, updated);
1118 restored.insert(path);
1119 }
1120 }
1121 }
1122
1123 let mut delayed_updates = finish_delayed_checkout(worktree_root, delayed_checkout)?;
1124 for (position, entry) in index.entries.iter().enumerate() {
1125 if let Some(updated) = delayed_updates.remove(entry.path.as_bytes()) {
1126 refreshed.insert(position, updated);
1127 }
1128 }
1129
1130 for (position, entry) in refreshed {
1131 index.entries[position] = entry;
1132 }
1133 if !index.entries.is_empty() {
1134 write_repository_index_ref(git_dir, format, &index)?;
1135 }
1136 Ok(RestoreResult {
1137 restored: restored.len(),
1138 })
1139}
1140
1141pub fn unresolve_index_paths(
1142 worktree_root: impl AsRef<Path>,
1143 git_dir: impl AsRef<Path>,
1144 format: ObjectFormat,
1145 paths: &[PathBuf],
1146) -> Result<()> {
1147 let worktree_root = worktree_root.as_ref();
1148 let git_dir = git_dir.as_ref();
1149 let index_path = repository_index_path(git_dir);
1150 if !index_path.exists() {
1151 return Ok(());
1152 }
1153 let mut index = Index::parse(&fs::read(&index_path)?, format)?;
1154 checkout_unmerge_resolve_undo_paths(worktree_root, &mut index, format, paths)?;
1155 write_repository_index_ref(git_dir, format, &index)
1156}
1157
1158pub(crate) fn checkout_selected_index_paths(
1159 worktree_root: &Path,
1160 index: &Index,
1161 paths: &[PathBuf],
1162) -> Result<BTreeSet<Vec<u8>>> {
1163 let index_paths = index
1164 .entries
1165 .iter()
1166 .map(|entry| entry.path.as_bytes().to_vec())
1167 .collect::<BTreeSet<_>>();
1168 checkout_selected_paths(
1169 worktree_root,
1170 paths,
1171 index_paths.iter().map(Vec::as_slice),
1172 false,
1173 )
1174}
1175
1176#[derive(Debug)]
1177struct CheckoutPathspec {
1178 display: String,
1179 element: PathspecElement,
1180 matched: bool,
1181}
1182
1183#[derive(Debug)]
1184struct CheckoutPathspecs {
1185 specs: Vec<CheckoutPathspec>,
1186 have_include: bool,
1187}
1188
1189impl CheckoutPathspecs {
1190 fn parse(worktree_root: &Path, paths: &[PathBuf]) -> Result<Self> {
1191 let mut specs = Vec::with_capacity(paths.len());
1192 let mut have_include = false;
1193 for path in paths {
1194 let git_path = checkout_pathspec_pattern(worktree_root, path)?;
1195 let element = PathspecElement::parse(&git_path, PathspecMatchMagic::default())
1196 .map_err(|err| GitError::Command(format!("bad pathspec: {err}")))?;
1197 have_include |= !element.is_exclude();
1198 specs.push(CheckoutPathspec {
1199 display: path.display().to_string(),
1200 element,
1201 matched: false,
1202 });
1203 }
1204 Ok(Self {
1205 specs,
1206 have_include,
1207 })
1208 }
1209
1210 fn is_empty(&self) -> bool {
1211 self.specs.is_empty()
1212 }
1213
1214 fn matches(&mut self, candidate: &[u8]) -> bool {
1215 if self.specs.is_empty() {
1216 return true;
1217 }
1218
1219 let mut included = false;
1220 let mut excluded = false;
1221 for spec in &mut self.specs {
1222 if spec.element.matches_path(candidate) {
1223 spec.matched = true;
1224 if spec.element.is_exclude() {
1225 excluded = true;
1226 } else {
1227 included = true;
1228 }
1229 }
1230 }
1231
1232 !excluded && (!self.have_include || included)
1233 }
1234
1235 fn require_matched_includes(&self, allow_unmatched: bool) -> Result<()> {
1236 if allow_unmatched {
1237 return Ok(());
1238 }
1239 if let Some(spec) = self
1240 .specs
1241 .iter()
1242 .find(|spec| !spec.element.is_exclude() && !spec.matched)
1243 {
1244 eprintln!(
1245 "error: pathspec '{}' did not match any file(s) known to git",
1246 spec.display
1247 );
1248 return Err(GitError::Exit(1));
1249 }
1250 Ok(())
1251 }
1252}
1253
1254fn checkout_pathspec_pattern(worktree_root: &Path, path: &Path) -> Result<Vec<u8>> {
1255 let absolute = if path.is_absolute() {
1256 path.to_path_buf()
1257 } else {
1258 worktree_root.join(path)
1259 };
1260 let absolute = normalize_absolute_path_lexically(&absolute);
1261 let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
1262 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
1263 })?;
1264 git_path_bytes(relative)
1265}
1266
1267fn checkout_selected_paths<'a, I>(
1268 worktree_root: &Path,
1269 paths: &[PathBuf],
1270 candidates: I,
1271 allow_unmatched: bool,
1272) -> Result<BTreeSet<Vec<u8>>>
1273where
1274 I: IntoIterator<Item = &'a [u8]>,
1275{
1276 let mut pathspecs = CheckoutPathspecs::parse(worktree_root, paths)?;
1277 if pathspecs.is_empty() {
1278 return Ok(BTreeSet::new());
1279 }
1280
1281 let mut selected = BTreeSet::new();
1282 for candidate in candidates {
1283 if pathspecs.matches(candidate) {
1284 selected.insert(candidate.to_vec());
1285 }
1286 }
1287 pathspecs.require_matched_includes(allow_unmatched)?;
1288 Ok(selected)
1289}
1290
1291fn checkout_selected_positions<'a, I>(
1292 worktree_root: &Path,
1293 paths: &[PathBuf],
1294 candidates: I,
1295 allow_unmatched: bool,
1296) -> Result<BTreeSet<usize>>
1297where
1298 I: IntoIterator<Item = (usize, &'a [u8])>,
1299{
1300 let mut pathspecs = CheckoutPathspecs::parse(worktree_root, paths)?;
1301 if pathspecs.is_empty() {
1302 return Ok(BTreeSet::new());
1303 }
1304
1305 let mut selected = BTreeSet::new();
1306 for (position, candidate) in candidates {
1307 if pathspecs.matches(candidate) {
1308 selected.insert(position);
1309 }
1310 }
1311 pathspecs.require_matched_includes(allow_unmatched)?;
1312 Ok(selected)
1313}
1314
1315pub(crate) fn checkout_unmerge_resolve_undo_paths(
1316 worktree_root: &Path,
1317 index: &mut Index,
1318 format: ObjectFormat,
1319 paths: &[PathBuf],
1320) -> Result<()> {
1321 let records = parse_resolve_undo_records(index.extension(b"REUC")?, format)?;
1322 if records.is_empty() {
1323 return Ok(());
1324 }
1325 let mut pathspecs = CheckoutPathspecs::parse(worktree_root, paths)?;
1326 let mut remaining = Vec::new();
1327 let mut unmerged_any = false;
1328 for record in records {
1329 if pathspecs.matches(&record.path) {
1330 remove_index_entries_with_path(&mut index.entries, &record.path);
1331 for (idx, stage) in record.stages.into_iter().enumerate() {
1332 let Some((mode, oid)) = stage else {
1333 continue;
1334 };
1335 index.entries.push(resolve_undo_index_entry(
1336 record.path.clone(),
1337 mode,
1338 oid,
1339 (idx + 1) as u16,
1340 ));
1341 }
1342 unmerged_any = true;
1343 } else {
1344 remaining.push(record);
1345 }
1346 }
1347 if unmerged_any {
1348 index.entries.sort_by(compare_index_key);
1349 normalize_index_version_for_extended_flags(index);
1350 set_resolve_undo_extension(index, &remaining)?;
1351 }
1352 Ok(())
1353}
1354
1355pub(crate) fn resolve_undo_index_entry(
1356 path: Vec<u8>,
1357 mode: u32,
1358 oid: ObjectId,
1359 stage: u16,
1360) -> IndexEntry {
1361 let name_len = (path
1362 .len()
1363 .min(sley_index::INDEX_FLAG_NAME_LENGTH_MASK as usize)) as u16;
1364 IndexEntry {
1365 ctime_seconds: 0,
1366 ctime_nanoseconds: 0,
1367 mtime_seconds: 0,
1368 mtime_nanoseconds: 0,
1369 dev: 0,
1370 ino: 0,
1371 mode,
1372 uid: 0,
1373 gid: 0,
1374 size: 0,
1375 oid,
1376 flags: name_len | (stage << 12),
1377 flags_extended: 0,
1378 path: path.into(),
1379 }
1380}
1381
1382pub(crate) fn checkout_path_is_unmerged(index: &Index, path: &[u8]) -> bool {
1383 index
1384 .entries
1385 .iter()
1386 .any(|entry| entry.path.as_bytes() == path && entry.stage() != Stage::Normal)
1387}
1388
1389pub(crate) fn checkout_merge_unmerged_path(
1390 worktree_root: &Path,
1391 db: &FileObjectDatabase,
1392 index: &Index,
1393 positions: &[usize],
1394 style: CheckoutConflictStyle,
1395) -> Result<()> {
1396 let mut base = None;
1397 let mut ours = None;
1398 let mut theirs = None;
1399 for position in positions {
1400 let entry = &index.entries[*position];
1401 match entry.stage() {
1402 Stage::Base => base = Some(entry),
1403 Stage::Ours => ours = Some(entry),
1404 Stage::Theirs => theirs = Some(entry),
1405 Stage::Normal => {}
1406 }
1407 }
1408 let Some(ours) = ours else {
1409 return Ok(());
1410 };
1411 let Some(theirs) = theirs else {
1412 return Ok(());
1413 };
1414 let base_body = match base {
1415 Some(entry) => read_expected_object(db, &entry.oid, ObjectType::Blob)?
1416 .body
1417 .clone(),
1418 None => Vec::new(),
1419 };
1420 let ours_body = read_expected_object(db, &ours.oid, ObjectType::Blob)?
1421 .body
1422 .clone();
1423 let theirs_body = read_expected_object(db, &theirs.oid, ObjectType::Blob)?
1424 .body
1425 .clone();
1426 let result = sley_diff_merge::merge_blobs(
1427 &base_body,
1428 &ours_body,
1429 &theirs_body,
1430 &sley_diff_merge::MergeBlobOptions {
1431 ours_label: "ours",
1432 theirs_label: "theirs",
1433 base_label: "base",
1434 style: match style {
1435 CheckoutConflictStyle::Merge => sley_diff_merge::ConflictStyle::Merge,
1436 CheckoutConflictStyle::Diff3 => sley_diff_merge::ConflictStyle::Diff3,
1437 },
1438 favor: sley_diff_merge::MergeFavor::None,
1439 ws_ignore: sley_diff_merge::WsIgnore::EMPTY,
1440 marker_size: 7,
1441 },
1442 );
1443 let file_path = worktree_path(worktree_root, ours.path.as_bytes())?;
1444 prepare_blob_parent_dirs(worktree_root, &file_path)?;
1445 remove_existing_worktree_path(&file_path)?;
1446 fs::write(&file_path, result.content)?;
1447 set_worktree_file_mode(&file_path, ours.mode)?;
1448 Ok(())
1449}
1450
1451pub fn restore_index_paths_from_head(
1452 worktree_root: impl AsRef<Path>,
1453 git_dir: impl AsRef<Path>,
1454 format: ObjectFormat,
1455 paths: &[PathBuf],
1456) -> Result<RestoreResult> {
1457 let worktree_root = worktree_root.as_ref();
1458 let git_dir = git_dir.as_ref();
1459 let index_path = repository_index_path(git_dir);
1460 let index = if index_path.exists() {
1461 Index::parse(&fs::read(&index_path)?, format)?
1462 } else {
1463 Index {
1464 version: 2,
1465 entries: Vec::new(),
1466 extensions: Vec::new(),
1467 checksum: None,
1468 }
1469 };
1470 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1471 let head_entries = head_tree_entries(git_dir, format, &db)?;
1472 restore_index_paths_from_entries(
1473 worktree_root,
1474 git_dir,
1475 format,
1476 &db,
1477 index,
1478 &head_entries,
1479 paths,
1480 false,
1481 )
1482}
1483
1484pub fn restore_index_paths_from_tree(
1485 worktree_root: impl AsRef<Path>,
1486 git_dir: impl AsRef<Path>,
1487 format: ObjectFormat,
1488 tree_oid: &ObjectId,
1489 paths: &[PathBuf],
1490) -> Result<RestoreResult> {
1491 let worktree_root = worktree_root.as_ref();
1492 let git_dir = git_dir.as_ref();
1493 let index_path = repository_index_path(git_dir);
1494 let index = if index_path.exists() {
1495 Index::parse(&fs::read(&index_path)?, format)?
1496 } else {
1497 Index {
1498 version: 2,
1499 entries: Vec::new(),
1500 extensions: Vec::new(),
1501 checksum: None,
1502 }
1503 };
1504 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1505 let source_entries = tree_entries(&db, format, tree_oid)?;
1506 restore_index_paths_from_entries(
1507 worktree_root,
1508 git_dir,
1509 format,
1510 &db,
1511 index,
1512 &source_entries,
1513 paths,
1514 false,
1515 )
1516}
1517
1518pub fn restore_index_paths_from_tree_allow_unmatched(
1519 worktree_root: impl AsRef<Path>,
1520 git_dir: impl AsRef<Path>,
1521 format: ObjectFormat,
1522 tree_oid: &ObjectId,
1523 paths: &[PathBuf],
1524) -> Result<RestoreResult> {
1525 let worktree_root = worktree_root.as_ref();
1526 let git_dir = git_dir.as_ref();
1527 let index_path = repository_index_path(git_dir);
1528 let index = if index_path.exists() {
1529 Index::parse(&fs::read(&index_path)?, format)?
1530 } else {
1531 Index {
1532 version: 2,
1533 entries: Vec::new(),
1534 extensions: Vec::new(),
1535 checksum: None,
1536 }
1537 };
1538 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1539 let source_entries = tree_entries(&db, format, tree_oid)?;
1540 restore_index_paths_from_entries(
1541 worktree_root,
1542 git_dir,
1543 format,
1544 &db,
1545 index,
1546 &source_entries,
1547 paths,
1548 true,
1549 )
1550}
1551
1552pub(crate) fn restore_index_paths_from_entries(
1553 worktree_root: &Path,
1554 git_dir: &Path,
1555 format: ObjectFormat,
1556 db: &FileObjectDatabase,
1557 mut index: Index,
1558 source_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1559 paths: &[PathBuf],
1560 allow_unmatched: bool,
1561) -> Result<RestoreResult> {
1562 let sparse = active_sparse_checkout(git_dir)?;
1563 if index.is_sparse() {
1564 expand_sparse_index(&mut index, db, format)?;
1565 }
1566 let index_version = index.version;
1567 let extensions = index_extensions_without_cache_tree(&index.extensions);
1568 let mut index_entries = index
1569 .entries
1570 .into_iter()
1571 .map(|entry| (entry.path.as_bytes().to_vec(), entry))
1572 .collect::<BTreeMap<_, _>>();
1573 let prior_skip_worktree = index_entries
1574 .iter()
1575 .filter(|(_, entry)| entry.is_skip_worktree())
1576 .map(|(path, _)| path.clone())
1577 .collect::<BTreeSet<_>>();
1578 let mut restored = BTreeSet::new();
1579 let matched_paths = checkout_selected_paths(
1580 worktree_root,
1581 paths,
1582 index_entries
1583 .keys()
1584 .chain(source_entries.keys())
1585 .map(Vec::as_slice),
1586 allow_unmatched,
1587 )?;
1588 for path in matched_paths {
1589 if let Some(entry) = source_entries.get(&path) {
1590 let unchanged = index_entries.get(&path).is_some_and(|existing| {
1597 existing.oid == entry.oid
1598 && existing.mode == entry.mode
1599 && !existing.is_intent_to_add()
1600 });
1601 if !unchanged {
1602 let mut restored = restored_head_index_entry(worktree_root, db, &path, entry)?;
1603 if prior_skip_worktree.contains(&path) {
1604 restored.set_skip_worktree(true);
1605 }
1606 index_entries.insert(path.clone(), restored);
1607 }
1608 } else {
1609 index_entries.remove(&path);
1610 }
1611 restored.insert(path);
1612 }
1613 let mut entries = index_entries.into_values().collect::<Vec<_>>();
1614 entries.sort_by(|left, right| left.path.cmp(&right.path));
1615 let restored_paths = restored.iter().cloned().collect::<Vec<_>>();
1616 let mut index = Index {
1617 version: index_version,
1618 entries,
1619 extensions,
1620 checksum: None,
1621 };
1622 invalidate_untracked_cache_for_git_paths(&mut index, format, &restored_paths)?;
1623 if let Some((sparse, mode)) = sparse
1624 && sparse.sparse_index
1625 {
1626 let matcher = SparseMatcher::new(&sparse, mode);
1627 collapse_to_sparse_index(&mut index, &matcher, db, format)?;
1628 }
1629 write_repository_index_ref(git_dir, format, &index)?;
1630 Ok(RestoreResult {
1631 restored: restored.len(),
1632 })
1633}
1634
1635pub fn restore_index_and_worktree_paths_from_head(
1636 worktree_root: impl AsRef<Path>,
1637 git_dir: impl AsRef<Path>,
1638 format: ObjectFormat,
1639 paths: &[PathBuf],
1640 overlay: bool,
1641) -> Result<RestoreResult> {
1642 let worktree_root = worktree_root.as_ref();
1643 let git_dir = git_dir.as_ref();
1644 let index_path = repository_index_path(git_dir);
1645 let index = if index_path.exists() {
1646 Index::parse(&fs::read(&index_path)?, format)?
1647 } else {
1648 Index {
1649 version: 2,
1650 entries: Vec::new(),
1651 extensions: Vec::new(),
1652 checksum: None,
1653 }
1654 };
1655 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1656 let head_entries = head_tree_entries(git_dir, format, &db)?;
1657 restore_index_and_worktree_paths_from_entries(
1658 worktree_root,
1659 git_dir,
1660 format,
1661 &db,
1662 index,
1663 &head_entries,
1664 paths,
1665 overlay,
1666 )
1667}
1668
1669pub fn restore_index_and_worktree_paths_from_tree(
1670 worktree_root: impl AsRef<Path>,
1671 git_dir: impl AsRef<Path>,
1672 format: ObjectFormat,
1673 tree_oid: &ObjectId,
1674 paths: &[PathBuf],
1675 overlay: bool,
1676) -> Result<RestoreResult> {
1677 let worktree_root = worktree_root.as_ref();
1678 let git_dir = git_dir.as_ref();
1679 let index_path = repository_index_path(git_dir);
1680 let index = if index_path.exists() {
1681 Index::parse(&fs::read(&index_path)?, format)?
1682 } else {
1683 Index {
1684 version: 2,
1685 entries: Vec::new(),
1686 extensions: Vec::new(),
1687 checksum: None,
1688 }
1689 };
1690 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1691 let source_entries = tree_entries(&db, format, tree_oid)?;
1692 restore_index_and_worktree_paths_from_entries(
1693 worktree_root,
1694 git_dir,
1695 format,
1696 &db,
1697 index,
1698 &source_entries,
1699 paths,
1700 overlay,
1701 )
1702}
1703
1704pub(crate) fn restore_index_and_worktree_paths_from_entries(
1705 worktree_root: &Path,
1706 git_dir: &Path,
1707 format: ObjectFormat,
1708 db: &FileObjectDatabase,
1709 index: Index,
1710 source_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1711 paths: &[PathBuf],
1712 overlay: bool,
1713) -> Result<RestoreResult> {
1714 let index_version = index.version;
1715 let extensions = index_extensions_without_cache_tree(&index.extensions);
1716 let config = sley_config::read_repo_config(git_dir, None).unwrap_or_default();
1717 let mut index_entries = index
1718 .entries
1719 .into_iter()
1720 .map(|entry| (entry.path.as_bytes().to_vec(), entry))
1721 .collect::<BTreeMap<_, _>>();
1722 let mut restored = BTreeSet::new();
1723 let matched_paths = checkout_selected_paths(
1724 worktree_root,
1725 paths,
1726 index_entries
1727 .keys()
1728 .chain(source_entries.keys())
1729 .map(Vec::as_slice),
1730 false,
1731 )?;
1732 for path in matched_paths {
1733 if let Some(entry) = source_entries.get(&path) {
1734 index_entries.insert(
1735 path.clone(),
1736 materialize_path_restore_entry_filtered(
1737 db,
1738 format,
1739 worktree_root,
1740 git_dir,
1741 &path,
1742 entry,
1743 &config,
1744 )?,
1745 );
1746 } else if overlay {
1747 continue;
1751 } else {
1752 index_entries.remove(&path);
1755 remove_worktree_file(worktree_root, &path)?;
1756 }
1757 restored.insert(path);
1758 }
1759 let mut entries = index_entries.into_values().collect::<Vec<_>>();
1760 entries.sort_by(|left, right| left.path.cmp(&right.path));
1761 let restored_paths = restored.iter().cloned().collect::<Vec<_>>();
1762 let mut index = Index {
1763 version: index_version,
1764 entries,
1765 extensions,
1766 checksum: None,
1767 };
1768 invalidate_untracked_cache_for_git_paths(&mut index, format, &restored_paths)?;
1769 write_repository_index_ref(git_dir, format, &index)?;
1770 Ok(RestoreResult {
1771 restored: restored.len(),
1772 })
1773}
1774
1775pub fn reset_index_and_worktree_to_commit(
1776 worktree_root: impl AsRef<Path>,
1777 git_dir: impl AsRef<Path>,
1778 format: ObjectFormat,
1779 commit_oid: &ObjectId,
1780) -> Result<RestoreResult> {
1781 let worktree_root = worktree_root.as_ref();
1782 let git_dir = git_dir.as_ref();
1783 let _process_filter_cwd = set_process_filter_cwd(Some(worktree_root.to_path_buf()));
1784 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1785 let commit = read_commit(&db, format, commit_oid)?;
1786 let mut target_entries = BTreeMap::new();
1787 collect_tree_entries(&db, format, &commit.tree, &mut target_entries)?;
1788 refuse_if_current_working_directory_becomes_file(worktree_root, &target_entries)?;
1789 let config = sley_config::read_repo_config(git_dir, None).unwrap_or_default();
1790 let attributes = build_tree_attribute_matcher(worktree_root, &db, format, &commit.tree)?;
1791
1792 for path in current_index_paths(git_dir, format, &db)? {
1799 if !target_entries.contains_key(&path) {
1800 remove_worktree_file(worktree_root, &path)?;
1801 }
1802 }
1803
1804 let mut index_entries = Vec::new();
1805 let mut delayed_checkout = DelayedCheckoutQueue::default();
1806 for (path, entry) in &target_entries {
1807 index_entries.push(materialize_tree_entry_with_optional_smudge(
1808 &db,
1809 format,
1810 worktree_root,
1811 path,
1812 entry,
1813 Some(&config),
1814 Some(&attributes),
1815 Some(&mut delayed_checkout),
1816 )?);
1817 }
1818 let mut delayed_updates = finish_delayed_checkout(worktree_root, delayed_checkout)?;
1819 for entry in &mut index_entries {
1820 if let Some(updated) = delayed_updates.remove(entry.path.as_bytes()) {
1821 *entry = updated;
1822 }
1823 }
1824 index_entries.sort_by(|left, right| left.path.cmp(&right.path));
1825 let extensions = preserved_index_extensions(git_dir, format)?;
1826 let mut index = Index {
1827 version: 2,
1828 entries: index_entries,
1829 extensions,
1830 checksum: None,
1831 };
1832 refresh_cache_tree(&mut index, &db);
1833 write_repository_index_ref(git_dir, format, &index)?;
1834 Ok(RestoreResult {
1835 restored: target_entries.len(),
1836 })
1837}
1838
1839pub fn reset_index_and_worktree_to_commit_with_process_filter_metadata(
1840 worktree_root: impl AsRef<Path>,
1841 git_dir: impl AsRef<Path>,
1842 format: ObjectFormat,
1843 commit_oid: &ObjectId,
1844 process_metadata: Option<ProcessFilterMetadata>,
1845) -> Result<RestoreResult> {
1846 let _process_filter_metadata = set_process_filter_metadata(process_metadata);
1847 reset_index_and_worktree_to_commit(worktree_root, git_dir, format, commit_oid)
1848}
1849
1850pub(crate) fn current_index_paths(
1856 git_dir: &Path,
1857 format: ObjectFormat,
1858 db: &FileObjectDatabase,
1859) -> Result<BTreeSet<Vec<u8>>> {
1860 let (index, _stat_cache, _head_matches) = read_index_with_stat_cache(git_dir, format, db)?;
1861 Ok(index
1862 .entries
1863 .into_iter()
1864 .map(|entry| entry.path.into_bytes())
1865 .collect())
1866}
1867
1868pub(crate) fn materialize_tree_entry(
1878 db: &FileObjectDatabase,
1879 worktree_root: &Path,
1880 path: &[u8],
1881 entry: &TrackedEntry,
1882) -> Result<IndexEntry> {
1883 if sley_index::is_gitlink(entry.mode) {
1884 let dir_path = worktree_path(worktree_root, path)?;
1885 materialize_gitlink_dir(worktree_root, &dir_path)?;
1886 return Ok(IndexEntry {
1887 ctime_seconds: 0,
1888 ctime_nanoseconds: 0,
1889 mtime_seconds: 0,
1890 mtime_nanoseconds: 0,
1891 dev: 0,
1892 ino: 0,
1893 mode: entry.mode,
1894 uid: 0,
1895 gid: 0,
1896 size: 0,
1897 oid: entry.oid,
1898 flags: path.len().min(0x0fff) as u16,
1899 flags_extended: 0,
1900 path: BString::from(path),
1901 });
1902 }
1903 let file_path = write_worktree_blob_entry(db, worktree_root, path, entry)?;
1904 let metadata = fs::symlink_metadata(&file_path)?;
1905 let mut index_entry = index_entry_from_metadata(path.to_vec(), entry.oid, &metadata);
1906 index_entry.mode = entry.mode;
1907 Ok(index_entry)
1908}
1909
1910pub(crate) fn materialize_gitlink_dir(worktree_root: &Path, dir_path: &Path) -> Result<()> {
1911 prepare_blob_parent_dirs(worktree_root, dir_path)?;
1912 if fs::symlink_metadata(dir_path).is_ok_and(|metadata| !metadata.is_dir()) {
1913 remove_existing_worktree_path(dir_path)?;
1914 }
1915 fs::create_dir_all(dir_path)?;
1916 Ok(())
1917}
1918
1919pub(crate) fn materialize_path_restore_entry_filtered(
1920 db: &FileObjectDatabase,
1921 format: ObjectFormat,
1922 worktree_root: &Path,
1923 git_dir: &Path,
1924 path: &[u8],
1925 entry: &TrackedEntry,
1926 config: &GitConfig,
1927) -> Result<IndexEntry> {
1928 if sley_index::is_gitlink(entry.mode) || (entry.mode & 0o170000) == 0o120000 {
1929 return materialize_tree_entry(db, worktree_root, path, entry);
1930 }
1931 let object = read_expected_object(db, &entry.oid, ObjectType::Blob)?;
1932 let checks = smudge_attribute_checks_from_index(worktree_root, git_dir, format, path)?;
1933 let body = apply_smudge_filter_with_attributes_cow_format(
1934 config,
1935 &checks,
1936 path,
1937 &object.body,
1938 format,
1939 )?;
1940 let file_path = worktree_path(worktree_root, path)?;
1941 prepare_blob_parent_dirs(worktree_root, &file_path)?;
1942 remove_existing_worktree_path(&file_path)?;
1943 fs::write(&file_path, &body)?;
1944 set_worktree_file_mode(&file_path, entry.mode)?;
1945 let metadata = fs::symlink_metadata(&file_path)?;
1946 let mut index_entry = index_entry_from_metadata(path.to_vec(), entry.oid, &metadata);
1947 index_entry.mode = entry.mode;
1948 Ok(index_entry)
1949}
1950
1951pub(crate) fn write_worktree_blob_entry(
1962 db: &FileObjectDatabase,
1963 worktree_root: &Path,
1964 path: &[u8],
1965 entry: &TrackedEntry,
1966) -> Result<PathBuf> {
1967 let object = read_expected_object(db, &entry.oid, ObjectType::Blob)?;
1968 let file_path = worktree_path(worktree_root, path)?;
1969 prepare_blob_parent_dirs(worktree_root, &file_path)?;
1972 remove_existing_worktree_path(&file_path)?;
1975 write_blob_body_or_symlink(&file_path, entry.mode, &object.body, &object.body)?;
1976 Ok(file_path)
1977}
1978
1979pub fn write_blob_body_or_symlink(
1998 file_path: &Path,
1999 mode: u32,
2000 body: &[u8],
2001 link_target: &[u8],
2002) -> Result<()> {
2003 if (mode & 0o170000) == 0o120000 {
2004 #[cfg(unix)]
2005 {
2006 use std::os::unix::ffi::OsStringExt;
2007 let target =
2008 std::path::PathBuf::from(std::ffi::OsString::from_vec(link_target.to_vec()));
2009 std::os::unix::fs::symlink(&target, file_path)?;
2010 }
2011 #[cfg(not(unix))]
2012 {
2013 let _ = link_target;
2014 fs::write(file_path, body)?;
2015 }
2016 } else {
2017 fs::write(file_path, body)?;
2018 set_worktree_file_mode(file_path, mode)?;
2019 }
2020 Ok(())
2021}
2022
2023pub(crate) fn prepare_blob_parent_dirs(worktree_root: &Path, file_path: &Path) -> Result<()> {
2037 let parent = match file_path.parent() {
2038 Some(parent) => parent,
2039 None => return Ok(()),
2040 };
2041 if parent.is_dir() {
2043 return Ok(());
2044 }
2045 let mut components: Vec<&Path> = Vec::new();
2049 let mut cursor = Some(parent);
2050 while let Some(dir) = cursor {
2051 if dir == worktree_root {
2052 break;
2053 }
2054 components.push(dir);
2055 cursor = dir.parent();
2056 if cursor.is_none() {
2057 break;
2058 }
2059 }
2060 for dir in components.iter().rev() {
2062 match fs::symlink_metadata(dir) {
2063 Ok(metadata) if metadata.is_dir() => {}
2064 Ok(_) => {
2065 fs::remove_file(dir)?;
2068 fs::create_dir(dir)?;
2069 }
2070 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
2071 fs::create_dir(dir)?;
2072 }
2073 Err(err) => return Err(err.into()),
2074 }
2075 }
2076 Ok(())
2077}
2078
2079pub(crate) fn remove_existing_worktree_path(file_path: &Path) -> Result<()> {
2084 let metadata = match fs::symlink_metadata(file_path) {
2085 Ok(metadata) => metadata,
2086 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
2087 Err(err) => return Err(err.into()),
2088 };
2089 if metadata.is_dir() {
2090 if path_is_original_cwd(file_path) {
2091 return refuse_remove_current_working_directory(file_path);
2092 }
2093 match fs::remove_dir_all(file_path) {
2096 Ok(()) => {}
2097 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
2098 Err(err) => return Err(err.into()),
2099 }
2100 } else {
2101 fs::remove_file(file_path)?;
2102 }
2103 Ok(())
2104}
2105
2106#[cfg(unix)]
2126pub(crate) fn set_worktree_file_mode(file_path: &Path, entry_mode: u32) -> Result<()> {
2127 use std::os::unix::fs::PermissionsExt;
2128 let perms = match entry_mode {
2129 0o100755 => 0o755,
2130 0o100644 => 0o644,
2131 _ => return Ok(()),
2132 };
2133 fs::set_permissions(file_path, fs::Permissions::from_mode(perms))?;
2134 Ok(())
2135}
2136
2137#[cfg(not(unix))]
2138pub(crate) fn set_worktree_file_mode(_file_path: &Path, _entry_mode: u32) -> Result<()> {
2139 Ok(())
2140}
2141
2142pub fn checkout_tree_to_index_and_worktree(
2144 worktree_root: impl AsRef<Path>,
2145 git_dir: impl AsRef<Path>,
2146 format: ObjectFormat,
2147 tree_oid: &ObjectId,
2148) -> Result<RestoreResult> {
2149 let worktree_root = worktree_root.as_ref();
2150 let git_dir = git_dir.as_ref();
2151 let db = FileObjectDatabase::from_git_dir(git_dir, format);
2152 let mut target_entries = BTreeMap::new();
2153 collect_tree_entries(&db, format, tree_oid, &mut target_entries)?;
2154
2155 for path in read_index_entries(git_dir, format)?.keys() {
2156 if !target_entries.contains_key(path) {
2157 remove_worktree_file(worktree_root, path)?;
2158 }
2159 }
2160
2161 let mut index_entries = Vec::new();
2162 for (path, entry) in &target_entries {
2163 index_entries.push(materialize_tree_entry(&db, worktree_root, path, entry)?);
2164 }
2165 index_entries.sort_by(|left, right| left.path.cmp(&right.path));
2166 let extensions = preserved_index_extensions(git_dir, format)?;
2167 let mut index = Index {
2168 version: 2,
2169 entries: index_entries,
2170 extensions,
2171 checksum: None,
2172 };
2173 refresh_cache_tree(&mut index, &db);
2174 write_repository_index_ref(git_dir, format, &index)?;
2175 Ok(RestoreResult {
2176 restored: target_entries.len(),
2177 })
2178}
2179
2180pub fn reset_index_to_commit(
2181 worktree_root: impl AsRef<Path>,
2182 git_dir: impl AsRef<Path>,
2183 format: ObjectFormat,
2184 commit_oid: &ObjectId,
2185) -> Result<RestoreResult> {
2186 let worktree_root = worktree_root.as_ref();
2187 let git_dir = git_dir.as_ref();
2188 let db = FileObjectDatabase::from_git_dir(git_dir, format);
2189 let commit = read_commit(&db, format, commit_oid)?;
2190 let mut target_entries = BTreeMap::new();
2191 collect_tree_entries(&db, format, &commit.tree, &mut target_entries)?;
2192 let index_path = repository_index_path(git_dir);
2196 let prior_skip_worktree: BTreeSet<Vec<u8>> = match fs::read(&index_path) {
2197 Ok(bytes) => Index::parse(&bytes, format)?
2198 .entries
2199 .iter()
2200 .filter(|entry| entry.is_skip_worktree())
2201 .map(|entry| entry.path.as_bytes().to_vec())
2202 .collect(),
2203 Err(err) if err.kind() == std::io::ErrorKind::NotFound => BTreeSet::new(),
2204 Err(err) => return Err(err.into()),
2205 };
2206 let mut index_entries = Vec::new();
2207 for (path, entry) in &target_entries {
2208 let mut restored = restored_head_index_entry(worktree_root, &db, path, entry)?;
2209 if prior_skip_worktree.contains(path) {
2210 restored.set_skip_worktree(true);
2211 }
2212 index_entries.push(restored);
2213 }
2214 index_entries.sort_by(|left, right| left.path.cmp(&right.path));
2215 let mut index = Index {
2216 version: 2,
2217 entries: index_entries,
2218 extensions: preserved_index_extensions(git_dir, format)?,
2219 checksum: None,
2220 };
2221 index.upgrade_version_for_flags();
2222 refresh_cache_tree(&mut index, &db);
2223 write_repository_index_ref(git_dir, format, &index)?;
2224 Ok(RestoreResult {
2225 restored: target_entries.len(),
2226 })
2227}
2228
2229pub fn index_from_tree(
2239 db: &FileObjectDatabase,
2240 format: ObjectFormat,
2241 tree_oid: &ObjectId,
2242) -> Result<Index> {
2243 let mut entries: Vec<IndexEntry> = Vec::new();
2244 if *tree_oid != ObjectId::empty_tree(format) {
2245 let mut tree_entries = BTreeMap::new();
2246 collect_tree_entries(db, format, tree_oid, &mut tree_entries)?;
2247 entries.reserve(tree_entries.len());
2248 for (path, entry) in tree_entries {
2249 let name_len = (path.len().min(0x0fff)) as u16;
2250 entries.push(IndexEntry {
2251 ctime_seconds: 0,
2252 ctime_nanoseconds: 0,
2253 mtime_seconds: 0,
2254 mtime_nanoseconds: 0,
2255 dev: 0,
2256 ino: 0,
2257 mode: entry.mode,
2258 uid: 0,
2259 gid: 0,
2260 size: 0,
2261 oid: entry.oid,
2262 flags: name_len,
2263 flags_extended: 0,
2264 path: path.into(),
2265 });
2266 }
2267 }
2268 entries.sort_by(|left, right| left.path.cmp(&right.path));
2271 Ok(Index {
2272 version: 2,
2273 entries,
2274 extensions: Vec::new(),
2275 checksum: None,
2276 })
2277}
2278
2279pub fn path_in_sparse_checkout(
2298 path: &[u8],
2299 sparse: &SparseCheckout,
2300 mode: SparseCheckoutMode,
2301) -> bool {
2302 SparseMatcher::new(sparse, mode).includes_file(path)
2303}
2304
2305pub(crate) fn active_sparse_checkout(
2306 git_dir: &Path,
2307) -> Result<Option<(SparseCheckout, SparseCheckoutMode)>> {
2308 let worktree_config = GitConfig::read(git_dir.join("config.worktree")).unwrap_or_default();
2309 let repo_config = GitConfig::read(git_dir.join("config")).unwrap_or_default();
2310 let sparse_enabled = worktree_config
2311 .get_bool("core", None, "sparseCheckout")
2312 .or_else(|| repo_config.get_bool("core", None, "sparseCheckout"))
2313 .unwrap_or(false);
2314 if !sparse_enabled {
2315 return Ok(None);
2316 }
2317 let sparse_file = git_dir.join("info").join("sparse-checkout");
2318 if !sparse_file.exists() {
2319 return Ok(None);
2320 }
2321 let cone = worktree_config
2322 .get_bool("core", None, "sparseCheckoutCone")
2323 .or_else(|| repo_config.get_bool("core", None, "sparseCheckoutCone"))
2324 .unwrap_or(false);
2325 let sparse_index = cone
2326 && worktree_config
2327 .get_bool("index", None, "sparse")
2328 .or_else(|| repo_config.get_bool("index", None, "sparse"))
2329 .unwrap_or(false);
2330 let bytes = fs::read(sparse_file)?;
2331 let mut patterns = bytes
2332 .split(|byte| *byte == b'\n')
2333 .map(<[u8]>::to_vec)
2334 .collect::<Vec<_>>();
2335 if patterns.last().map(Vec::is_empty) == Some(true) {
2336 patterns.pop();
2337 }
2338 let mode = if cone {
2339 SparseCheckoutMode::Cone
2340 } else {
2341 SparseCheckoutMode::Full
2342 };
2343 Ok(Some((
2344 SparseCheckout {
2345 patterns,
2346 sparse_index,
2347 },
2348 mode,
2349 )))
2350}
2351
2352pub fn apply_sparse_checkout(
2355 worktree_root: impl AsRef<Path>,
2356 git_dir: impl AsRef<Path>,
2357 format: ObjectFormat,
2358 sparse: &SparseCheckout,
2359) -> Result<ApplySparseResult> {
2360 apply_sparse_checkout_with_mode(
2361 worktree_root,
2362 git_dir,
2363 format,
2364 sparse,
2365 SparseCheckoutMode::Auto,
2366 )
2367}
2368
2369pub fn apply_sparse_checkout_with_mode(
2372 worktree_root: impl AsRef<Path>,
2373 git_dir: impl AsRef<Path>,
2374 format: ObjectFormat,
2375 sparse: &SparseCheckout,
2376 mode: SparseCheckoutMode,
2377) -> Result<ApplySparseResult> {
2378 let worktree_root = worktree_root.as_ref();
2379 let git_dir = git_dir.as_ref();
2380 let index_path = repository_index_path(git_dir);
2381 let mut index = if index_path.exists() {
2382 Index::parse(&fs::read(&index_path)?, format)?
2383 } else {
2384 return Ok(ApplySparseResult {
2385 materialized: Vec::new(),
2386 skipped: Vec::new(),
2387 not_up_to_date: Vec::new(),
2388 });
2389 };
2390 let matcher = SparseMatcher::new(sparse, mode);
2391 let db = FileObjectDatabase::from_git_dir(git_dir, format);
2392 if index.entries.iter().any(IndexEntry::is_sparse_dir) {
2397 expand_sparse_index(&mut index, &db, format)?;
2398 }
2399 let mut materialized = Vec::new();
2400 let mut skipped = Vec::new();
2401 let mut not_up_to_date = Vec::new();
2402 for entry in &mut index.entries {
2403 if index_entry_stage(entry) != 0 {
2405 continue;
2406 }
2407 if matcher.includes_file(entry.path.as_bytes()) {
2408 clear_skip_worktree(entry);
2409 let file_path = worktree_path(worktree_root, entry.path.as_bytes())?;
2410 if !file_path.exists() {
2411 materialize_index_entry_file(&db, worktree_root, &file_path, entry)?;
2412 let metadata = fs::symlink_metadata(&file_path)?;
2413 *entry = index_entry_with_refreshed_stat(entry, &metadata);
2414 }
2415 materialized.push(entry.path.as_bytes().to_vec());
2416 } else {
2417 let file_path = worktree_path(worktree_root, entry.path.as_bytes())?;
2424 match fs::symlink_metadata(&file_path) {
2425 Ok(metadata) if !worktree_entry_is_uptodate(entry, &metadata) => {
2426 clear_skip_worktree(entry);
2427 not_up_to_date.push(entry.path.as_bytes().to_vec());
2428 }
2429 _ => {
2430 set_skip_worktree(entry);
2431 remove_worktree_file(worktree_root, entry.path.as_bytes())?;
2432 skipped.push(entry.path.as_bytes().to_vec());
2433 }
2434 }
2435 }
2436 }
2437 not_up_to_date.sort();
2438 normalize_index_version_for_extended_flags(&mut index);
2439 if sparse.sparse_index {
2444 collapse_to_sparse_index(&mut index, &matcher, &db, format)?;
2445 } else {
2446 index.clear_sparse_extension()?;
2447 }
2448 write_repository_index_ref(git_dir, format, &index)?;
2449 Ok(ApplySparseResult {
2450 materialized,
2451 skipped,
2452 not_up_to_date,
2453 })
2454}
2455
2456pub fn expand_sparse_index(
2466 index: &mut Index,
2467 db: &FileObjectDatabase,
2468 format: ObjectFormat,
2469) -> Result<bool> {
2470 if !index.entries.iter().any(IndexEntry::is_sparse_dir) {
2471 let had_marker = index.is_sparse();
2473 index.clear_sparse_extension()?;
2474 if had_marker {
2475 sley_core::trace2::region("index", "ensure_full_index");
2476 }
2477 return Ok(had_marker);
2478 }
2479 let mut expanded: Vec<IndexEntry> = Vec::with_capacity(index.entries.len());
2480 for entry in std::mem::take(&mut index.entries) {
2481 if !entry.is_sparse_dir() {
2482 expanded.push(entry);
2483 continue;
2484 }
2485 let dir = entry.path.as_bytes();
2487 let dir_prefix = dir; for (rel, (mode, oid)) in sley_diff_merge::flatten_tree(db, format, &entry.oid)? {
2489 let mut full_path = dir_prefix.to_vec();
2490 full_path.extend_from_slice(&rel);
2491 let mut blob = blank_sparse_blob_entry(format, &full_path, mode, oid);
2492 blob.set_skip_worktree(true);
2494 expanded.push(blob);
2495 }
2496 }
2497 expanded.sort_by(|a, b| a.path.as_bytes().cmp(b.path.as_bytes()));
2498 index.entries = expanded;
2499 index.clear_sparse_extension()?;
2500 normalize_index_version_for_extended_flags(index);
2501 sley_core::trace2::region("index", "ensure_full_index");
2502 Ok(true)
2503}
2504
2505pub(crate) fn index_sparse_dir_contains_path(index: &Index, git_path: &[u8]) -> bool {
2506 index.entries.iter().any(|entry| {
2507 entry.is_sparse_dir()
2508 && git_path.starts_with(entry.path.as_bytes())
2509 && git_path.len() > entry.path.len()
2510 })
2511}
2512
2513pub(crate) fn blank_sparse_blob_entry(
2518 format: ObjectFormat,
2519 path: &[u8],
2520 mode: u32,
2521 oid: ObjectId,
2522) -> IndexEntry {
2523 let _ = format;
2524 let mut entry = IndexEntry {
2525 ctime_seconds: 0,
2526 ctime_nanoseconds: 0,
2527 mtime_seconds: 0,
2528 mtime_nanoseconds: 0,
2529 dev: 0,
2530 ino: 0,
2531 mode,
2532 uid: 0,
2533 gid: 0,
2534 size: 0,
2535 oid,
2536 flags: 0,
2537 flags_extended: 0,
2538 path: path.into(),
2539 };
2540 entry.refresh_name_length();
2541 entry
2542}
2543
2544pub(crate) fn collapse_to_sparse_index(
2551 index: &mut Index,
2552 matcher: &SparseMatcher,
2553 db: &FileObjectDatabase,
2554 format: ObjectFormat,
2555) -> Result<()> {
2556 if index.entries.iter().any(IndexEntry::is_sparse_dir) {
2559 expand_sparse_index(index, db, format)?;
2560 }
2561
2562 if index.entries.iter().any(|e| index_entry_stage(e) != 0) {
2565 index.clear_sparse_extension()?;
2566 return Ok(());
2567 }
2568
2569 index
2570 .entries
2571 .sort_by(|a, b| a.path.as_bytes().cmp(b.path.as_bytes()));
2572
2573 use std::collections::BTreeMap;
2576 let mut dir_has_in_cone: BTreeMap<Vec<u8>, bool> = BTreeMap::new();
2577 for entry in &index.entries {
2578 let path = entry.path.as_bytes();
2579 let in_cone = matcher.includes_file(path);
2580 let mut start = 0usize;
2581 while let Some(rel) = path
2582 .get(start..)
2583 .and_then(|s| s.iter().position(|b| *b == b'/'))
2584 {
2585 let end = start + rel;
2586 let dir = path[..end].to_vec();
2587 let flag = dir_has_in_cone.entry(dir).or_insert(false);
2588 *flag = *flag || in_cone;
2589 start = end + 1;
2590 }
2591 }
2592
2593 let collapsible: Vec<Vec<u8>> = {
2596 let all: Vec<Vec<u8>> = dir_has_in_cone
2597 .iter()
2598 .filter(|(_, has)| !**has)
2599 .map(|(dir, _)| dir.clone())
2600 .collect();
2601 all.iter()
2602 .filter(|dir| {
2603 !all.iter().any(|other| {
2604 other != *dir
2605 && dir
2606 .strip_prefix(other.as_slice())
2607 .is_some_and(|rest| rest.first() == Some(&b'/'))
2608 })
2609 })
2610 .cloned()
2611 .collect()
2612 };
2613 if collapsible.is_empty() {
2614 index.clear_sparse_extension()?;
2615 return Ok(());
2616 }
2617
2618 let mut checker = db.presence_checker();
2619 let mut new_entries: Vec<IndexEntry> = Vec::with_capacity(index.entries.len());
2620 let mut consumed: std::collections::HashSet<Vec<u8>> = std::collections::HashSet::new();
2621 for dir in &collapsible {
2622 let mut subtree: Vec<&IndexEntry> = index
2624 .entries
2625 .iter()
2626 .filter(|e| {
2627 e.path
2628 .as_bytes()
2629 .strip_prefix(dir.as_slice())
2630 .is_some_and(|rest| rest.first() == Some(&b'/'))
2631 })
2632 .collect();
2633 if subtree.is_empty() {
2634 continue;
2635 }
2636 subtree.sort_by(|a, b| a.path.as_bytes().cmp(b.path.as_bytes()));
2637 let mut prefix = dir.clone();
2639 prefix.push(b'/');
2640 let tree_entries: Vec<WriteTreeEntry<'_>> = subtree
2641 .iter()
2642 .map(|e| WriteTreeEntry {
2643 path: e.path.as_bytes(),
2644 mode: e.mode,
2645 oid: e.oid.clone(),
2646 })
2647 .collect();
2648 let tree_oid =
2649 write_tree_entries_stream(&tree_entries, &prefix, None, db, &mut checker, false)?;
2650 for e in &subtree {
2652 consumed.insert(e.path.as_bytes().to_vec());
2653 }
2654 let mut sparse_path = dir.clone();
2656 sparse_path.push(b'/');
2657 let mut sparse_entry =
2658 blank_sparse_blob_entry(format, &sparse_path, SPARSE_DIR_MODE, tree_oid);
2659 sparse_entry.set_skip_worktree(true);
2660 new_entries.push(sparse_entry);
2661 }
2662 for entry in &index.entries {
2664 if consumed.contains(entry.path.as_bytes()) {
2665 continue;
2666 }
2667 new_entries.push(entry.clone());
2668 }
2669 new_entries.sort_by(|a, b| a.path.as_bytes().cmp(b.path.as_bytes()));
2670 index.entries = new_entries;
2671 index.set_sparse_extension();
2672 normalize_index_version_for_extended_flags(index);
2673 sley_core::trace2::region("index", "convert_to_sparse");
2674 Ok(())
2675}
2676
2677pub(crate) fn worktree_entry_is_uptodate(entry: &IndexEntry, metadata: &fs::Metadata) -> bool {
2684 if u64::from(entry.size) != metadata.len() {
2685 return false;
2686 }
2687 let Some((mtime_seconds, mtime_nanoseconds)) = file_mtime_parts(metadata) else {
2688 return false;
2691 };
2692 u64::from(entry.mtime_seconds) == mtime_seconds
2693 && u64::from(entry.mtime_nanoseconds) == mtime_nanoseconds
2694}
2695
2696pub(crate) fn worktree_entry_ref_is_uptodate(
2697 entry: &IndexEntryRef<'_>,
2698 metadata: &fs::Metadata,
2699) -> bool {
2700 if u64::from(entry.size) != metadata.len() {
2701 return false;
2702 }
2703 let Some((mtime_seconds, mtime_nanoseconds)) = file_mtime_parts(metadata) else {
2704 return false;
2705 };
2706 u64::from(entry.mtime_seconds) == mtime_seconds
2707 && u64::from(entry.mtime_nanoseconds) == mtime_nanoseconds
2708}
2709
2710pub(crate) fn file_mtime_parts(metadata: &fs::Metadata) -> Option<(u64, u64)> {
2713 let modified = metadata.modified().ok()?;
2714 let duration = modified.duration_since(UNIX_EPOCH).ok()?;
2715 Some((duration.as_secs(), u64::from(duration.subsec_nanos())))
2716}
2717
2718pub fn write_metadata_file_atomic(
2725 path: impl AsRef<Path>,
2726 bytes: &[u8],
2727 options: AtomicMetadataWriteOptions,
2728) -> Result<AtomicMetadataWriteResult> {
2729 let path = path.as_ref();
2730 let parent = path.parent().ok_or_else(|| {
2731 GitError::InvalidPath(format!("metadata path has no parent: {}", path.display()))
2732 })?;
2733 if !parent.as_os_str().is_empty() {
2734 fs::create_dir_all(parent)?;
2735 }
2736 let lock_path = metadata_lock_path(path)?;
2737 let mut lock = match fs::OpenOptions::new()
2738 .write(true)
2739 .create_new(true)
2740 .open(&lock_path)
2741 {
2742 Ok(lock) => lock,
2743 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
2744 return Err(GitError::Transaction(format!(
2745 "metadata lock already exists: {}",
2746 lock_path.display()
2747 )));
2748 }
2749 Err(err) => return Err(err.into()),
2750 };
2751 if let Err(err) = lock.write_all(bytes) {
2752 let _ = fs::remove_file(&lock_path);
2753 return Err(err.into());
2754 }
2755 if options.fsync_file
2756 && let Err(err) = lock.sync_all()
2757 {
2758 let _ = fs::remove_file(&lock_path);
2759 return Err(err.into());
2760 }
2761 drop(lock);
2762 if let Err(err) = fs::rename(&lock_path, path) {
2763 let _ = fs::remove_file(&lock_path);
2764 return Err(err.into());
2765 }
2766 if options.fsync_dir
2767 && let Ok(dir) = fs::File::open(parent)
2768 {
2769 dir.sync_all()?;
2770 }
2771 let metadata = fs::metadata(path)?;
2772 Ok(AtomicMetadataWriteResult {
2773 path: path.to_path_buf(),
2774 len: metadata.len(),
2775 mtime: file_mtime_parts(&metadata),
2776 })
2777}
2778
2779pub(crate) fn metadata_lock_path(path: &Path) -> Result<PathBuf> {
2780 let file_name = path.file_name().ok_or_else(|| {
2781 GitError::InvalidPath(format!("metadata path has no filename: {}", path.display()))
2782 })?;
2783 let mut lock_name = file_name.to_os_string();
2784 lock_name.push(".lock");
2785 Ok(path.with_file_name(lock_name))
2786}
2787
2788pub fn checkout_detached_sparse(
2798 worktree_root: impl AsRef<Path>,
2799 git_dir: impl AsRef<Path>,
2800 format: ObjectFormat,
2801 target: &ObjectId,
2802 committer: Vec<u8>,
2803 message: Vec<u8>,
2804 sparse: &SparseCheckout,
2805) -> Result<CheckoutResult> {
2806 let worktree_root = worktree_root.as_ref();
2807 let git_dir = git_dir.as_ref();
2808 let files = checkout_commit_to_index_and_worktree_sparse(
2809 worktree_root,
2810 git_dir,
2811 format,
2812 target,
2813 Some((sparse, SparseCheckoutMode::Auto)),
2814 None,
2815 None,
2816 )?;
2817 let refs = FileRefStore::new(git_dir, format);
2818 let zero = ObjectId::null(format);
2819 let mut tx = refs.transaction();
2820 tx.update(RefUpdate {
2821 name: "HEAD".into(),
2822 expected: None,
2823 new: RefTarget::Direct(*target),
2824 reflog: Some(ReflogEntry {
2825 old_oid: zero,
2826 new_oid: *target,
2827 committer,
2828 message,
2829 }),
2830 });
2831 tx.commit()?;
2832 Ok(CheckoutResult {
2833 branch: target.to_string(),
2834 oid: *target,
2835 files,
2836 })
2837}
2838
2839pub(crate) fn materialize_index_entry_file(
2840 db: &FileObjectDatabase,
2841 worktree_root: &Path,
2842 file_path: &Path,
2843 entry: &IndexEntry,
2844) -> Result<()> {
2845 if sley_index::is_gitlink(entry.mode) {
2851 materialize_gitlink_dir(worktree_root, file_path)?;
2852 return Ok(());
2853 }
2854 let object = read_expected_object(db, &entry.oid, ObjectType::Blob)?;
2855 prepare_blob_parent_dirs(worktree_root, file_path)?;
2856 remove_existing_worktree_path(file_path)?;
2857 write_blob_body_or_symlink(file_path, entry.mode, &object.body, &object.body)?;
2858 Ok(())
2859}
2860
2861pub(crate) fn set_skip_worktree(entry: &mut IndexEntry) {
2862 entry.flags |= INDEX_FLAG_EXTENDED;
2863 entry.flags_extended |= INDEX_EXTENDED_FLAG_SKIP_WORKTREE;
2864}
2865
2866pub(crate) fn clear_skip_worktree(entry: &mut IndexEntry) {
2867 entry.flags_extended &= !INDEX_EXTENDED_FLAG_SKIP_WORKTREE;
2868 if entry.flags_extended == 0 {
2869 entry.flags &= !INDEX_FLAG_EXTENDED;
2870 }
2871}
2872
2873pub fn restore_worktree_paths_from_head(
2874 worktree_root: impl AsRef<Path>,
2875 git_dir: impl AsRef<Path>,
2876 format: ObjectFormat,
2877 paths: &[PathBuf],
2878) -> Result<RestoreResult> {
2879 let worktree_root = worktree_root.as_ref();
2880 let git_dir = git_dir.as_ref();
2881 let index_path = repository_index_path(git_dir);
2882 let index = if index_path.exists() {
2883 Index::parse(&fs::read(&index_path)?, format)?
2884 } else {
2885 Index {
2886 version: 2,
2887 entries: Vec::new(),
2888 extensions: Vec::new(),
2889 checksum: None,
2890 }
2891 };
2892 let db = FileObjectDatabase::from_git_dir(git_dir, format);
2893 let head_entries = head_tree_entries(git_dir, format, &db)?;
2894 restore_worktree_paths_from_entries(
2895 worktree_root,
2896 git_dir,
2897 format,
2898 &db,
2899 index,
2900 &head_entries,
2901 paths,
2902 )
2903}
2904
2905pub fn restore_worktree_paths_from_tree(
2906 worktree_root: impl AsRef<Path>,
2907 git_dir: impl AsRef<Path>,
2908 format: ObjectFormat,
2909 tree_oid: &ObjectId,
2910 paths: &[PathBuf],
2911) -> Result<RestoreResult> {
2912 let worktree_root = worktree_root.as_ref();
2913 let git_dir = git_dir.as_ref();
2914 let index_path = repository_index_path(git_dir);
2915 let index = if index_path.exists() {
2916 Index::parse(&fs::read(&index_path)?, format)?
2917 } else {
2918 Index {
2919 version: 2,
2920 entries: Vec::new(),
2921 extensions: Vec::new(),
2922 checksum: None,
2923 }
2924 };
2925 let db = FileObjectDatabase::from_git_dir(git_dir, format);
2926 let source_entries = tree_entries(&db, format, tree_oid)?;
2927 restore_worktree_paths_from_entries(
2928 worktree_root,
2929 git_dir,
2930 format,
2931 &db,
2932 index,
2933 &source_entries,
2934 paths,
2935 )
2936}
2937
2938pub(crate) fn restore_worktree_paths_from_entries(
2939 worktree_root: &Path,
2940 git_dir: &Path,
2941 format: ObjectFormat,
2942 db: &FileObjectDatabase,
2943 index: Index,
2944 source_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
2945 paths: &[PathBuf],
2946) -> Result<RestoreResult> {
2947 let index_entries = index
2948 .entries
2949 .into_iter()
2950 .map(|entry| entry.path.into_bytes())
2951 .collect::<BTreeSet<_>>();
2952 let config = sley_config::read_repo_config(git_dir, None).unwrap_or_default();
2953 let mut restored = BTreeSet::new();
2954 let matched_paths = checkout_selected_paths(
2955 worktree_root,
2956 paths,
2957 index_entries
2958 .iter()
2959 .chain(source_entries.keys())
2960 .map(Vec::as_slice),
2961 false,
2962 )?;
2963 for path in matched_paths {
2964 if let Some(entry) = source_entries.get(&path) {
2965 materialize_path_restore_entry_filtered(
2966 db,
2967 format,
2968 worktree_root,
2969 git_dir,
2970 &path,
2971 entry,
2972 &config,
2973 )?;
2974 } else {
2975 remove_worktree_file(worktree_root, &path)?;
2976 }
2977 restored.insert(path);
2978 }
2979 Ok(RestoreResult {
2980 restored: restored.len(),
2981 })
2982}