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 no_repo.then_some("empty").unwrap_or_default(),
119 should_interrupt
120 .load(Ordering::Relaxed)
121 .then(|| {
122 format!(
123 " of {}",
124 entries_for_checkout
125 .saturating_sub(errors.len() + collisions.len() + delayed_paths_unprocessed.len())
126 )
127 })
128 .unwrap_or_default(),
129 gix::progress::bytes()
130 .unwrap()
131 .display(bytes_written as usize, None, None)
132 ));
133
134 let mut messages = Vec::new();
135 if !errors.is_empty() {
136 messages.push(format!("kept going through {} errors(s)", errors.len()));
137 for record in errors {
138 writeln!(err, "{}: {}", record.path, record.error).ok();
139 }
140 }
141 if !collisions.is_empty() {
142 messages.push(format!("encountered {} collision(s)", collisions.len()));
143 for col in collisions {
144 writeln!(err, "{}: collision ({:?})", col.path, col.error_kind).ok();
145 }
146 }
147 if !delayed_paths_unknown.is_empty() {
148 messages.push(format!(
149 "A delayed process provided us with {} paths we never sent to it",
150 delayed_paths_unknown.len()
151 ));
152 for unknown in delayed_paths_unknown {
153 writeln!(err, "{unknown}: unknown").ok();
154 }
155 }
156 if !delayed_paths_unprocessed.is_empty() {
157 messages.push(format!(
158 "A delayed process forgot to process {} paths",
159 delayed_paths_unprocessed.len()
160 ));
161 for unprocessed in delayed_paths_unprocessed {
162 writeln!(err, "{unprocessed}: unprocessed and forgotten").ok();
163 }
164 }
165 if !messages.is_empty() {
166 bail!(
167 "One or more errors occurred - checkout is incomplete: {}",
168 messages.join(", ")
169 );
170 }
171 Ok(())
172}
173
174#[derive(Clone)]
175struct EmptyOrDb<Find> {
176 empty_files: bool,
177 db: Find,
178}
179
180impl<Find> gix::objs::Find for EmptyOrDb<Find>
181where
182 Find: gix::objs::Find,
183{
184 fn try_find<'a>(&self, id: &gix::oid, buf: &'a mut Vec<u8>) -> Result<Option<gix::objs::Data<'a>>, Error> {
185 if self.empty_files {
186 let Some(kind) = self.db.try_find(id, buf)?.map(|d| d.kind) else {
188 return Ok(None);
189 };
190 buf.clear();
191 Ok(Some(gix::objs::Data { kind, data: buf }))
193 } else {
194 self.db.try_find(id, buf)
195 }
196 }
197}
198
199#[derive(Clone)]
200struct Empty;
201
202impl gix::objs::Find for Empty {
203 fn try_find<'a>(&self, _id: &gix::oid, buffer: &'a mut Vec<u8>) -> Result<Option<gix::objs::Data<'a>>, Error> {
204 buffer.clear();
205 Ok(Some(gix::objs::Data {
206 kind: gix::object::Kind::Blob,
207 data: buffer,
208 }))
209 }
210}