feat(freeze): crafter-review freeze, and --verify that names a git upgrade
Freeze: identity, AI trailers by the consumer's rule (the pattern with case-insensitive folds and its \s* that crosses lines), a clean tree, no merges, a fast-forward, then format-patch, the hash, the dispositions register and the optional detached worktree; --verify at push time. The command line takes --opt=value, unique prefixes and the last of a repeat, with a usage error at exit 3 like every command that cannot start; the config is loaded and its warnings and problems reported first. The run line it prints names this binary. freeze.json records gitVersion. format-patch ends every patch with the git version, so a fresh export under another git hashes differently; --verify's MISMATCH says which git exported the artifact and which the fresh copy, reading the patches' own signature when freeze.json does not record it. freeze becomes a subreaper and takes down whatever gpg left running for %G?. tests/Freeze: 18 checks, plus the gitVersion record and both MISMATCH messages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
71dba5690f
commit
61acd6a72e
6 changed files with 943 additions and 13 deletions
94
interfaces/Crafter.Review-Freeze.cppm
Normal file
94
interfaces/Crafter.Review-Freeze.cppm
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
export module Crafter.Review:Freeze;
|
||||
import std;
|
||||
import :Json;
|
||||
|
||||
// `crafter-review freeze`: cut the frozen commit-range artifact for the push
|
||||
// gates — identity, AI trailers by the consumer's rule, a clean tree, no
|
||||
// merges, a fast-forward — then `git format-patch` it, hash it, and
|
||||
// optionally check out a detached worktree the lenses read; with --verify,
|
||||
// prove at push time that the repository still is what was reviewed.
|
||||
//
|
||||
// freeze.json records gitVersion, the git that exported the patches:
|
||||
// format-patch ends every patch with its version, so a fresh export under
|
||||
// another git hashes differently, and --verify says so when that is why.
|
||||
export namespace Crafter::Review::Freeze {
|
||||
// .review-gates/trees: inside the consumer's checkout, gitignored, so no
|
||||
// --add-dir is needed for the lenses to read a worktree there.
|
||||
constexpr std::string_view Worktrees = ".review-gates/trees";
|
||||
constexpr std::string_view PushDir = ".review-gates/push";
|
||||
|
||||
// `git --version` without its "git version " prefix; "" when git does not
|
||||
// answer.
|
||||
std::string GitVersion();
|
||||
|
||||
struct Commit {
|
||||
std::string sha;
|
||||
std::string author;
|
||||
std::string committer;
|
||||
std::string sig;
|
||||
std::string subject;
|
||||
std::string body;
|
||||
};
|
||||
// base..HEAD, oldest first. Throws Dyn::Error (RuntimeError) with git's
|
||||
// message.
|
||||
std::vector<Commit> CommitsIn(const std::filesystem::path& repo, std::string_view base);
|
||||
// The tracked changes that count, and the ones the config allows
|
||||
// (allowDirty): `XY path` lines of git status --porcelain.
|
||||
std::pair<std::vector<std::string>, std::vector<std::string>> DirtyPaths(const std::filesystem::path& repo, std::span<const std::string> allowDirty = {});
|
||||
|
||||
struct Base {
|
||||
std::string sha;
|
||||
std::optional<std::string> upstream;
|
||||
std::string how;
|
||||
std::string failure;
|
||||
};
|
||||
// An explicit --base wins; with rangeFrom 'base' the merge-base of HEAD and
|
||||
// the configured branch (the whole pull request); else the branch's
|
||||
// @{upstream} (the push delta), else that merge-base (a new branch that
|
||||
// tracks nothing yet).
|
||||
Base ResolveBase(const std::filesystem::path& repo, std::string_view base, const std::optional<std::string>& configBase, std::string_view rangeFrom);
|
||||
|
||||
struct Options {
|
||||
std::string base;
|
||||
bool replace = false;
|
||||
bool worktree = false;
|
||||
bool requireSigned = false;
|
||||
std::vector<std::string> authors;
|
||||
std::string aiTrailers = "forbid";
|
||||
std::vector<std::string> allowDirty;
|
||||
std::optional<std::string> configBase;
|
||||
std::string rangeFrom = "upstream";
|
||||
std::string date;
|
||||
std::filesystem::path cwd;
|
||||
};
|
||||
// The report: ok, repo, out, date, failures, warnings, then base, baseFrom,
|
||||
// upstream, head, authors, aiTrailers, commits, files, hash, worktree as
|
||||
// they come. Nothing is exported while a check fails; every check is
|
||||
// listed, not just the first.
|
||||
Json Run(const std::filesystem::path& repo, const std::filesystem::path& out, const Options& options);
|
||||
// Push-time check: HEAD is the frozen head, the tree is clean (the allowed
|
||||
// paths aside), a fresh export hashes to the frozen hash, and the artifact
|
||||
// dir still does too. The problems; none: it matches.
|
||||
std::vector<std::string> Verify(const std::filesystem::path& repo, const std::filesystem::path& out, std::span<const std::string> allowDirty = {});
|
||||
// The exact `run` command for a freeze report: built from its fields, never
|
||||
// retyped (2026-09-29).
|
||||
std::string RunLine(const Json& report, std::string_view outRel, std::string_view trees);
|
||||
|
||||
struct Arguments {
|
||||
std::optional<std::string> repo;
|
||||
std::optional<std::string> slug;
|
||||
std::optional<std::string> out;
|
||||
std::optional<std::string> base;
|
||||
bool replace = false;
|
||||
bool worktree = false;
|
||||
bool requireSigned = false;
|
||||
std::string date;
|
||||
bool verify = false;
|
||||
};
|
||||
// `crafter-review freeze ...` with the consumer's config loaded: 0, 1 for
|
||||
// a --verify MISMATCH, 3 for a failed check.
|
||||
std::int32_t Main(const Arguments& arguments, const Json& config);
|
||||
}
|
||||
Loading…
Reference in a new issue