Unified · Split
| @@ -1282,6 +1282,7 @@ dependencies = [ · 1281 unmodified lines | ||
| 1282 | 1282 | "futures-io", |
| 1283 | 1283 | "futures-lite", |
| 1284 | 1284 | "gix", |
| 1285 | + "gix-archive", | |
| 1285 | 1286 | "gix-pack", |
| 1286 | 1287 | "gix-transport", |
| 1287 | 1288 | "gix-url", |
| @@ -128,7 +128,7 @@ prodash-render-line = ["prodash/render-line", "prodash-render-line-crossterm", " · 127 unmodified lines | ||
| 128 | 128 | cache-efficiency-debug = ["gix-features/cache-efficiency-debug"] |
| 129 | 129 | |
| 130 | 130 | ## 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"] | |
| 132 | 132 | |
| 133 | 133 | ## A program to perform analytics on a `git` repository, using an auto-maintained sqlite database |
| 134 | 134 | gitoxide-core-tools-query = ["gitoxide-core/query"] |
| @@ -136,6 +136,9 @@ gitoxide-core-tools-query = ["gitoxide-core/query"] · 1 unmodified line | ||
| 136 | 136 | ## A program to run algorithms on a corpus of repositories, recording each run for later comparison. |
| 137 | 137 | gitoxide-core-tools-corpus = ["gitoxide-core/corpus"] |
| 138 | 138 | |
| 139 | +## A sub-command to generate archive from virtual worktree checkouts. | |
| 140 | +gitoxide-core-tools-archive = ["gitoxide-core/archive"] | |
| 141 | + | |
| 139 | 142 | #! ### Building Blocks for mutually exclusive networking |
| 140 | 143 | #! Blocking and async features are mutually exclusive and cause a compile-time error. This also means that `cargo … --all-features` will fail. |
| 141 | 144 | #! Within each section, features can be combined. |
| @@ -25,6 +25,9 @@ query = ["dep:rusqlite"] · 24 unmodified lines | ||
| 25 | 25 | ## *Note that* `organize` we need for finding git repositories fast. |
| 26 | 26 | corpus = [ "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`. | |
| 29 | +archive = ["dep:gix-archive-for-configuration-only", "gix/worktree-archive"] | |
| 30 | + | |
| 28 | 31 | #! ### Mutually Exclusive Networking |
| 29 | 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 | ||
| 44 | 47 | gix = { version = "^0.49.1", path = "../gix", default-features = false } |
| 45 | 48 | 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"] } |
| 46 | 49 | 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"] } | |
| 47 | 51 | serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } |
| 48 | 52 | anyhow = "1.0.42" |
| 49 | 53 | thiserror = "1.0.34" |
| @@ -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 | +} | |
| @@ -11,6 +11,8 @@ pub fn init(directory: Option<PathBuf>) -> Result<gix::discover::repository::Pat · 10 unmodified lines | ||
| 11 | 11 | .with_context(|| "Repository initialization failed") |
| 12 | 12 | } |
| 13 | 13 | |
| 14 | +#[cfg(feature = "archive")] | |
| 15 | +pub mod archive; | |
| 14 | 16 | pub mod commit; |
| 15 | 17 | pub mod config; |
| 16 | 18 | mod credential; |
| @@ -135,6 +135,33 @@ pub fn main() -> Result<()> { · 134 unmodified lines | ||
| 135 | 135 | })?; |
| 136 | 136 | |
| 137 | 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 | 165 | #[cfg(feature = "gitoxide-core-tools-corpus")] |
| 139 | 166 | Subcommands::Corpus(crate::plumbing::options::corpus::Platform { db, path, cmd }) => { |
| 140 | 167 | let reverse_trace_lines = progress; |
| @@ -77,6 +77,9 @@ pub struct Args { · 76 unmodified lines | ||
| 77 | 77 | |
| 78 | 78 | #[derive(Debug, clap::Subcommand)] |
| 79 | 79 | pub enum Subcommands { |
| 80 | + /// Subcommands for creating worktree archivs | |
| 81 | + #[cfg(feature = "gitoxide-core-tools-archive")] | |
| 82 | + Archive(archive::Platform), | |
| 80 | 83 | /// Subcommands for interacting with commit-graphs |
| 81 | 84 | #[clap(subcommand)] |
| 82 | 85 | CommitGraph(commitgraph::Subcommands), |
| @@ -129,6 +132,37 @@ pub enum Subcommands { · 46 unmodified lines | ||
| 129 | 132 | Free(free::Subcommands), |
| 130 | 133 | } |
| 131 | 134 | |
| 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 | + | |
| 132 | 166 | #[cfg(feature = "gitoxide-core-tools-corpus")] |
| 133 | 167 | pub mod corpus { |
| 134 | 168 | use std::path::PathBuf; |