1use super::*;
6use crate::attributes::*;
7use crate::filter::*;
8use crate::index::*;
9use crate::index_io::*;
10use crate::status::*;
11use crate::types_admin::*;
12
13pub fn deleted_index_entries(
14 worktree_root: impl AsRef<Path>,
15 git_dir: impl AsRef<Path>,
16 format: ObjectFormat,
17) -> Result<Vec<IndexEntry>> {
18 let worktree_root = worktree_root.as_ref();
19 let git_dir = git_dir.as_ref();
20 let index_path = repository_index_path(git_dir);
21 if !index_path.exists() {
22 return Ok(Vec::new());
23 }
24 let index = Index::parse(&fs::read(index_path)?, format)?;
25 let mut deleted = Vec::new();
26 for entry in index.entries {
27 if !worktree_path(worktree_root, entry.path.as_bytes())?.exists()
28 && !index_entry_skip_worktree(&entry)
29 {
30 deleted.push(entry);
31 }
32 }
33 Ok(deleted)
34}
35
36pub fn modified_index_entries(
37 worktree_root: impl AsRef<Path>,
38 git_dir: impl AsRef<Path>,
39 format: ObjectFormat,
40) -> Result<Vec<IndexEntry>> {
41 let worktree_root = worktree_root.as_ref();
42 let git_dir = git_dir.as_ref();
43 let index_path = repository_index_path(git_dir);
44 if !index_path.exists() {
45 return Ok(Vec::new());
46 }
47 let mut index = Index::parse(&fs::read(&index_path)?, format)?;
48 if index.entries.iter().any(IndexEntry::is_sparse_dir) {
49 let db = FileObjectDatabase::from_git_dir(git_dir, format);
50 expand_sparse_index(&mut index, &db, format)?;
51 }
52 let stat_cache = IndexStatCache::from_index(&index, &index_path);
57 let mut modified = Vec::new();
58 for entry in index.entries {
59 let worktree_entry = worktree_entry_for_git_path(
60 worktree_root,
61 git_dir,
62 format,
63 entry.path.as_bytes(),
64 &entry.oid,
65 entry.mode,
66 Some(&stat_cache),
67 )?;
68 let Some(worktree_entry) = worktree_entry else {
69 if !index_entry_skip_worktree(&entry) {
70 modified.push(entry);
71 }
72 continue;
73 };
74 if worktree_entry.mode != entry.mode || worktree_entry.oid != entry.oid {
75 modified.push(entry);
76 }
77 }
78 Ok(modified)
79}
80
81pub fn checkout_branch(
82 worktree_root: impl AsRef<Path>,
83 git_dir: impl AsRef<Path>,
84 format: ObjectFormat,
85 branch: &str,
86 committer: Vec<u8>,
87) -> Result<CheckoutResult> {
88 let worktree_root = worktree_root.as_ref();
89 let git_dir = git_dir.as_ref();
90 let branch_ref = branch_ref_name(branch)?;
91 let refs = FileRefStore::new(git_dir, format);
92 let target = match sley_refs::resolve_ref_peeled(&refs, &branch_ref)? {
93 Some(oid) => oid,
94 None => {
95 checkout_switch_head_symbolic(&refs, branch_ref, committer, branch, None, None)?;
96 return Ok(CheckoutResult {
97 branch: branch.into(),
98 oid: ObjectId::null(format),
99 files: 0,
100 });
101 }
102 };
103 let current_head = resolve_head_commit_oid(git_dir, format)?;
104 let files = if current_head == Some(target) {
105 0
106 } else {
107 checkout_commit_to_index_and_worktree(worktree_root, git_dir, format, &target)?
108 };
109 checkout_switch_head_symbolic(
110 &refs,
111 branch_ref,
112 committer,
113 branch,
114 Some(target),
115 Some(target),
116 )?;
117 Ok(CheckoutResult {
118 branch: branch.into(),
119 oid: target,
120 files,
121 })
122}
123
124pub fn checkout_detached(
125 worktree_root: impl AsRef<Path>,
126 git_dir: impl AsRef<Path>,
127 format: ObjectFormat,
128 target: &ObjectId,
129 committer: Vec<u8>,
130 message: Vec<u8>,
131) -> Result<CheckoutResult> {
132 let worktree_root = worktree_root.as_ref();
133 let git_dir = git_dir.as_ref();
134 let files = checkout_commit_to_index_and_worktree(worktree_root, git_dir, format, target)?;
135 let refs = FileRefStore::new(git_dir, format);
136 let zero = ObjectId::null(format);
137 let mut tx = refs.transaction();
138 tx.update(RefUpdate {
139 name: "HEAD".into(),
140 expected: None,
141 new: RefTarget::Direct(*target),
142 reflog: Some(ReflogEntry {
143 old_oid: zero,
144 new_oid: *target,
145 committer,
146 message,
147 }),
148 });
149 tx.commit()?;
150 Ok(CheckoutResult {
151 branch: target.to_string(),
152 oid: *target,
153 files,
154 })
155}
156
157pub fn checkout_branch_filtered(
162 worktree_root: impl AsRef<Path>,
163 git_dir: impl AsRef<Path>,
164 format: ObjectFormat,
165 branch: &str,
166 committer: Vec<u8>,
167 config: &GitConfig,
168) -> Result<CheckoutResult> {
169 let worktree_root = worktree_root.as_ref();
170 let git_dir = git_dir.as_ref();
171 let branch_ref = branch_ref_name(branch)?;
172 let refs = FileRefStore::new(git_dir, format);
173 let target = match sley_refs::resolve_ref_peeled(&refs, &branch_ref)? {
174 Some(oid) => oid,
175 None => {
176 checkout_switch_head_symbolic(&refs, branch_ref, committer, branch, None, None)?;
177 return Ok(CheckoutResult {
178 branch: branch.into(),
179 oid: ObjectId::null(format),
180 files: 0,
181 });
182 }
183 };
184 let current_head = resolve_head_commit_oid(git_dir, format)?;
185 let files = if current_head == Some(target) {
186 0
187 } else {
188 checkout_commit_to_index_and_worktree_filtered(
189 worktree_root,
190 git_dir,
191 format,
192 &target,
193 Some(config),
194 Some(vec![
195 ("ref".to_string(), branch_ref.clone()),
196 ("treeish".to_string(), target.to_hex()),
197 ]),
198 )?
199 };
200 checkout_switch_head_symbolic(
201 &refs,
202 branch_ref,
203 committer,
204 branch,
205 Some(target),
206 Some(target),
207 )?;
208 Ok(CheckoutResult {
209 branch: branch.into(),
210 oid: target,
211 files,
212 })
213}
214
215pub fn checkout_detached_filtered(
218 worktree_root: impl AsRef<Path>,
219 git_dir: impl AsRef<Path>,
220 format: ObjectFormat,
221 target: &ObjectId,
222 committer: Vec<u8>,
223 message: Vec<u8>,
224 config: &GitConfig,
225) -> Result<CheckoutResult> {
226 let worktree_root = worktree_root.as_ref();
227 let git_dir = git_dir.as_ref();
228 let files = checkout_commit_to_index_and_worktree_filtered(
229 worktree_root,
230 git_dir,
231 format,
232 target,
233 Some(config),
234 Some(vec![("treeish".to_string(), target.to_hex())]),
235 )?;
236 let refs = FileRefStore::new(git_dir, format);
237 let zero = ObjectId::null(format);
238 let mut tx = refs.transaction();
239 tx.update(RefUpdate {
240 name: "HEAD".into(),
241 expected: None,
242 new: RefTarget::Direct(*target),
243 reflog: Some(ReflogEntry {
244 old_oid: zero,
245 new_oid: *target,
246 committer,
247 message,
248 }),
249 });
250 tx.commit()?;
251 Ok(CheckoutResult {
252 branch: target.to_string(),
253 oid: *target,
254 files,
255 })
256}
257
258pub(crate) fn checkout_commit_to_index_and_worktree(
259 worktree_root: &Path,
260 git_dir: &Path,
261 format: ObjectFormat,
262 target: &ObjectId,
263) -> Result<usize> {
264 checkout_commit_to_index_and_worktree_filtered(
265 worktree_root,
266 git_dir,
267 format,
268 target,
269 None,
270 None,
271 )
272}
273
274pub(crate) fn checkout_commit_to_index_and_worktree_filtered(
279 worktree_root: &Path,
280 git_dir: &Path,
281 format: ObjectFormat,
282 target: &ObjectId,
283 smudge_config: Option<&GitConfig>,
284 process_metadata: Option<Vec<(String, String)>>,
285) -> Result<usize> {
286 if let Some((sparse, mode)) = active_sparse_checkout(git_dir)? {
287 return checkout_commit_to_index_and_worktree_sparse(
288 worktree_root,
289 git_dir,
290 format,
291 target,
292 Some((&sparse, mode)),
293 smudge_config,
294 process_metadata,
295 );
296 }
297 let _process_filter_metadata = set_process_filter_metadata(process_metadata);
298 let mut dirty = false;
299 if smudge_config.is_some() {
300 dirty = !modified_index_entries(worktree_root, git_dir, format)?.is_empty();
301 } else {
302 stream_short_status(worktree_root, git_dir, format, |entry| {
303 if !status_row_is_untracked_or_ignored(entry) {
304 dirty = true;
305 return Ok(StreamControl::Stop);
306 }
307 Ok(StreamControl::Continue)
308 })?;
309 }
310 if dirty {
311 return Err(GitError::Transaction(
312 "checkout requires a clean working tree".into(),
313 ));
314 }
315 let db = FileObjectDatabase::from_git_dir(git_dir, format);
316 let commit = read_commit(&db, format, target)?;
317 let mut target_entries = BTreeMap::new();
318 collect_tree_entries(&db, format, &commit.tree, &mut target_entries)?;
319 refuse_if_current_working_directory_becomes_file(worktree_root, &target_entries)?;
320
321 let attributes = smudge_config
322 .map(|_| build_tree_attribute_matcher(worktree_root, &db, format, &commit.tree))
323 .transpose()?;
324
325 for path in read_index_entries(git_dir, format)?.keys() {
326 if !target_entries.contains_key(path) {
327 remove_worktree_file(worktree_root, path)?;
328 }
329 }
330
331 let mut index_entries = Vec::new();
332 for (path, entry) in &target_entries {
333 index_entries.push(materialize_tree_entry_with_optional_smudge(
339 &db,
340 format,
341 worktree_root,
342 path,
343 entry,
344 smudge_config,
345 attributes.as_ref(),
346 )?);
347 }
348 index_entries.sort_by(|left, right| left.path.cmp(&right.path));
349 let extensions = preserved_index_extensions(git_dir, format)?;
350 fs::write(
351 repository_index_path(git_dir),
352 Index {
353 version: 2,
354 entries: index_entries,
355 extensions,
356 checksum: None,
357 }
358 .write(format)?,
359 )?;
360 Ok(target_entries.len())
361}
362
363pub(crate) fn build_tree_attribute_matcher(
367 worktree_root: &Path,
368 db: &FileObjectDatabase,
369 format: ObjectFormat,
370 tree_oid: &ObjectId,
371) -> Result<AttributeMatcher> {
372 let mut matcher = AttributeMatcher::default();
373 let git_dir = worktree_root.join(".git");
374 matcher.configure_case_sensitivity(&git_dir);
375 if !matcher.read_configured_attributes(worktree_root, &git_dir) {
376 matcher.read_default_global_attributes();
377 }
378 collect_attribute_patterns_from_tree(db, format, tree_oid, Vec::new(), &mut matcher)?;
379 read_attribute_patterns(
380 worktree_root.join(".git").join("info").join("attributes"),
381 &mut matcher,
382 &[],
383 b".git/info/attributes",
384 false,
385 );
386 Ok(matcher)
387}
388
389pub(crate) fn materialize_tree_entry_with_optional_smudge(
390 db: &FileObjectDatabase,
391 format: ObjectFormat,
392 worktree_root: &Path,
393 path: &[u8],
394 entry: &TrackedEntry,
395 smudge_config: Option<&GitConfig>,
396 attributes: Option<&AttributeMatcher>,
397) -> Result<IndexEntry> {
398 if smudge_config.is_none()
405 || sley_index::is_gitlink(entry.mode)
406 || (entry.mode & 0o170000) == 0o120000
407 {
408 return materialize_tree_entry(db, worktree_root, path, entry);
409 }
410 let config = smudge_config.expect("checked above");
411 let matcher = attributes.expect("attributes are built when smudge_config is set");
412 let object = read_expected_object(db, &entry.oid, ObjectType::Blob)?;
413 let checks = matcher.attributes_for_path(path, &filter_attribute_names(), false);
414 let body = apply_smudge_filter_with_attributes_cow_format(
415 config,
416 &checks,
417 path,
418 &object.body,
419 format,
420 )?;
421 let file_path = worktree_path(worktree_root, path)?;
422 prepare_blob_parent_dirs(worktree_root, &file_path)?;
423 remove_existing_worktree_path(&file_path)?;
424 fs::write(&file_path, &body)?;
425 set_worktree_file_mode(&file_path, entry.mode)?;
426 let metadata = fs::metadata(&file_path)?;
427 let mut index_entry = index_entry_from_metadata(path.to_vec(), entry.oid, &metadata);
428 index_entry.mode = entry.mode;
429 Ok(index_entry)
430}
431
432pub(crate) fn checkout_commit_to_index_and_worktree_sparse(
443 worktree_root: &Path,
444 git_dir: &Path,
445 format: ObjectFormat,
446 target: &ObjectId,
447 sparse: Option<(&SparseCheckout, SparseCheckoutMode)>,
448 smudge_config: Option<&GitConfig>,
449 process_metadata: Option<Vec<(String, String)>>,
450) -> Result<usize> {
451 let _process_filter_metadata = set_process_filter_metadata(process_metadata);
452 let previously_skipped = skip_worktree_paths(git_dir, format)?;
453 let db = FileObjectDatabase::from_git_dir(git_dir, format);
454 let commit = read_commit(&db, format, target)?;
455 let mut target_entries = BTreeMap::new();
456 collect_tree_entries(&db, format, &commit.tree, &mut target_entries)?;
457
458 let mut dirty = false;
461 stream_short_status(worktree_root, git_dir, format, |entry| {
462 if previously_skipped.contains(entry.path) {
463 return Ok(StreamControl::Continue);
464 }
465 if entry.index_mode.is_some_and(sley_index::is_gitlink)
470 || entry.worktree_mode.is_some_and(sley_index::is_gitlink)
471 {
472 return Ok(StreamControl::Continue);
473 }
474 if entry.index == b'?' && entry.worktree == b'?' {
478 let path = entry.path.strip_suffix(b"/").unwrap_or(entry.path);
479 if target_entries
480 .get(path)
481 .is_some_and(|target| sley_index::is_gitlink(target.mode))
482 {
483 return Ok(StreamControl::Continue);
484 }
485 }
486 dirty = true;
487 Ok(StreamControl::Stop)
488 })?;
489 if dirty {
490 return Err(GitError::Transaction(
491 "checkout requires a clean working tree".into(),
492 ));
493 }
494
495 let matcher = sparse.map(|(spec, mode)| SparseMatcher::new(spec, mode));
496 let attributes = smudge_config
497 .map(|_| build_tree_attribute_matcher(worktree_root, &db, format, &commit.tree))
498 .transpose()?;
499
500 for path in read_index_entries(git_dir, format)?.keys() {
501 if target_entries.contains_key(path) {
502 continue;
503 }
504 if previously_skipped.contains(path) {
506 continue;
507 }
508 remove_worktree_file(worktree_root, path)?;
509 }
510
511 let mut index_entries = Vec::new();
512 for (path, entry) in &target_entries {
513 let in_cone = matcher.as_ref().map_or_else(
514 || !previously_skipped.contains(path),
515 |matcher| matcher.includes_file(path),
516 );
517 let index_entry = if in_cone {
518 materialize_tree_entry_with_optional_smudge(
519 &db,
520 format,
521 worktree_root,
522 path,
523 entry,
524 smudge_config,
525 attributes.as_ref(),
526 )?
527 } else {
528 remove_worktree_file(worktree_root, path)?;
532 let mut index_entry = restored_head_index_entry(worktree_root, &db, path, entry)?;
533 set_skip_worktree(&mut index_entry);
534 index_entry
535 };
536 index_entries.push(index_entry);
537 }
538 index_entries.sort_by(|left, right| left.path.cmp(&right.path));
539 let mut index = Index {
540 version: 2,
541 entries: index_entries,
542 extensions: preserved_index_extensions(git_dir, format)?,
543 checksum: None,
544 };
545 normalize_index_version_for_extended_flags(&mut index);
546 write_repository_index_ref(git_dir, format, &index)?;
547 Ok(target_entries.len())
548}
549
550pub(crate) fn skip_worktree_paths(
551 git_dir: &Path,
552 format: ObjectFormat,
553) -> Result<BTreeSet<Vec<u8>>> {
554 let index_path = repository_index_path(git_dir);
555 if !index_path.exists() {
556 return Ok(BTreeSet::new());
557 }
558 let index = Index::parse(&fs::read(index_path)?, format)?;
559 Ok(index
560 .entries
561 .into_iter()
562 .filter(index_entry_skip_worktree)
563 .map(|entry| entry.path.into_bytes())
564 .collect())
565}
566
567pub fn restore_worktree_paths(
568 worktree_root: impl AsRef<Path>,
569 git_dir: impl AsRef<Path>,
570 format: ObjectFormat,
571 paths: &[PathBuf],
572) -> Result<RestoreResult> {
573 restore_worktree_paths_inner(
574 worktree_root.as_ref(),
575 git_dir.as_ref(),
576 format,
577 paths,
578 None,
579 )
580}
581
582pub fn restore_worktree_paths_filtered(
585 worktree_root: impl AsRef<Path>,
586 git_dir: impl AsRef<Path>,
587 format: ObjectFormat,
588 paths: &[PathBuf],
589 config: &GitConfig,
590) -> Result<RestoreResult> {
591 restore_worktree_paths_inner(
592 worktree_root.as_ref(),
593 git_dir.as_ref(),
594 format,
595 paths,
596 Some(config),
597 )
598}
599
600pub(crate) fn restore_worktree_paths_inner(
601 worktree_root: &Path,
602 git_dir: &Path,
603 format: ObjectFormat,
604 paths: &[PathBuf],
605 smudge_config: Option<&GitConfig>,
606) -> Result<RestoreResult> {
607 let index_path = repository_index_path(git_dir);
608 if !index_path.exists() {
609 return Err(GitError::Exit(1));
610 }
611 let mut index = Index::parse(&fs::read(&index_path)?, format)?;
612 let stat_cache = IndexStatCache::from_index(&index, &index_path);
613 let db = FileObjectDatabase::from_git_dir(git_dir, format);
614 let mut restored = BTreeSet::new();
615 for path in paths {
616 let absolute = if path.is_absolute() {
617 path.clone()
618 } else {
619 worktree_root.join(path)
620 };
621 let absolute = normalize_absolute_path_lexically(&absolute);
622 let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
623 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
624 })?;
625 let git_path = git_path_bytes(relative)?;
626 let recursive = path == Path::new(".")
627 || path.to_string_lossy().ends_with('/')
628 || absolute.is_dir()
629 || index_has_entry_under(&index.entries, &git_path);
630 let mut matched = false;
631 let matched_positions = index
632 .entries
633 .iter()
634 .enumerate()
635 .filter_map(|(position, entry)| {
636 (entry.path.as_bytes() == git_path.as_slice()
637 || (recursive && index_entry_is_under_path(entry.path.as_bytes(), &git_path)))
638 .then_some(position)
639 })
640 .collect::<Vec<_>>();
641 for position in matched_positions {
642 let refreshed = restore_index_entry(
643 worktree_root,
644 git_dir,
645 format,
646 &db,
647 &index.entries[position],
648 smudge_config,
649 Some(&stat_cache),
650 )?;
651 restored.insert(index.entries[position].path.clone());
652 matched = true;
653 if let Some(refreshed) = refreshed {
654 index.entries[position] = refreshed;
655 }
656 }
657 if !matched {
658 eprintln!(
659 "error: pathspec '{}' did not match any file(s) known to git",
660 path.display()
661 );
662 return Err(GitError::Exit(1));
663 }
664 }
665 write_repository_index_ref(git_dir, format, &index)?;
666 Ok(RestoreResult {
667 restored: restored.len(),
668 })
669}
670
671pub fn checkout_index_paths(
672 worktree_root: impl AsRef<Path>,
673 git_dir: impl AsRef<Path>,
674 format: ObjectFormat,
675 paths: &[PathBuf],
676 options: CheckoutIndexPathOptions<'_>,
677) -> Result<RestoreResult> {
678 let worktree_root = worktree_root.as_ref();
679 let git_dir = git_dir.as_ref();
680 let index_path = repository_index_path(git_dir);
681 if !index_path.exists() {
682 return Err(GitError::Exit(1));
683 }
684 let mut index = Index::parse(&fs::read(&index_path)?, format)?;
685 if options.merge {
686 checkout_unmerge_resolve_undo_paths(worktree_root, &mut index, format, paths)?;
687 }
688 let stat_cache = IndexStatCache::from_index(&index, &index_path);
689 let db = FileObjectDatabase::from_git_dir(git_dir, format);
690 let selected = checkout_selected_index_paths(worktree_root, &index, paths)?;
691
692 if options.stage.is_none() && !options.merge && !options.force {
693 for path in &selected {
694 if checkout_path_is_unmerged(&index, path) {
695 eprintln!(
696 "error: path '{}' is unmerged",
697 String::from_utf8_lossy(path)
698 );
699 return Err(GitError::Exit(1));
700 }
701 }
702 }
703
704 let mut refreshed = BTreeMap::new();
705 let mut restored = BTreeSet::new();
706 for path in selected {
707 let positions = index
708 .entries
709 .iter()
710 .enumerate()
711 .filter_map(|(position, entry)| (entry.path.as_bytes() == path).then_some(position))
712 .collect::<Vec<_>>();
713 let stage0 = positions
714 .iter()
715 .copied()
716 .find(|position| index.entries[*position].stage() == Stage::Normal);
717 let is_unmerged = positions
718 .iter()
719 .any(|position| index.entries[*position].stage() != Stage::Normal);
720
721 if is_unmerged {
722 if let Some(stage) = options.stage {
723 let wanted = match stage {
724 CheckoutStage::Ours => Stage::Ours,
725 CheckoutStage::Theirs => Stage::Theirs,
726 };
727 let Some(position) = positions
728 .iter()
729 .copied()
730 .find(|position| index.entries[*position].stage() == wanted)
731 else {
732 eprintln!(
733 "error: path '{}' does not have {} version",
734 String::from_utf8_lossy(&path),
735 match stage {
736 CheckoutStage::Ours => "our",
737 CheckoutStage::Theirs => "their",
738 }
739 );
740 return Err(GitError::Exit(1));
741 };
742 checkout_write_index_entry_to_worktree(
743 worktree_root,
744 git_dir,
745 format,
746 &db,
747 &index.entries[position],
748 options.smudge_config,
749 Some(&stat_cache),
750 )?;
751 restored.insert(path);
752 continue;
753 }
754 if options.merge {
755 checkout_merge_unmerged_path(
756 worktree_root,
757 &db,
758 &index,
759 &positions,
760 options.conflict_style,
761 )?;
762 restored.insert(path);
763 continue;
764 }
765 if options.force {
766 continue;
767 }
768 }
769
770 if let Some(position) = stage0 {
771 if let Some(updated) = checkout_write_index_entry_to_worktree(
772 worktree_root,
773 git_dir,
774 format,
775 &db,
776 &index.entries[position],
777 options.smudge_config,
778 Some(&stat_cache),
779 )? {
780 refreshed.insert(position, updated);
781 }
782 restored.insert(path);
783 }
784 }
785
786 for (position, entry) in refreshed {
787 index.entries[position] = entry;
788 }
789 if !index.entries.is_empty() {
790 write_repository_index_ref(git_dir, format, &index)?;
791 }
792 Ok(RestoreResult {
793 restored: restored.len(),
794 })
795}
796
797pub fn unresolve_index_paths(
798 worktree_root: impl AsRef<Path>,
799 git_dir: impl AsRef<Path>,
800 format: ObjectFormat,
801 paths: &[PathBuf],
802) -> Result<()> {
803 let worktree_root = worktree_root.as_ref();
804 let git_dir = git_dir.as_ref();
805 let index_path = repository_index_path(git_dir);
806 if !index_path.exists() {
807 return Ok(());
808 }
809 let mut index = Index::parse(&fs::read(&index_path)?, format)?;
810 checkout_unmerge_resolve_undo_paths(worktree_root, &mut index, format, paths)?;
811 write_repository_index_ref(git_dir, format, &index)
812}
813
814pub(crate) fn checkout_selected_index_paths(
815 worktree_root: &Path,
816 index: &Index,
817 paths: &[PathBuf],
818) -> Result<BTreeSet<Vec<u8>>> {
819 let index_paths = index
820 .entries
821 .iter()
822 .map(|entry| entry.path.as_bytes().to_vec())
823 .collect::<BTreeSet<_>>();
824 let mut selected = BTreeSet::new();
825 for path in paths {
826 let absolute = if path.is_absolute() {
827 path.clone()
828 } else {
829 worktree_root.join(path)
830 };
831 let absolute = normalize_absolute_path_lexically(&absolute);
832 let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
833 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
834 })?;
835 let git_path = git_path_bytes(relative)?;
836 let recursive = path == Path::new(".")
837 || path.to_string_lossy().ends_with('/')
838 || absolute.is_dir()
839 || index_paths
840 .iter()
841 .any(|entry| index_entry_is_under_path(entry, &git_path));
842 let matched = index_paths
843 .iter()
844 .filter(|entry| {
845 entry.as_slice() == git_path.as_slice()
846 || (recursive && index_entry_is_under_path(entry, &git_path))
847 })
848 .cloned()
849 .collect::<Vec<_>>();
850 if matched.is_empty() {
851 eprintln!(
852 "error: pathspec '{}' did not match any file(s) known to git",
853 path.display()
854 );
855 return Err(GitError::Exit(1));
856 }
857 selected.extend(matched);
858 }
859 Ok(selected)
860}
861
862pub(crate) fn checkout_unmerge_resolve_undo_paths(
863 worktree_root: &Path,
864 index: &mut Index,
865 format: ObjectFormat,
866 paths: &[PathBuf],
867) -> Result<()> {
868 let records = parse_resolve_undo_records(index.extension(b"REUC")?, format)?;
869 if records.is_empty() {
870 return Ok(());
871 }
872 let mut remaining = Vec::new();
873 let mut unmerged_any = false;
874 for record in records {
875 if checkout_pathspecs_match_git_path(worktree_root, paths, &record.path)? {
876 remove_index_entries_with_path(&mut index.entries, &record.path);
877 for (idx, stage) in record.stages.into_iter().enumerate() {
878 let Some((mode, oid)) = stage else {
879 continue;
880 };
881 index.entries.push(resolve_undo_index_entry(
882 record.path.clone(),
883 mode,
884 oid,
885 (idx + 1) as u16,
886 ));
887 }
888 unmerged_any = true;
889 } else {
890 remaining.push(record);
891 }
892 }
893 if unmerged_any {
894 index.entries.sort_by(compare_index_key);
895 normalize_index_version_for_extended_flags(index);
896 set_resolve_undo_extension(index, &remaining)?;
897 }
898 Ok(())
899}
900
901pub(crate) fn checkout_pathspecs_match_git_path(
902 worktree_root: &Path,
903 paths: &[PathBuf],
904 candidate: &[u8],
905) -> Result<bool> {
906 for path in paths {
907 let absolute = if path.is_absolute() {
908 path.clone()
909 } else {
910 worktree_root.join(path)
911 };
912 let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
913 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
914 })?;
915 let git_path = git_path_bytes(relative)?;
916 let recursive = path == Path::new(".")
917 || path.to_string_lossy().ends_with('/')
918 || absolute.is_dir()
919 || index_entry_is_under_path(candidate, &git_path);
920 if candidate == git_path.as_slice()
921 || (recursive && index_entry_is_under_path(candidate, &git_path))
922 {
923 return Ok(true);
924 }
925 }
926 Ok(false)
927}
928
929pub(crate) fn resolve_undo_index_entry(
930 path: Vec<u8>,
931 mode: u32,
932 oid: ObjectId,
933 stage: u16,
934) -> IndexEntry {
935 let name_len = (path
936 .len()
937 .min(sley_index::INDEX_FLAG_NAME_LENGTH_MASK as usize)) as u16;
938 IndexEntry {
939 ctime_seconds: 0,
940 ctime_nanoseconds: 0,
941 mtime_seconds: 0,
942 mtime_nanoseconds: 0,
943 dev: 0,
944 ino: 0,
945 mode,
946 uid: 0,
947 gid: 0,
948 size: 0,
949 oid,
950 flags: name_len | (stage << 12),
951 flags_extended: 0,
952 path: path.into(),
953 }
954}
955
956pub(crate) fn checkout_path_is_unmerged(index: &Index, path: &[u8]) -> bool {
957 index
958 .entries
959 .iter()
960 .any(|entry| entry.path.as_bytes() == path && entry.stage() != Stage::Normal)
961}
962
963pub(crate) fn checkout_write_index_entry_to_worktree(
964 worktree_root: &Path,
965 git_dir: &Path,
966 format: ObjectFormat,
967 db: &FileObjectDatabase,
968 entry: &IndexEntry,
969 smudge_config: Option<&GitConfig>,
970 stat_cache: Option<&IndexStatCache>,
971) -> Result<Option<IndexEntry>> {
972 restore_index_entry(
973 worktree_root,
974 git_dir,
975 format,
976 db,
977 entry,
978 smudge_config,
979 stat_cache,
980 )
981}
982
983pub(crate) fn checkout_merge_unmerged_path(
984 worktree_root: &Path,
985 db: &FileObjectDatabase,
986 index: &Index,
987 positions: &[usize],
988 style: CheckoutConflictStyle,
989) -> Result<()> {
990 let mut base = None;
991 let mut ours = None;
992 let mut theirs = None;
993 for position in positions {
994 let entry = &index.entries[*position];
995 match entry.stage() {
996 Stage::Base => base = Some(entry),
997 Stage::Ours => ours = Some(entry),
998 Stage::Theirs => theirs = Some(entry),
999 Stage::Normal => {}
1000 }
1001 }
1002 let Some(ours) = ours else {
1003 return Ok(());
1004 };
1005 let Some(theirs) = theirs else {
1006 return Ok(());
1007 };
1008 let base_body = match base {
1009 Some(entry) => read_expected_object(db, &entry.oid, ObjectType::Blob)?
1010 .body
1011 .clone(),
1012 None => Vec::new(),
1013 };
1014 let ours_body = read_expected_object(db, &ours.oid, ObjectType::Blob)?
1015 .body
1016 .clone();
1017 let theirs_body = read_expected_object(db, &theirs.oid, ObjectType::Blob)?
1018 .body
1019 .clone();
1020 let result = sley_diff_merge::merge_blobs(
1021 &base_body,
1022 &ours_body,
1023 &theirs_body,
1024 &sley_diff_merge::MergeBlobOptions {
1025 ours_label: "ours",
1026 theirs_label: "theirs",
1027 base_label: "base",
1028 style: match style {
1029 CheckoutConflictStyle::Merge => sley_diff_merge::ConflictStyle::Merge,
1030 CheckoutConflictStyle::Diff3 => sley_diff_merge::ConflictStyle::Diff3,
1031 },
1032 favor: sley_diff_merge::MergeFavor::None,
1033 ws_ignore: sley_diff_merge::WsIgnore::EMPTY,
1034 },
1035 );
1036 let file_path = worktree_path(worktree_root, ours.path.as_bytes())?;
1037 prepare_blob_parent_dirs(worktree_root, &file_path)?;
1038 remove_existing_worktree_path(&file_path)?;
1039 fs::write(&file_path, result.content)?;
1040 set_worktree_file_mode(&file_path, ours.mode)?;
1041 Ok(())
1042}
1043
1044pub fn restore_index_paths_from_head(
1045 worktree_root: impl AsRef<Path>,
1046 git_dir: impl AsRef<Path>,
1047 format: ObjectFormat,
1048 paths: &[PathBuf],
1049) -> Result<RestoreResult> {
1050 let worktree_root = worktree_root.as_ref();
1051 let git_dir = git_dir.as_ref();
1052 let index_path = repository_index_path(git_dir);
1053 let index = if index_path.exists() {
1054 Index::parse(&fs::read(&index_path)?, format)?
1055 } else {
1056 Index {
1057 version: 2,
1058 entries: Vec::new(),
1059 extensions: Vec::new(),
1060 checksum: None,
1061 }
1062 };
1063 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1064 let head_entries = head_tree_entries(git_dir, format, &db)?;
1065 restore_index_paths_from_entries(
1066 worktree_root,
1067 git_dir,
1068 format,
1069 &db,
1070 index,
1071 &head_entries,
1072 paths,
1073 false,
1074 )
1075}
1076
1077pub fn restore_index_paths_from_tree(
1078 worktree_root: impl AsRef<Path>,
1079 git_dir: impl AsRef<Path>,
1080 format: ObjectFormat,
1081 tree_oid: &ObjectId,
1082 paths: &[PathBuf],
1083) -> Result<RestoreResult> {
1084 let worktree_root = worktree_root.as_ref();
1085 let git_dir = git_dir.as_ref();
1086 let index_path = repository_index_path(git_dir);
1087 let index = if index_path.exists() {
1088 Index::parse(&fs::read(&index_path)?, format)?
1089 } else {
1090 Index {
1091 version: 2,
1092 entries: Vec::new(),
1093 extensions: Vec::new(),
1094 checksum: None,
1095 }
1096 };
1097 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1098 let source_entries = tree_entries(&db, format, tree_oid)?;
1099 restore_index_paths_from_entries(
1100 worktree_root,
1101 git_dir,
1102 format,
1103 &db,
1104 index,
1105 &source_entries,
1106 paths,
1107 false,
1108 )
1109}
1110
1111pub fn restore_index_paths_from_tree_allow_unmatched(
1112 worktree_root: impl AsRef<Path>,
1113 git_dir: impl AsRef<Path>,
1114 format: ObjectFormat,
1115 tree_oid: &ObjectId,
1116 paths: &[PathBuf],
1117) -> Result<RestoreResult> {
1118 let worktree_root = worktree_root.as_ref();
1119 let git_dir = git_dir.as_ref();
1120 let index_path = repository_index_path(git_dir);
1121 let index = if index_path.exists() {
1122 Index::parse(&fs::read(&index_path)?, format)?
1123 } else {
1124 Index {
1125 version: 2,
1126 entries: Vec::new(),
1127 extensions: Vec::new(),
1128 checksum: None,
1129 }
1130 };
1131 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1132 let source_entries = tree_entries(&db, format, tree_oid)?;
1133 restore_index_paths_from_entries(
1134 worktree_root,
1135 git_dir,
1136 format,
1137 &db,
1138 index,
1139 &source_entries,
1140 paths,
1141 true,
1142 )
1143}
1144
1145pub(crate) fn restore_index_paths_from_entries(
1146 worktree_root: &Path,
1147 git_dir: &Path,
1148 format: ObjectFormat,
1149 db: &FileObjectDatabase,
1150 mut index: Index,
1151 source_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1152 paths: &[PathBuf],
1153 allow_unmatched: bool,
1154) -> Result<RestoreResult> {
1155 let sparse = active_sparse_checkout(git_dir)?;
1156 if index.is_sparse() {
1157 expand_sparse_index(&mut index, db, format)?;
1158 }
1159 let index_version = index.version;
1160 let extensions = index_extensions_without_cache_tree(&index.extensions);
1161 let mut index_entries = index
1162 .entries
1163 .into_iter()
1164 .map(|entry| (entry.path.as_bytes().to_vec(), entry))
1165 .collect::<BTreeMap<_, _>>();
1166 let prior_skip_worktree = index_entries
1167 .iter()
1168 .filter(|(_, entry)| entry.is_skip_worktree())
1169 .map(|(path, _)| path.clone())
1170 .collect::<BTreeSet<_>>();
1171 let mut restored = BTreeSet::new();
1172 for path in paths {
1173 let absolute = if path.is_absolute() {
1174 path.clone()
1175 } else {
1176 worktree_root.join(path)
1177 };
1178 let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
1179 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
1180 })?;
1181 let git_path = git_path_bytes(relative)?;
1182 let recursive = path == Path::new(".")
1183 || path.to_string_lossy().ends_with('/')
1184 || absolute.is_dir()
1185 || index_entries
1186 .keys()
1187 .any(|entry| index_entry_is_under_path(entry, &git_path))
1188 || source_entries
1189 .keys()
1190 .any(|entry| index_entry_is_under_path(entry, &git_path));
1191 let mut matched_paths = BTreeSet::new();
1192 for path in index_entries.keys().chain(source_entries.keys()) {
1193 if path.as_slice() == git_path.as_slice()
1194 || (recursive && index_entry_is_under_path(path, &git_path))
1195 {
1196 matched_paths.insert(path.clone());
1197 }
1198 }
1199 if matched_paths.is_empty() {
1200 if allow_unmatched {
1201 continue;
1202 }
1203 eprintln!(
1204 "error: pathspec '{}' did not match any file(s) known to git",
1205 path.display()
1206 );
1207 return Err(GitError::Exit(1));
1208 }
1209 for path in matched_paths {
1210 if let Some(entry) = source_entries.get(&path) {
1211 let unchanged = index_entries.get(&path).is_some_and(|existing| {
1218 existing.oid == entry.oid
1219 && existing.mode == entry.mode
1220 && !existing.is_intent_to_add()
1221 });
1222 if !unchanged {
1223 let mut restored = restored_head_index_entry(worktree_root, db, &path, entry)?;
1224 if prior_skip_worktree.contains(&path) {
1225 restored.set_skip_worktree(true);
1226 }
1227 index_entries.insert(path.clone(), restored);
1228 }
1229 } else {
1230 index_entries.remove(&path);
1231 }
1232 restored.insert(path);
1233 }
1234 }
1235 let mut entries = index_entries.into_values().collect::<Vec<_>>();
1236 entries.sort_by(|left, right| left.path.cmp(&right.path));
1237 let restored_paths = restored.iter().cloned().collect::<Vec<_>>();
1238 let mut index = Index {
1239 version: index_version,
1240 entries,
1241 extensions,
1242 checksum: None,
1243 };
1244 invalidate_untracked_cache_for_git_paths(&mut index, format, &restored_paths)?;
1245 if let Some((sparse, mode)) = sparse
1246 && sparse.sparse_index
1247 {
1248 let matcher = SparseMatcher::new(&sparse, mode);
1249 collapse_to_sparse_index(&mut index, &matcher, db, format)?;
1250 }
1251 write_repository_index_ref(git_dir, format, &index)?;
1252 Ok(RestoreResult {
1253 restored: restored.len(),
1254 })
1255}
1256
1257pub fn restore_index_and_worktree_paths_from_head(
1258 worktree_root: impl AsRef<Path>,
1259 git_dir: impl AsRef<Path>,
1260 format: ObjectFormat,
1261 paths: &[PathBuf],
1262 overlay: bool,
1263) -> Result<RestoreResult> {
1264 let worktree_root = worktree_root.as_ref();
1265 let git_dir = git_dir.as_ref();
1266 let index_path = repository_index_path(git_dir);
1267 let index = if index_path.exists() {
1268 Index::parse(&fs::read(&index_path)?, format)?
1269 } else {
1270 Index {
1271 version: 2,
1272 entries: Vec::new(),
1273 extensions: Vec::new(),
1274 checksum: None,
1275 }
1276 };
1277 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1278 let head_entries = head_tree_entries(git_dir, format, &db)?;
1279 restore_index_and_worktree_paths_from_entries(
1280 worktree_root,
1281 git_dir,
1282 format,
1283 &db,
1284 index,
1285 &head_entries,
1286 paths,
1287 overlay,
1288 )
1289}
1290
1291pub fn restore_index_and_worktree_paths_from_tree(
1292 worktree_root: impl AsRef<Path>,
1293 git_dir: impl AsRef<Path>,
1294 format: ObjectFormat,
1295 tree_oid: &ObjectId,
1296 paths: &[PathBuf],
1297 overlay: bool,
1298) -> Result<RestoreResult> {
1299 let worktree_root = worktree_root.as_ref();
1300 let git_dir = git_dir.as_ref();
1301 let index_path = repository_index_path(git_dir);
1302 let index = if index_path.exists() {
1303 Index::parse(&fs::read(&index_path)?, format)?
1304 } else {
1305 Index {
1306 version: 2,
1307 entries: Vec::new(),
1308 extensions: Vec::new(),
1309 checksum: None,
1310 }
1311 };
1312 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1313 let source_entries = tree_entries(&db, format, tree_oid)?;
1314 restore_index_and_worktree_paths_from_entries(
1315 worktree_root,
1316 git_dir,
1317 format,
1318 &db,
1319 index,
1320 &source_entries,
1321 paths,
1322 overlay,
1323 )
1324}
1325
1326pub(crate) fn restore_index_and_worktree_paths_from_entries(
1327 worktree_root: &Path,
1328 git_dir: &Path,
1329 format: ObjectFormat,
1330 db: &FileObjectDatabase,
1331 index: Index,
1332 source_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
1333 paths: &[PathBuf],
1334 overlay: bool,
1335) -> Result<RestoreResult> {
1336 let index_version = index.version;
1337 let extensions = index_extensions_without_cache_tree(&index.extensions);
1338 let mut index_entries = index
1339 .entries
1340 .into_iter()
1341 .map(|entry| (entry.path.as_bytes().to_vec(), entry))
1342 .collect::<BTreeMap<_, _>>();
1343 let mut restored = BTreeSet::new();
1344 for path in paths {
1345 let absolute = if path.is_absolute() {
1346 path.clone()
1347 } else {
1348 worktree_root.join(path)
1349 };
1350 let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
1351 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
1352 })?;
1353 let git_path = git_path_bytes(relative)?;
1354 let recursive = path == Path::new(".")
1355 || path.to_string_lossy().ends_with('/')
1356 || absolute.is_dir()
1357 || index_entries
1358 .keys()
1359 .any(|entry| index_entry_is_under_path(entry, &git_path))
1360 || source_entries
1361 .keys()
1362 .any(|entry| index_entry_is_under_path(entry, &git_path));
1363 let mut matched_paths = BTreeSet::new();
1364 for path in index_entries.keys().chain(source_entries.keys()) {
1365 if path.as_slice() == git_path.as_slice()
1366 || (recursive && index_entry_is_under_path(path, &git_path))
1367 {
1368 matched_paths.insert(path.clone());
1369 }
1370 }
1371 if matched_paths.is_empty() {
1372 eprintln!(
1373 "error: pathspec '{}' did not match any file(s) known to git",
1374 path.display()
1375 );
1376 return Err(GitError::Exit(1));
1377 }
1378 for path in matched_paths {
1379 if let Some(entry) = source_entries.get(&path) {
1380 index_entries.insert(
1381 path.clone(),
1382 restore_head_entry_to_worktree_and_index(worktree_root, db, &path, entry)?,
1383 );
1384 } else if overlay {
1385 continue;
1389 } else {
1390 index_entries.remove(&path);
1393 remove_worktree_file(worktree_root, &path)?;
1394 }
1395 restored.insert(path);
1396 }
1397 }
1398 let mut entries = index_entries.into_values().collect::<Vec<_>>();
1399 entries.sort_by(|left, right| left.path.cmp(&right.path));
1400 let restored_paths = restored.iter().cloned().collect::<Vec<_>>();
1401 let mut index = Index {
1402 version: index_version,
1403 entries,
1404 extensions,
1405 checksum: None,
1406 };
1407 invalidate_untracked_cache_for_git_paths(&mut index, format, &restored_paths)?;
1408 write_repository_index_ref(git_dir, format, &index)?;
1409 Ok(RestoreResult {
1410 restored: restored.len(),
1411 })
1412}
1413
1414pub fn reset_index_and_worktree_to_commit(
1415 worktree_root: impl AsRef<Path>,
1416 git_dir: impl AsRef<Path>,
1417 format: ObjectFormat,
1418 commit_oid: &ObjectId,
1419) -> Result<RestoreResult> {
1420 let worktree_root = worktree_root.as_ref();
1421 let git_dir = git_dir.as_ref();
1422 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1423 let commit = read_commit(&db, format, commit_oid)?;
1424 let mut target_entries = BTreeMap::new();
1425 collect_tree_entries(&db, format, &commit.tree, &mut target_entries)?;
1426 refuse_if_current_working_directory_becomes_file(worktree_root, &target_entries)?;
1427 let config = sley_config::read_repo_config(git_dir, None).unwrap_or_default();
1428 let attributes = build_tree_attribute_matcher(worktree_root, &db, format, &commit.tree)?;
1429
1430 for path in current_index_paths(git_dir, format, &db)? {
1437 if !target_entries.contains_key(&path) {
1438 remove_worktree_file(worktree_root, &path)?;
1439 }
1440 }
1441
1442 let mut index_entries = Vec::new();
1443 for (path, entry) in &target_entries {
1444 index_entries.push(materialize_tree_entry_filtered(
1445 &db,
1446 format,
1447 worktree_root,
1448 path,
1449 entry,
1450 &config,
1451 &attributes,
1452 )?);
1453 }
1454 index_entries.sort_by(|left, right| left.path.cmp(&right.path));
1455 let extensions = preserved_index_extensions(git_dir, format)?;
1456 fs::write(
1457 repository_index_path(git_dir),
1458 Index {
1459 version: 2,
1460 entries: index_entries,
1461 extensions,
1462 checksum: None,
1463 }
1464 .write(format)?,
1465 )?;
1466 Ok(RestoreResult {
1467 restored: target_entries.len(),
1468 })
1469}
1470
1471pub(crate) fn current_index_paths(
1477 git_dir: &Path,
1478 format: ObjectFormat,
1479 db: &FileObjectDatabase,
1480) -> Result<BTreeSet<Vec<u8>>> {
1481 let (index, _stat_cache, _head_matches) = read_index_with_stat_cache(git_dir, format, db)?;
1482 Ok(index
1483 .entries
1484 .into_iter()
1485 .map(|entry| entry.path.into_bytes())
1486 .collect())
1487}
1488
1489pub(crate) fn materialize_tree_entry(
1499 db: &FileObjectDatabase,
1500 worktree_root: &Path,
1501 path: &[u8],
1502 entry: &TrackedEntry,
1503) -> Result<IndexEntry> {
1504 if sley_index::is_gitlink(entry.mode) {
1505 let dir_path = worktree_path(worktree_root, path)?;
1506 materialize_gitlink_dir(worktree_root, &dir_path)?;
1507 return Ok(IndexEntry {
1508 ctime_seconds: 0,
1509 ctime_nanoseconds: 0,
1510 mtime_seconds: 0,
1511 mtime_nanoseconds: 0,
1512 dev: 0,
1513 ino: 0,
1514 mode: entry.mode,
1515 uid: 0,
1516 gid: 0,
1517 size: 0,
1518 oid: entry.oid,
1519 flags: path.len().min(0x0fff) as u16,
1520 flags_extended: 0,
1521 path: BString::from(path),
1522 });
1523 }
1524 let file_path = write_worktree_blob_entry(db, worktree_root, path, entry)?;
1525 let metadata = fs::symlink_metadata(&file_path)?;
1526 let mut index_entry = index_entry_from_metadata(path.to_vec(), entry.oid, &metadata);
1527 index_entry.mode = entry.mode;
1528 Ok(index_entry)
1529}
1530
1531pub(crate) fn materialize_gitlink_dir(worktree_root: &Path, dir_path: &Path) -> Result<()> {
1532 prepare_blob_parent_dirs(worktree_root, dir_path)?;
1533 if fs::symlink_metadata(dir_path).is_ok_and(|metadata| !metadata.is_dir()) {
1534 remove_existing_worktree_path(dir_path)?;
1535 }
1536 fs::create_dir_all(dir_path)?;
1537 Ok(())
1538}
1539
1540pub(crate) fn materialize_tree_entry_filtered(
1541 db: &FileObjectDatabase,
1542 format: ObjectFormat,
1543 worktree_root: &Path,
1544 path: &[u8],
1545 entry: &TrackedEntry,
1546 config: &GitConfig,
1547 attributes: &AttributeMatcher,
1548) -> Result<IndexEntry> {
1549 if sley_index::is_gitlink(entry.mode) || (entry.mode & 0o170000) == 0o120000 {
1550 return materialize_tree_entry(db, worktree_root, path, entry);
1551 }
1552 let object = read_expected_object(db, &entry.oid, ObjectType::Blob)?;
1553 let checks = attributes.attributes_for_path(path, &filter_attribute_names(), false);
1554 let body = apply_smudge_filter_with_attributes_cow_format(
1555 config,
1556 &checks,
1557 path,
1558 &object.body,
1559 format,
1560 )?;
1561 let file_path = worktree_path(worktree_root, path)?;
1562 prepare_blob_parent_dirs(worktree_root, &file_path)?;
1563 remove_existing_worktree_path(&file_path)?;
1564 fs::write(&file_path, &body)?;
1565 set_worktree_file_mode(&file_path, entry.mode)?;
1566 let metadata = fs::symlink_metadata(&file_path)?;
1567 let mut index_entry = index_entry_from_metadata(path.to_vec(), entry.oid, &metadata);
1568 index_entry.mode = entry.mode;
1569 Ok(index_entry)
1570}
1571
1572pub(crate) fn write_worktree_blob_entry(
1583 db: &FileObjectDatabase,
1584 worktree_root: &Path,
1585 path: &[u8],
1586 entry: &TrackedEntry,
1587) -> Result<PathBuf> {
1588 let object = read_expected_object(db, &entry.oid, ObjectType::Blob)?;
1589 let file_path = worktree_path(worktree_root, path)?;
1590 prepare_blob_parent_dirs(worktree_root, &file_path)?;
1593 remove_existing_worktree_path(&file_path)?;
1596 write_blob_body_or_symlink(&file_path, entry.mode, &object.body, &object.body)?;
1597 Ok(file_path)
1598}
1599
1600pub fn write_blob_body_or_symlink(
1619 file_path: &Path,
1620 mode: u32,
1621 body: &[u8],
1622 link_target: &[u8],
1623) -> Result<()> {
1624 if (mode & 0o170000) == 0o120000 {
1625 #[cfg(unix)]
1626 {
1627 use std::os::unix::ffi::OsStringExt;
1628 let target =
1629 std::path::PathBuf::from(std::ffi::OsString::from_vec(link_target.to_vec()));
1630 std::os::unix::fs::symlink(&target, file_path)?;
1631 }
1632 #[cfg(not(unix))]
1633 {
1634 let _ = link_target;
1635 fs::write(file_path, body)?;
1636 }
1637 } else {
1638 fs::write(file_path, body)?;
1639 set_worktree_file_mode(file_path, mode)?;
1640 }
1641 Ok(())
1642}
1643
1644pub(crate) fn prepare_blob_parent_dirs(worktree_root: &Path, file_path: &Path) -> Result<()> {
1658 let parent = match file_path.parent() {
1659 Some(parent) => parent,
1660 None => return Ok(()),
1661 };
1662 if parent.is_dir() {
1664 return Ok(());
1665 }
1666 let mut components: Vec<&Path> = Vec::new();
1670 let mut cursor = Some(parent);
1671 while let Some(dir) = cursor {
1672 if dir == worktree_root {
1673 break;
1674 }
1675 components.push(dir);
1676 cursor = dir.parent();
1677 if cursor.is_none() {
1678 break;
1679 }
1680 }
1681 for dir in components.iter().rev() {
1683 match fs::symlink_metadata(dir) {
1684 Ok(metadata) if metadata.is_dir() => {}
1685 Ok(_) => {
1686 fs::remove_file(dir)?;
1689 fs::create_dir(dir)?;
1690 }
1691 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
1692 fs::create_dir(dir)?;
1693 }
1694 Err(err) => return Err(err.into()),
1695 }
1696 }
1697 Ok(())
1698}
1699
1700pub(crate) fn remove_existing_worktree_path(file_path: &Path) -> Result<()> {
1705 let metadata = match fs::symlink_metadata(file_path) {
1706 Ok(metadata) => metadata,
1707 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
1708 Err(err) => return Err(err.into()),
1709 };
1710 if metadata.is_dir() {
1711 if path_is_original_cwd(file_path) {
1712 return refuse_remove_current_working_directory(file_path);
1713 }
1714 match fs::remove_dir_all(file_path) {
1717 Ok(()) => {}
1718 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
1719 Err(err) => return Err(err.into()),
1720 }
1721 } else {
1722 fs::remove_file(file_path)?;
1723 }
1724 Ok(())
1725}
1726
1727#[cfg(unix)]
1747pub(crate) fn set_worktree_file_mode(file_path: &Path, entry_mode: u32) -> Result<()> {
1748 use std::os::unix::fs::PermissionsExt;
1749 let perms = match entry_mode {
1750 0o100755 => 0o755,
1751 0o100644 => 0o644,
1752 _ => return Ok(()),
1753 };
1754 fs::set_permissions(file_path, fs::Permissions::from_mode(perms))?;
1755 Ok(())
1756}
1757
1758#[cfg(not(unix))]
1759pub(crate) fn set_worktree_file_mode(_file_path: &Path, _entry_mode: u32) -> Result<()> {
1760 Ok(())
1761}
1762
1763pub fn checkout_tree_to_index_and_worktree(
1765 worktree_root: impl AsRef<Path>,
1766 git_dir: impl AsRef<Path>,
1767 format: ObjectFormat,
1768 tree_oid: &ObjectId,
1769) -> Result<RestoreResult> {
1770 let worktree_root = worktree_root.as_ref();
1771 let git_dir = git_dir.as_ref();
1772 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1773 let mut target_entries = BTreeMap::new();
1774 collect_tree_entries(&db, format, tree_oid, &mut target_entries)?;
1775
1776 for path in read_index_entries(git_dir, format)?.keys() {
1777 if !target_entries.contains_key(path) {
1778 remove_worktree_file(worktree_root, path)?;
1779 }
1780 }
1781
1782 let mut index_entries = Vec::new();
1783 for (path, entry) in &target_entries {
1784 index_entries.push(materialize_tree_entry(&db, worktree_root, path, entry)?);
1785 }
1786 index_entries.sort_by(|left, right| left.path.cmp(&right.path));
1787 let extensions = preserved_index_extensions(git_dir, format)?;
1788 fs::write(
1789 repository_index_path(git_dir),
1790 Index {
1791 version: 2,
1792 entries: index_entries,
1793 extensions,
1794 checksum: None,
1795 }
1796 .write(format)?,
1797 )?;
1798 Ok(RestoreResult {
1799 restored: target_entries.len(),
1800 })
1801}
1802
1803pub fn reset_index_to_commit(
1804 worktree_root: impl AsRef<Path>,
1805 git_dir: impl AsRef<Path>,
1806 format: ObjectFormat,
1807 commit_oid: &ObjectId,
1808) -> Result<RestoreResult> {
1809 let worktree_root = worktree_root.as_ref();
1810 let git_dir = git_dir.as_ref();
1811 let db = FileObjectDatabase::from_git_dir(git_dir, format);
1812 let commit = read_commit(&db, format, commit_oid)?;
1813 let mut target_entries = BTreeMap::new();
1814 collect_tree_entries(&db, format, &commit.tree, &mut target_entries)?;
1815 let index_path = repository_index_path(git_dir);
1819 let prior_skip_worktree: BTreeSet<Vec<u8>> = match fs::read(&index_path) {
1820 Ok(bytes) => Index::parse(&bytes, format)?
1821 .entries
1822 .iter()
1823 .filter(|entry| entry.is_skip_worktree())
1824 .map(|entry| entry.path.as_bytes().to_vec())
1825 .collect(),
1826 Err(err) if err.kind() == std::io::ErrorKind::NotFound => BTreeSet::new(),
1827 Err(err) => return Err(err.into()),
1828 };
1829 let mut index_entries = Vec::new();
1830 for (path, entry) in &target_entries {
1831 let mut restored = restored_head_index_entry(worktree_root, &db, path, entry)?;
1832 if prior_skip_worktree.contains(path) {
1833 restored.set_skip_worktree(true);
1834 }
1835 index_entries.push(restored);
1836 }
1837 index_entries.sort_by(|left, right| left.path.cmp(&right.path));
1838 let mut index = Index {
1839 version: 2,
1840 entries: index_entries,
1841 extensions: preserved_index_extensions(git_dir, format)?,
1842 checksum: None,
1843 };
1844 index.upgrade_version_for_flags();
1845 write_repository_index_ref(git_dir, format, &index)?;
1846 Ok(RestoreResult {
1847 restored: target_entries.len(),
1848 })
1849}
1850
1851pub fn index_from_tree(
1861 db: &FileObjectDatabase,
1862 format: ObjectFormat,
1863 tree_oid: &ObjectId,
1864) -> Result<Index> {
1865 let mut entries: Vec<IndexEntry> = Vec::new();
1866 if *tree_oid != ObjectId::empty_tree(format) {
1867 let mut tree_entries = BTreeMap::new();
1868 collect_tree_entries(db, format, tree_oid, &mut tree_entries)?;
1869 entries.reserve(tree_entries.len());
1870 for (path, entry) in tree_entries {
1871 let name_len = (path.len().min(0x0fff)) as u16;
1872 entries.push(IndexEntry {
1873 ctime_seconds: 0,
1874 ctime_nanoseconds: 0,
1875 mtime_seconds: 0,
1876 mtime_nanoseconds: 0,
1877 dev: 0,
1878 ino: 0,
1879 mode: entry.mode,
1880 uid: 0,
1881 gid: 0,
1882 size: 0,
1883 oid: entry.oid,
1884 flags: name_len,
1885 flags_extended: 0,
1886 path: path.into(),
1887 });
1888 }
1889 }
1890 entries.sort_by(|left, right| left.path.cmp(&right.path));
1893 Ok(Index {
1894 version: 2,
1895 entries,
1896 extensions: Vec::new(),
1897 checksum: None,
1898 })
1899}
1900
1901pub fn path_in_sparse_checkout(
1920 path: &[u8],
1921 sparse: &SparseCheckout,
1922 mode: SparseCheckoutMode,
1923) -> bool {
1924 SparseMatcher::new(sparse, mode).includes_file(path)
1925}
1926
1927pub(crate) fn active_sparse_checkout(
1928 git_dir: &Path,
1929) -> Result<Option<(SparseCheckout, SparseCheckoutMode)>> {
1930 let worktree_config = GitConfig::read(git_dir.join("config.worktree")).unwrap_or_default();
1931 let repo_config = GitConfig::read(git_dir.join("config")).unwrap_or_default();
1932 let sparse_enabled = worktree_config
1933 .get_bool("core", None, "sparseCheckout")
1934 .or_else(|| repo_config.get_bool("core", None, "sparseCheckout"))
1935 .unwrap_or(false);
1936 if !sparse_enabled {
1937 return Ok(None);
1938 }
1939 let sparse_file = git_dir.join("info").join("sparse-checkout");
1940 if !sparse_file.exists() {
1941 return Ok(None);
1942 }
1943 let cone = worktree_config
1944 .get_bool("core", None, "sparseCheckoutCone")
1945 .or_else(|| repo_config.get_bool("core", None, "sparseCheckoutCone"))
1946 .unwrap_or(false);
1947 let sparse_index = cone
1948 && worktree_config
1949 .get_bool("index", None, "sparse")
1950 .or_else(|| repo_config.get_bool("index", None, "sparse"))
1951 .unwrap_or(false);
1952 let bytes = fs::read(sparse_file)?;
1953 let mut patterns = bytes
1954 .split(|byte| *byte == b'\n')
1955 .map(<[u8]>::to_vec)
1956 .collect::<Vec<_>>();
1957 if patterns.last().map(Vec::is_empty) == Some(true) {
1958 patterns.pop();
1959 }
1960 let mode = if cone {
1961 SparseCheckoutMode::Cone
1962 } else {
1963 SparseCheckoutMode::Full
1964 };
1965 Ok(Some((
1966 SparseCheckout {
1967 patterns,
1968 sparse_index,
1969 },
1970 mode,
1971 )))
1972}
1973
1974pub fn apply_sparse_checkout(
1977 worktree_root: impl AsRef<Path>,
1978 git_dir: impl AsRef<Path>,
1979 format: ObjectFormat,
1980 sparse: &SparseCheckout,
1981) -> Result<ApplySparseResult> {
1982 apply_sparse_checkout_with_mode(
1983 worktree_root,
1984 git_dir,
1985 format,
1986 sparse,
1987 SparseCheckoutMode::Auto,
1988 )
1989}
1990
1991pub fn apply_sparse_checkout_with_mode(
1994 worktree_root: impl AsRef<Path>,
1995 git_dir: impl AsRef<Path>,
1996 format: ObjectFormat,
1997 sparse: &SparseCheckout,
1998 mode: SparseCheckoutMode,
1999) -> Result<ApplySparseResult> {
2000 let worktree_root = worktree_root.as_ref();
2001 let git_dir = git_dir.as_ref();
2002 let index_path = repository_index_path(git_dir);
2003 let mut index = if index_path.exists() {
2004 Index::parse(&fs::read(&index_path)?, format)?
2005 } else {
2006 return Ok(ApplySparseResult {
2007 materialized: Vec::new(),
2008 skipped: Vec::new(),
2009 not_up_to_date: Vec::new(),
2010 });
2011 };
2012 let matcher = SparseMatcher::new(sparse, mode);
2013 let db = FileObjectDatabase::from_git_dir(git_dir, format);
2014 if index.entries.iter().any(IndexEntry::is_sparse_dir) {
2019 expand_sparse_index(&mut index, &db, format)?;
2020 }
2021 let mut materialized = Vec::new();
2022 let mut skipped = Vec::new();
2023 let mut not_up_to_date = Vec::new();
2024 for entry in &mut index.entries {
2025 if index_entry_stage(entry) != 0 {
2027 continue;
2028 }
2029 if matcher.includes_file(entry.path.as_bytes()) {
2030 clear_skip_worktree(entry);
2031 let file_path = worktree_path(worktree_root, entry.path.as_bytes())?;
2032 if !file_path.exists() {
2033 materialize_index_entry_file(&db, worktree_root, &file_path, entry)?;
2034 let metadata = fs::symlink_metadata(&file_path)?;
2035 *entry = index_entry_with_refreshed_stat(entry, &metadata);
2036 }
2037 materialized.push(entry.path.as_bytes().to_vec());
2038 } else {
2039 let file_path = worktree_path(worktree_root, entry.path.as_bytes())?;
2046 match fs::symlink_metadata(&file_path) {
2047 Ok(metadata) if !worktree_entry_is_uptodate(entry, &metadata) => {
2048 clear_skip_worktree(entry);
2049 not_up_to_date.push(entry.path.as_bytes().to_vec());
2050 }
2051 _ => {
2052 set_skip_worktree(entry);
2053 remove_worktree_file(worktree_root, entry.path.as_bytes())?;
2054 skipped.push(entry.path.as_bytes().to_vec());
2055 }
2056 }
2057 }
2058 }
2059 not_up_to_date.sort();
2060 normalize_index_version_for_extended_flags(&mut index);
2061 if sparse.sparse_index {
2066 collapse_to_sparse_index(&mut index, &matcher, &db, format)?;
2067 } else {
2068 index.clear_sparse_extension()?;
2069 }
2070 write_repository_index_ref(git_dir, format, &index)?;
2071 Ok(ApplySparseResult {
2072 materialized,
2073 skipped,
2074 not_up_to_date,
2075 })
2076}
2077
2078pub fn expand_sparse_index(
2088 index: &mut Index,
2089 db: &FileObjectDatabase,
2090 format: ObjectFormat,
2091) -> Result<bool> {
2092 if !index.entries.iter().any(IndexEntry::is_sparse_dir) {
2093 let had_marker = index.is_sparse();
2095 index.clear_sparse_extension()?;
2096 if had_marker {
2097 sley_core::trace2::region("index", "ensure_full_index");
2098 }
2099 return Ok(had_marker);
2100 }
2101 let mut expanded: Vec<IndexEntry> = Vec::with_capacity(index.entries.len());
2102 for entry in std::mem::take(&mut index.entries) {
2103 if !entry.is_sparse_dir() {
2104 expanded.push(entry);
2105 continue;
2106 }
2107 let dir = entry.path.as_bytes();
2109 let dir_prefix = dir; for (rel, (mode, oid)) in sley_diff_merge::flatten_tree(db, format, &entry.oid)? {
2111 let mut full_path = dir_prefix.to_vec();
2112 full_path.extend_from_slice(&rel);
2113 let mut blob = blank_sparse_blob_entry(format, &full_path, mode, oid);
2114 blob.set_skip_worktree(true);
2116 expanded.push(blob);
2117 }
2118 }
2119 expanded.sort_by(|a, b| a.path.as_bytes().cmp(b.path.as_bytes()));
2120 index.entries = expanded;
2121 index.clear_sparse_extension()?;
2122 normalize_index_version_for_extended_flags(index);
2123 sley_core::trace2::region("index", "ensure_full_index");
2124 Ok(true)
2125}
2126
2127pub(crate) fn index_sparse_dir_contains_path(index: &Index, git_path: &[u8]) -> bool {
2128 index.entries.iter().any(|entry| {
2129 entry.is_sparse_dir()
2130 && git_path.starts_with(entry.path.as_bytes())
2131 && git_path.len() > entry.path.len()
2132 })
2133}
2134
2135pub(crate) fn blank_sparse_blob_entry(
2140 format: ObjectFormat,
2141 path: &[u8],
2142 mode: u32,
2143 oid: ObjectId,
2144) -> IndexEntry {
2145 let _ = format;
2146 let mut entry = IndexEntry {
2147 ctime_seconds: 0,
2148 ctime_nanoseconds: 0,
2149 mtime_seconds: 0,
2150 mtime_nanoseconds: 0,
2151 dev: 0,
2152 ino: 0,
2153 mode,
2154 uid: 0,
2155 gid: 0,
2156 size: 0,
2157 oid,
2158 flags: 0,
2159 flags_extended: 0,
2160 path: path.into(),
2161 };
2162 entry.refresh_name_length();
2163 entry
2164}
2165
2166pub(crate) fn collapse_to_sparse_index(
2173 index: &mut Index,
2174 matcher: &SparseMatcher,
2175 db: &FileObjectDatabase,
2176 format: ObjectFormat,
2177) -> Result<()> {
2178 if index.entries.iter().any(IndexEntry::is_sparse_dir) {
2181 expand_sparse_index(index, db, format)?;
2182 }
2183
2184 if index.entries.iter().any(|e| index_entry_stage(e) != 0) {
2187 index.clear_sparse_extension()?;
2188 return Ok(());
2189 }
2190
2191 index
2192 .entries
2193 .sort_by(|a, b| a.path.as_bytes().cmp(b.path.as_bytes()));
2194
2195 use std::collections::BTreeMap;
2198 let mut dir_has_in_cone: BTreeMap<Vec<u8>, bool> = BTreeMap::new();
2199 for entry in &index.entries {
2200 let path = entry.path.as_bytes();
2201 let in_cone = matcher.includes_file(path);
2202 let mut start = 0usize;
2203 while let Some(rel) = path
2204 .get(start..)
2205 .and_then(|s| s.iter().position(|b| *b == b'/'))
2206 {
2207 let end = start + rel;
2208 let dir = path[..end].to_vec();
2209 let flag = dir_has_in_cone.entry(dir).or_insert(false);
2210 *flag = *flag || in_cone;
2211 start = end + 1;
2212 }
2213 }
2214
2215 let collapsible: Vec<Vec<u8>> = {
2218 let all: Vec<Vec<u8>> = dir_has_in_cone
2219 .iter()
2220 .filter(|(_, has)| !**has)
2221 .map(|(dir, _)| dir.clone())
2222 .collect();
2223 all.iter()
2224 .filter(|dir| {
2225 !all.iter().any(|other| {
2226 other != *dir
2227 && dir
2228 .strip_prefix(other.as_slice())
2229 .is_some_and(|rest| rest.first() == Some(&b'/'))
2230 })
2231 })
2232 .cloned()
2233 .collect()
2234 };
2235 if collapsible.is_empty() {
2236 index.clear_sparse_extension()?;
2237 return Ok(());
2238 }
2239
2240 let mut checker = db.presence_checker();
2241 let mut new_entries: Vec<IndexEntry> = Vec::with_capacity(index.entries.len());
2242 let mut consumed: std::collections::HashSet<Vec<u8>> = std::collections::HashSet::new();
2243 for dir in &collapsible {
2244 let mut subtree: Vec<&IndexEntry> = index
2246 .entries
2247 .iter()
2248 .filter(|e| {
2249 e.path
2250 .as_bytes()
2251 .strip_prefix(dir.as_slice())
2252 .is_some_and(|rest| rest.first() == Some(&b'/'))
2253 })
2254 .collect();
2255 if subtree.is_empty() {
2256 continue;
2257 }
2258 subtree.sort_by(|a, b| a.path.as_bytes().cmp(b.path.as_bytes()));
2259 let mut prefix = dir.clone();
2261 prefix.push(b'/');
2262 let tree_entries: Vec<WriteTreeEntry<'_>> = subtree
2263 .iter()
2264 .map(|e| WriteTreeEntry {
2265 path: e.path.as_bytes(),
2266 mode: e.mode,
2267 oid: e.oid.clone(),
2268 })
2269 .collect();
2270 let tree_oid =
2271 write_tree_entries_stream(&tree_entries, &prefix, None, db, &mut checker, false)?;
2272 for e in &subtree {
2274 consumed.insert(e.path.as_bytes().to_vec());
2275 }
2276 let mut sparse_path = dir.clone();
2278 sparse_path.push(b'/');
2279 let mut sparse_entry =
2280 blank_sparse_blob_entry(format, &sparse_path, SPARSE_DIR_MODE, tree_oid);
2281 sparse_entry.set_skip_worktree(true);
2282 new_entries.push(sparse_entry);
2283 }
2284 for entry in &index.entries {
2286 if consumed.contains(entry.path.as_bytes()) {
2287 continue;
2288 }
2289 new_entries.push(entry.clone());
2290 }
2291 new_entries.sort_by(|a, b| a.path.as_bytes().cmp(b.path.as_bytes()));
2292 index.entries = new_entries;
2293 index.set_sparse_extension();
2294 normalize_index_version_for_extended_flags(index);
2295 sley_core::trace2::region("index", "convert_to_sparse");
2296 Ok(())
2297}
2298
2299pub(crate) fn worktree_entry_is_uptodate(entry: &IndexEntry, metadata: &fs::Metadata) -> bool {
2306 if u64::from(entry.size) != metadata.len() {
2307 return false;
2308 }
2309 let Some((mtime_seconds, mtime_nanoseconds)) = file_mtime_parts(metadata) else {
2310 return false;
2313 };
2314 u64::from(entry.mtime_seconds) == mtime_seconds
2315 && u64::from(entry.mtime_nanoseconds) == mtime_nanoseconds
2316}
2317
2318pub(crate) fn worktree_entry_ref_is_uptodate(
2319 entry: &IndexEntryRef<'_>,
2320 metadata: &fs::Metadata,
2321) -> bool {
2322 if u64::from(entry.size) != metadata.len() {
2323 return false;
2324 }
2325 let Some((mtime_seconds, mtime_nanoseconds)) = file_mtime_parts(metadata) else {
2326 return false;
2327 };
2328 u64::from(entry.mtime_seconds) == mtime_seconds
2329 && u64::from(entry.mtime_nanoseconds) == mtime_nanoseconds
2330}
2331
2332pub(crate) fn file_mtime_parts(metadata: &fs::Metadata) -> Option<(u64, u64)> {
2335 let modified = metadata.modified().ok()?;
2336 let duration = modified.duration_since(UNIX_EPOCH).ok()?;
2337 Some((duration.as_secs(), u64::from(duration.subsec_nanos())))
2338}
2339
2340pub fn write_metadata_file_atomic(
2347 path: impl AsRef<Path>,
2348 bytes: &[u8],
2349 options: AtomicMetadataWriteOptions,
2350) -> Result<AtomicMetadataWriteResult> {
2351 let path = path.as_ref();
2352 let parent = path.parent().ok_or_else(|| {
2353 GitError::InvalidPath(format!("metadata path has no parent: {}", path.display()))
2354 })?;
2355 if !parent.as_os_str().is_empty() {
2356 fs::create_dir_all(parent)?;
2357 }
2358 let lock_path = metadata_lock_path(path)?;
2359 let mut lock = match fs::OpenOptions::new()
2360 .write(true)
2361 .create_new(true)
2362 .open(&lock_path)
2363 {
2364 Ok(lock) => lock,
2365 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
2366 return Err(GitError::Transaction(format!(
2367 "metadata lock already exists: {}",
2368 lock_path.display()
2369 )));
2370 }
2371 Err(err) => return Err(err.into()),
2372 };
2373 if let Err(err) = lock.write_all(bytes) {
2374 let _ = fs::remove_file(&lock_path);
2375 return Err(err.into());
2376 }
2377 if options.fsync_file
2378 && let Err(err) = lock.sync_all()
2379 {
2380 let _ = fs::remove_file(&lock_path);
2381 return Err(err.into());
2382 }
2383 drop(lock);
2384 if let Err(err) = fs::rename(&lock_path, path) {
2385 let _ = fs::remove_file(&lock_path);
2386 return Err(err.into());
2387 }
2388 if options.fsync_dir
2389 && let Ok(dir) = fs::File::open(parent)
2390 {
2391 dir.sync_all()?;
2392 }
2393 let metadata = fs::metadata(path)?;
2394 Ok(AtomicMetadataWriteResult {
2395 path: path.to_path_buf(),
2396 len: metadata.len(),
2397 mtime: file_mtime_parts(&metadata),
2398 })
2399}
2400
2401pub(crate) fn metadata_lock_path(path: &Path) -> Result<PathBuf> {
2402 let file_name = path.file_name().ok_or_else(|| {
2403 GitError::InvalidPath(format!("metadata path has no filename: {}", path.display()))
2404 })?;
2405 let mut lock_name = file_name.to_os_string();
2406 lock_name.push(".lock");
2407 Ok(path.with_file_name(lock_name))
2408}
2409
2410pub fn checkout_detached_sparse(
2420 worktree_root: impl AsRef<Path>,
2421 git_dir: impl AsRef<Path>,
2422 format: ObjectFormat,
2423 target: &ObjectId,
2424 committer: Vec<u8>,
2425 message: Vec<u8>,
2426 sparse: &SparseCheckout,
2427) -> Result<CheckoutResult> {
2428 let worktree_root = worktree_root.as_ref();
2429 let git_dir = git_dir.as_ref();
2430 let files = checkout_commit_to_index_and_worktree_sparse(
2431 worktree_root,
2432 git_dir,
2433 format,
2434 target,
2435 Some((sparse, SparseCheckoutMode::Auto)),
2436 None,
2437 None,
2438 )?;
2439 let refs = FileRefStore::new(git_dir, format);
2440 let zero = ObjectId::null(format);
2441 let mut tx = refs.transaction();
2442 tx.update(RefUpdate {
2443 name: "HEAD".into(),
2444 expected: None,
2445 new: RefTarget::Direct(*target),
2446 reflog: Some(ReflogEntry {
2447 old_oid: zero,
2448 new_oid: *target,
2449 committer,
2450 message,
2451 }),
2452 });
2453 tx.commit()?;
2454 Ok(CheckoutResult {
2455 branch: target.to_string(),
2456 oid: *target,
2457 files,
2458 })
2459}
2460
2461pub(crate) fn materialize_index_entry_file(
2462 db: &FileObjectDatabase,
2463 worktree_root: &Path,
2464 file_path: &Path,
2465 entry: &IndexEntry,
2466) -> Result<()> {
2467 if sley_index::is_gitlink(entry.mode) {
2473 materialize_gitlink_dir(worktree_root, file_path)?;
2474 return Ok(());
2475 }
2476 let object = read_expected_object(db, &entry.oid, ObjectType::Blob)?;
2477 prepare_blob_parent_dirs(worktree_root, file_path)?;
2478 remove_existing_worktree_path(file_path)?;
2479 write_blob_body_or_symlink(file_path, entry.mode, &object.body, &object.body)?;
2480 Ok(())
2481}
2482
2483pub(crate) fn set_skip_worktree(entry: &mut IndexEntry) {
2484 entry.flags |= INDEX_FLAG_EXTENDED;
2485 entry.flags_extended |= INDEX_EXTENDED_FLAG_SKIP_WORKTREE;
2486}
2487
2488pub(crate) fn clear_skip_worktree(entry: &mut IndexEntry) {
2489 entry.flags_extended &= !INDEX_EXTENDED_FLAG_SKIP_WORKTREE;
2490 if entry.flags_extended == 0 {
2491 entry.flags &= !INDEX_FLAG_EXTENDED;
2492 }
2493}
2494
2495pub fn restore_worktree_paths_from_head(
2496 worktree_root: impl AsRef<Path>,
2497 git_dir: impl AsRef<Path>,
2498 format: ObjectFormat,
2499 paths: &[PathBuf],
2500) -> Result<RestoreResult> {
2501 let worktree_root = worktree_root.as_ref();
2502 let git_dir = git_dir.as_ref();
2503 let index_path = repository_index_path(git_dir);
2504 let index = if index_path.exists() {
2505 Index::parse(&fs::read(&index_path)?, format)?
2506 } else {
2507 Index {
2508 version: 2,
2509 entries: Vec::new(),
2510 extensions: Vec::new(),
2511 checksum: None,
2512 }
2513 };
2514 let db = FileObjectDatabase::from_git_dir(git_dir, format);
2515 let head_entries = head_tree_entries(git_dir, format, &db)?;
2516 restore_worktree_paths_from_entries(worktree_root, &db, index, &head_entries, paths)
2517}
2518
2519pub fn restore_worktree_paths_from_tree(
2520 worktree_root: impl AsRef<Path>,
2521 git_dir: impl AsRef<Path>,
2522 format: ObjectFormat,
2523 tree_oid: &ObjectId,
2524 paths: &[PathBuf],
2525) -> Result<RestoreResult> {
2526 let worktree_root = worktree_root.as_ref();
2527 let git_dir = git_dir.as_ref();
2528 let index_path = repository_index_path(git_dir);
2529 let index = if index_path.exists() {
2530 Index::parse(&fs::read(&index_path)?, format)?
2531 } else {
2532 Index {
2533 version: 2,
2534 entries: Vec::new(),
2535 extensions: Vec::new(),
2536 checksum: None,
2537 }
2538 };
2539 let db = FileObjectDatabase::from_git_dir(git_dir, format);
2540 let source_entries = tree_entries(&db, format, tree_oid)?;
2541 restore_worktree_paths_from_entries(worktree_root, &db, index, &source_entries, paths)
2542}
2543
2544pub(crate) fn restore_worktree_paths_from_entries(
2545 worktree_root: &Path,
2546 db: &FileObjectDatabase,
2547 index: Index,
2548 source_entries: &BTreeMap<Vec<u8>, TrackedEntry>,
2549 paths: &[PathBuf],
2550) -> Result<RestoreResult> {
2551 let index_entries = index
2552 .entries
2553 .into_iter()
2554 .map(|entry| entry.path.into_bytes())
2555 .collect::<BTreeSet<_>>();
2556 let mut restored = BTreeSet::new();
2557 for path in paths {
2558 let absolute = if path.is_absolute() {
2559 path.clone()
2560 } else {
2561 worktree_root.join(path)
2562 };
2563 let relative = absolute.strip_prefix(worktree_root).map_err(|_| {
2564 GitError::InvalidPath(format!("path {} is outside worktree", path.display()))
2565 })?;
2566 let git_path = git_path_bytes(relative)?;
2567 let recursive = path == Path::new(".")
2568 || path.to_string_lossy().ends_with('/')
2569 || absolute.is_dir()
2570 || index_entries
2571 .iter()
2572 .any(|entry| index_entry_is_under_path(entry, &git_path))
2573 || source_entries
2574 .keys()
2575 .any(|entry| index_entry_is_under_path(entry, &git_path));
2576 let mut matched_paths = BTreeSet::new();
2577 for path in index_entries.iter().chain(source_entries.keys()) {
2578 if path.as_slice() == git_path.as_slice()
2579 || (recursive && index_entry_is_under_path(path, &git_path))
2580 {
2581 matched_paths.insert(path.clone());
2582 }
2583 }
2584 if matched_paths.is_empty() {
2585 eprintln!(
2586 "error: pathspec '{}' did not match any file(s) known to git",
2587 path.display()
2588 );
2589 return Err(GitError::Exit(1));
2590 }
2591 for path in matched_paths {
2592 if let Some(entry) = source_entries.get(&path) {
2593 restore_head_entry_to_worktree(worktree_root, db, &path, entry)?;
2594 } else {
2595 remove_worktree_file(worktree_root, &path)?;
2596 }
2597 restored.insert(path);
2598 }
2599 }
2600 Ok(RestoreResult {
2601 restored: restored.len(),
2602 })
2603}