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