Skip to content

Commit f23b8d2

Browse files
committed
basic version of index checkout via command-line (#301)
For now just with empty files, but that shows that with a single thread it's faster than git at 27k files per second created compared to 14.5k of a single thread of git. We do the least amount of work necessary even without an lstat cache, but that probably changes once we have to access the ODB to obtain actual data. Note that git might also do an additional exists check per file to see if it changed, something we don't do as we may assume exclusive access to the directory and just go with that for now. Long story short: it looks like there is a lot of potential for performance improvements and I think there is a lot of room for being faster especially in multi-threaded mode.
1 parent 039e822 commit f23b8d2

File tree

9 files changed

+97
-18
lines changed

9 files changed

+97
-18
lines changed

Diff for: Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: git-index/src/access.rs

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ impl State {
1010
pub fn entries(&self) -> &[Entry] {
1111
&self.entries
1212
}
13+
pub fn entries_mut(&mut self) -> &mut [Entry] {
14+
&mut self.entries
15+
}
1316
pub fn entries_mut_with_paths(&mut self) -> impl Iterator<Item = (&mut Entry, &BStr)> {
1417
let paths = &self.path_backing;
1518
self.entries.iter_mut().map(move |e| {

Diff for: git-repository/Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ max-performance = ["git-features/parallel", "git-features/zlib-ng-compat", "git-
4949
local-time-support = ["git-actor/local-time-support"]
5050
## Re-export stability tier 2 crates for convenience and make `Repository` struct fields with types from these crates publicly accessible.
5151
## Doing so is less stable than the stability tier 1 that `git-repository` is a member of.
52-
unstable = ["git-index"]
52+
unstable = ["git-index", "git-worktree"]
5353
## Print debugging information about usage of object database caches, useful for tuning cache sizes.
5454
cache-efficiency-debug = ["git-features/cache-efficiency-debug"]
5555

@@ -77,6 +77,7 @@ git-features = { version = "^0.19.1", path = "../git-features", features = ["pro
7777

7878
# unstable only
7979
git-index = { version ="^0.1.0", path = "../git-index", optional = true }
80+
git-worktree = { version ="^0.0.0", path = "../git-worktree", optional = true }
8081

8182
signal-hook = { version = "0.3.9", default-features = false }
8283
thiserror = "1.0.26"

Diff for: git-repository/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
//! * [`actor`]
8989
//! * [`bstr`][bstr]
9090
//! * [`index`]
91+
//! * [`worktree`]
9192
//! * [`objs`]
9293
//! * [`odb`]
9394
//! * [`pack`][odb::pack]
@@ -144,6 +145,8 @@ pub use git_url as url;
144145
#[doc(inline)]
145146
#[cfg(all(feature = "unstable", feature = "git-url"))]
146147
pub use git_url::Url;
148+
#[cfg(all(feature = "unstable", feature = "git-worktree"))]
149+
pub use git_worktree as worktree;
147150
pub use hash::{oid, ObjectId};
148151

149152
pub mod interrupt;

Diff for: git-worktree/src/index/mod.rs

-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use git_features::progress;
21
use git_features::progress::Progress;
32
use git_hash::oid;
43

@@ -27,9 +26,6 @@ where
2726
let mut buf = Vec::new();
2827
let mut collisions = Vec::new();
2928

30-
files.init(Some(index.entries().len()), progress::count("files"));
31-
bytes.init(Some(index.entries().len()), progress::bytes());
32-
3329
for (entry, entry_path) in index.entries_mut_with_paths() {
3430
// TODO: write test for that
3531
if entry.flags.contains(git_index::entry::Flags::SKIP_WORKTREE) {

Diff for: git-worktree/tests/index/mod.rs

+1-8
Original file line numberDiff line numberDiff line change
@@ -173,14 +173,7 @@ mod checkout {
173173
let odb = git_odb::at(git_dir.join("objects"))?;
174174
let destination = tempfile::tempdir()?;
175175

176-
let outcome = index::checkout(
177-
&mut index,
178-
&destination,
179-
move |oid, buf| odb.find_blob(oid, buf).ok(),
180-
&mut progress::Discard,
181-
&mut progress::Discard,
182-
opts,
183-
)?;
176+
let outcome = index::checkout(&mut index)?;
184177
Ok((source_tree, destination, index, outcome))
185178
}
186179

Diff for: gitoxide-core/src/index/mod.rs

+67
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
use anyhow::bail;
12
use std::path::Path;
23

34
use git_repository as git;
5+
use git_repository::Progress;
46

57
pub struct Options {
68
pub object_hash: git::hash::Kind,
@@ -98,3 +100,68 @@ fn parse_file(index_path: impl AsRef<Path>, object_hash: git::hash::Kind) -> any
98100
)
99101
.map_err(Into::into)
100102
}
103+
104+
pub fn checkout_exclusive(
105+
index_path: impl AsRef<Path>,
106+
dest_directory: impl AsRef<Path>,
107+
mut progress: impl Progress,
108+
Options { object_hash, .. }: Options,
109+
) -> anyhow::Result<()> {
110+
let dest_directory = dest_directory.as_ref();
111+
if dest_directory.exists() {
112+
bail!(
113+
"Refusing to checkout index into existing directory '{}' - remove it and try again",
114+
dest_directory.display()
115+
)
116+
}
117+
std::fs::create_dir_all(dest_directory)?;
118+
119+
let mut index = parse_file(index_path, object_hash)?;
120+
121+
let mut num_skipped = 0;
122+
for entry in index.entries_mut().iter_mut().filter(|e| {
123+
e.mode
124+
.contains(git::index::entry::Mode::DIR | git::index::entry::Mode::SYMLINK | git::index::entry::Mode::COMMIT)
125+
}) {
126+
entry.flags.insert(git::index::entry::Flags::SKIP_WORKTREE);
127+
num_skipped += 1;
128+
}
129+
if num_skipped > 0 {
130+
progress.info(format!("Skipping {} DIR/SYMLINK/COMMIT entries", num_skipped));
131+
}
132+
133+
let opts = git::worktree::index::checkout::Options {
134+
fs: git::worktree::fs::Capabilities::probe(dest_directory),
135+
136+
// TODO: turn the two following flags into an enum
137+
destination_is_initially_empty: true,
138+
overwrite_existing: false,
139+
..Default::default()
140+
};
141+
142+
let mut files = progress.add_child("checkout");
143+
let mut bytes = progress.add_child("writing");
144+
145+
let entries_for_checkout = index.entries().len() - num_skipped;
146+
files.init(Some(entries_for_checkout), git::progress::count("files"));
147+
bytes.init(Some(entries_for_checkout), git::progress::bytes());
148+
149+
let start = std::time::Instant::now();
150+
git::worktree::index::checkout(
151+
&mut index,
152+
dest_directory,
153+
|_, buf| {
154+
buf.clear();
155+
Some(git::objs::BlobRef { data: buf })
156+
},
157+
&mut files,
158+
&mut bytes,
159+
opts,
160+
)?;
161+
162+
files.show_throughput(start);
163+
bytes.show_throughput(start);
164+
165+
progress.done(format!("Created {} empty files", entries_for_checkout));
166+
Ok(())
167+
}

Diff for: src/plumbing/main.rs

+15
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,21 @@ pub fn main() -> Result<()> {
7878
index_path,
7979
cmd,
8080
}) => match cmd {
81+
index::Subcommands::CheckoutExclusive { directory } => prepare_and_run(
82+
"index-checkout",
83+
verbose,
84+
progress,
85+
progress_keep_open,
86+
None,
87+
move |progress, _out, _err| {
88+
core::index::checkout_exclusive(
89+
index_path,
90+
directory,
91+
progress,
92+
core::index::Options { object_hash, format },
93+
)
94+
},
95+
),
8196
index::Subcommands::Info { no_details } => prepare_and_run(
8297
"index-entries",
8398
verbose,

Diff for: src/plumbing/options.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -206,11 +206,9 @@ pub mod pack {
206206
sink_compress: bool,
207207

208208
/// The '.pack' or '.idx' file to explode into loose objects
209-
#[clap(parse(from_os_str))]
210209
pack_path: PathBuf,
211210

212211
/// The path into which all objects should be written. Commonly '.git/objects'
213-
#[clap(parse(from_os_str))]
214212
object_path: Option<PathBuf>,
215213
},
216214
/// Verify the integrity of a pack, index or multi-index file
@@ -219,7 +217,6 @@ pub mod pack {
219217
args: VerifyOptions,
220218

221219
/// The '.pack', '.idx' or 'multi-pack-index' file to validate.
222-
#[clap(parse(from_os_str))]
223220
path: PathBuf,
224221
},
225222
}
@@ -316,7 +313,6 @@ pub mod pack {
316313
/// The folder into which to place the pack and the generated index file
317314
///
318315
/// If unset, only informational output will be provided to standard output.
319-
#[clap(parse(from_os_str))]
320316
directory: Option<PathBuf>,
321317
},
322318
}
@@ -371,6 +367,11 @@ pub mod index {
371367
#[clap(long)]
372368
no_details: bool,
373369
},
370+
/// Checkout the index into a directory with exclusive write access, similar to what would happen during clone.
371+
CheckoutExclusive {
372+
/// The directory into which to write all index entries.
373+
directory: PathBuf,
374+
},
374375
}
375376
}
376377

@@ -383,7 +384,6 @@ pub mod commitgraph {
383384
/// Verify the integrity of a commit graph
384385
Verify {
385386
/// The path to '.git/objects/info/', '.git/objects/info/commit-graphs/', or '.git/objects/info/commit-graph' to validate.
386-
#[clap(parse(from_os_str))]
387387
path: PathBuf,
388388
/// output statistical information about the pack
389389
#[clap(long, short = 's')]

0 commit comments

Comments
 (0)