94 lines
4.3 KiB
Text
94 lines
4.3 KiB
Text
|
|
// 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);
|
||
|
|
}
|