1use std::{
2 path::{Path, PathBuf},
3 sync::atomic::{AtomicBool, Ordering},
4};
5
6use anyhow::bail;
7use gix::{objs::find::Error, worktree::state::checkout, NestedProgress, Progress};
8
9use crate::{
10 index,
11 index::{parse_file, Options},
12};
13
14pub fn checkout_exclusive(
15 index_path: impl AsRef<Path>,
16 dest_directory: impl AsRef<Path>,
17 repo: Option<PathBuf>,
18 mut err: impl std::io::Write,
19 mut progress: impl NestedProgress,
20 should_interrupt: &AtomicBool,
21 index::checkout_exclusive::Options {
22 index: Options { object_hash, .. },
23 empty_files,
24 keep_going,
25 thread_limit,
26 }: index::checkout_exclusive::Options,
27) -> anyhow::Result<()> {
28 let repo = repo.map(gix::discover).transpose()?;
29
30 let dest_directory = dest_directory.as_ref();
31 if dest_directory.exists() {
32 bail!(
33 "Refusing to checkout index into existing directory '{}' - remove it and try again",
34 dest_directory.display()
35 )
36 }
37 std::fs::create_dir_all(dest_directory)?;
38
39 let mut index = parse_file(index_path, object_hash)?;
40
41 let mut num_skipped = 0;
42 let maybe_symlink_mode = if !empty_files && repo.is_some() {
43 gix::index::entry::Mode::DIR
44 } else {
45 gix::index::entry::Mode::SYMLINK
46 };
47 for entry in index.entries_mut().iter_mut().filter(|e| {
48 e.mode
49 .contains(maybe_symlink_mode | gix::index::entry::Mode::DIR | gix::index::entry::Mode::COMMIT)
50 }) {
51 entry.flags.insert(gix::index::entry::Flags::SKIP_WORKTREE);
52 num_skipped += 1;
53 }
54 if num_skipped > 0 {
55 progress.info(format!("Skipping {num_skipped} DIR/SYMLINK/COMMIT entries"));
56 }
57
58 let opts = gix::worktree::state::checkout::Options {
59 fs: gix::fs::Capabilities::probe(dest_directory),
60
61 destination_is_initially_empty: true,
62 overwrite_existing: false,
63 keep_going,
64 thread_limit,
65 filters: repo
66 .as_ref()
67 .and_then(|repo| repo.filter_pipeline(None).ok().map(|t| t.0.into_parts().0))
68 .unwrap_or_default(),
69 ..Default::default()
70 };
71
72 let mut files = progress.add_child("checkout");
73 let mut bytes = progress.add_child("writing");
74
75 let entries_for_checkout = index.entries().len() - num_skipped;
76 files.init(Some(entries_for_checkout), gix::progress::count("files"));
77 bytes.init(None, gix::progress::bytes());
78
79 let start = std::time::Instant::now();
80 let no_repo = repo.is_none();
81 let checkout::Outcome {
82 errors,
83 collisions,
84 files_updated,
85 bytes_written,
86 delayed_paths_unknown,
87 delayed_paths_unprocessed,
88 } = match repo {
89 Some(repo) => gix::worktree::state::checkout(
90 &mut index,
91 dest_directory,
92 EmptyOrDb {
93 empty_files,
94 db: repo.objects.into_arc()?,
95 },
96 &files,
97 &bytes,
98 should_interrupt,
99 opts,
100 ),
101 None => gix::worktree::state::checkout(
102 &mut index,
103 dest_directory,
104 Empty,
105 &files,
106 &bytes,
107 should_interrupt,
108 opts,
109 ),
110 }?;
111
112 files.show_throughput(start);
113 bytes.show_throughput(start);
114
115 progress.done(format!(
116 "Created {} {} files{} ({})",
117 files_updated,
118 if no_repo { "empty" } else { Default::default() },
119 if should_interrupt.load(Ordering::Relaxed) {
120 {
121 format!(
122 " of {}",
123 entries_for_checkout
124 .saturating_sub(errors.len() + collisions.len() + delayed_paths_unprocessed.len())
125 )
126 }
127 } else {
128 Default::default()
129 },
130 gix::progress::bytes()
131 .unwrap()
132 .display(bytes_written as usize, None, None)
133 ));
134
135 let mut messages = Vec::new();
136 if !errors.is_empty() {
137 messages.push(format!("kept going through {} errors(s)", errors.len()));
138 for record in errors {
139 writeln!(err, "{}: {}", record.path, record.error).ok();
140 }
141 }
142 if !collisions.is_empty() {
143 messages.push(format!("encountered {} collision(s)", collisions.len()));
144 for col in collisions {
145 writeln!(err, "{}: collision ({:?})", col.path, col.error_kind).ok();
146 }
147 }
148 if !delayed_paths_unknown.is_empty() {
149 messages.push(format!(
150 "A delayed process provided us with {} paths we never sent to it",
151 delayed_paths_unknown.len()
152 ));
153 for unknown in delayed_paths_unknown {
154 writeln!(err, "{unknown}: unknown").ok();
155 }
156 }
157 if !delayed_paths_unprocessed.is_empty() {
158 messages.push(format!(
159 "A delayed process forgot to process {} paths",
160 delayed_paths_unprocessed.len()
161 ));
162 for unprocessed in delayed_paths_unprocessed {
163 writeln!(err, "{unprocessed}: unprocessed and forgotten").ok();
164 }
165 }
166 if !messages.is_empty() {
167 bail!(
168 "One or more errors occurred - checkout is incomplete: {}",
169 messages.join(", ")
170 );
171 }
172 Ok(())
173}
174
175#[derive(Clone)]
176struct EmptyOrDb<Find> {
177 empty_files: bool,
178 db: Find,
179}
180
181impl<Find> gix::objs::Find for EmptyOrDb<Find>
182where
183 Find: gix::objs::Find,
184{
185 fn try_find<'a>(&self, id: &gix::oid, buf: &'a mut Vec<u8>) -> Result<Option<gix::objs::Data<'a>>, Error> {
186 if self.empty_files {
187 let Some(kind) = self.db.try_find(id, buf)?.map(|d| d.kind) else {
189 return Ok(None);
190 };
191 buf.clear();
192 Ok(Some(gix::objs::Data { kind, data: buf }))
194 } else {
195 self.db.try_find(id, buf)
196 }
197 }
198}
199
200#[derive(Clone)]
201struct Empty;
202
203impl gix::objs::Find for Empty {
204 fn try_find<'a>(&self, _id: &gix::oid, buffer: &'a mut Vec<u8>) -> Result<Option<gix::objs::Data<'a>>, Error> {
205 buffer.clear();
206 Ok(Some(gix::objs::Data {
207 kind: gix::object::Kind::Blob,
208 data: buffer,
209 }))
210 }
211}