feat(freeze): no authors in the config, no author check

A repository that takes commits from people it cannot list has no way
to freeze a range today: a null `authors` falls back to
`git config user.email`, so every foreign author or committer fails.
Null, or no key, now means neither is checked; the freeze warns that it
did not check them and records `authors` as null. A list is checked as
before. An empty list, which no commit could pass, is a config error
that points at null.
Released as 1.1.0.
This commit is contained in:
Jorijn van der Graaf 2026-11-02 13:57:20 +01:00
commit c6b4d78245
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
10 changed files with 52 additions and 49 deletions

View file

@ -211,20 +211,18 @@ namespace Crafter::Review::Freeze {
fail(std::format("{} is not the toplevel of a git checkout", repo));
return report;
}
std::vector<std::string> authors = options.authors;
if (authors.empty()) authors = Config::Authors({{"authors", nullptr}}, repo);
if (authors.empty()) {
fail("no author identity: set `authors` in .review-gates/config.json or git config user.email");
return report;
}
const std::optional<std::vector<std::string>>& authors = options.authors;
const Base base = ResolveBase(repo, options.base, options.configBase, options.rangeFrom);
if (!base.failure.empty()) {
fail(base.failure);
return report;
}
const std::string head = Artifact::TreeHead(repo);
Json authorList = Json::array();
for (const std::string& author : authors) authorList.push_back(author);
Json authorList;
if (authors) {
authorList = Json::array();
for (const std::string& author : *authors) authorList.push_back(author);
}
report["base"] = base.sha;
report["baseFrom"] = base.how;
report["upstream"] = base.upstream ? Json(*base.upstream) : Json();
@ -261,11 +259,12 @@ namespace Crafter::Review::Freeze {
commitList.push_back({{"sha", c.sha}, {"subject", c.subject}, {"author", c.author}, {"committer", c.committer}, {"sig", c.sig}});
}
report["commits"] = commitList;
const std::string authorText = Dyn::Join(authors, ", ");
if (!authors) warn("no authors in .review-gates/config.json: author and committer not checked");
const std::string authorText = authors ? Dyn::Join(*authors, ", ") : std::string();
for (const Commit& c : commits) {
const std::string who = std::format("{} {}", Head12(c.sha), Dyn::Repr(Dyn::Slice(c.subject, 0, 50)));
if (!std::ranges::contains(authors, c.author)) fail(std::format("{}: author {} not in {}", who, c.author, authorText));
if (!std::ranges::contains(authors, c.committer)) fail(std::format("{}: committer {} not in {}", who, c.committer, authorText));
if (authors && !std::ranges::contains(*authors, c.author)) fail(std::format("{}: author {} not in {}", who, c.author, authorText));
if (authors && !std::ranges::contains(*authors, c.committer)) fail(std::format("{}: committer {} not in {}", who, c.committer, authorText));
if (const std::optional<std::string> trailer = AiTrailer(c.body)) {
const bool forbid = options.aiTrailers == "forbid";
const std::string message = std::format("{}: AI attribution trailer {}{}", who, Dyn::Repr(Dyn::Strip(*trailer)),
@ -472,7 +471,7 @@ namespace Crafter::Review::Freeze {
options.replace = arguments.replace;
options.worktree = arguments.worktree;
options.requireSigned = arguments.requireSigned || Dyn::Truthy(Dyn::Get(config, "requireSigned"));
options.authors = Config::Authors(config, repo);
options.authors = Config::Authors(config);
options.aiTrailers = Dyn::Str(Dyn::Get(config, "aiTrailers"));
options.allowDirty = allowDirty;
const Json& configBase = Dyn::Get(config, "base");