Snapshot only committed extensions and publish the last-good marker atomically

This commit is contained in:
2026-11-08 07:00:39 +00:00
parent fcfaff6018
commit 8afab591fe
38 changed files with 535 additions and 117 deletions
+1
View File
@@ -16,3 +16,4 @@ gix-zlib = "0.4"
url = "2"
wasi = "0.14"
sausage-ssh = { path = "../ssh-client" }
tar = { version = "0.4", default-features = false }
+3
View File
@@ -78,6 +78,9 @@ It also checks these forms:
- `show --stat`, `--oneline`, `--name-only`, `show HEAD:path`, and merge commits without a combined diff.
- `cat-file -t`, `-p`, `-s`, and `-e` for object inspection.
- `ls-tree [-r] [--name-only] <rev> [path]` for literal paths.
- `archive [--format=tar] [-o <file>] <rev> [path]` for a tree or literal subtree.
The archive preserves file contents, executable modes, and symbolic links.
`archive-test.nu` compares extracted output with native Git.
- `diff HEAD~1`, `diff A..B`, `--name-only`, `--name-status`, and one literal path after `--`.
- `diff --check` and `diff --cached --check` for added whitespace errors and conflict markers.
- `branch -a`, `-d`, `-D`, `-f`, `-m`, and `--show-current`.
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env nu
# Compare the literal subtree archive with native Git, including binary files and links.
def main [component: path] {
let component = ($component | path expand)
let root = (mktemp -d -p /tmp git-archive.XXXXXX)
let repo = ($root | path join repo)
mkdir ($repo | path join extensions/demo) ($root | path join native) ($root | path join wasi)
^git init -q -b main $repo
'outside' | save ($repo | path join outside.txt)
0x[00 ff 7f 80] | save ($repo | path join 'extensions/demo/payload space.bin')
'executable' | save ($repo | path join extensions/demo/run)
^chmod +x ($repo | path join extensions/demo/run)
^ln -s 'payload space.bin' ($repo | path join extensions/demo/link)
^git -C $repo add .
^git -C $repo -c user.name=Test -c user.email=test@example.com commit --no-gpg-sign -q -m fixture
^git -C $repo archive --format=tar $'--output=($root)/native.tar' HEAD extensions
^wasmtime run -S http -S cli-exit-with-code=y --dir $root $component -C $repo archive --format=tar $'--output=($root)/wasi.tar' HEAD extensions
if $env.LAST_EXIT_CODE != 0 { error make {msg: 'WASI archive failed'} }
for side in [native wasi] { ^tar -xf $'($root)/($side).tar' -C $'($root)/($side)' }
^diff -r $'($root)/native' $'($root)/wasi'
if $env.LAST_EXIT_CODE != 0 { error make {msg: 'Extracted contents differ'} }
if ($'($root)/wasi/outside.txt' | path exists) { error make {msg: 'Archive includes unrelated files'} }
let native_mode = (^stat -c '%a' $'($root)/native/extensions/demo/run')
let wasi_mode = (^stat -c '%a' $'($root)/wasi/extensions/demo/run')
if $native_mode != $wasi_mode { error make {msg: 'Executable mode differs'} }
if (^readlink $'($root)/wasi/extensions/demo/link') != 'payload space.bin' { error make {msg: 'Symlink differs'} }
for args in [[archive bad-revision extensions] [archive HEAD missing] [archive --format=zip HEAD]] {
let result = (^wasmtime run -S http -S cli-exit-with-code=y --dir $root $component -C $repo ...$args | complete)
if $result.exit_code == 0 { error make {msg: $'Invalid archive succeeded: ($args)'} }
}
print $'Archive comparison passed: ($root)'
}
+86
View File
@@ -0,0 +1,86 @@
//! Export a committed tree or literal subtree as tar, without an index or worktree.
use crate::{Result, repo::error};
use std::{io::Write, path::Path};
pub(crate) fn archive(
repo: &gix::Repository,
revision: &str,
path: Option<&str>,
output: Option<&Path>,
) -> Result<()> {
if let Some(path) = path {
crate::repo::validate_path(path)?;
}
let root = crate::local::tree(repo, revision)?;
let writer: Box<dyn Write> = match output {
Some(path) => {
Box::new(std::fs::File::create(path).map_err(|e| error("archive output", e))?)
}
None => Box::new(std::io::stdout().lock()),
};
let mut tar = tar::Builder::new(writer);
fn walk(
repo: &gix::Repository,
id: gix::ObjectId,
prefix: &str,
filter: Option<&str>,
tar: &mut tar::Builder<Box<dyn Write>>,
) -> Result<bool> {
let tree = repo.find_tree(id).map_err(|e| error("archive tree", e))?;
let mut found = filter.is_none();
for entry in tree.iter() {
let entry = entry.map_err(|e| error("archive entry", e))?;
let path = format!("{prefix}{}", entry.filename());
let selected = filter.is_none_or(|f| path == f || path.starts_with(&format!("{f}/")));
let ancestor = filter.is_some_and(|f| f.starts_with(&format!("{path}/")));
if !selected && !ancestor {
continue;
}
found |= selected;
if entry.mode().is_tree() {
if selected {
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Directory);
header.set_mode(0o755);
header.set_size(0);
tar.append_data(&mut header, &path, std::io::empty())
.map_err(|e| error("archive directory", e))?;
}
found |= walk(
repo,
entry.oid().to_owned(),
&format!("{path}/"),
filter,
tar,
)?;
} else if selected && !entry.mode().is_commit() {
let object = repo
.find_blob(entry.oid())
.map_err(|e| error("archive blob", e))?;
let mut header = tar::Header::new_gnu();
header.set_mode(entry.mode().kind() as u32 & 0o777);
if entry.mode().is_link() {
header.set_entry_type(tar::EntryType::Symlink);
header.set_size(0);
header
.set_link_name(
std::str::from_utf8(&object.data)
.map_err(|e| error("archive link", e))?,
)
.map_err(|e| error("archive link", e))?;
tar.append_data(&mut header, &path, std::io::empty())
.map_err(|e| error("archive link", e))?;
} else {
header.set_size(object.data.len() as u64);
tar.append_data(&mut header, &path, object.data.as_slice())
.map_err(|e| error("archive file", e))?;
}
}
}
Ok(found)
}
if !walk(repo, root, "", path, &mut tar)? {
return Err(error("archive", "path did not match any files"));
}
tar.finish().map_err(|e| error("archive finish", e))
}
+17
View File
@@ -1,5 +1,6 @@
//! Local Git operations adapted from the v2 gix plugin, without RPC or host imports.
#![allow(dead_code)]
mod archive;
mod branches;
mod changes;
mod checkout;
@@ -78,6 +79,14 @@ pub struct Cli {
}
#[derive(Subcommand)]
enum Command {
Archive {
#[arg(long, default_value = "tar", value_parser = ["tar"])]
format: String,
#[arg(short = 'o', long)]
output: Option<std::path::PathBuf>,
revision: String,
path: Option<String>,
},
Show {
#[arg(short = 's', long = "no-patch")]
no_patch: bool,
@@ -1501,6 +1510,14 @@ pub fn run(cli: Cli) -> Result<i32> {
)?;
}
},
Command::Archive {
format: _,
output,
revision,
path,
} => {
archive::archive(&repository, &revision, path.as_deref(), output.as_deref())?;
}
Command::CatFile {
kind,
pretty: _,