Merge v3-t73: reflog writes without identity, world default identity, show/cat-file/ls-tree, which (T73)
This commit is contained in:
@@ -44,6 +44,7 @@ impl World {
|
||||
/// Copy an optional bundle into a new world. Existing worlds retain their programs.
|
||||
/// Each `.wasm` tool keeps its independent file and gains a relative `/bin/<name>` symlink.
|
||||
/// `/dev` exposes generated null, zero, and random byte devices in every world.
|
||||
/// Missing `/etc/gitconfig` defaults to Sausage <sausage@localhost>. Local repository config overrides it.
|
||||
pub fn open_with_userland(dir: &Path, userland: Option<&Path>) -> anyhow::Result<Self> {
|
||||
let root_dir = dir.join("world");
|
||||
let host_dir = dir.join("host");
|
||||
@@ -64,6 +65,14 @@ impl World {
|
||||
}
|
||||
}
|
||||
}
|
||||
let gitconfig = root_dir.join("etc/gitconfig");
|
||||
if !gitconfig.try_exists()? {
|
||||
std::fs::create_dir_all(root_dir.join("etc"))?;
|
||||
std::fs::write(
|
||||
gitconfig,
|
||||
"[user]\n\tname = Sausage\n\temail = sausage@localhost\n",
|
||||
)?;
|
||||
}
|
||||
std::fs::create_dir_all(&host_dir)?;
|
||||
let mut mounts = Mounts::default();
|
||||
mounts.mount(
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
//! Fresh worlds provide a Git identity and resolve conventional tool names through which.
|
||||
use sausage::{
|
||||
exec::{Command, Options, Stream},
|
||||
world::World,
|
||||
};
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn default_identity_and_which() -> anyhow::Result<()> {
|
||||
let repo = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
|
||||
let userland = repo.join("userland");
|
||||
assert!(
|
||||
std::process::Command::new("nu")
|
||||
.args(["--no-config-file", "cwd-init/build.nu"])
|
||||
.current_dir(&userland)
|
||||
.status()?
|
||||
.success()
|
||||
);
|
||||
assert!(
|
||||
std::process::Command::new("cargo")
|
||||
.args([
|
||||
"build",
|
||||
"--release",
|
||||
"-p",
|
||||
"git",
|
||||
"-p",
|
||||
"sausage-exec-tools",
|
||||
"--locked"
|
||||
])
|
||||
.current_dir(&userland)
|
||||
.status()?
|
||||
.success()
|
||||
);
|
||||
let target = std::path::PathBuf::from(std::env::var("CARGO_TARGET_DIR")?);
|
||||
let bundle = tempfile::tempdir()?;
|
||||
std::fs::create_dir_all(bundle.path().join("bin"))?;
|
||||
std::fs::create_dir_all(bundle.path().join("src"))?;
|
||||
for name in ["git", "which"] {
|
||||
std::fs::copy(
|
||||
target.join(format!("wasm32-wasip2/release/{name}.wasm")),
|
||||
bundle.path().join(format!("bin/{name}.wasm")),
|
||||
)?;
|
||||
}
|
||||
let dir = tempfile::tempdir()?;
|
||||
let world = World::open_with_userland(dir.path(), Some(bundle.path()))?;
|
||||
std::fs::create_dir_all(dir.path().join("world/repo"))?;
|
||||
std::fs::write(dir.path().join("world/repo/file.txt"), "base\n")?;
|
||||
let exec = sausage::process::exec(World {
|
||||
root: world.root.clone(),
|
||||
host_dir: world.host_dir.clone(),
|
||||
processes: world.processes.clone(),
|
||||
})?;
|
||||
for (argv, expected) in [
|
||||
(vec!["git", "-C", "/repo", "init", "-q", "-b", "main"], ""),
|
||||
(
|
||||
vec!["git", "-C", "/repo", "config", "--get", "user.name"],
|
||||
"Sausage\n",
|
||||
),
|
||||
(
|
||||
vec!["git", "-C", "/repo", "config", "--get", "user.email"],
|
||||
"sausage@localhost\n",
|
||||
),
|
||||
(vec!["git", "-C", "/repo", "add", "."], ""),
|
||||
(vec!["git", "-C", "/repo", "commit", "-q", "-m", "base"], ""),
|
||||
(vec!["which", "git"], "/bin/git\n"),
|
||||
(vec!["which", "-a", "git"], "/bin/git\n/bin/git.wasm\n"),
|
||||
] {
|
||||
let mut stdout = Vec::new();
|
||||
let mut stderr = Vec::new();
|
||||
let code = exec
|
||||
.exec(
|
||||
Command::Argv(argv.iter().map(|s| (*s).into()).collect()),
|
||||
Options::default(),
|
||||
&mut |stream, bytes| match stream {
|
||||
Stream::Stdout => stdout.extend_from_slice(bytes),
|
||||
Stream::Stderr => stderr.extend_from_slice(bytes),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(code, 0, "{argv:?}: {}", String::from_utf8_lossy(&stderr));
|
||||
assert_eq!(stdout, expected.as_bytes(), "{argv:?}");
|
||||
}
|
||||
std::fs::copy(
|
||||
dir.path().join("world/bin/git.wasm"),
|
||||
dir.path().join("world/usr/local/bin/git.wasm"),
|
||||
)?;
|
||||
let mut stdout = Vec::new();
|
||||
assert_eq!(
|
||||
exec.exec(
|
||||
Command::Argv(vec!["which".into(), "git".into()]),
|
||||
Options::default(),
|
||||
&mut |stream, bytes| if matches!(stream, Stream::Stdout) {
|
||||
stdout.extend_from_slice(bytes);
|
||||
}
|
||||
)
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
assert_eq!(stdout, b"/usr/local/bin/git.wasm\n");
|
||||
let config = dir.path().join("world/etc/gitconfig");
|
||||
std::fs::write(
|
||||
&config,
|
||||
"[user]\nname = Custom\nemail = custom@example.test\n",
|
||||
)?;
|
||||
let _ = World::open(dir.path())?;
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(config)?,
|
||||
"[user]\nname = Custom\nemail = custom@example.test\n"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -116,8 +116,6 @@ fn prepare(dir: &Path) -> anyhow::Result<()> {
|
||||
std::fs::write(project.join("base.txt"), "base")?;
|
||||
for path in [&project, &repo] {
|
||||
git(path, &["init", "-b", "main"])?;
|
||||
git(path, &["config", "user.name", "Test"])?;
|
||||
git(path, &["config", "user.email", "test@example.com"])?;
|
||||
git(path, &["add", "."])?;
|
||||
git(path, &["commit", "--no-gpg-sign", "-m", "fixture"])?;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ edition = "2024"
|
||||
name = "env"
|
||||
path = "src/env.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "which"
|
||||
path = "src/which.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "timeout"
|
||||
path = "src/timeout.rs"
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
//! Resolve tool names through PATH, with the same .wasm fallback as the host.
|
||||
use std::path::Path;
|
||||
fn main() {
|
||||
let mut args = std::env::args().skip(1).peekable();
|
||||
let all = args.peek().is_some_and(|arg| arg == "-a");
|
||||
if all {
|
||||
args.next();
|
||||
}
|
||||
let names: Vec<_> = args.collect();
|
||||
if names.is_empty() || names.iter().any(|name| name.starts_with('-')) {
|
||||
eprintln!("usage: which [-a] <name>...");
|
||||
std::process::exit(2);
|
||||
}
|
||||
let path = std::env::var("PATH").unwrap_or_else(|_| "/usr/local/bin:/bin".into());
|
||||
let mut missing = false;
|
||||
for name in names {
|
||||
let paths: Vec<_> = if name.contains('/') {
|
||||
vec![name.clone()]
|
||||
} else {
|
||||
path.split(':')
|
||||
.map(|dir| Path::new(dir).join(&name).to_string_lossy().into_owned())
|
||||
.collect()
|
||||
};
|
||||
let mut found = false;
|
||||
'search: for path in paths {
|
||||
for candidate in [path.clone(), format!("{path}.wasm")] {
|
||||
if std::fs::metadata(&candidate).is_ok_and(|metadata| metadata.is_file()) {
|
||||
println!("{candidate}");
|
||||
found = true;
|
||||
if !all {
|
||||
break 'search;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
missing |= !found;
|
||||
}
|
||||
std::process::exit(i32::from(missing));
|
||||
}
|
||||
+16
-2
@@ -49,6 +49,18 @@ The component uses WASI's `exit-with-code`, not a custom host import.
|
||||
The world host must enable that optional WASI function.
|
||||
The tests pass explicit `-C` paths.
|
||||
|
||||
## Identity
|
||||
|
||||
The host creates `/etc/gitconfig` when it is absent.
|
||||
It sets `user.name = Sausage` and `user.email = sausage@localhost`.
|
||||
Git reads this system configuration unless `GIT_CONFIG_NOSYSTEM` disables it.
|
||||
Local repository configuration overrides these defaults. The host preserves an existing system configuration.
|
||||
The `Sausage-Conversation` trailer retains conversation attribution.
|
||||
|
||||
Reflog-only operations use a fallback committer when no identity exists.
|
||||
They include reset, checkout, branch changes, and reference updates.
|
||||
New commits and annotated tags still require an identity.
|
||||
|
||||
## Agent command forms
|
||||
|
||||
The comparison suite covers per-command `-c key=value` overrides and quiet local commands.
|
||||
@@ -60,10 +72,12 @@ It also checks these forms:
|
||||
- `log --format` and `--pretty=format:` with `%H %h %s %an %ae %ad %d`.
|
||||
- `log A..B`, `--name-only`, `--no-renames`, and `--diff-merges=first-parent`.
|
||||
- `%x00`, `%x1e`, and `%(trailers:key=Sausage-Conversation,valueonly)` in log formats.
|
||||
- `show --stat`, `show HEAD:path`, and merge commits without a combined diff.
|
||||
- `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.
|
||||
- `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`, `-m`, and `--show-current`.
|
||||
- `branch -a`, `-d`, `-D`, `-f`, `-m`, and `--show-current`.
|
||||
- `checkout -b`, `switch -c`, and `merge --no-ff`.
|
||||
- `rev-parse --short HEAD`, `ls-files`, and `config --get`.
|
||||
- Quiet `init`, `commit`, branch deletion, `checkout`, `switch`, `merge`, and `rebase`.
|
||||
|
||||
@@ -57,30 +57,54 @@ pub(crate) struct BranchDeleted {
|
||||
pub reference: String,
|
||||
}
|
||||
|
||||
/// Reflogs require an identity, but switching branches must not require configuring one.
|
||||
/// With no repository identity, leave reflogs untouched and report that explicitly; never invent an author.
|
||||
/// Reflog-only writes use a fallback identity when the repository has none.
|
||||
pub(crate) fn reflog_signature(repo: &gix::Repository) -> Result<gix::actor::Signature> {
|
||||
if let Some(signature) = repo
|
||||
.committer()
|
||||
.transpose()
|
||||
.map_err(|e| error("reflog identity", e))?
|
||||
{
|
||||
return signature
|
||||
.to_owned()
|
||||
.map_err(|e| error("reflog identity", e));
|
||||
}
|
||||
let config = repo.config_snapshot();
|
||||
let name = std::env::var("GIT_COMMITTER_NAME")
|
||||
.ok()
|
||||
.or_else(|| config.string("user.name").map(|s| s.to_string()))
|
||||
.or_else(|| std::env::var("USER").ok())
|
||||
.unwrap_or_else(|| "Sausage".into());
|
||||
let email = std::env::var("GIT_COMMITTER_EMAIL")
|
||||
.ok()
|
||||
.or_else(|| config.string("user.email").map(|s| s.to_string()))
|
||||
.unwrap_or_else(|| {
|
||||
format!(
|
||||
"{}@{}",
|
||||
std::env::var("USER").unwrap_or_else(|_| "sausage".into()),
|
||||
std::env::var("HOSTNAME").unwrap_or_else(|_| "localhost".into())
|
||||
)
|
||||
});
|
||||
let seconds = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64;
|
||||
Ok(gix::actor::Signature {
|
||||
name: name.into(),
|
||||
email: email.into(),
|
||||
time: gix::date::Time::new(seconds, 0),
|
||||
})
|
||||
}
|
||||
|
||||
/// Publish references and their reflog entries without requiring a commit identity.
|
||||
pub(crate) fn edit_refs(
|
||||
repo: &gix::Repository,
|
||||
edits: Vec<gix::refs::transaction::RefEdit>,
|
||||
) -> Result<Vec<String>> {
|
||||
let committer = repo
|
||||
.committer()
|
||||
.transpose()
|
||||
.map_err(|e| error("reflog identity", e))?;
|
||||
if let Some(committer) = committer {
|
||||
repo.edit_references_as(edits, Some(committer))
|
||||
.map_err(|e| error("checkout refs", e))?;
|
||||
Ok(Vec::new())
|
||||
} else {
|
||||
let mut local = repo.clone();
|
||||
local.refs.write_reflog = gix::refs::store::WriteReflog::Disable;
|
||||
local
|
||||
.edit_references_as(edits, None)
|
||||
.map_err(|e| error("checkout refs", e))?;
|
||||
Ok(vec![
|
||||
"reflog not written: no repository committer identity configured".into(),
|
||||
])
|
||||
}
|
||||
let signature = reflog_signature(repo)?;
|
||||
let mut time = Default::default();
|
||||
repo.edit_references_as(edits, Some(signature.to_ref(&mut time)))
|
||||
.map_err(|e| error("checkout refs", e))?;
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
pub(crate) fn branch_name(name: &str) -> Result<String> {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
//! Read Git object types, sizes, contents, and tree entries without changing repository state.
|
||||
use crate::{Result, repo::error};
|
||||
use std::io::Write;
|
||||
|
||||
pub(crate) fn cat_file(
|
||||
repo: &gix::Repository,
|
||||
revision: &str,
|
||||
kind: bool,
|
||||
size: bool,
|
||||
exists: bool,
|
||||
) -> Result<()> {
|
||||
let object = repo
|
||||
.rev_parse_single(revision)
|
||||
.map_err(|e| error("cat-file", e))?
|
||||
.object()
|
||||
.map_err(|e| error("cat-file", e))?;
|
||||
if exists {
|
||||
return Ok(());
|
||||
}
|
||||
if kind {
|
||||
println!("{}", object.kind);
|
||||
} else if size {
|
||||
println!("{}", object.data.len());
|
||||
} else if object.kind == gix::objs::Kind::Tree {
|
||||
ls_tree(repo, revision, false, false, None)?;
|
||||
} else {
|
||||
std::io::stdout()
|
||||
.write_all(&object.data)
|
||||
.map_err(|e| error("cat-file", e))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn ls_tree(
|
||||
repo: &gix::Repository,
|
||||
revision: &str,
|
||||
recursive: bool,
|
||||
names: bool,
|
||||
path: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let id = crate::local::tree(repo, revision)?;
|
||||
fn walk(
|
||||
repo: &gix::Repository,
|
||||
id: gix::ObjectId,
|
||||
prefix: &str,
|
||||
recursive: bool,
|
||||
names: bool,
|
||||
filter: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let tree = repo.find_tree(id).map_err(|e| error("ls-tree", e))?;
|
||||
for entry in tree.iter() {
|
||||
let entry = entry.map_err(|e| error("ls-tree", e))?;
|
||||
let filename = format!("{prefix}{}", entry.filename());
|
||||
let directory = entry.mode().is_tree();
|
||||
let descend = directory
|
||||
&& (recursive
|
||||
|| filter.is_some_and(|path| path.starts_with(&format!("{filename}/"))));
|
||||
if descend {
|
||||
walk(
|
||||
repo,
|
||||
entry.oid().to_owned(),
|
||||
&format!("{filename}/"),
|
||||
recursive,
|
||||
names,
|
||||
filter,
|
||||
)?;
|
||||
} else if filter.is_none_or(|path| {
|
||||
filename == path
|
||||
|| filename.starts_with(&format!("{}/", path.trim_end_matches('/')))
|
||||
}) {
|
||||
if names {
|
||||
println!("{filename}");
|
||||
} else {
|
||||
let kind = if directory {
|
||||
"tree"
|
||||
} else if entry.mode().is_commit() {
|
||||
"commit"
|
||||
} else {
|
||||
"blob"
|
||||
};
|
||||
println!(
|
||||
"{:06o} {kind} {}\t{filename}",
|
||||
entry.mode().kind() as u32,
|
||||
entry.oid()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
walk(repo, id, "", recursive, names, path)
|
||||
}
|
||||
+82
-5
@@ -59,6 +59,7 @@ fn identity(name: Option<String>, email: Option<String>) -> Result<Identity> {
|
||||
mod cherry_pick;
|
||||
mod history;
|
||||
mod local;
|
||||
mod inspect;
|
||||
mod plumbing;
|
||||
mod stash;
|
||||
use clap::{Parser, Subcommand};
|
||||
@@ -79,8 +80,32 @@ enum Command {
|
||||
Show {
|
||||
#[arg(long)]
|
||||
stat: bool,
|
||||
#[arg(long)]
|
||||
oneline: bool,
|
||||
#[arg(long)]
|
||||
name_only: bool,
|
||||
revision: Option<String>,
|
||||
},
|
||||
#[command(group(clap::ArgGroup::new("mode").required(true).multiple(false)))]
|
||||
CatFile {
|
||||
#[arg(short = 't', group = "mode")]
|
||||
kind: bool,
|
||||
#[arg(short = 'p', group = "mode")]
|
||||
pretty: bool,
|
||||
#[arg(short = 's', group = "mode")]
|
||||
size: bool,
|
||||
#[arg(short = 'e', group = "mode")]
|
||||
exists: bool,
|
||||
revision: String,
|
||||
},
|
||||
LsTree {
|
||||
#[arg(short = 'r')]
|
||||
recursive: bool,
|
||||
#[arg(long)]
|
||||
name_only: bool,
|
||||
revision: String,
|
||||
path: Option<String>,
|
||||
},
|
||||
Clone {
|
||||
url: String,
|
||||
path: Option<String>,
|
||||
@@ -257,6 +282,8 @@ enum Command {
|
||||
delete: bool,
|
||||
#[arg(short = 'D')]
|
||||
force_delete: bool,
|
||||
#[arg(short = 'f', long)]
|
||||
force: bool,
|
||||
#[arg(short = 'm')]
|
||||
rename: bool,
|
||||
name: Option<String>,
|
||||
@@ -1124,6 +1151,7 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
list,
|
||||
delete,
|
||||
force_delete,
|
||||
force,
|
||||
show_current,
|
||||
all,
|
||||
rename,
|
||||
@@ -1164,6 +1192,14 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
if !quiet {
|
||||
println!("Deleted branch {name} (was {}).", &id[..7]);
|
||||
}
|
||||
} else if force {
|
||||
let full = branches::branch_name(&name)?;
|
||||
worktrees::ensure_available(&repository, &full)?;
|
||||
if head_info(&repository)?.branch.as_deref() == Some(&full) {
|
||||
return Err(error("branch", "cannot force update current branch"));
|
||||
}
|
||||
let id = branches::resolve(&repository, start.as_deref().unwrap_or("HEAD"))?;
|
||||
plumbing::update_ref(&repository, &full, &id.to_string(), None)?;
|
||||
} else {
|
||||
branches::create(
|
||||
&repository,
|
||||
@@ -1488,7 +1524,35 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
)?;
|
||||
}
|
||||
},
|
||||
Command::Show { revision, stat } => {
|
||||
Command::CatFile {
|
||||
kind,
|
||||
pretty: _,
|
||||
size,
|
||||
exists,
|
||||
revision,
|
||||
} => {
|
||||
inspect::cat_file(&repository, &revision, kind, size, exists)?;
|
||||
}
|
||||
Command::LsTree {
|
||||
recursive,
|
||||
name_only,
|
||||
revision,
|
||||
path,
|
||||
} => {
|
||||
inspect::ls_tree(
|
||||
&repository,
|
||||
&revision,
|
||||
recursive,
|
||||
name_only,
|
||||
path.as_deref(),
|
||||
)?;
|
||||
}
|
||||
Command::Show {
|
||||
revision,
|
||||
stat,
|
||||
oneline,
|
||||
name_only,
|
||||
} => {
|
||||
let object = repository
|
||||
.rev_parse_single(revision.as_deref().unwrap_or("HEAD"))
|
||||
.map_err(|e| error("show", e))?
|
||||
@@ -1528,9 +1592,18 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
return Ok(129);
|
||||
}
|
||||
}
|
||||
output::commit(&commit)?;
|
||||
println!();
|
||||
if merge_commit && !stat {
|
||||
if oneline {
|
||||
let decoded = commit.decode().map_err(|e| error("show", e))?;
|
||||
println!(
|
||||
"{} {}",
|
||||
&commit.id.to_string()[..7],
|
||||
decoded.message.to_str_lossy().lines().next().unwrap_or("")
|
||||
);
|
||||
} else {
|
||||
output::commit(&commit)?;
|
||||
println!();
|
||||
}
|
||||
if merge_commit && !stat && !name_only {
|
||||
return Ok(0);
|
||||
}
|
||||
let old = match commit.parent_ids().next() {
|
||||
@@ -1548,7 +1621,11 @@ pub fn run(cli: Cli) -> Result<i32> {
|
||||
path: None,
|
||||
context: None,
|
||||
})?;
|
||||
if stat {
|
||||
if name_only {
|
||||
for diff in diffs {
|
||||
println!("{}", diff.path);
|
||||
}
|
||||
} else if stat {
|
||||
output::stat(&diffs, false);
|
||||
} else {
|
||||
for diff in diffs {
|
||||
|
||||
@@ -118,7 +118,7 @@ pub(crate) fn reset(
|
||||
repo,
|
||||
Some(gix::ObjectId::from_hex(old.as_bytes()).map_err(|e| error("reset HEAD", e))?),
|
||||
id,
|
||||
&staging::signature(None, None)?,
|
||||
&branches::reflog_signature(repo)?,
|
||||
"reset",
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -64,6 +64,22 @@ def main [component: path, server: path = '/tmp/p1-git/server-target/debug/git-t
|
||||
{args: [log -n 1 --stat], compare: true}
|
||||
{args: [log --oneline -- file.txt], compare: true}
|
||||
{args: [show --stat], compare: true}
|
||||
{args: [show --oneline], compare: true}
|
||||
{args: [show --name-only], compare: true}
|
||||
{args: [show --oneline --stat], compare: true}
|
||||
{args: [cat-file -t HEAD], compare: true}
|
||||
{args: [cat-file -p HEAD], compare: true}
|
||||
{args: [cat-file -s HEAD], compare: true}
|
||||
{args: [cat-file -e HEAD], compare: true}
|
||||
{args: [cat-file -e absent], compare: false, exit: 128}
|
||||
{args: [cat-file -t 'HEAD:file.txt'], compare: true}
|
||||
{args: [cat-file -p 'HEAD:file.txt'], compare: true}
|
||||
{args: [cat-file -s 'HEAD:file.txt'], compare: true}
|
||||
{args: [cat-file -p 'HEAD^{tree}'], compare: true}
|
||||
{args: [ls-tree HEAD], compare: true}
|
||||
{args: [ls-tree -r HEAD], compare: true}
|
||||
{args: [ls-tree --name-only HEAD], compare: true}
|
||||
{args: [ls-tree -r --name-only HEAD file.txt], compare: true}
|
||||
{args: [rev-parse --short HEAD], compare: true}
|
||||
{args: [show HEAD], compare: true}
|
||||
{args: [show HEAD:file.txt], compare: true}
|
||||
@@ -451,6 +467,43 @@ def main [component: path, server: path = '/tmp/p1-git/server-target/debug/git-t
|
||||
if not $equal { $failures += 1; print ($expected | to json); print ($actual | to json) }
|
||||
}
|
||||
}
|
||||
# Reflog-only writes do not require the strict identity that new commits require.
|
||||
let no_identity = ($root | path join no-identity)
|
||||
mkdir $no_identity
|
||||
^git -C $no_identity init -q -b main
|
||||
'base' | save ($no_identity | path join file.txt)
|
||||
^git -C $no_identity add file.txt
|
||||
^git -C $no_identity commit --no-gpg-sign -q -m base
|
||||
mkdir ($no_identity | path join nested/sub)
|
||||
'nested' | save ($no_identity | path join nested/sub/file.txt)
|
||||
^git -C $no_identity add .
|
||||
let inspection_tree = (^git -C $no_identity write-tree | str trim)
|
||||
for args in [[ls-tree $inspection_tree] [ls-tree -r $inspection_tree] [ls-tree --name-only $inspection_tree nested] [ls-tree -r --name-only $inspection_tree nested] [ls-tree $inspection_tree nested/sub/file.txt] [cat-file -p $inspection_tree]] {
|
||||
let expected = (^git -C $no_identity ...$args | complete)
|
||||
let actual = (^wasmtime run -S http=y -S cli-exit-with-code=y --dir $'($no_identity)::/repo' $component -C /repo ...$args | complete)
|
||||
let equal = ($expected.exit_code == 0 and $actual.exit_code == 0 and $expected.stdout == $actual.stdout)
|
||||
print $'nested tree ($args): (if $equal {"PASS"} else {"FAIL"})'
|
||||
if not $equal { $failures += 1; print ($expected | to json); print ($actual | to json) }
|
||||
}
|
||||
^git -C $no_identity reset --hard -q HEAD
|
||||
^git -C $no_identity config user.useConfigOnly true
|
||||
let identity_failures = (do {
|
||||
mut failed = 0
|
||||
hide-env -i GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL
|
||||
for args in [[reset --hard HEAD] [checkout -q --detach HEAD] [checkout -q main] [branch -f other HEAD] [update-ref refs/heads/other HEAD]] {
|
||||
let expected = (^git -C $no_identity ...$args | complete)
|
||||
let actual = (^wasmtime run -S http=y -S cli-exit-with-code=y --dir $'($no_identity)::/repo' $component -C /repo ...$args | complete)
|
||||
let equal = ($expected.exit_code == 0 and $actual.exit_code == 0 and $expected.stdout == $actual.stdout)
|
||||
print $'no identity ($args): (if $equal {"PASS"} else {"FAIL"})'
|
||||
if not $equal { $failed += 1; print ($expected | to json); print ($actual | to json) }
|
||||
}
|
||||
let expected = (^git -C $no_identity commit --allow-empty -m missing | complete)
|
||||
let actual = (^wasmtime run -S http=y -S cli-exit-with-code=y --dir $'($no_identity)::/repo' $component -C /repo commit --allow-empty -m missing | complete)
|
||||
if $expected.exit_code != 128 or $actual.exit_code != 128 { $failed += 1; print ($expected | to json); print ($actual | to json) }
|
||||
print 'no identity commit requires identity: checked'
|
||||
$failed
|
||||
})
|
||||
$failures += $identity_failures
|
||||
if $failures > 0 {error make {msg: $'($failures) workflow comparisons failed'}}
|
||||
}
|
||||
} catch {|err| job kill $server_job; error make {msg: $err.msg} }
|
||||
|
||||
Reference in New Issue
Block a user