gitoxide_core/index/
checkout.rs

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            // We always want to query the ODB here…
187            let Some(kind) = self.db.try_find(id, buf)?.map(|d| d.kind) else {
188                return Ok(None);
189            };
190            buf.clear();
191            // …but write nothing
192            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}