Krolyxon
Tag v0.2.0

ferreniumsrcgitarchive.rs

97 lines · 2.6 KB · Rust

1//! Snapshots of a commit's tree as `.tar.gz` or `.zip`.
2
3use std::io::{self, Seek, SeekFrom, Write};
4use std::sync::atomic::AtomicBool;
5
6use anyhow::Context;
7
8use super::ObjectId;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum Format {
12 TarGz,
13 Zip,
14}
15
16impl Format {
17 pub fn extension(self) -> &'static str {
18 match self {
19 Format::TarGz => "tar.gz",
20 Format::Zip => "zip",
21 }
22 }
23
24 pub fn content_type(self) -> &'static str {
25 match self {
26 Format::TarGz => "application/gzip",
27 Format::Zip => "application/zip",
28 }
29 }
30}
31
32/// Write the tree of `commit` to `out`, every path under `prefix` (which
33/// should end in `/`), with the commit's time as every file's mtime.
34pub fn write(
35 repo: &gix::Repository,
36 commit: ObjectId,
37 format: Format,
38 prefix: &str,
39 out: impl Write,
40) -> anyhow::Result<()> {
41 let commit = repo.find_commit(commit).context("commit not found")?;
42 let tree = commit.tree_id().context("commit has no tree")?.detach();
43 let mtime = commit.time().context("malformed commit")?.seconds;
44 let (stream, _index) = repo.worktree_stream(tree).context("cannot read the tree")?;
45 let options = gix::worktree::archive::Options {
46 format: match format {
47 Format::TarGz => gix::worktree::archive::Format::TarGz {
48 compression_level: Some(6),
49 },
50 Format::Zip => gix::worktree::archive::Format::Zip {
51 compression_level: Some(6),
52 },
53 },
54 tree_prefix: Some(prefix.into()),
55 modification_time: mtime,
56 };
57 repo.worktree_archive(
58 stream,
59 Positioned { inner: out, pos: 0 },
60 gix::progress::Discard,
61 &AtomicBool::new(false),
62 options,
63 )
64 .context("cannot write archive")?;
65 Ok(())
66}
67
68/// A writer that can report its position but not move. The zip writer
69/// only asks where it is, so archives stream without a temporary file.
70struct Positioned<W> {
71 inner: W,
72 pos: u64,
73}
74
75impl<W: Write> Write for Positioned<W> {
76 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
77 let n = self.inner.write(buf)?;
78 self.pos += n as u64;
79 Ok(n)
80 }
81
82 fn flush(&mut self) -> io::Result<()> {
83 self.inner.flush()
84 }
85}
86
87impl<W> Seek for Positioned<W> {
88 fn seek(&mut self, to: SeekFrom) -> io::Result<u64> {
89 match to {
90 SeekFrom::Current(0) => Ok(self.pos),
91 _ => Err(io::Error::new(
92 io::ErrorKind::Unsupported,
93 "archive output is a stream",
94 )),
95 }
96 }
97}