pr-report-core 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/checks.d.mts +61 -62
- package/dist/checks.d.mts.map +1 -1
- package/dist/checks.mjs +8 -9
- package/dist/checks.mjs.map +1 -1
- package/dist/claim.d.mts +13 -14
- package/dist/claim.d.mts.map +1 -1
- package/dist/claim.mjs +0 -1
- package/dist/claim.mjs.map +1 -1
- package/dist/closing-issues.d.mts +18 -19
- package/dist/closing-issues.d.mts.map +1 -1
- package/dist/closing-issues.mjs +0 -1
- package/dist/closing-issues.mjs.map +1 -1
- package/dist/code-owners.d.mts +48 -49
- package/dist/code-owners.d.mts.map +1 -1
- package/dist/code-owners.mjs +41 -43
- package/dist/code-owners.mjs.map +1 -1
- package/dist/labels.d.mts +38 -40
- package/dist/labels.d.mts.map +1 -1
- package/dist/labels.mjs +0 -2
- package/dist/labels.mjs.map +1 -1
- package/dist/merge-after.d.mts +22 -16
- package/dist/merge-after.d.mts.map +1 -1
- package/dist/merge-after.mjs +97 -34
- package/dist/merge-after.mjs.map +1 -1
- package/dist/queue-comments.d.mts +2 -2
- package/dist/queue-order.d.mts +43 -30
- package/dist/queue-order.d.mts.map +1 -1
- package/dist/queue-order.mjs +33 -18
- package/dist/queue-order.mjs.map +1 -1
- package/dist/queue-owner.d.mts +30 -30
- package/dist/queue-owner.d.mts.map +1 -1
- package/dist/queue-owner.mjs +51 -51
- package/dist/queue-owner.mjs.map +1 -1
- package/dist/report.d.mts +9 -9
- package/dist/report.d.mts.map +1 -1
- package/dist/report.mjs +23 -23
- package/dist/report.mjs.map +1 -1
- package/dist/review-hold.d.mts +14 -14
- package/dist/ruleset.d.mts +10 -11
- package/dist/ruleset.d.mts.map +1 -1
- package/dist/ruleset.mjs +0 -1
- package/dist/ruleset.mjs.map +1 -1
- package/dist/set-aside.d.mts +28 -28
- package/dist/stack.d.mts +14 -15
- package/dist/stack.d.mts.map +1 -1
- package/dist/stack.mjs +0 -1
- package/dist/stack.mjs.map +1 -1
- package/dist/start-after.d.mts +20 -21
- package/dist/start-after.d.mts.map +1 -1
- package/dist/start-after.mjs +0 -1
- package/dist/start-after.mjs.map +1 -1
- package/dist/summarize.d.mts +2 -2
- package/dist/timestamp.d.mts +10 -10
- package/dist/tree.d.mts +5 -5
- package/dist/types.d.mts +32 -33
- package/dist/types.d.mts.map +1 -1
- package/package.json +8 -8
- package/src/checks.mts +74 -75
- package/src/checks.test.mts +30 -31
- package/src/claim.mts +13 -14
- package/src/closing-issues.mts +18 -19
- package/src/closing-issues.test.mts +2 -2
- package/src/code-owners.mts +107 -109
- package/src/labels.mts +38 -40
- package/src/merge-after.mts +164 -71
- package/src/merge-after.test.mts +80 -3
- package/src/queue-comments.mts +2 -2
- package/src/queue-order.mts +103 -83
- package/src/queue-order.test.mts +41 -4
- package/src/queue-owner.mts +103 -103
- package/src/queue-owner.test.mts +2 -2
- package/src/report.mts +49 -49
- package/src/review-hold.mts +14 -14
- package/src/review-hold.test.mts +3 -3
- package/src/ruleset.mts +10 -11
- package/src/set-aside.mts +28 -28
- package/src/stack.mts +14 -15
- package/src/stack.test.mts +4 -4
- package/src/start-after.mts +20 -21
- package/src/summarize.mts +2 -2
- package/src/timestamp.mts +14 -14
- package/src/tree.mts +5 -5
- package/src/types.mts +32 -33
package/src/merge-after.mts
CHANGED
|
@@ -8,11 +8,10 @@ import { Arr, Num, Result } from 'ts-data-forge';
|
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* `#N`, and only when nothing that could end a repository name comes right
|
|
11
|
-
* before the `#`: `other/repo#5` is another repository's #5, and treating it
|
|
12
|
-
*
|
|
13
|
-
* `noshiro-pf/mono#5` is not read either, though it means this repository —
|
|
14
|
-
*
|
|
15
|
-
* it.
|
|
11
|
+
* before the `#`: `other/repo#5` is another repository's #5, and treating it as
|
|
12
|
+
* this one's would wait for a pull request the author never named.
|
|
13
|
+
* `noshiro-pf/mono#5` is not read either, though it means this repository — the
|
|
14
|
+
* declaration takes `#N` and nothing else, so there is one way to write it.
|
|
16
15
|
*/
|
|
17
16
|
const NUMBER_REF = /(?<![\w./-])#(?<number>\d+)\b/gu;
|
|
18
17
|
|
|
@@ -21,76 +20,84 @@ const DECLARATION_LINE = {
|
|
|
21
20
|
'start-after': /^[^\S\n]*start-after[^\S\n]*:(?<refs>[^\n]*)$/gimu,
|
|
22
21
|
} as const;
|
|
23
22
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
23
|
+
/**
|
|
24
|
+
* A line that can open or close a fenced code block: a run of three or more
|
|
25
|
+
* backticks or tildes, and what follows it on the line.
|
|
26
|
+
*/
|
|
27
|
+
const FENCE_LINE = /^[^\S\n]*(?<marker>`{3,}|~{3,})(?<rest>[^\n]*)$/u;
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* The pull request numbers a body declares this one must merge after.
|
|
29
31
|
*
|
|
30
32
|
* Duplicates are dropped and the order within the declaration is not kept:
|
|
31
33
|
* what is being read is a set of things that have to have happened, not a
|
|
32
|
-
* sequence. A number that names no pull request, or one that is already
|
|
33
|
-
*
|
|
34
|
+
* sequence. A number that names no pull request, or one that is already closed,
|
|
35
|
+
* simply never appears in the open list and so constrains nothing.
|
|
34
36
|
*/
|
|
35
37
|
export const parseMergeAfter = (body: string): readonly number[] =>
|
|
36
38
|
parseDeclaration(body, 'merge-after');
|
|
37
39
|
|
|
38
40
|
/**
|
|
39
|
-
* The numbers an issue body declares it waits for before it is started,
|
|
40
|
-
*
|
|
41
|
+
* The numbers an issue body declares it waits for before it is started, read
|
|
42
|
+
* exactly as {@link parseMergeAfter} reads its trailer
|
|
41
43
|
* (`docs/issue-start-after.md`).
|
|
42
44
|
*/
|
|
43
45
|
export const parseStartAfter = (body: string): readonly number[] =>
|
|
44
46
|
parseDeclaration(body, 'start-after');
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* request is prose, and treating it as a declaration would make every
|
|
50
|
-
* cross-reference a constraint.
|
|
51
|
-
*/
|
|
52
|
-
const parseDeclaration = (
|
|
53
|
-
body: string,
|
|
54
|
-
key: 'merge-after' | 'start-after',
|
|
55
|
-
): readonly number[] =>
|
|
56
|
-
Arr.uniq(
|
|
57
|
-
outsideCodeFences(body)
|
|
58
|
-
.matchAll(DECLARATION_LINE[key])
|
|
59
|
-
.flatMap((line) =>
|
|
60
|
-
(line.groups?.['refs'] ?? '').matchAll(NUMBER_REF).flatMap((ref) => {
|
|
61
|
-
const parsed = Num.safeParseInt(ref.groups?.['number'] ?? '');
|
|
62
|
-
|
|
63
|
-
return Result.isOk(parsed) ? [parsed.value] : [];
|
|
64
|
-
}),
|
|
65
|
-
)
|
|
66
|
-
.toArray(),
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* The body with the contents of its fenced code blocks blanked out, line
|
|
71
|
-
* count preserved.
|
|
49
|
+
* The body with the contents of its fenced code blocks blanked out, line count
|
|
50
|
+
* preserved.
|
|
72
51
|
*
|
|
73
52
|
* A declaration and an example of one are the same text, and the pull request
|
|
74
53
|
* that introduced this feature had to describe it: written in a fence, the
|
|
75
|
-
* example would have been read as a constraint on the pull request
|
|
76
|
-
*
|
|
77
|
-
*
|
|
54
|
+
* example would have been read as a constraint on the pull request documenting
|
|
55
|
+
* it. A fence is how every document here already says "this is a sample, not
|
|
56
|
+
* the thing itself", so it is the line to draw.
|
|
78
57
|
*
|
|
79
|
-
* Exported because `pr-report` reads the closing keywords (`Closes #12`) out
|
|
80
|
-
*
|
|
58
|
+
* Exported because `pr-report` reads the closing keywords (`Closes #12`) out of
|
|
59
|
+
* the same bodies and has to draw the line in the same place — a document
|
|
81
60
|
* showing what a declaration looks like must not become one.
|
|
82
61
|
*/
|
|
83
62
|
export const outsideCodeFences = (body: string): string => {
|
|
84
|
-
|
|
63
|
+
// The marker that opened the fence the current line is in, if any.
|
|
64
|
+
let mut_openedBy: string | undefined = undefined;
|
|
85
65
|
|
|
86
66
|
return body
|
|
87
67
|
.split('\n')
|
|
88
68
|
.map((line) => {
|
|
89
|
-
|
|
90
|
-
|
|
69
|
+
const fence = FENCE_LINE.exec(line)?.groups;
|
|
70
|
+
|
|
71
|
+
const marker = fence?.['marker'];
|
|
72
|
+
|
|
73
|
+
const rest = fence?.['rest'] ?? '';
|
|
74
|
+
|
|
75
|
+
if (mut_openedBy === undefined) {
|
|
76
|
+
// A backtick fence's info string cannot hold a backtick (CommonMark),
|
|
77
|
+
// so ```` ```x``` ```` is inline code, not an opening line.
|
|
78
|
+
if (
|
|
79
|
+
marker === undefined ||
|
|
80
|
+
(marker.startsWith('`') && rest.includes('`'))
|
|
81
|
+
) {
|
|
82
|
+
return line;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
mut_openedBy = marker;
|
|
86
|
+
|
|
87
|
+
return '';
|
|
91
88
|
}
|
|
92
89
|
|
|
93
|
-
|
|
90
|
+
// Only a run of the opening character, at least as long, with nothing
|
|
91
|
+
// after it, closes the fence (CommonMark); any other fence line is part
|
|
92
|
+
// of the code, so a ``` inside a ```` or ~~~ block does not end it.
|
|
93
|
+
if (
|
|
94
|
+
marker !== undefined &&
|
|
95
|
+
marker.at(0) === mut_openedBy.at(0) &&
|
|
96
|
+
marker.length >= mut_openedBy.length &&
|
|
97
|
+
rest.trim() === ''
|
|
98
|
+
) {
|
|
99
|
+
mut_openedBy = undefined;
|
|
100
|
+
}
|
|
94
101
|
|
|
95
102
|
return '';
|
|
96
103
|
})
|
|
@@ -98,55 +105,141 @@ export const outsideCodeFences = (body: string): string => {
|
|
|
98
105
|
};
|
|
99
106
|
|
|
100
107
|
/**
|
|
101
|
-
* The `Merge-After` cycles,
|
|
102
|
-
*
|
|
103
|
-
*
|
|
108
|
+
* The `Merge-After` cycles, each listing the numbers in the order the edges run
|
|
109
|
+
* and rotated to start at its lowest number so that the same cycle is always
|
|
110
|
+
* reported the same way.
|
|
111
|
+
*
|
|
112
|
+
* Every number that lies on any cycle appears in at least one entry — callers
|
|
113
|
+
* take `cycles.flat()` as the set of numbers on a cycle. Each entry is the
|
|
114
|
+
* shortest cycle through the first number, in key order, that no earlier entry
|
|
115
|
+
* covers, so a component with several loops yields one entry per loop needed to
|
|
116
|
+
* cover it rather than every loop it holds.
|
|
104
117
|
*
|
|
105
118
|
* A cycle is not a state anything here can resolve: every pull request on one
|
|
106
119
|
* waits for another that waits, in the end, for it. Naming it is the whole of
|
|
107
120
|
* what can be done about it — without this the members would each report
|
|
108
121
|
* "waiting on #N" for as long as the loop runs, with nothing saying why that
|
|
109
|
-
* never changes. A pull request that declares itself is a cycle of one, and
|
|
110
|
-
*
|
|
122
|
+
* never changes. A pull request that declares itself is a cycle of one, and is
|
|
123
|
+
* found by the same search.
|
|
111
124
|
*/
|
|
112
125
|
export const findMergeAfterCycles = (
|
|
113
126
|
dependencies: ReadonlyMap<number, readonly number[]>,
|
|
114
127
|
): readonly (readonly number[])[] => {
|
|
115
|
-
const
|
|
128
|
+
const mut_covered = new Set<number>();
|
|
116
129
|
|
|
117
130
|
const mut_cycles = new Map<string, readonly number[]>();
|
|
118
131
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// A back edge into the path being walked: everything from there on is
|
|
124
|
-
// the cycle.
|
|
125
|
-
const cycle = rotateToLowest(walked.slice(at));
|
|
132
|
+
for (const node of dependencies.keys()) {
|
|
133
|
+
if (mut_covered.has(node)) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
126
136
|
|
|
127
|
-
|
|
137
|
+
// A search per node rather than one depth-first walk: a walk that skips
|
|
138
|
+
// finished nodes misses a node whose only way back runs through one.
|
|
139
|
+
const cycle = shortestCycleThrough(node, dependencies);
|
|
128
140
|
|
|
129
|
-
|
|
141
|
+
if (cycle === undefined) {
|
|
142
|
+
continue;
|
|
130
143
|
}
|
|
131
144
|
|
|
132
|
-
|
|
133
|
-
|
|
145
|
+
for (const member of cycle) {
|
|
146
|
+
mut_covered.add(member);
|
|
134
147
|
}
|
|
135
148
|
|
|
136
|
-
|
|
137
|
-
|
|
149
|
+
const rotated = rotateToLowest(cycle);
|
|
150
|
+
|
|
151
|
+
mut_cycles.set(rotated.join(','), rotated);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return Array.from(mut_cycles.values());
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* `<Key>: #1901, #1903` in a body, anywhere in it, on as many lines as it
|
|
159
|
+
* likes. Only this exact trailer is read — a sentence mentioning another pull
|
|
160
|
+
* request is prose, and treating it as a declaration would make every
|
|
161
|
+
* cross-reference a constraint.
|
|
162
|
+
*/
|
|
163
|
+
const parseDeclaration = (
|
|
164
|
+
body: string,
|
|
165
|
+
key: 'merge-after' | 'start-after',
|
|
166
|
+
): readonly number[] =>
|
|
167
|
+
Arr.uniq(
|
|
168
|
+
outsideCodeFences(body)
|
|
169
|
+
.matchAll(DECLARATION_LINE[key])
|
|
170
|
+
.flatMap((line) =>
|
|
171
|
+
(line.groups?.['refs'] ?? '').matchAll(NUMBER_REF).flatMap((ref) => {
|
|
172
|
+
const parsed = Num.safeParseInt(ref.groups?.['number'] ?? '');
|
|
173
|
+
|
|
174
|
+
return Result.isOk(parsed) ? [parsed.value] : [];
|
|
175
|
+
}),
|
|
176
|
+
)
|
|
177
|
+
.toArray(),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
/** `start`, …, `end`, following `reachedFrom` back from `end`. */
|
|
181
|
+
const pathFromStart = (
|
|
182
|
+
start: number,
|
|
183
|
+
end: number,
|
|
184
|
+
reachedFrom: ReadonlyMap<number, number>,
|
|
185
|
+
): readonly number[] => {
|
|
186
|
+
const mut_reversed: number[] = [end];
|
|
187
|
+
|
|
188
|
+
let mut_node = end;
|
|
189
|
+
|
|
190
|
+
while (mut_node !== start) {
|
|
191
|
+
const previous = reachedFrom.get(mut_node);
|
|
192
|
+
|
|
193
|
+
if (previous === undefined) {
|
|
194
|
+
break;
|
|
138
195
|
}
|
|
139
196
|
|
|
140
|
-
|
|
141
|
-
// this node's subtree can be missed by the walk that comes next.
|
|
142
|
-
mut_finished.add(node);
|
|
143
|
-
};
|
|
197
|
+
mut_reversed.push(previous);
|
|
144
198
|
|
|
145
|
-
|
|
146
|
-
visit(node, []);
|
|
199
|
+
mut_node = previous;
|
|
147
200
|
}
|
|
148
201
|
|
|
149
|
-
return
|
|
202
|
+
return mut_reversed.toReversed();
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The shortest cycle through `start`, from `start` along the edges back to the
|
|
207
|
+
* node whose edge returns to it, or `undefined` when no path leads back.
|
|
208
|
+
*/
|
|
209
|
+
const shortestCycleThrough = (
|
|
210
|
+
start: number,
|
|
211
|
+
dependencies: ReadonlyMap<number, readonly number[]>,
|
|
212
|
+
): readonly number[] | undefined => {
|
|
213
|
+
// Each reached node, with the node it was first reached from.
|
|
214
|
+
const mut_reachedFrom = new Map<number, number>();
|
|
215
|
+
|
|
216
|
+
// Level by level: every node in the frontier is one edge further from `start`
|
|
217
|
+
// than the last, so the first edge back to `start` closes a shortest cycle.
|
|
218
|
+
let mut_frontier: readonly number[] = [start];
|
|
219
|
+
|
|
220
|
+
while (Arr.isNonEmpty(mut_frontier)) {
|
|
221
|
+
const mut_nextFrontier: number[] = [];
|
|
222
|
+
|
|
223
|
+
for (const node of mut_frontier) {
|
|
224
|
+
for (const next of dependencies.get(node) ?? []) {
|
|
225
|
+
if (next === start) {
|
|
226
|
+
return pathFromStart(start, node, mut_reachedFrom);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (mut_reachedFrom.has(next)) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
mut_reachedFrom.set(next, node);
|
|
234
|
+
|
|
235
|
+
mut_nextFrontier.push(next);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
mut_frontier = mut_nextFrontier;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return undefined;
|
|
150
243
|
};
|
|
151
244
|
|
|
152
245
|
const rotateToLowest = (cycle: readonly number[]): readonly number[] => {
|
package/src/merge-after.test.mts
CHANGED
|
@@ -51,9 +51,9 @@ describe(parseMergeAfter, () => {
|
|
|
51
51
|
);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
// The fence markers and the column they sit in are what these two are
|
|
55
|
-
//
|
|
56
|
-
//
|
|
54
|
+
// The fence markers and the column they sit in are what these two are about,
|
|
55
|
+
// so the fixtures are arrays of lines rather than a template literal: what
|
|
56
|
+
// `dedent` would remove is what is being tested.
|
|
57
57
|
test('ignores a trailer inside a fenced code block', () => {
|
|
58
58
|
assert.deepStrictEqual(
|
|
59
59
|
parseMergeAfter(
|
|
@@ -79,6 +79,64 @@ describe(parseMergeAfter, () => {
|
|
|
79
79
|
);
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
+
test('ignores the trailers inside a longer fence that holds a shorter one', () => {
|
|
83
|
+
assert.deepStrictEqual(
|
|
84
|
+
parseMergeAfter(
|
|
85
|
+
[
|
|
86
|
+
'````markdown',
|
|
87
|
+
'```',
|
|
88
|
+
'Merge-After: #1901',
|
|
89
|
+
'```',
|
|
90
|
+
'Merge-After: #1902',
|
|
91
|
+
'````',
|
|
92
|
+
'Merge-After: #1903',
|
|
93
|
+
'',
|
|
94
|
+
].join('\n'),
|
|
95
|
+
),
|
|
96
|
+
[1903],
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('ignores a trailer after a backtick fence inside a tilde fence', () => {
|
|
101
|
+
assert.deepStrictEqual(
|
|
102
|
+
parseMergeAfter(
|
|
103
|
+
['~~~', '```', 'Merge-After: #1901', '~~~', ''].join('\n'),
|
|
104
|
+
),
|
|
105
|
+
[],
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('does not close a fence on a line with an info string', () => {
|
|
110
|
+
assert.deepStrictEqual(
|
|
111
|
+
parseMergeAfter(
|
|
112
|
+
['```', '```ts', 'Merge-After: #1901', '```', ''].join('\n'),
|
|
113
|
+
),
|
|
114
|
+
[],
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('closes a fence on a longer run of the same character', () => {
|
|
119
|
+
assert.deepStrictEqual(
|
|
120
|
+
parseMergeAfter(
|
|
121
|
+
['```', 'Merge-After: #1901', '`````', 'Merge-After: #1903', ''].join(
|
|
122
|
+
'\n',
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
[1903],
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('closes a fence on a CRLF line', () => {
|
|
130
|
+
assert.deepStrictEqual(
|
|
131
|
+
parseMergeAfter(
|
|
132
|
+
['```', 'Merge-After: #1901', '```', 'Merge-After: #1903', ''].join(
|
|
133
|
+
'\r\n',
|
|
134
|
+
),
|
|
135
|
+
),
|
|
136
|
+
[1903],
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
82
140
|
test('reads a body with the CRLF line endings GitHub returns', () => {
|
|
83
141
|
assert.deepStrictEqual(
|
|
84
142
|
parseMergeAfter('Adds the parser.\r\n\r\nMerge-After: #1901\r\n'),
|
|
@@ -218,6 +276,25 @@ describe(findMergeAfterCycles, () => {
|
|
|
218
276
|
);
|
|
219
277
|
});
|
|
220
278
|
|
|
279
|
+
test('reports a member whose only way back runs through a finished node', () => {
|
|
280
|
+
// 3 → 2 → 1 → 3 is a cycle as well as 1 ⇄ 2; a walk from 1 finishes 2
|
|
281
|
+
// before it reaches 3, so 3's edge into 2 must still put 3 on a cycle.
|
|
282
|
+
const cycles = findMergeAfterCycles(
|
|
283
|
+
new Map<number, readonly number[]>([
|
|
284
|
+
[1, [2, 3]],
|
|
285
|
+
[2, [1]],
|
|
286
|
+
[3, [2]],
|
|
287
|
+
]),
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
assert.deepStrictEqual(cycles, [
|
|
291
|
+
[1, 2],
|
|
292
|
+
[1, 3, 2],
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
assert.deepStrictEqual(new Set(cycles.flat()), new Set<number>([1, 2, 3]));
|
|
296
|
+
});
|
|
297
|
+
|
|
221
298
|
test('follows an edge to a pull request that declared nothing', () => {
|
|
222
299
|
assert.deepStrictEqual(
|
|
223
300
|
findMergeAfterCycles(new Map<number, readonly number[]>([[1, [2]]])),
|
package/src/queue-comments.mts
CHANGED
|
@@ -41,8 +41,8 @@ export type QueueComment<A> = Readonly<{
|
|
|
41
41
|
}>;
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Reads `comments`, oldest first, for the oldest comment of each kind: each
|
|
45
|
-
*
|
|
44
|
+
* Reads `comments`, oldest first, for the oldest comment of each kind: each is
|
|
45
|
+
* one comment edited in place, and of several the oldest is the one.
|
|
46
46
|
*/
|
|
47
47
|
export const readQueueComments = (
|
|
48
48
|
comments: readonly OwnComment[],
|