Skip to content

Commit d7de528

Browse files
authored
Merge pull request lidge-jun#3341 from lidge-jun/codex/promote-preview-2410-v2
[WRONG BRANCH] release: v2.41.0-preview.20260903
2 parents 49812c9 + 798611e commit d7de528

245 files changed

Lines changed: 17446 additions & 1699 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
"use strict";
2+
3+
/**
4+
* Attribution for work carried from another author's pull request.
5+
*
6+
* When a maintainer lands someone else's pull request by reimplementing,
7+
* carrying, or rebasing it, the resulting commit is authored by the maintainer.
8+
* The contributor survives only through a Co-authored-by trailer -- that trailer
9+
* is what GitHub reads for the contributor graph, the repository's contributor
10+
* list, and the author's own profile activity.
11+
*
12+
* This exists because the repository did it both ways for months. 53c09a247
13+
* says "Clean reimplementation of #3193" and names alan7629 in a trailer;
14+
* 5734a1caf says "Reimplements #2797 by @rrmlima" and names nobody. Both
15+
* sentences are equally sincere, and only the first is data. A scan of dev
16+
* found 27 landings whose author is named in prose and nowhere a tool can read;
17+
* CREDITS.md is the record of those, and this check is why the list should not
18+
* grow.
19+
*
20+
* The check reads the pull request's own text, not its diff, because that is
21+
* where a carry declares itself.
22+
*/
23+
24+
const CARRY_VERB_RE =
25+
/\b(?:re-?implement(?:s|ed|ing|ation of)?|supersed(?:e|es|ed|ing)|carry(?: of)?|carries|carrying|carried(?: from)?|rebase(?: of)?|rebasing|adopts the design from)\b/gi;
26+
27+
/**
28+
* Every reference in one window, keeping any owner/repo qualifier.
29+
*
30+
* A bare "#2797" means this repository. "other/project#2797" does not, and
31+
* resolving it here would look up an unrelated pull request of the same number
32+
* in this one -- comparing the trailer against the wrong person. Qualified
33+
* references are captured so they can be dropped rather than misread.
34+
*/
35+
const REF_RE = /(?:([\w.-]+\/[\w.-]+))?#(\d+)/g;
36+
37+
/**
38+
* The window a carry verb governs: to the end of its sentence, capped at 80
39+
* characters. Both bounds are load-bearing.
40+
*
41+
* The sentence bound is why "Supersedes #3193. Fixes #3192." reports only
42+
* #3193 -- that is 53c09a247's real body, and a fixed-width window would have
43+
* pulled the issue it closes into the carry set and demanded a trailer for the
44+
* reporter. The width cap is why a verb cannot reach across a paragraph into an
45+
* unrelated reference list.
46+
*/
47+
const SENTENCE_END_RE = /[.!?](?:\s|$)|\n/;
48+
49+
function carryWindow(text, from) {
50+
const slice = text.slice(from, from + 80);
51+
const end = slice.search(SENTENCE_END_RE);
52+
return end === -1 ? slice : slice.slice(0, end);
53+
}
54+
55+
const TRAILER_RE = /^[ \t]*co-authored-by:[ \t]*(.+)$/gim;
56+
57+
const FENCED_CODE_RE = /^[ \t]*(\u0060{3,}|~{3,})[\s\S]*?^[ \t]*\1[ \t]*$/gm;
58+
const INLINE_CODE_RE = /\u0060[^\u0060\n]*\u0060/g;
59+
const HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
60+
61+
/**
62+
* Carry language inside a fenced block, an inline span, or an HTML comment is
63+
* quoted material, not a declaration. A pull request that explains the gate
64+
* itself -- this one does -- must not trip it.
65+
*/
66+
function strippedText(text) {
67+
if (typeof text !== "string") return "";
68+
return text
69+
.replace(FENCED_CODE_RE, "")
70+
.replace(HTML_COMMENT_RE, "")
71+
.replace(INLINE_CODE_RE, "");
72+
}
73+
74+
function hasLabel(labels, name) {
75+
return (labels || []).some(
76+
(label) => (typeof label === "string" ? label : label?.name) === name,
77+
);
78+
}
79+
80+
/** Pull request numbers this text claims to carry, supersede, or rebase. */
81+
function referencedCarryNumbers(...texts) {
82+
const found = new Set();
83+
for (const text of texts) {
84+
const stripped = strippedText(text);
85+
CARRY_VERB_RE.lastIndex = 0;
86+
let verb;
87+
while ((verb = CARRY_VERB_RE.exec(stripped)) !== null) {
88+
const window = carryWindow(stripped, verb.index + verb[0].length);
89+
REF_RE.lastIndex = 0;
90+
let ref;
91+
while ((ref = REF_RE.exec(window)) !== null) {
92+
// A qualified reference names a pull request in another repository.
93+
if (ref[1]) continue;
94+
found.add(Number(ref[2]));
95+
}
96+
}
97+
}
98+
return found;
99+
}
100+
101+
function trailerValues(...texts) {
102+
const values = [];
103+
for (const text of texts) {
104+
if (typeof text !== "string") continue;
105+
TRAILER_RE.lastIndex = 0;
106+
let match;
107+
while ((match = TRAILER_RE.exec(text)) !== null) values.push(match[1].toLowerCase());
108+
}
109+
return values;
110+
}
111+
112+
/**
113+
* A GitHub login is not a git identity. The scan behind CREDITS.md produced
114+
* eleven false positives from that assumption alone: a login like "asmith92"
115+
* does not appear anywhere in a trailer that reads "A. Smith <a@example.com>",
116+
* even though they are the same person. Match on any of the three identifiers
117+
* the referenced pull request actually carries.
118+
*/
119+
function parseTrailer(value) {
120+
const match = /^\s*(.*?)\s*<([^>]*)>\s*$/.exec(value);
121+
if (match) return { name: match[1].toLowerCase(), email: match[2].toLowerCase() };
122+
return { name: value.trim().toLowerCase(), email: "" };
123+
}
124+
125+
/**
126+
* Substring matching is not good enough here, and the failure is not exotic:
127+
* an author named "Ann" would be satisfied by "Co-authored-by: Joanne
128+
* <other@example.com>", and a short login can appear inside an unrelated
129+
* address. A trailer credits someone only when its name or its email equals an
130+
* identifier the referenced pull request actually carries.
131+
*/
132+
function trailerNames(author, trailers) {
133+
if (!author) return true;
134+
const names = new Set(
135+
[author.login, ...(author.names || [])]
136+
.filter((value) => typeof value === "string" && value.trim() !== "")
137+
.map((value) => value.trim().toLowerCase()),
138+
);
139+
const emails = new Set(
140+
(author.emails || [])
141+
.filter((value) => typeof value === "string" && value.trim() !== "")
142+
.map((value) => value.trim().toLowerCase()),
143+
);
144+
if (names.size === 0 && emails.size === 0) return true;
145+
return trailers.some(
146+
(trailer) =>
147+
(trailer.name !== "" && names.has(trailer.name)) ||
148+
(trailer.email !== "" && emails.has(trailer.email)) ||
149+
// A GitHub noreply address carries the login after the numeric id,
150+
// before the "@" -- that is the only identifier many trailers have.
151+
(trailer.email.endsWith("@users.noreply.github.com") &&
152+
names.has(trailer.email.replace(/^[^@]*?(\d+\+)?/, "").split("@")[0])),
153+
);
154+
}
155+
156+
/**
157+
* @returns {{ code: string, paths: string[] }[]} empty when the pull request may proceed
158+
*/
159+
function assessCarryAttribution({
160+
prAuthorLogin = "",
161+
title = "",
162+
body = "",
163+
commits = [],
164+
labels = [],
165+
referencedAuthors = {},
166+
} = {}) {
167+
if (hasLabel(labels, "attribution-approved")) return [];
168+
169+
const referenced = referencedCarryNumbers(title, body, ...commits);
170+
if (referenced.size === 0) return [];
171+
172+
// The squash body is assembled from the pull request body and the branch's
173+
// commit messages, so both are where an author can put the trailer today.
174+
const trailers = trailerValues(body, ...commits).map(parseTrailer);
175+
const uncredited = [];
176+
177+
for (const number of referenced) {
178+
const author = referencedAuthors[number];
179+
// An unresolved author is a pass. A rate limit or a deleted account must
180+
// never be the reason a merge is blocked.
181+
if (!author) continue;
182+
// Referencing your own earlier branch is ordinary maintenance.
183+
if (
184+
author.login &&
185+
prAuthorLogin &&
186+
author.login.toLowerCase() === prAuthorLogin.toLowerCase()
187+
) {
188+
continue;
189+
}
190+
if (!trailerNames(author, trailers)) uncredited.push("#" + number);
191+
}
192+
193+
if (uncredited.length === 0) return [];
194+
return [
195+
{
196+
code: "missing_coauthor_credit",
197+
paths: uncredited.sort(),
198+
},
199+
];
200+
}
201+
202+
module.exports = {
203+
CARRY_VERB_RE,
204+
carryWindow,
205+
assessCarryAttribution,
206+
referencedCarryNumbers,
207+
strippedText,
208+
trailerValues,
209+
};

0 commit comments

Comments
 (0)