-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbugzilla.nu
More file actions
519 lines (467 loc) · 14.3 KB
/
bugzilla.nu
File metadata and controls
519 lines (467 loc) · 14.3 KB
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
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
const HOST = "https://bugzilla.mozilla.org"
const USER_AGENT_HEADER = { "User-Agent": "ErichDonGubler-Bugzilla-Nushell/1.0" }
def "auth-headers from-api-key" [
--required-for: string | null = null,
] {
use std/log
mut api_key = null
const env_var_name = 'BUGZILLA_API_KEY'
try {
$api_key = $env | get $env_var_name
} catch {
log debug $"no `($env_var_name)` defined"
}
const config_path = $'($nu.home-dir)/.config/bugzilla.toml'
const toml_key = 'api_key'
if $api_key == null {
try {
$api_key = open $config_path | get $toml_key
} catch {
log debug $"failed to access `($toml_key)` field in `($config_path)`"
}
}
if $api_key != null {
{
'X-BUGZILLA-API-KEY': $api_key
}
} else {
if $required_for == null {
{}
} else {
error make --unspanned {
msg: ([
"failed to get Bugzilla API key from the following sources:"
$"- `($env_var_name)` environment variable"
$"- `($toml_key)` field in `($config_path)`"
""
$"…and at least one is required for ($required_for)."
] | str join "\n")
}
}
}
}
def "rest-api get-json" [
url_path: string,
--auth-required-for: oneof<nothing, string> = null,
]: nothing -> any {
use std/log
let full_url = $"($HOST)/rest/($url_path)"
log debug $"`GET`ting ($full_url | to nuon)"
mut headers = {
"Content-Type": "application/json"
"Accept": "application/json"
}
$headers = $headers
| merge $USER_AGENT_HEADER
| merge (auth-headers from-api-key --required-for $auth_required_for)
let response = http get $full_url --headers $headers
if ("error" in $response and $response.error) {
error make {
msg: $"error returned ($response.code): ($response.message)"
label: {
text: ""
span: (metadata $url_path).span
}
}
}
$response
}
def "rest-api post-json" [
url_path: string,
input: any,
what: string = "`POST` request"
] {
use std/log
let full_url = $"($HOST)/rest/($url_path)"
log debug $"`POST`ing to ($full_url | to nuon) with input ($input | to nuon)"
let headers = auth-headers from-api-key --required-for $what
| merge $USER_AGENT_HEADER
http post --headers $headers --content-type "application/json" $full_url $input
}
def "rest-api put-json" [
url_path: string,
input: any,
what: string = "`PUT` request"
] {
use std/log
let full_url = $"($HOST)/rest/($url_path)"
log debug $"`PUT`ting ($full_url | to nuon)"
let headers = auth-headers from-api-key --required-for $what
| merge $USER_AGENT_HEADER
http put --headers $headers --content-type "application/json" $full_url $input
}
# Create a bug via the `Create Bug` API:
# <https://bmo.readthedocs.io/en/latest/api/core/v1/bug.html#create-bug>
export def "bug create" [
--assign-to-me,
--type: oneof<nothing, string>@"nu-complete bug type" = null,
--summary: oneof<nothing, string> = null,
--description: oneof<nothing, string> = null,
--product: oneof<nothing, string> = null,
--component: oneof<nothing, string> = null,
--priority: oneof<nothing, string>@"nu-complete bug field priority" = null,
--severity: oneof<nothing, string>@"nu-complete bug field severity" = null,
--version: string = "unspecified",
--alias: oneof<nothing, string> = null,
--extra: record = {},
] {
let input = $extra
| merge_with_input "assigned_to" "--assign-to-me" (
if $assign_to_me { (whoami | get name) } else { null }
)
| merge_with_input "type" "--type" $type
| merge_with_input "summary" "--summary" $summary
| merge_with_input "description" "--description" $description
| merge_with_input "product" "--product" $product
| merge_with_input "component" "--component" $component
| merge_with_input "priority" "--priority" $priority
| merge_with_input "severity" "--severity" $severity
| merge_with_input "version" "--version" $version
| merge_with_input "alias" "--alias" $alias
| if $in.assigned_to? != null and $in.status? == null {
insert status 'ASSIGNED'
}
rest-api post-json "bug" $input "bug creation"
}
export def "bug field" [
id_or_name: oneof<int, string>,
]: nothing -> record<name: string, id: int, type: int, display_name: string, values: list<record<name: string>>> {
let fields = rest-api get-json $'field/bug/($id_or_name)'
| parse-response get "fields"
match ($fields | length) {
0 => (error make --unspanned {
msg: "internal error: service returned an empty list of matching fields"
})
1 => ($fields | first --strict)
$len => (error make --unspanned {
msg: $"internal error: service returned ($len) matching fields"
})
}
}
def "bug field-values to-completions" [
]: record<values: list<record<name: string>>> -> list<string> {
get values | get name
}
const BUGLIST_FIELDS = [
id
type
summary
product
component
assigned_to
status
resolution
last_change_time
]
# Fetch a single bug via the `Bug Get` API:
# <https://bmo.readthedocs.io/en/latest/api/core/v1/bug.html#get-bug>
export def "bug get" [
id_or_alias: oneof<int, string>,
--include-fields: oneof<nothing, list<string>> = null,
# A subset of bug fields that the server should return. Fewer fields generally receive a faster
# answer.
--output-fmt: oneof<nothing, string>@"nu-complete bugs output-fmt" = null,
# The formatting to apply to bugs returned here. See `bugzilla search`'s `--output-fmt` for more
# details.
] {
let bugs = search $id_or_alias --include-fields $include_fields --output-fmt $output_fmt
match ($bugs | length) {
1 => { $bugs | first --strict }
0 => {
error make --unspanned {
msg: "no bug found"
}
}
_ => {
error make --unspanned {
msg: $"multiple bugs found: ($bugs | get id | to nuon)"
}
}
}
}
# Update a bug via the `Update Bug` API:
# <https://bmo.readthedocs.io/en/latest/api/core/v1/bug.html#rest-update-bug>
export def "bug update" [
id_or_alias: oneof<int, string>,
input: any,
] {
rest-api put-json $'bug/($id_or_alias)' $input "bug update" | get bugs
}
# Apply a filter to the raw data of a bug returned by `bugzilla bug get` and the like.
export def "bugs apply-output-fmt" [
fmt: oneof<nothing, string>@"nu-complete bugs output-fmt" = null,
]: any -> any {
let bugs = $in
match $fmt {
"full" => { $bugs }
null |"buglist" => {
def _buglist_type_diags []: table<id: int type: any summary: any product: any assigned_to: string status: any resolution: any last_change_time: any> -> any {}
$bugs | _buglist_type_diags
# NOTE: This tries to emulate the format found in `buglist.cgi`.
$bugs
| select ...$BUGLIST_FIELDS
| update cells { detect type }
}
_ => {
error make {
msg: $"unrecognized output format ($fmt | to nuon)"
label: {
text: "provided here"
span: (metadata $fmt).span
}
}
}
}
}
def "ids-or-names to-record" []: list<oneof<int, string>> -> record<ids: list<int>, names: list<string>> {
reduce --fold {} {|id_or_name, acc|
match ($id_or_name | describe) {
"int" => {
$acc | upsert ids { default [] | append $id_or_name }
}
"string" => {
$acc | upsert names { default [] | append $id_or_name }
}
$type => {
error make --unspanned {
msg: ([
$"internal error: unexpected type `($type)` "
"in element of `$ids_or_names`"
] | str join)
}
}
}
}
}
def "ids-or-names to-url" []: list<oneof<int, string>> -> record<ids: list<int>, names: list<string>> {
let ids_or_names = $in
match ($ids_or_names | length) {
0 => {
error make {
msg: "internal error: empty list provided to `ids-or-names to-url`"
label: {
text: ""
span: (metadata $ids_or_names).span
}
}
}
1 => {
$"/($ids_or_names | first --strict)"
}
_ => {
$ids_or_names | ids-or-names to-record | $"?($in | url build-query)"
}
}
}
def "merge_with_input" [
field_name: string,
option_name: string,
option_value: any,
]: record -> record {
use std/log
mut input = $in
if $option_value != null {
if $field_name in $input {
log warning ([
"conflicting assignment info. provided; "
$"both the option `($option_name)` and the `($field_name)` field "
$"in `extra` \(($input | get $field_name | to nuon)\) "
"were specified; resolving with the option's value"
] | str join)
}
$input = $input | merge ({} | insert $field_name $option_value)
}
$input
}
def "nu-complete bug field priority" [] {
bug field 'priority' | bug field-values to-completions
}
def "nu-complete bug field severity" [] {
bug field 'bug_severity' | bug field-values to-completions
}
def "nu-complete product type" [] {
[
"selectable"
"enterable"
"accessible"
]
}
def "nu-complete bug type" [] {
[
"defect"
"task"
"enhancement"
]
}
def "parse-response get" [success_field_name: string]: any -> any {
let response = $in
if not ("faults" in $response) or ($response.faults | is-empty) {
$response | get $success_field_name
} else {
error make --unspanned {
msg: $"`faults` found in response: ($response.faults | to nuon)"
}
}
}
def "nu-complete bugs output-fmt" [] {
[
"full"
"buglist"
]
}
export def "product get" [
...ids_or_names: oneof<int, string>,
]: nothing -> list<record<products: list<record>>> {
if ($ids_or_names | is-empty) {
error make --unspanned {
msg: "no ID(s) or name(s) specified"
label: {
text: ""
span: (metadata $ids_or_names).span
}
}
}
rest-api get-json $'product($ids_or_names | ids-or-names to-url)'
| parse-response get "products"
}
export def "product list" [
--type: string@"nu-complete product type" = "enterable",
--output-fmt: string@["full" "ids-only"] = "full",
# What data to fetch for queried products. `full` takes significantly more time to receive, but
# includes human-friendly information like names.
]: nothing -> record<ids: list<int>> {
match $output_fmt {
"full" => {
# NOTE: We don't use `product get` here to sidestep validation for a non-zero number of ID(s)
# or name(s).
rest-api get-json $'product?type=($type)' | parse-response get "products"
}
"ids-only" => {
rest-api get-json $'product_($type)' | parse-response get "ids"
}
_ => {
error make {
msg: $"unrecognized output format `($output_fmt)`"
label: {
text: ""
span: (metadata $output_fmt).span
}
}
}
}
}
# Look up multiple bugs using the `quicksearch` field in the `Search Bugs` API:
# <https://bmo.readthedocs.io/en/latest/api/core/v1/bug.html#search-bugs>
export def "quicksearch" [
query: string,
--output-fmt: oneof<nothing, string>@"nu-complete bugs output-fmt" = null,
# The formatting to apply to bugs returned here. See `bugzilla search`'s `--output-fmt` for more
# details.
] {
search --criteria { quicksearch: $query } --output-fmt $output_fmt
}
# Look up multiple bugs via the `Search Bugs` API:
# <https://bmo.readthedocs.io/en/latest/api/core/v1/bug.html#search-bugs>
export def "search" [
id_or_alias?: oneof<int, string>,
--criteria: record = {},
--include-fields: oneof<nothing, list<string>> = null,
# A subset of bug fields that the server should return. Fewer fields generally receive a faster
# answer.
--output-fmt: oneof<nothing, string>@"nu-complete bugs output-fmt" = null,
# The formatting to apply to bugs returned here.
] {
mut criteria = $criteria
let final_path_segment = if $id_or_alias == null {
""
} else {
$"/($id_or_alias)"
}
let output_fmt = $output_fmt | default {
match $include_fields {
null => "buglist",
_ => "full"
}
}
$criteria = $criteria | merge (match [$include_fields, $output_fmt] {
[null, null] | [null, 'buglist'] => {
include_fields: $BUGLIST_FIELDS
}
[null, _] => ({})
_ => { include_fields: $include_fields }
})
rest-api get-json $"bug($final_path_segment)?($criteria | url build-query)"
| parse-response get "bugs"
| bugs apply-output-fmt $output_fmt
}
# Fetch a single user via the `Get User` API:
# <https://bmo.readthedocs.io/en/latest/api/core/v1/user.html#get-user>
export def "user get" [
id_or_name: oneof<int, string>,
--auth,
]: nothing -> record<id: int real_name: string nick: string name: string> {
let auth_required_for = if $auth {
"explicit request by user"
} else {
null
}
(
rest-api get-json
$'user([$id_or_name] | ids-or-names to-url)'
--auth-required-for $auth_required_for
)
| parse-response get "users"
| match ($in | length) {
1 => ($in | first --strict)
0 => (
error make --unspanned {
msg: "no such ID or name found"
}
)
_ => (
error make --unspanned {
msg: "internal error: multiple users found"
}
)
}
}
# Look up multiple users via the `Get User` API:
# <https://bmo.readthedocs.io/en/latest/api/core/v1/user.html#get-user>
export def "users search" [
...ids_or_names: oneof<int, string>,
--match: list<string>,
--limit: int,
--group-ids: list<int>,
--groups: list<string>,
--include-disabled,
--auth,
--extra: record = {},
]: nothing -> table<id: int real_name: string nick: string name: string> {
# TODO: handle `permissive` field
let ids_or_names = if ($ids_or_names | is-not-empty) {
$ids_or_names | ids-or-names to-record
} else {
null
}
let input = $extra
| merge_with_input "ids" "<ids_or_names>" ($ids_or_names.ids?)
| merge_with_input "names" "<ids_or_names>" ($ids_or_names.names?)
| merge_with_input "match" "--match" $match
| merge_with_input "limit" "--limit" $limit
| merge_with_input "group_ids" "--group-ids" $group_ids
| merge_with_input "groups" "--groups" $groups
| merge_with_input "include_disabled" "--include-disabled" $include_disabled
let auth_required_for = if $auth {
"explicit request by user"
} else {
null
}
(
rest-api get-json $"user?($input | url build-query)"
--auth-required-for $auth_required_for
| parse-response get "users"
)
}
export def "whoami" []: nothing -> record<id: int real_name: string nick: string name: string> {
rest-api get-json "whoami" --auth-required-for "`whoami` queries"
}