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
1282 "futures-io",1282 "futures-io",
1283 "futures-lite",1283 "futures-lite",
1284 "gix",1284 "gix",
1285 "gix-archive",
1285 "gix-pack",1286 "gix-pack",
1286 "gix-transport",1287 "gix-transport",
1287 "gix-url",1288 "gix-url",
Cargo.toml+4−1
@@ -128,7 +128,7 @@ prodash-render-line = ["prodash/render-line", "prodash-render-line-crossterm", " · 127 unmodified lines
128cache-efficiency-debug = ["gix-features/cache-efficiency-debug"]128cache-efficiency-debug = ["gix-features/cache-efficiency-debug"]
129 129
130## A way to enable most `gitoxide-core` tools found in `ein tools`, namely `organize` and `estimate hours`.130## A way to enable most `gitoxide-core` tools found in `ein tools`, namely `organize` and `estimate hours`.
131gitoxide-core-tools = ["gitoxide-core/organize", "gitoxide-core/estimate-hours"]131gitoxide-core-tools = ["gitoxide-core/organize", "gitoxide-core/estimate-hours", "gitoxide-core-tools-archive"]
132 132
133## A program to perform analytics on a `git` repository, using an auto-maintained sqlite database133## A program to perform analytics on a `git` repository, using an auto-maintained sqlite database
134gitoxide-core-tools-query = ["gitoxide-core/query"]134gitoxide-core-tools-query = ["gitoxide-core/query"]
@@ -136,6 +136,9 @@ gitoxide-core-tools-query = ["gitoxide-core/query"] · 1 unmodified line
136## A program to run algorithms on a corpus of repositories, recording each run for later comparison.136## A program to run algorithms on a corpus of repositories, recording each run for later comparison.
137gitoxide-core-tools-corpus = ["gitoxide-core/corpus"]137gitoxide-core-tools-corpus = ["gitoxide-core/corpus"]
138 138
139## A sub-command to generate archive from virtual worktree checkouts.
140gitoxide-core-tools-archive = ["gitoxide-core/archive"]
141
139#! ### Building Blocks for mutually exclusive networking142#! ### Building Blocks for mutually exclusive networking
140#! Blocking and async features are mutually exclusive and cause a compile-time error. This also means that `cargo … --all-features` will fail.143#! Blocking and async features are mutually exclusive and cause a compile-time error. This also means that `cargo … --all-features` will fail.
141#! Within each section, features can be combined.144#! Within each section, features can be combined.
gitoxide-core/Cargo.toml+4−0
@@ -25,6 +25,9 @@ query = ["dep:rusqlite"] · 24 unmodified lines
25## *Note that* `organize` we need for finding git repositories fast.25## *Note that* `organize` we need for finding git repositories fast.
26corpus = [ "dep:rusqlite", "dep:sysinfo", "organize", "dep:crossbeam-channel", "dep:serde_json", "dep:tracing-forest", "dep:tracing-subscriber", "dep:tracing", "dep:parking_lot" ]26corpus = [ "dep:rusqlite", "dep:sysinfo", "organize", "dep:crossbeam-channel", "dep:serde_json", "dep:tracing-forest", "dep:tracing-subscriber", "dep:tracing", "dep:parking_lot" ]
27 27
28## The ability to create archives from virtual worktrees, similar to `git archive`.
29archive = ["dep:gix-archive-for-configuration-only", "gix/worktree-archive"]
30
28#! ### Mutually Exclusive Networking31#! ### Mutually Exclusive Networking
29#! If both are set, _blocking-client_ will take precedence, allowing `--all-features` to be used.32#! If both are set, _blocking-client_ will take precedence, allowing `--all-features` to be used.
30 33
@@ -44,6 +47,7 @@ serde = ["gix/serde", "dep:serde_json", "dep:serde", "bytesize/serde"] · 13 unmodified lines
44gix = { version = "^0.49.1", path = "../gix", default-features = false }47gix = { version = "^0.49.1", path = "../gix", default-features = false }
45gix-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"] }48gix-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"] }
46gix-transport-configuration-only = { package = "gix-transport", version = "^0.34.0", path = "../gix-transport", default-features = false }49gix-transport-configuration-only = { package = "gix-transport", version = "^0.34.0", path = "../gix-transport", default-features = false }
50gix-archive-for-configuration-only = { package = "gix-archive", version = "^0.2.0", path = "../gix-archive", optional = true, features = ["tar"] }
47serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] }51serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] }
48anyhow = "1.0.42"52anyhow = "1.0.42"
49thiserror = "1.0.34"53thiserror = "1.0.34"
gitoxide-core/src/repository/archive.rs+80−0
@@ -0,0 +1,80 @@
1use anyhow::bail;
2use gix::worktree::archive;
3use gix::Progress;
4use std::ops::Add;
5use std::path::Path;
6
7pub 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
56fn 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
70fn 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
11 .with_context(|| "Repository initialization failed")11 .with_context(|| "Repository initialization failed")
12}12}
13 13
14#[cfg(feature = "archive")]
15pub mod archive;
14pub mod commit;16pub mod commit;
15pub mod config;17pub mod config;
16mod credential;18mod credential;
src/plumbing/main.rs+27−0
@@ -135,6 +135,33 @@ pub fn main() -> Result<()> { · 134 unmodified lines
135 })?;135 })?;
136 136
137 match cmd {137 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 ),
138 #[cfg(feature = "gitoxide-core-tools-corpus")]165 #[cfg(feature = "gitoxide-core-tools-corpus")]
139 Subcommands::Corpus(crate::plumbing::options::corpus::Platform { db, path, cmd }) => {166 Subcommands::Corpus(crate::plumbing::options::corpus::Platform { db, path, cmd }) => {
140 let reverse_trace_lines = progress;167 let reverse_trace_lines = progress;
src/plumbing/options/mod.rs+34−0
@@ -77,6 +77,9 @@ pub struct Args { · 76 unmodified lines
77 77
78#[derive(Debug, clap::Subcommand)]78#[derive(Debug, clap::Subcommand)]
79pub enum Subcommands {79pub enum Subcommands {
80 /// Subcommands for creating worktree archivs
81 #[cfg(feature = "gitoxide-core-tools-archive")]
82 Archive(archive::Platform),
80 /// Subcommands for interacting with commit-graphs83 /// Subcommands for interacting with commit-graphs
81 #[clap(subcommand)]84 #[clap(subcommand)]
82 CommitGraph(commitgraph::Subcommands),85 CommitGraph(commitgraph::Subcommands),
@@ -129,6 +132,37 @@ pub enum Subcommands { · 46 unmodified lines
129 Free(free::Subcommands),132 Free(free::Subcommands),
130}133}
131 134
135#[cfg(feature = "gitoxide-core-tools-archive")]
136pub 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
132#[cfg(feature = "gitoxide-core-tools-corpus")]166#[cfg(feature = "gitoxide-core-tools-corpus")]
133pub mod corpus {167pub mod corpus {
134 use std::path::PathBuf;168 use std::path::PathBuf;