1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
//! The PR side of the labels/project/milestone standard (#7274).
//!
//! Why: until the owner ruling of 2026-09-09 the standard reached issues only.
//! A PR `tm pr open` created carried the assignee, `trusty-mpm` and
//! `ws/<session>` and nothing else — no component label for the crates its own
//! diff touched, and no project or milestone from the issue its `Refs #N`
//! named. The two artifacts then sorted differently on every board they shared.
//! Neither value is typed by anyone: the component labels are read off the
//! diff and the project and milestone are read off the issue, so both belong
//! in one derivation the command runs rather than in a checklist an agent
//! remembers.
//!
//! What: [`first_linked_issue`] finds the issue the body links, and [`plan`] is
//! the whole decision — a pure function of (that issue's milestone and
//! projects, the changed paths, the workspace's crate ownership) with no `gh`
//! and no filesystem in it. Every gap it finds becomes a note the caller
//! prints, never a failure: a PR that cannot inherit a milestone still opens.
//! [`edit_argv`] renders the single `gh pr edit` that applies the result.
//!
//! Test: the sibling `tests.rs` — `metadata_*`, `open_applies_pr_metadata`,
//! `open_without_refs_says_so`.
use CrateOwnership;
use IssueFacts;
use argv;
/// Whether the linked number turned out to name an issue or a pull request.
///
/// Why (#7786): `tm pr open --issue N` and a body's own link line both take a
/// bare number, and a number that names a PR made `gh issue view` 404 — so the
/// milestone and projects that PR carried were never read. Knowing which of
/// the two was read is what lets the notes name the right artifact instead of
/// calling a pull request an issue.
/// What: the noun the notes and warnings print.
/// Test: `pr_7786_a_pr_ref_inherits_through_gh_pr_view`.
pub
/// What the linked issue (or pull request) contributes to the PR.
///
/// Why: `plan` must be testable without a `gh`, so the issue arrives as data.
/// What: the number, which artifact it turned out to be, its single milestone,
/// and its project titles.
/// Test: `metadata_inherits_milestone_and_projects`.
pub
/// What the caller learned about the `Refs #N` issue.
///
/// Why (#7274 round 2): a body with no `Refs` line and a body whose `Refs`
/// issue could not be read both used to arrive as `None`, so [`plan`] printed
/// "the body carries no `Refs #N`" for a body that carried one. The two cases
/// have different causes and different fixes, so they arrive as different
/// values.
/// What: `Absent` when no `Refs` line exists, `Unreadable` when one exists and
/// the `gh issue view` behind it failed, `Found` when the read succeeded.
/// Test: `metadata_notes_an_unreadable_refs_issue`,
/// `open_notes_an_unreadable_refs_issue`.
pub
/// What the caller learned about the diff.
///
/// Why (#7274 round 2): a diff `git` refused to produce and a diff that no
/// crate owns both used to arrive as an empty slice, so [`plan`] blamed the
/// workspace for a `git` failure.
/// What: `Read` carries the paths, however few; `Unreadable` says the read
/// itself failed.
/// Test: `metadata_notes_an_unreadable_diff`, `open_notes_an_unreadable_diff`.
pub
/// The labels, milestone and projects a PR earns, plus what it could not get.
///
/// Why: the caller needs both halves — what to apply, and what to say it could
/// not apply — and computing them together is what keeps a silent gap from
/// looking like a satisfied requirement.
/// What: `labels` are component labels only (the shipped `trusty-mpm` and
/// `ws/<session>` are already on the create call); `notes` are one line each,
/// printed verbatim.
/// Test: `metadata_without_refs_applies_nothing`, `metadata_multi_crate_diff`.
pub
/// Decide a PR's component labels, milestone and projects.
///
/// Why: this is the standard itself, stated once as code. Both inputs are
/// derived — the labels from the diff, the milestone and projects from the
/// linked issue — so there is nothing here for a caller to override, and
/// nothing that needs a network to decide.
/// What: component labels are the crates the changed paths touch. A `Found`
/// issue contributes its milestone and its projects. Each gap adds a note
/// rather than an error — a PR whose diff no crate owns, or whose issue has no
/// milestone, still opens — and the note names the actual cause, so a failed
/// read never reads as an empty answer (#7274 round 2).
/// Test: `metadata_inherits_milestone_and_projects`,
/// `metadata_without_refs_applies_nothing`, `metadata_multi_crate_diff`,
/// `metadata_notes_an_issue_with_no_milestone`,
/// `metadata_notes_an_unreadable_diff`, `metadata_notes_an_unreadable_refs_issue`.
pub
/// The single `gh pr edit` that applies a [`PrMetadata`].
///
/// Why: one call rather than three keeps the apply atomic from the operator's
/// point of view — either the PR ends up carrying the standard or the one
/// warning names what failed. `gh pr edit` takes `--add-label`, `--milestone`
/// and `--add-project` together (gh 2.98), so the split would buy nothing.
/// What: `gh pr edit <pr> [--repo r] [--title t] [--add-label l]…
/// [--milestone m] [--add-project p]…`. Callers must not call it on an empty
/// plan.
/// Test: `metadata_edit_argv_carries_every_field`.
pub
/// The reference keyword that does NOT auto-close, lowercased.
const REFS_KEYWORD: &str = "refs";
/// Does `word` open a line that links this PR to an issue?
///
/// Why (#7869): the inheritance must not depend on which sanctioned keyword
/// the body used. `Closes #N` (what `--closes` writes) links the issue exactly
/// as `Refs #N` does, and reading only `Refs` opened every `--closes` PR with
/// no project and no milestone. GitHub's own closing keywords come from
/// [`super::body::CLOSING_KEYWORDS`], so the two rules cannot drift apart.
/// What: `refs` or any closing keyword, compared lowercase.
/// Test: `pr_7869_a_closes_link_inherits_the_issues_metadata`,
/// `metadata_finds_the_first_refs`.
/// The issue number of the body's first link line.
///
/// Why: that line IS a PR's relationship — the issue side keeps parent/child
/// and blocked-by, and a PR inherits from exactly one issue — so the FIRST
/// reference decides, and a body naming several is not an error.
/// What: scans lines for a leading link keyword ([`is_link_keyword`]),
/// tolerating an `owner/repo#N` qualifier, and returns the number.
/// Case-insensitive on the keyword; a keyword appearing mid-sentence is
/// ignored, since the contract puts the link on its own line. Lines inside a
/// fenced code block are skipped: a body that quotes the convention in a sample
/// commit message before stating its own link line would otherwise inherit the
/// sample's issue (#7274 round 2).
/// Test: `metadata_finds_the_first_refs`, `metadata_finds_a_qualified_refs`,
/// `metadata_ignores_refs_mid_sentence`, `metadata_ignores_refs_inside_a_fence`,
/// `pr_7869_a_closes_link_inherits_the_issues_metadata`.
pub
/// The `--json` fields a linked PULL REQUEST is read with (#7786).
///
/// Why: `issue_audit::AUDIT_JSON_FIELDS` cannot be reused for a PR —
/// `parent`, `blockedBy` and `subIssues` are issue-only, and `gh pr view`
/// rejects the whole call when asked for one. These three are exactly what
/// [`RefsIssue::from_facts`] reads.
/// Test: `metadata_pr_view_argv_asks_only_for_pr_fields`.
pub const PR_VIEW_JSON_FIELDS: &str = "number,milestone,projectItems";
/// The `gh pr view` that reads a linked pull request's inheritable metadata.
///
/// Why (#7786): a link line may name a PR rather than an issue, and
/// `gh issue view` answers that with `Could not resolve to an Issue` — so the
/// milestone and projects that PR carried were dropped with no second attempt.
/// What: `pr view <n> --json <PR_VIEW_JSON_FIELDS>`.
/// Test: `metadata_pr_view_argv_asks_only_for_pr_fields`,
/// `pr_7786_a_pr_ref_inherits_through_gh_pr_view`.
pub