@knowttl/gitea-axi 0.1.1 → 0.1.3
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/LICENSE +21 -0
- package/README.md +41 -10
- package/dist/api.js +21 -11
- package/dist/credentials.js +9 -0
- package/dist/git.js +3 -3
- package/dist/main.js +6 -6
- package/dist/output.js +56 -0
- package/dist/pulls.js +42 -18
- package/dist/reads.js +21 -11
- package/dist/registry.js +1 -1
- package/dist/search.js +4 -2
- package/dist/stacks.js +61 -8
- package/dist/transport.js +4 -3
- package/dist/writes.js +63 -18
- package/generated/help.txt +1 -1
- package/generated/host-inventory.json +1 -1
- package/generated/routes.json +425 -0
- package/package.json +2 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) knowttl
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -71,6 +71,7 @@ Full workflow run APIs require Gitea 1.24 or later; the retained workflow, artif
|
|
|
71
71
|
### Search
|
|
72
72
|
|
|
73
73
|
`search issues|prs|repos|commits "text"` searches at most 20 pages and 1000 candidates per collection, then filters, sorts and applies the total output limit (default 1000).
|
|
74
|
+
Search sorts creation and update times chronologically across time zone offsets before applying the output limit.
|
|
74
75
|
Issue and PR searches support exact arbitrary author and assignee names and comma-separated label ANY matching; an unknown label alone matches nothing.
|
|
75
76
|
Use `--repo OWNER/NAME` for repository scope or `--owner OWNER` for issue, PR and repository searches.
|
|
76
77
|
Repository search supports `--language` and integer `--stars` comparisons.
|
|
@@ -97,27 +98,40 @@ gitea-axi config path
|
|
|
97
98
|
gitea-axi doctor
|
|
98
99
|
```
|
|
99
100
|
|
|
101
|
+
Raw `api GET` contents reads support root and nested directories, returning directory arrays (including empty arrays) or individual file, symlink and submodule objects under `data`.
|
|
102
|
+
Nullable contents and link fields are accepted while non-null values retain their schema type checks.
|
|
103
|
+
Use literal slashes for nested paths such as `repos/OWNER/NAME/contents/src/main.ts`, `repos/OWNER/NAME/editorconfig/src/main.ts` and Git ref filters such as `repos/OWNER/NAME/git/refs/heads/main`.
|
|
104
|
+
This multi-segment support applies only to those GET routes; other route parameters and raw writes retain single-segment matching.
|
|
105
|
+
Encoded route separators and traversal remain refused.
|
|
106
|
+
|
|
100
107
|
Anonymous reads require an explicit origin; repository selection follows the rules below.
|
|
101
108
|
Anonymous HTTP reads require `--allow-insecure-http` for that selected origin.
|
|
102
109
|
The tool follows no redirects and never imports tea or Git credentials.
|
|
103
110
|
Output uses TOON; structured errors use stdout, with exit 1 for operational failures and exit 2 for usage errors.
|
|
104
111
|
Business reads share a 30-second deadline, except check watches use their explicit timeout; each request has a 2,000,000-byte response limit.
|
|
112
|
+
The check-watch timeout includes initial repository reads; polling cannot extend the client's deadline.
|
|
105
113
|
`repo view` instead gives each of its server version, specification and repository requests a separate 30-second timeout.
|
|
106
114
|
An explicit repository wins over a matching local origin remote; a remote never changes the selected profile.
|
|
107
|
-
`-R` aliases `--repo
|
|
108
|
-
For the default dashboard, use `gitea-axi --repo OWNER/NAME
|
|
115
|
+
`-R` aliases `--repo`, with the same duplicate-selector checks.
|
|
116
|
+
For the default dashboard, use `gitea-axi --repo OWNER/NAME` or `gitea-axi -R OWNER/NAME`.
|
|
109
117
|
Hosted repository reads require an explicit repository.
|
|
110
|
-
Lists default to 30 returned records, except `run list` and `release list` default to 10; collection scans
|
|
118
|
+
Lists default to 30 returned records, except `run list` and `release list` default to 10; collection scans stop at 20 pages or at least 1000 unique records and retain at most 1000.
|
|
119
|
+
Automatic page queries apply only to routes whose retained Gitea specification declares a `page` parameter.
|
|
120
|
+
Unpaginated collections, including inline review comments and attachment metadata, are complete unless the server supplies a continuation link or the collection exceeds the record bound; a response of 50 records alone does not imply another page.
|
|
111
121
|
Issue and PR lists filter before globally sorting the collected records and applying the return limit.
|
|
112
122
|
Milestone selectors use exact names, including numeric-looking names.
|
|
113
|
-
List output distinguishes unknown
|
|
123
|
+
List output distinguishes unknown totals from known totals and reports continuation and truncation.
|
|
124
|
+
For an unpaginated collection without a continuation link, a missing server total is inferred from the unique records received.
|
|
114
125
|
Issue, PR and run lists expose `unfiltered_total` as the server count before client-side filtering; `total` is the filtered count when the bounded read is complete, otherwise null.
|
|
115
126
|
Truncated output includes guidance to raise `--limit` (maximum 1000), or use raw API pagination when the bounded read is incomplete.
|
|
116
127
|
The dashboard shows a compact repository projection (ID, full name, visibility, default branch, open issue and PR counts, and update time), plus up to five issues, PRs, labels, releases and task summaries per section.
|
|
117
128
|
Dashboard sections report permission failures as partial results and point truncated sections to the corresponding standalone list command with `--repo` and `--limit`.
|
|
118
129
|
Issue and PR bodies default to 1000 characters, diffs to 10000 characters, and rendered output to 100000 characters.
|
|
119
130
|
`--full` expands bodies and diffs within the response and output ceilings.
|
|
120
|
-
|
|
131
|
+
Diffs retain an unchanged source prefix sized to fit the serialized output, including escaping and metadata; `chars` reports the original length and `truncated` indicates omitted text.
|
|
132
|
+
`pr checks` exits 8 for pending checks and 1 for failed or absent checks; watches require an explicit deadline.
|
|
133
|
+
During check polling, SIGINT or SIGTERM aborts in-flight PR and status requests, including paginated responses, and returns `cancelled: true` with exit code 8.
|
|
134
|
+
Cancellation preserves the last completed check result; before any completed result, it returns empty items and zero counts, with the head SHA if already read, otherwise null.
|
|
121
135
|
Commits without statuses return an empty check result; watches continue waiting for checks to register until the deadline or cancellation.
|
|
122
136
|
`--fields` refuses unknown names and fields without an evidenced Gitea equivalent, and cannot combine with detail expansion flags.
|
|
123
137
|
PR statusCheckRollup explicitly identifies partial commit statuses, and `fullDatabaseId` is a numeric vendor ID rather than a GraphQL node ID.
|
|
@@ -145,8 +159,11 @@ Issue and label deletion require a complete collection and at least one remainin
|
|
|
145
159
|
|
|
146
160
|
### Pull requests
|
|
147
161
|
|
|
162
|
+
`pr view` includes both user and team review requests in `reviewRequests`: users have `login` and `name`, while teams have `type: team`, `id` and `name`.
|
|
148
163
|
Pull request mutations include `create`, `edit`, `close`, `reopen`, `comment`, `update-branch` and `merge`.
|
|
149
164
|
Creation requires explicit `--title`, `--base` and `--head` branch names in the selected repository.
|
|
165
|
+
PR creation, base edits, updates and merges support slash-containing branch names such as `work/topic` and `release/stable`, resolving exact Git refs rather than prefix matches.
|
|
166
|
+
Branch resolution binds commit identities into preview and confirmation while preserving API path separator and traversal protections.
|
|
150
167
|
Cross-repository heads, draft and project inputs remain unavailable.
|
|
151
168
|
Create accepts repeatable `--reviewer` user names and `--team-reviewer` team names, plus issue-style assignees, labels and milestone selectors.
|
|
152
169
|
Edit accepts `--base`, text fields and assignee/label deltas, plus repeatable `--add-reviewer`, `--remove-reviewer`, `--add-team-reviewer` and `--remove-team-reviewer` deltas.
|
|
@@ -158,6 +175,7 @@ If the reviewer step fails after creation, the error reports the created PR ID a
|
|
|
158
175
|
Close with `--comment` records closure and comment as separate steps and reports partial outcomes without retrying.
|
|
159
176
|
Successful writes verify the same PR identity, expected fields and memberships through fresh reads.
|
|
160
177
|
Update-branch verifies that the resulting head contains both its previous head and base commits; conflicts never report completion.
|
|
178
|
+
Verification stops once both commits are proven present, so older base history need not be traversed; it reads at most 1000 commits and refuses completion if that bound prevents proof.
|
|
161
179
|
Merge requires exactly one of `--merge`, `--squash`, `--rebase` or `--method merge|squash|rebase`, and a full `--match-head-commit` SHA.
|
|
162
180
|
It sends the vendor `head_commit_id` precondition and verifies the closed, merged PR, base branch tip and final commit topology.
|
|
163
181
|
Merge commits retain the previous base and checked head as parents; squash uses one new commit on the previous base; rebase verifies the rewritten linear chain and original commit messages.
|
|
@@ -250,9 +268,16 @@ gitea-axi issue close 42 --repo OWNER/NAME --comment 'Resolved' --allow-write is
|
|
|
250
268
|
gitea-axi label create --repo OWNER/NAME --name bug --color ff0000 --allow-write issueCreateLabel --preview
|
|
251
269
|
```
|
|
252
270
|
|
|
253
|
-
The preview identifies the exact operations
|
|
271
|
+
The preview identifies the exact operations and payload without sending a mutation.
|
|
272
|
+
To fit the rendered output ceiling, previews, write results and reconciliation output may summarize `preconditions`, then `observations`, then `results`, stopping as soon as the output fits.
|
|
273
|
+
Summarized output sets `truncated: true` and lists affected fields in `truncated_fields`.
|
|
274
|
+
Summaries report `summary: true` with `total_items`, `total_fields` or `total_chars`; object summaries retain numeric `id` and `number` when present.
|
|
275
|
+
Summarized observations retain paths, and observations and results retain at most ten entries with their original counts in `observations_total` and `results_total`.
|
|
276
|
+
Preview steps and requested payloads remain complete subject to credential redaction; output that still exceeds the ceiling fails with `OUTPUT_LIMIT`.
|
|
254
277
|
Confirmation binds the credential revision, origin, target, operations, canonical payload and preconditions.
|
|
278
|
+
Display summaries do not weaken confirmation binding or readback verification, which use the complete data.
|
|
255
279
|
Changing any binding requires a fresh preview.
|
|
280
|
+
Organization casing must match the preview exactly.
|
|
256
281
|
For issues that remain present, successful named writes verify identity and the expected final title, body, assignees, labels, milestone and state through fresh reads.
|
|
257
282
|
Pin changes verify the complete expected pin membership; deletion verifies absence of the selected identity and preservation of all remaining records.
|
|
258
283
|
A mismatched or failed verification reports an unknown outcome.
|
|
@@ -272,8 +297,13 @@ Each attempt is persisted before dispatch.
|
|
|
272
297
|
API timeouts, interrupted requests and partial failures return `WRITE_OUTCOME_UNKNOWN`, completed/pending step information and an exact `gitea-axi write reconcile CONFIRMATION_ID` command.
|
|
273
298
|
Dispatched local Git failures retain their specific error code and include reconciliation information; stack recovery follows the [metadata and recovery contract](docs/local-stack-contract.md).
|
|
274
299
|
Reconciliation performs complete metadata reads for inspection and never repeats a mutation.
|
|
300
|
+
If its display is summarized as described above, use targeted reads to inspect the exact identity and requested fields before authorizing another attempt.
|
|
301
|
+
Raw repository edits with a nonempty `name` verify and reconcile through the repository's immutable ID, so a rename does not depend on the old URL.
|
|
302
|
+
Raw wiki edits use the returned escaped `sub_url` for verification and reconciliation; if the response is lost or its page identity is unusable, reconciliation reads the bounded wiki page list instead.
|
|
303
|
+
These read targets preserve the original confirmation binding and attempted-effect identity.
|
|
275
304
|
An unresolved dispatched attempt cannot be replayed by reusing its confirmation or obtaining another preview for the same effect.
|
|
276
|
-
Replay guards also treat repository owner and
|
|
305
|
+
Replay guards also treat repository owner, repository name and organization name case variants as the same identity for named and raw writes.
|
|
306
|
+
Unresolved organization attempts from older versions conservatively block all writes to the same organization at the same origin, including different names or payloads, and return the original attempt's reconciliation command.
|
|
277
307
|
Keep its private receipts until the outcome has been investigated.
|
|
278
308
|
Native Windows write protection and live write commissioning remain unverified; Windows writes are refused.
|
|
279
309
|
|
|
@@ -292,7 +322,7 @@ Incomplete environment profiles report missing variables; profile schema errors
|
|
|
292
322
|
For file profiles, an explicit profile must match; otherwise exactly one matching profile is required, ignoring `defaultProfile`.
|
|
293
323
|
Without a host selector, file selection uses the explicit profile, otherwise the file's `defaultProfile` or its sole profile; multiple profiles without a selection or default are refused.
|
|
294
324
|
A host selector cannot construct an anonymous origin, bypass tokenOrigin binding or enable HTTP.
|
|
295
|
-
Search commands
|
|
325
|
+
Search commands support `--hostname` and `GH_HOST` with the same profile selection and credential binding checks.
|
|
296
326
|
Hosted invocations refuse credential/configuration overrides and ignore ambient `GH_HOST` for the injected Artium profile.
|
|
297
327
|
On Linux, the config file must be owned by the invoking account and must not be group- or world-writable.
|
|
298
328
|
Config files and file SecretRefs must be regular files; symlinks are refused.
|
|
@@ -301,6 +331,7 @@ See [the credential schema](axi-credential.schema.json) for profile fields and e
|
|
|
301
331
|
Inline tokens and secret arguments are refused.
|
|
302
332
|
Resolved credentials are redacted from response and error string values before TOON serialization, including their JSON-escaped and URL-encoded forms.
|
|
303
333
|
Redaction also masks recognized secret fields, secret query values, URL credentials, JWTs, private keys and account keys.
|
|
334
|
+
Generated confirmation IDs and fixed confirmation/reconciliation instructions remain intact when they overlap a protected value; payloads and upstream response text remain subject to redaction.
|
|
304
335
|
The complete environment profile uses `GITEA_AXI_ORIGIN`, `GITEA_AXI_TOKEN_ORIGIN` and a JSON SecretRef in `GITEA_AXI_TOKEN`.
|
|
305
336
|
Set `GITEA_AXI_ALLOW_INSECURE_HTTP=true` to permit HTTP for that environment profile; only the exact values `true` and `false` are accepted.
|
|
306
337
|
HTTP requires `allowInsecureHttp: true` on that exact profile; a runtime origin override cannot reuse the token for another origin.
|
|
@@ -389,7 +420,7 @@ Set `GITEA_AXI_REPORT` to retain their reports at a chosen path.
|
|
|
389
420
|
Their default reports go to unique files in the OS temporary directory; temporary installation files are removed.
|
|
390
421
|
For S5, set `GITEA_AXI_REPORT_DIR` to an existing directory to retain `S5.1.json` or `S5.2.json`; otherwise only a console summary is emitted.
|
|
391
422
|
|
|
392
|
-
`npm test` first runs the package-contract check in `scripts/verify-package.ts`, then the offline live-read regressions in `scripts/verify-live-reads.ts`, then the installed-package checks with local Gitea fakes for S1.1, S2.1 through S2.4, S3.1 through S3.5, S4.1 through S4.3, S5.1, S5.2 and S6.1 through S6.4 under an inherited outbound network guard.
|
|
423
|
+
`npm test` first runs the package-contract check in `scripts/verify-package.ts`, then the native Git workspace checks in `scripts/verify-git.ts`, then the offline live-read regressions in `scripts/verify-live-reads.ts`, then the installed-package checks with local Gitea fakes for S1.1, S2.1 through S2.4, S3.1 through S3.5, S4.1 through S4.3, S5.1, S5.2 and S6.1 through S6.4 under an inherited outbound network guard.
|
|
393
424
|
The package-contract check builds, packs and installs offline, validates the tarball file list and bin entry, smoke-tests the installed `--help` and `--version`, parses the consumed JSON artifacts and imports the modules Artium consumes from the installed package.
|
|
394
425
|
Run it independently with `node --import ./scripts/deny-network.ts scripts/verify-package.ts`.
|
|
395
426
|
Each S2 task also has an independent entrypoint, for example `node scripts/verify.mjs --task S2.3`.
|
|
@@ -412,4 +443,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution rules.
|
|
|
412
443
|
|
|
413
444
|
## License
|
|
414
445
|
|
|
415
|
-
|
|
446
|
+
This project is licensed under the MIT License. See [LICENSE](LICENSE).
|
package/dist/api.js
CHANGED
|
@@ -30,7 +30,11 @@ for (const definition of Object.values(vendor.definitions)) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
const requestDefinitions = structuredClone(vendor.definitions);
|
|
33
|
-
for (const [name, fields] of Object.entries({
|
|
33
|
+
for (const [name, fields] of Object.entries({
|
|
34
|
+
PullRequest: ['merge_commit_sha'],
|
|
35
|
+
ContentsResponse: ['submodule_git_url', 'target', 'content', 'encoding', 'download_url', 'html_url', 'git_url'],
|
|
36
|
+
FileLinksResponse: ['git', 'html'],
|
|
37
|
+
})) {
|
|
34
38
|
for (const field of fields) {
|
|
35
39
|
const properties = vendor.definitions[name].properties;
|
|
36
40
|
properties[field] = { anyOf: [properties[field], { type: 'null' }] };
|
|
@@ -45,7 +49,11 @@ export function object(value) {
|
|
|
45
49
|
}
|
|
46
50
|
export function effectFor(method, path) {
|
|
47
51
|
path = canonicalPath(path);
|
|
48
|
-
return routes.find(route => route.method === method && new RegExp('^' + route.path.split(/(\{[^}]+\})/).map(part =>
|
|
52
|
+
return routes.find(route => route.method === method && new RegExp('^' + route.path.split(/(\{[^}]+\})/).map(part => {
|
|
53
|
+
if (/^\{[^}]+\}$/.test(part))
|
|
54
|
+
return route.multiSegmentParameters?.includes(part.slice(1, -1)) ? '[^/]+(?:/[^/]+)*' : '[^/]+';
|
|
55
|
+
return part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
56
|
+
}).join('') + '$').test(path.split('?')[0]));
|
|
49
57
|
}
|
|
50
58
|
export function canonicalPath(path) {
|
|
51
59
|
const pathname = path.split('?')[0];
|
|
@@ -96,7 +104,8 @@ export function validate(path, value) {
|
|
|
96
104
|
if (route.schema) {
|
|
97
105
|
let validator = validators.get(route.path);
|
|
98
106
|
if (!validator) {
|
|
99
|
-
|
|
107
|
+
const schema = route.operation === 'repoGetContents' ? { anyOf: [route.schema, { type: 'array', items: route.schema }] } : route.schema;
|
|
108
|
+
validator = ajv.compile({ ...schema, definitions: vendor.definitions });
|
|
100
109
|
validators.set(route.path, validator);
|
|
101
110
|
}
|
|
102
111
|
if (!validator(value))
|
|
@@ -127,9 +136,9 @@ export function rawPath(input, method = 'GET') {
|
|
|
127
136
|
throw new CliError('READ_ONLY', 'Unclassified API route');
|
|
128
137
|
return path + (query ? '?' + query : '');
|
|
129
138
|
}
|
|
130
|
-
export async function read(client, path) {
|
|
139
|
+
export async function read(client, path, signal) {
|
|
131
140
|
routeFor(path.split('?')[0]);
|
|
132
|
-
const value = await client.get(path);
|
|
141
|
+
const value = await client.get(path, signal);
|
|
133
142
|
return validate(path.split('?')[0], value);
|
|
134
143
|
}
|
|
135
144
|
export async function resolveCommit(client, base, ref) {
|
|
@@ -157,8 +166,9 @@ export async function rawRead(client, path) {
|
|
|
157
166
|
const count = response.headers.get('x-total-count');
|
|
158
167
|
return { data, total: count && /^\d+$/.test(count) ? Number(count) : null, next: nextPage(response.headers, path, client.origin), truncated: false };
|
|
159
168
|
}
|
|
160
|
-
export async function collect(client, path, envelope) {
|
|
161
|
-
|
|
169
|
+
export async function collect(client, path, envelope, signal) {
|
|
170
|
+
const paginated = routeFor(path).paginated;
|
|
171
|
+
let next = paginated ? path + (path.includes('?') ? '&' : '?') + 'limit=50&page=1' : path;
|
|
162
172
|
const seenPages = new Set();
|
|
163
173
|
const items = new Map();
|
|
164
174
|
let total = null;
|
|
@@ -166,14 +176,14 @@ export async function collect(client, path, envelope) {
|
|
|
166
176
|
if (seenPages.has(next))
|
|
167
177
|
throw new CliError('INVALID_RESPONSE', 'Gitea repeated a pagination URL');
|
|
168
178
|
seenPages.add(next);
|
|
169
|
-
const response = await client.request(next);
|
|
179
|
+
const response = await client.request(next, false, undefined, signal);
|
|
170
180
|
const data = validate(new URL(next, client.origin).pathname, response.data);
|
|
171
181
|
const rows = envelope ? object(data)[envelope] : data;
|
|
172
182
|
if (!Array.isArray(rows))
|
|
173
183
|
throw new CliError('INVALID_RESPONSE', 'Expected a Gitea list' + (envelope ? ' envelope ' + envelope : ''));
|
|
174
184
|
for (const value of rows) {
|
|
175
185
|
const row = object(value);
|
|
176
|
-
const id = /\/(?:tags|actions\/(?:secrets|variables))$/.test(path.split('?')[0]) ? row.name : row.id ?? row.number ?? row.sha ?? row.filename ?? (row.user && row.content ? String(object(row.user).id) + ':' + row.content : undefined);
|
|
186
|
+
const id = /\/(?:tags|actions\/(?:secrets|variables))$/.test(path.split('?')[0]) ? row.name : row.id ?? row.number ?? row.sha ?? row.ref ?? row.sub_url ?? row.filename ?? (row.user && row.content ? String(object(row.user).id) + ':' + row.content : undefined);
|
|
177
187
|
if (typeof id !== 'number' && typeof id !== 'string')
|
|
178
188
|
throw new CliError('INVALID_RESPONSE', 'List record has no identity');
|
|
179
189
|
items.set(String(id), row);
|
|
@@ -185,11 +195,11 @@ export async function collect(client, path, envelope) {
|
|
|
185
195
|
total = object(data).total_count;
|
|
186
196
|
const link = response.headers.get('link');
|
|
187
197
|
next = nextPage(response.headers, path, client.origin);
|
|
188
|
-
if (!next && !link && rows.length >= 50) {
|
|
198
|
+
if (paginated && !next && !link && rows.length >= 50) {
|
|
189
199
|
const url = new URL([...seenPages].at(-1), client.origin);
|
|
190
200
|
url.searchParams.set('page', String(Number(url.searchParams.get('page')) + 1));
|
|
191
201
|
next = url.pathname + url.search;
|
|
192
202
|
}
|
|
193
203
|
}
|
|
194
|
-
return { items: [...items.values()], total, next, truncated: next !== null };
|
|
204
|
+
return { items: [...items.values()].slice(0, 1000), total: total ?? (!paginated && !next ? items.size : null), next, truncated: next !== null || items.size > 1000 };
|
|
195
205
|
}
|
package/dist/credentials.js
CHANGED
|
@@ -35,6 +35,11 @@ function validateProfile(profile) {
|
|
|
35
35
|
const secrets = [];
|
|
36
36
|
export function registerSecret(value) { if (value)
|
|
37
37
|
secrets.push(value); }
|
|
38
|
+
const controls = new Set();
|
|
39
|
+
export function controlText(value) {
|
|
40
|
+
controls.add(value);
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
38
43
|
export function redact(value) {
|
|
39
44
|
if (Array.isArray(value))
|
|
40
45
|
return value.map(redact);
|
|
@@ -43,6 +48,10 @@ export function redact(value) {
|
|
|
43
48
|
/^(?:token|access_token|refresh_token|password|secret|authorization|private_key)$/i.test(key) ? '[REDACTED]' : redact(item)]));
|
|
44
49
|
if (typeof value !== 'string')
|
|
45
50
|
return value;
|
|
51
|
+
// Callers register only generated control text, never payloads or upstream response text.
|
|
52
|
+
const preserved = [...controls].filter(text => value.includes(text)).sort((a, b) => b.length - a.length);
|
|
53
|
+
if (preserved.length)
|
|
54
|
+
return value.split(new RegExp('(' + preserved.map(text => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|') + ')')).map(part => controls.has(part) ? part : redact(part)).join('');
|
|
46
55
|
let output = value;
|
|
47
56
|
for (const secret of secrets) {
|
|
48
57
|
for (const form of [secret, JSON.stringify(secret).slice(1, -1), encodeURIComponent(secret)]) {
|
package/dist/git.js
CHANGED
|
@@ -43,9 +43,9 @@ export class Git {
|
|
|
43
43
|
if (!isAbsolute(this.cwd) || await realpath(this.cwd) !== this.cwd || await this.run(['rev-parse', '--show-toplevel']) !== this.cwd) {
|
|
44
44
|
throw new CliError('USAGE', '--workspace must be the absolute native repository root', 2);
|
|
45
45
|
}
|
|
46
|
-
const config = await this.run(['config', '--
|
|
47
|
-
|
|
48
|
-
if (config.split('\n').some(key => /^(?:url\.|http\.|credential\.|include\.|includeif\.|filter\.|diff\.|core\.sshcommand$|remote\..*\.(?:proxy|uploadpack|receivepack|pushurl|vcs)$)/i.test(key))) {
|
|
46
|
+
const config = await this.run(['config', '--no-includes', '--show-scope', '--name-only', '--list']);
|
|
47
|
+
// Inspect repository settings without requiring config.worktree or rejecting our command-scope transport settings.
|
|
48
|
+
if (config.split('\n').some(key => /^(?:local|worktree)\t(?:url\.|http\.|credential\.|include\.|includeif\.|filter\.|diff\.|core\.sshcommand$|remote\..*\.(?:proxy|uploadpack|receivepack|pushurl|vcs)$)/i.test(key))) {
|
|
49
49
|
throw new CliError('UNSAFE_GIT_CONFIG', 'Workspace transport overrides must be removed before using origin-bound Git');
|
|
50
50
|
}
|
|
51
51
|
return this;
|
package/dist/main.js
CHANGED
|
@@ -19,6 +19,7 @@ import { actionValues, valuePlan } from './action-values.js';
|
|
|
19
19
|
import { assetPath, attachToPlan, download, uploadPlan } from './attachments.js';
|
|
20
20
|
import { isLocal, localWorkflow, validateLocal } from './local-workflows.js';
|
|
21
21
|
import { capabilityOutput, commandGap } from './catalogue.js';
|
|
22
|
+
import { diffOutput, maxOutputChars } from './output.js';
|
|
22
23
|
async function input(path) {
|
|
23
24
|
if (path !== '-')
|
|
24
25
|
return readPrivateFile(path, true, 1_000_000);
|
|
@@ -42,7 +43,7 @@ export async function main(args) {
|
|
|
42
43
|
if (args.some(arg => /^--(?:token|api-key|password|authorization)(?:=|$)/i.test(arg))) {
|
|
43
44
|
throw new CliError('CONFIG_INVALID', 'token and other secret arguments are not accepted');
|
|
44
45
|
}
|
|
45
|
-
const command = args.length === 0 || args[0]?.startsWith('
|
|
46
|
+
const command = args.length === 0 || args[0]?.startsWith('-') ? commands.find(command => command.path[0] === 'dashboard') :
|
|
46
47
|
[...commands].sort((a, b) => b.path.length - a.path.length).find(command => command.path.every((part, index) => args[index] === part));
|
|
47
48
|
if (!command) {
|
|
48
49
|
const children = commands.filter(command => command.path[0] === args[0]);
|
|
@@ -54,7 +55,7 @@ export async function main(args) {
|
|
|
54
55
|
}
|
|
55
56
|
const path = command.path;
|
|
56
57
|
operation = command.path.join(' ');
|
|
57
|
-
const rest = args[0]?.startsWith('
|
|
58
|
+
const rest = args[0]?.startsWith('-') || !args.length ? args : args.slice(path.length);
|
|
58
59
|
let parsed;
|
|
59
60
|
try {
|
|
60
61
|
const options = Object.fromEntries(Object.entries(command.flags).map(([name, flag]) => [name, { type: flag.type,
|
|
@@ -119,7 +120,7 @@ export async function main(args) {
|
|
|
119
120
|
});
|
|
120
121
|
const output = await localWorkflow(new Client(identity.origin, identity.token, 300_000), identity, operation, positionals, options);
|
|
121
122
|
const rendered = encode(redact(output));
|
|
122
|
-
if (rendered.length >
|
|
123
|
+
if (rendered.length > maxOutputChars)
|
|
123
124
|
throw new CliError('OUTPUT_LIMIT', 'Rendered output exceeds 100000 characters');
|
|
124
125
|
console.log(rendered);
|
|
125
126
|
return;
|
|
@@ -359,12 +360,11 @@ export async function main(args) {
|
|
|
359
360
|
else if (operation === 'pr diff') {
|
|
360
361
|
const response = await client.request(base + '/pulls/' + positionals[0] + '.diff', true);
|
|
361
362
|
const text = String(response.data);
|
|
362
|
-
|
|
363
|
-
output = { diff: text.slice(0, ceiling), chars: text.length, truncated: text.length > ceiling, ...(text.length > ceiling ? { help: 'Use --full within the 100000 character diff ceiling' } : {}) };
|
|
363
|
+
output = diffOutput(text, Boolean(options.full));
|
|
364
364
|
}
|
|
365
365
|
}
|
|
366
366
|
const rendered = encode(redact(output));
|
|
367
|
-
if (rendered.length >
|
|
367
|
+
if (rendered.length > maxOutputChars)
|
|
368
368
|
throw new CliError('OUTPUT_LIMIT', 'Rendered output exceeds 100000 characters; select fewer fields or a lower --limit');
|
|
369
369
|
console.log(rendered);
|
|
370
370
|
}
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { encode } from '@toon-format/toon';
|
|
2
|
+
import { redact } from './credentials.js';
|
|
3
|
+
export const maxOutputChars = 100_000;
|
|
4
|
+
// Reserve the newline added by console.log.
|
|
5
|
+
const fits = (value) => encode(redact(value)).length < maxOutputChars;
|
|
6
|
+
function summary(value) {
|
|
7
|
+
if (Array.isArray(value))
|
|
8
|
+
return { summary: true, total_items: value.length };
|
|
9
|
+
if (value && typeof value === 'object') {
|
|
10
|
+
const row = value;
|
|
11
|
+
return { summary: true, total_fields: Object.keys(row).length,
|
|
12
|
+
...(typeof row.id === 'number' ? { id: row.id } : {}), ...(typeof row.number === 'number' ? { number: row.number } : {}) };
|
|
13
|
+
}
|
|
14
|
+
return typeof value === 'string' ? { summary: true, total_chars: value.length } : value;
|
|
15
|
+
}
|
|
16
|
+
export function writeOutput(value) {
|
|
17
|
+
const displayed = { ...value };
|
|
18
|
+
const truncated = [];
|
|
19
|
+
// Keep requested effects complete; project only their verification context.
|
|
20
|
+
for (const field of ['preconditions', 'observations', 'results']) {
|
|
21
|
+
if (fits(displayed))
|
|
22
|
+
return displayed;
|
|
23
|
+
if (!(field in value))
|
|
24
|
+
continue;
|
|
25
|
+
const context = value[field];
|
|
26
|
+
if (field === 'observations' && Array.isArray(context)) {
|
|
27
|
+
displayed[field] = context.slice(0, 10).map(row => ({ path: row.path, data: summary(row.data) }));
|
|
28
|
+
displayed.observations_total = context.length;
|
|
29
|
+
}
|
|
30
|
+
else if (field === 'results' && Array.isArray(context)) {
|
|
31
|
+
displayed[field] = context.slice(0, 10).map(summary);
|
|
32
|
+
displayed.results_total = context.length;
|
|
33
|
+
}
|
|
34
|
+
else
|
|
35
|
+
displayed[field] = summary(context);
|
|
36
|
+
truncated.push(field);
|
|
37
|
+
displayed.truncated = true;
|
|
38
|
+
displayed.truncated_fields = [...truncated];
|
|
39
|
+
}
|
|
40
|
+
return displayed;
|
|
41
|
+
}
|
|
42
|
+
export function diffOutput(text, full) {
|
|
43
|
+
const candidate = (length) => ({ diff: text.slice(0, length), chars: text.length, truncated: text.length > length,
|
|
44
|
+
...(text.length > length ? { help: full ? 'Diff is bounded to the rendered output ceiling' : 'Use --full for a larger bounded diff' } : {}) });
|
|
45
|
+
let low = 0, high = Math.min(text.length, full ? maxOutputChars : 10_000);
|
|
46
|
+
if (fits(candidate(high)))
|
|
47
|
+
return candidate(high);
|
|
48
|
+
while (low < high) {
|
|
49
|
+
const middle = Math.ceil((low + high) / 2);
|
|
50
|
+
if (fits(candidate(middle)))
|
|
51
|
+
low = middle;
|
|
52
|
+
else
|
|
53
|
+
high = middle - 1;
|
|
54
|
+
}
|
|
55
|
+
return candidate(low);
|
|
56
|
+
}
|
package/dist/pulls.js
CHANGED
|
@@ -7,7 +7,27 @@ const sorted = (values) => [...values].sort();
|
|
|
7
7
|
const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
8
8
|
const delta = (previous, add, remove) => [...new Set([...previous.map(String), ...names(add)])].filter(value => !names(remove).includes(value));
|
|
9
9
|
const members = (pull, field, key) => (pull[field] ?? []).map(row => object(row)[key]);
|
|
10
|
+
function branchPath(base, branch) {
|
|
11
|
+
const segments = branch.split('/');
|
|
12
|
+
if (segments.some(segment => !segment || segment === '.' || segment === '..') || /[\s\\\x00-\x1f\x7f]/.test(branch))
|
|
13
|
+
usage('Select a branch without unsafe path segments');
|
|
14
|
+
return base + '/git/refs/heads/' + segments.map(encodeURIComponent).join('/');
|
|
15
|
+
}
|
|
16
|
+
function branchSHA(value, branch) {
|
|
17
|
+
if (!Array.isArray(value))
|
|
18
|
+
throw new CliError('INVALID_RESPONSE', 'Expected branch references');
|
|
19
|
+
const selected = value.map(object).find(row => row.ref === 'refs/heads/' + branch);
|
|
20
|
+
if (!selected)
|
|
21
|
+
throw new CliError('NOT_FOUND_OR_HIDDEN', 'The selected branch is missing or hidden');
|
|
22
|
+
const commit = object(selected.object);
|
|
23
|
+
if (commit.type !== 'commit' || typeof commit.sha !== 'string' || !/^[a-f0-9]{40}$/.test(commit.sha))
|
|
24
|
+
throw new CliError('INVALID_RESPONSE', 'Expected an immutable branch commit identity');
|
|
25
|
+
return commit.sha;
|
|
26
|
+
}
|
|
10
27
|
export function validatePull(operation, options) {
|
|
28
|
+
for (const ref of [options.head, options.base])
|
|
29
|
+
if (ref !== undefined)
|
|
30
|
+
branchPath('', String(ref));
|
|
11
31
|
if (operation === 'pr review') {
|
|
12
32
|
if (['approve', 'request-changes', 'comment'].filter(key => options[key]).length !== 1)
|
|
13
33
|
usage('Choose exactly one of --approve, --request-changes or --comment');
|
|
@@ -49,6 +69,7 @@ export async function pullPlan(client, operation, base, number, options) {
|
|
|
49
69
|
reads.add(path);
|
|
50
70
|
return value;
|
|
51
71
|
};
|
|
72
|
+
const snapshotBranch = async (branch) => branchSHA(await snapshot(branchPath(base, branch), true), branch);
|
|
52
73
|
const add = (method, path, payload) => steps.push({ method, path, ...(payload === undefined ? {} : { payload }) });
|
|
53
74
|
const before = action === 'create' ? undefined : object(await snapshot(path));
|
|
54
75
|
const expected = before ? { id: before.id, number: before.number, ...fields(before), merged: before.merged ?? false, headSHA: object(before.head).sha } :
|
|
@@ -101,15 +122,14 @@ export async function pullPlan(client, operation, base, number, options) {
|
|
|
101
122
|
payload.assignees = names(options.assignee);
|
|
102
123
|
expected.reviewers = sorted(addedReviewers);
|
|
103
124
|
expected.teams = sorted(names(options['team-reviewer']));
|
|
104
|
-
|
|
105
|
-
expected.headSHA = object(head.commit).id;
|
|
125
|
+
expected.headSHA = await snapshotBranch(String(options.head));
|
|
106
126
|
}
|
|
107
127
|
else {
|
|
108
128
|
if (options['add-assignee'] !== undefined || options['remove-assignee'] !== undefined)
|
|
109
129
|
payload.assignees = delta(expected.assignees, options['add-assignee'], options['remove-assignee']);
|
|
110
130
|
}
|
|
111
131
|
if (options.base !== undefined)
|
|
112
|
-
await
|
|
132
|
+
await snapshotBranch(String(options.base));
|
|
113
133
|
if (options.milestone !== undefined) {
|
|
114
134
|
const list = await snapshot(base + '/milestones?state=all', true);
|
|
115
135
|
const selected = list.find(row => row.title === options.milestone);
|
|
@@ -171,12 +191,12 @@ export async function pullPlan(client, operation, base, number, options) {
|
|
|
171
191
|
payload.MergeMessageField = options.body;
|
|
172
192
|
if (style === 'rebase')
|
|
173
193
|
await snapshot(path + '/commits', true);
|
|
174
|
-
await
|
|
194
|
+
await snapshotBranch(String(expected.base));
|
|
175
195
|
add('POST', path + '/merge', payload);
|
|
176
196
|
expected.state = 'closed';
|
|
177
197
|
}
|
|
178
198
|
else if (action === 'update-branch') {
|
|
179
|
-
await
|
|
199
|
+
await snapshotBranch(String(expected.base));
|
|
180
200
|
add('POST', path + '/update');
|
|
181
201
|
}
|
|
182
202
|
if (action === 'comment' || action === 'close' && options.comment !== undefined) {
|
|
@@ -228,8 +248,8 @@ export async function verifyPull(client, plan, results, observations) {
|
|
|
228
248
|
}
|
|
229
249
|
if (action !== 'merge' && action !== 'update-branch')
|
|
230
250
|
return;
|
|
231
|
-
const
|
|
232
|
-
const baseSHA =
|
|
251
|
+
const baseBranchPath = branchPath(base, String(expected.base));
|
|
252
|
+
const baseSHA = branchSHA(before[baseBranchPath], String(expected.base));
|
|
233
253
|
const commit = async (sha) => {
|
|
234
254
|
if (typeof sha !== 'string' || !/^[a-f0-9]{40}$/.test(sha))
|
|
235
255
|
return mismatch();
|
|
@@ -241,25 +261,29 @@ export async function verifyPull(client, plan, results, observations) {
|
|
|
241
261
|
return data;
|
|
242
262
|
};
|
|
243
263
|
if (action === 'update-branch') {
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
264
|
+
const required = new Set([baseSHA, expected.headSHA]);
|
|
265
|
+
if ([...required].some(sha => typeof sha !== 'string' || !/^[a-f0-9]{40}$/.test(sha)))
|
|
266
|
+
mismatch();
|
|
267
|
+
const pending = [object(pull.head).sha], visited = new Set();
|
|
268
|
+
for (let cursor = 0; cursor < pending.length && visited.size < 1000; cursor++) {
|
|
269
|
+
const sha = pending[cursor];
|
|
270
|
+
if (visited.has(sha))
|
|
249
271
|
continue;
|
|
272
|
+
const current = await commit(sha);
|
|
250
273
|
visited.add(current.sha);
|
|
251
|
-
|
|
274
|
+
required.delete(current.sha);
|
|
275
|
+
const parents = members(current, 'parents', 'sha');
|
|
276
|
+
for (const parent of parents)
|
|
277
|
+
required.delete(parent);
|
|
278
|
+
if (!required.size)
|
|
252
279
|
return;
|
|
253
|
-
|
|
254
|
-
if (!visited.has(parent))
|
|
255
|
-
pending.push(await commit(parent));
|
|
256
|
-
}
|
|
280
|
+
pending.push(...parents.filter(parent => !visited.has(parent)));
|
|
257
281
|
}
|
|
258
282
|
mismatch();
|
|
259
283
|
}
|
|
260
284
|
if (pull.merged !== true || !pull.merge_commit_sha)
|
|
261
285
|
mismatch();
|
|
262
|
-
const finalSHA =
|
|
286
|
+
const finalSHA = branchSHA(observation(baseBranchPath), String(expected.base));
|
|
263
287
|
if (finalSHA !== pull.merge_commit_sha)
|
|
264
288
|
mismatch();
|
|
265
289
|
const final = await commit(finalSHA);
|
package/dist/reads.js
CHANGED
|
@@ -111,8 +111,8 @@ export async function listIssues(client, base, kind, options) {
|
|
|
111
111
|
truncated: page.truncated || rows.length > limit, next: page.next, continuation: page.truncated || rows.length > limit,
|
|
112
112
|
help: [`gitea-axi ${kind} view <number> --repo ${base.split('/').slice(-2).join('/')}`, ...continuationHelp(page, rows.length, limit)] };
|
|
113
113
|
}
|
|
114
|
-
async function complete(client, path) {
|
|
115
|
-
const page = await collect(client, path);
|
|
114
|
+
async function complete(client, path, signal) {
|
|
115
|
+
const page = await collect(client, path, undefined, signal);
|
|
116
116
|
if (page.truncated)
|
|
117
117
|
throw new CliError('RESPONSE_LIMIT', 'Related collection exceeds the bounded read; use the raw API to page explicitly');
|
|
118
118
|
return page.items;
|
|
@@ -127,11 +127,11 @@ async function reviews(client, path, full) {
|
|
|
127
127
|
}
|
|
128
128
|
return result;
|
|
129
129
|
}
|
|
130
|
-
export async function checks(client, base, sha) {
|
|
131
|
-
const combined = object(await read(client, base + '/commits/' + encodeURIComponent(sha) + '/status'));
|
|
130
|
+
export async function checks(client, base, sha, signal) {
|
|
131
|
+
const combined = object(await read(client, base + '/commits/' + encodeURIComponent(sha) + '/status', signal));
|
|
132
132
|
if (combined.sha !== sha && !(combined.sha === '' && combined.total_count === 0 && (combined.statuses === null || Array.isArray(combined.statuses) && !combined.statuses.length)))
|
|
133
133
|
throw new CliError('INVALID_RESPONSE', 'Check response does not match the pull request head SHA');
|
|
134
|
-
const statuses = await complete(client, base + '/commits/' + encodeURIComponent(sha) + '/statuses');
|
|
134
|
+
const statuses = await complete(client, base + '/commits/' + encodeURIComponent(sha) + '/statuses', signal);
|
|
135
135
|
statuses.sort((a, b) => Number(b.id) - Number(a.id));
|
|
136
136
|
const latest = new Map();
|
|
137
137
|
for (const status of statuses)
|
|
@@ -146,28 +146,37 @@ export async function checks(client, base, sha) {
|
|
|
146
146
|
export async function watchChecks(client, base, number, options) {
|
|
147
147
|
const seconds = positive(options.timeout, 30, 3600);
|
|
148
148
|
const interval = positive(options.interval, 5, 60) * 1000;
|
|
149
|
-
const stop = Date.now() + seconds * 1000;
|
|
149
|
+
const stop = Math.min(client.deadline, Date.now() + seconds * 1000);
|
|
150
150
|
const controller = new AbortController();
|
|
151
|
+
let head = null;
|
|
152
|
+
let latest;
|
|
151
153
|
const cancel = () => controller.abort();
|
|
152
154
|
process.once('SIGINT', cancel);
|
|
153
155
|
process.once('SIGTERM', cancel);
|
|
154
156
|
try {
|
|
155
157
|
while (true) {
|
|
156
|
-
const pull = object(await read(client, base + '/pulls/' + number));
|
|
158
|
+
const pull = object(await read(client, base + '/pulls/' + number, controller.signal));
|
|
157
159
|
const sha = object(pull.head).sha;
|
|
158
160
|
if (typeof sha !== 'string' || !sha)
|
|
159
161
|
throw new CliError('INVALID_RESPONSE', 'Pull request head SHA is missing');
|
|
160
|
-
|
|
162
|
+
head = sha;
|
|
163
|
+
const result = await checks(client, base, sha, controller.signal);
|
|
164
|
+
latest = result;
|
|
161
165
|
if (!options.watch || result.items.length > 0 && !result.pending || result.failed || Date.now() >= stop || controller.signal.aborted)
|
|
162
|
-
return { ...result, cancelled: controller.signal.aborted };
|
|
166
|
+
return { ...result, exitCode: controller.signal.aborted ? 8 : result.exitCode, cancelled: controller.signal.aborted };
|
|
163
167
|
await delay(Math.min(interval, Math.max(1, stop - Date.now())), undefined, { signal: controller.signal }).catch(error => {
|
|
164
168
|
if (!controller.signal.aborted)
|
|
165
169
|
throw error;
|
|
166
170
|
});
|
|
167
171
|
if (Date.now() >= stop || controller.signal.aborted)
|
|
168
|
-
return { ...result, cancelled: controller.signal.aborted };
|
|
172
|
+
return { ...result, exitCode: controller.signal.aborted ? 8 : result.exitCode, cancelled: controller.signal.aborted };
|
|
169
173
|
}
|
|
170
174
|
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (!controller.signal.aborted)
|
|
177
|
+
throw error;
|
|
178
|
+
return { ...(latest ?? { head, items: [], passed: 0, failed: 0, pending: 0 }), exitCode: 8, cancelled: true };
|
|
179
|
+
}
|
|
171
180
|
finally {
|
|
172
181
|
process.removeListener('SIGINT', cancel);
|
|
173
182
|
process.removeListener('SIGTERM', cancel);
|
|
@@ -196,7 +205,8 @@ export async function view(client, base, kind, number, options) {
|
|
|
196
205
|
headRepository: head.repo == null ? null : { name: object(head.repo).name, nameWithOwner: object(head.repo).full_name },
|
|
197
206
|
headRepositoryOwner: head.repo == null ? null : user(object(head.repo).owner), isCrossRepository: head.repo_id == null || tail.repo_id == null ? null : head.repo_id !== tail.repo_id,
|
|
198
207
|
mergeCommit: row.merge_commit_sha ? { oid: row.merge_commit_sha } : null, mergedBy: user(row.merged_by),
|
|
199
|
-
mergeable: row.mergeable == null ? 'UNKNOWN' : row.mergeable ? 'MERGEABLE' : 'CONFLICTING', reviewRequests: users(row.requested_reviewers)
|
|
208
|
+
mergeable: row.mergeable == null ? 'UNKNOWN' : row.mergeable ? 'MERGEABLE' : 'CONFLICTING', reviewRequests: [...users(row.requested_reviewers),
|
|
209
|
+
...(row.requested_reviewers_teams ?? []).map(team => ({ type: 'team', id: team.id, name: team.name }))] });
|
|
200
210
|
}
|
|
201
211
|
if (options.reviews || requested.has('reviews') || requested.has('latestReviews')) {
|
|
202
212
|
const all = await reviews(client, path, Boolean(options.full || fields));
|
package/dist/registry.js
CHANGED
|
@@ -91,7 +91,7 @@ const definitions = [
|
|
|
91
91
|
['pr view', { ...repo, ...detail, reviews: { type: 'boolean' }, checks: { type: 'boolean' } }],
|
|
92
92
|
['pr checks', { ...repo, watch: { type: 'boolean' }, timeout: { type: 'string', description: 'Watch deadline seconds, required with --watch' }, interval: { type: 'string', description: 'Watch interval seconds, default 5' } }],
|
|
93
93
|
['pr diff', { ...repo, full: detail.full }],
|
|
94
|
-
['release list', { ...repo, limit: list.limit, 'exclude-drafts': { type: 'boolean' }, 'exclude-pre-releases': { type: 'boolean' } }],
|
|
94
|
+
['release list', { ...repo, limit: { ...list.limit, description: 'Maximum returned records, default 10, maximum 1000' }, 'exclude-drafts': { type: 'boolean' }, 'exclude-pre-releases': { type: 'boolean' } }],
|
|
95
95
|
['release view', { ...repo, full: detail.full }], ['api', { ...writeFlags,
|
|
96
96
|
method: { type: 'string', short: 'X', description: 'HTTP method; cannot combine with a positional method' },
|
|
97
97
|
field: { type: 'string', multiple: true, description: 'Repeatable key=value; numbers, booleans and null become JSON values' },
|