| 1 | //! Snapshots of a commit's tree as `.tar.gz` or `.zip`. |
| 2 | |
| 3 | use std::io::{self, Seek, SeekFrom, Write}; |
| 4 | use std::sync::atomic::AtomicBool; |
| 5 | |
| 6 | use anyhow::Context; |
| 7 | |
| 8 | use super::ObjectId; |
| 9 | |
| 10 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] |
| 11 | pub enum Format { |
| 12 | TarGz, |
| 13 | Zip, |
| 14 | } |
| 15 | |
| 16 | impl 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. |
| 34 | pub 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. |
| 70 | struct Positioned<W> { |
| 71 | inner: W, |
| 72 | pos: u64, |
| 73 | } |
| 74 | |
| 75 | impl<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 | |
| 87 | impl<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 | } |