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

@ -9,7 +9,6 @@ import :Core;
import :Plan;
import :Tools;
import :PgWire;
import :Process;
import :Platform;
import :Prompt;
import std;
@ -151,8 +150,8 @@ namespace Crafter::Review::Config {
// when HEAD tracks nothing; base: always merge-base HEAD
// <base>..HEAD, the whole pull request.
{"rangeFrom", "upstream"},
// Author and committer emails the freeze accepts; null: the
// repository's `git config user.email`.
// Author and committer emails the freeze accepts; null: neither
// is checked.
{"authors", nullptr},
// forbid | allow: Assisted-by / Co-Authored-By trailers in the
// range.
@ -266,6 +265,8 @@ namespace Crafter::Review::Config {
return a.is_string() && a.get_ref<const std::string&>().contains('@');
}))) {
out.emplace_back("authors must be null or a list of email addresses");
} else if (authors.is_array() && authors.empty()) {
out.emplace_back("authors is an empty list, which no commit passes - null leaves author and committer unchecked");
}
for (const std::string_view key : {"allowDirty", "gate0"}) {
if (!AllStrings(Dyn::At(config, key))) out.push_back(std::format("{} must be a list of strings", key));
@ -453,21 +454,12 @@ namespace Crafter::Review::Config {
return copy;
}
std::string GitUserEmail(const fs::path& repo) {
const Process::Captured run = Process::Run({"git", "-C", repo.string(), "config", "user.email"});
return run.returnCode == 0 ? Dyn::Strip(Dyn::UniversalNewlines(Dyn::DecodeReplace(run.out))) : std::string();
}
std::vector<std::string> Authors(const Json& config, const fs::path& repo) {
std::optional<std::vector<std::string>> Authors(const Json& config) {
const Json& authors = Dyn::Get(config, "authors");
if (Dyn::Truthy(authors)) {
std::vector<std::string> out;
for (const Json& author : authors) out.push_back(Dyn::Str(author));
return out;
}
const std::string email = GitUserEmail(repo);
if (email.empty()) return {};
return {email};
if (!authors.is_array()) return std::nullopt;
std::vector<std::string> out;
for (const Json& author : authors) out.push_back(Dyn::Str(author));
return out;
}
std::map<std::string, fs::path> LensSectionsOf(const Json& config, const fs::path& repo) {