History

feat: add simple CLI for `gix archive`

Sebastian Thiel committed 3 years ago

Commit 32bbb8b1 parent c4a1fb17 files · +152 −1Browse files

Unified · Split

7 files
  1. Cargo.lock +1 −0
  2. Cargo.toml +4 −1
  3. gitoxide-core/Cargo.toml +4 −0
  4. gitoxide-core/src/repository/archive.rs +80 −0
  5. gitoxide-core/src/repository/mod.rs +2 −0
  6. src/plumbing/main.rs +27 −0
  7. src/plumbing/options/mod.rs +34 −0
Cargo.lock+1−0
@@ -1282,6 +1282,7 @@ dependencies = [ · 1281 unmodified lines
12821282 "futures-io",
12831283 "futures-lite",
12841284 "gix",
1285+ "gix-archive",
12851286 "gix-pack",
12861287 "gix-transport",
12871288 "gix-url",
Cargo.toml+4−1
@@ -128,7 +128,7 @@ prodash-render-line = ["prodash/render-line", "prodash-render-line-crossterm", " · 127 unmodified lines
128128 cache-efficiency-debug = ["gix-features/cache-efficiency-debug"]
129129
130130 ## A way to enable most `gitoxide-core` tools found in `ein tools`, namely `organize` and `estimate hours`.
131−gitoxide-core-tools = ["gitoxide-core/organize", "gitoxide-core/estimate-hours"]
131+gitoxide-core-tools = ["gitoxide-core/organize", "gitoxide-core/estimate-hours", "gitoxide-core-tools-archive"]
132132
133133 ## A program to perform analytics on a `git` repository, using an auto-maintained sqlite database
134134 gitoxide-core-tools-query = ["gitoxide-core/query"]
@@ -136,6 +136,9 @@ gitoxide-core-tools-query = ["gitoxide-core/query"] · 1 unmodified line
136136 ## A program to run algorithms on a corpus of repositories, recording each run for later comparison.
137137 gitoxide-core-tools-corpus = ["gitoxide-core/corpus"]
138138
139+## A sub-command to generate archive from virtual worktree checkouts.
140+gitoxide-core-tools-archive = ["gitoxide-core/archive"]
141+
139142 #! ### Building Blocks for mutually exclusive networking
140143 #! Blocking and async features are mutually exclusive and cause a compile-time error. This also means that `cargo … --all-features` will fail.
141144 #! Within each section, features can be combined.
gitoxide-core/Cargo.toml+4−0
@@ -25,6 +25,9 @@ query = ["dep:rusqlite"] · 24 unmodified lines
2525 ## *Note that* `organize` we need for finding git repositories fast.
2626 corpus = [ "dep:rusqlite", "dep:sysinfo", "organize", "dep:crossbeam-channel", "dep:serde_json", "dep:tracing-forest", "dep:tracing-subscriber", "dep:tracing", "dep:parking_lot" ]
2727
28+## The ability to create archives from virtual worktrees, similar to `git archive`.
29+archive = ["dep:gix-archive-for-configuration-only", "gix/worktree-archive"]
30+
2831 #! ### Mutually Exclusive Networking
2932 #! If both are set, _blocking-client_ will take precedence, allowing `--all-features` to be used.
3033
@@ -44,6 +47,7 @@ serde = ["gix/serde", "dep:serde_json", "dep:serde", "bytesize/serde"] · 13 unmodified lines
4447 gix = { version = "^0.49.1", path = "../gix", default-features = false }
4548 gix-pack-for-configuration-only = { package = "gix-pack", version = "^0.40.0", path = "../gix-pack", default-features = false, features = ["pack-cache-lru-dynamic", "pack-cache-lru-static"] }
4649 gix-transport-configuration-only = { package = "gix-transport", version = "^0.34.0", path = "../gix-transport", default-features = false }
50+gix-archive-for-configuration-only = { package = "gix-archive", version = "^0.2.0", path = "../gix-archive", optional = true, features = ["tar"] }
4751 serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] }
4852 anyhow = "1.0.42"
4953 thiserror = "1.0.34"
gitoxide-core/src/repository/archive.rs+80−0
@@ -0,0 +1,80 @@
1+use anyhow::bail;
2+use gix::worktree::archive;
3+use gix::Progress;
4+use std::ops::Add;
5+use std::path::Path;
6+
7+pub fn stream(
8+ repo: gix::Repository,
9+ destination_path: Option<&Path>,
10+ rev_spec: Option<&str>,
11+ mut progress: impl Progress,
12+ format: Option<archive::Format>,
13+) -> anyhow::Result<()> {
14+ let format = format.map_or_else(|| format_from_ext(destination_path), Ok)?;
15+ let object = repo.rev_parse_single(rev_spec.unwrap_or("HEAD"))?.object()?;
16+ let (modification_date, tree) = fetch_rev_info(object)?;
17+
18+ let start = std::time::Instant::now();
19+ let (stream, index) = repo.worktree_stream(tree)?;
20+
21+ let mut entries = progress.add_child("entries");
22+ entries.init(Some(index.entries().len()), gix::progress::count("entries"));
23+ let mut bytes = progress.add_child("written");
24+ bytes.init(None, gix::progress::bytes());
25+
26+ let mut file = gix::progress::Write {
27+ inner: match destination_path {
28+ Some(path) => Box::new(std::io::BufWriter::with_capacity(
29+ 128 * 1024,
30+ std::fs::File::create(path)?,
31+ )) as Box<dyn std::io::Write>,
32+ None => Box::new(std::io::sink()),
33+ },
34+ progress: &mut bytes,
35+ };
36+ repo.worktree_archive(
37+ stream,
38+ &mut file,
39+ &mut entries,
40+ &gix::interrupt::IS_INTERRUPTED,
41+ gix::worktree::archive::Options {
42+ format,
43+ tree_prefix: None,
44+ modification_time: modification_date
45+ .map(|t| std::time::UNIX_EPOCH.add(std::time::Duration::from_secs(t as u64)))
46+ .unwrap_or_else(std::time::SystemTime::now),
47+ },
48+ )?;
49+
50+ entries.show_throughput(start);
51+ bytes.show_throughput(start);
52+
53+ Ok(())
54+}
55+
56+fn fetch_rev_info(
57+ object: gix::Object<'_>,
58+) -> anyhow::Result<(Option<gix::date::SecondsSinceUnixEpoch>, gix::ObjectId)> {
59+ Ok(match object.kind {
60+ gix::object::Kind::Commit => {
61+ let commit = object.into_commit();
62+ (Some(commit.committer()?.time.seconds), commit.tree_id()?.detach())
63+ }
64+ gix::object::Kind::Tree => (None, object.id),
65+ gix::object::Kind::Tag => fetch_rev_info(object.peel_to_kind(gix::object::Kind::Commit)?)?,
66+ gix::object::Kind::Blob => bail!("Cannot derive commit or tree from blob at {}", object.id),
67+ })
68+}
69+
70+fn format_from_ext(path: Option<&Path>) -> anyhow::Result<archive::Format> {
71+ Ok(match path {
72+ Some(path) => match path.extension().and_then(|ext| ext.to_str()) {
73+ None => bail!("Cannot derive archive format from a file without extension"),
74+ Some("tar") => archive::Format::Tar,
75+ Some("stream") => archive::Format::InternalTransientNonPersistable,
76+ Some(ext) => bail!("Format for extendion '{ext}' is unsupported"),
77+ },
78+ None => archive::Format::InternalTransientNonPersistable,
79+ })
80+}
gitoxide-core/src/repository/mod.rs+2−0
@@ -11,6 +11,8 @@ pub fn init(directory: Option<PathBuf>) -> Result<gix::discover::repository::Pat · 10 unmodified lines
1111 .with_context(|| "Repository initialization failed")
1212 }
1313
14+#[cfg(feature = "archive")]
15+pub mod archive;
1416 pub mod commit;
1517 pub mod config;
1618 mod credential;
src/plumbing/main.rs+27−0
@@ -135,6 +135,33 @@ pub fn main() -> Result<()> { · 134 unmodified lines
135135 })?;
136136
137137 match cmd {
138+ #[cfg(feature = "gitoxide-core-tools-archive")]
139+ Subcommands::Archive(crate::plumbing::options::archive::Platform {
140+ format,
141+ output_file,
142+ treeish,
143+ }) => prepare_and_run(
144+ "archive",
145+ trace,
146+ auto_verbose,
147+ progress,
148+ progress_keep_open,
149+ None,
150+ move |progress, _out, _err| {
151+ core::repository::archive::stream(
152+ repository(Mode::Lenient)?,
153+ output_file.as_deref(),
154+ treeish.as_deref(),
155+ progress,
156+ format.map(|f| match f {
157+ crate::plumbing::options::archive::Format::Internal => {
158+ gix::worktree::archive::Format::InternalTransientNonPersistable
159+ }
160+ crate::plumbing::options::archive::Format::Tar => gix::worktree::archive::Format::Tar,
161+ }),
162+ )
163+ },
164+ ),
138165 #[cfg(feature = "gitoxide-core-tools-corpus")]
139166 Subcommands::Corpus(crate::plumbing::options::corpus::Platform { db, path, cmd }) => {
140167 let reverse_trace_lines = progress;
src/plumbing/options/mod.rs+34−0
@@ -77,6 +77,9 @@ pub struct Args { · 76 unmodified lines
7777
7878 #[derive(Debug, clap::Subcommand)]
7979 pub enum Subcommands {
80+ /// Subcommands for creating worktree archivs
81+ #[cfg(feature = "gitoxide-core-tools-archive")]
82+ Archive(archive::Platform),
8083 /// Subcommands for interacting with commit-graphs
8184 #[clap(subcommand)]
8285 CommitGraph(commitgraph::Subcommands),
@@ -129,6 +132,37 @@ pub enum Subcommands { · 46 unmodified lines
129132 Free(free::Subcommands),
130133 }
131134
135+#[cfg(feature = "gitoxide-core-tools-archive")]
136+pub mod archive {
137+ use std::path::PathBuf;
138+
139+ #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)]
140+ pub enum Format {
141+ /// An internal format that is for debugging, it should not be persisted and cannot be read back.
142+ ///
143+ /// However, it represents that bare data stream without with minimal overhead, and is a good
144+ /// metric for throughput.
145+ Internal,
146+ /// Use the `.tar` file format, uncompressed.
147+ Tar,
148+ }
149+
150+ #[derive(Debug, clap::Parser)]
151+ pub struct Platform {
152+ #[clap(long, short = 'f', value_enum)]
153+ pub format: Option<Format>,
154+ /// The file to write the archive to, or discard the output immediately.
155+ ///
156+ /// It's extension determines the archive format, unless `--format` is set.
157+ pub output_file: Option<PathBuf>,
158+
159+ /// The revspec of the commit or tree to traverse, or the tree at `HEAD` if unspecified.
160+ ///
161+ /// If commit, the commit timestamp will be used as timestamp for each file in the archive.
162+ pub treeish: Option<String>,
163+ }
164+}
165+
132166 #[cfg(feature = "gitoxide-core-tools-corpus")]
133167 pub mod corpus {
134168 use std::path::PathBuf;