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:
parent
0d3d8ed2d2
commit
c6b4d78245
10 changed files with 52 additions and 49 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in a new issue