hiiro 0.1.289 → 0.1.291
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/bin/h-pr +2 -0
- data/lib/hiiro/git/pr.rb +5 -2
- data/lib/hiiro/options.rb +40 -4
- data/lib/hiiro/pinned_pr_manager.rb +71 -44
- data/lib/hiiro/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 03a9ad967ffc6e338367882dd85b3305099e521dc7a0a2341acf71257ca22bde
|
|
4
|
+
data.tar.gz: 80f4746d3cdb168b9275e8ca4e55822d356966f4467f147b68741cde3045cfa6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6f5076627ad3767f575122e566090160a35953059efb115e89509a855af089dec0f5f32336c33cf001cc3bc8d6fa3a364bde840ad4e1d7a2274c354c0e59b35f
|
|
7
|
+
data.tar.gz: 7b6cdcbe3c7640f6fbf27d42ee42aa72fcc6644d5f11c0ddec482d6ea98de8dc5f3712606d221e1c520acbbc66cb3c60fae1eea8ae2c8bf4d7175345465da2bd
|
data/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
```markdown
|
|
2
|
+
## v0.1.291 (2026-03-26)
|
|
3
|
+
|
|
4
|
+
### Added
|
|
5
|
+
- `Options#mutual_exclusion(*names)` — star-topology mutual exclusion: first flag is the hub (clears all others when set); any other flag only clears the hub (spokes can coexist freely); last encountered in argv wins
|
|
6
|
+
- `h pr ls`: new `--all`/`-a` flag (show all tracked PRs, no filter); all filter flags are declared mutually exclusive so `-oa`, `-ao`, `--all --active` etc. do the right thing
|
|
7
|
+
|
|
8
|
+
## v0.1.290 (2026-03-26)
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- `h pr ls`/`h pr update`: revert `statusCheckRollup` contexts limit to 100 and add pagination to retrieve all checks beyond the first 100; GitHub's GraphQL API silently returns null when limit is exceeded, causing all checks to vanish
|
|
12
|
+
|
|
2
13
|
## v0.1.289 (2026-03-26)
|
|
3
14
|
|
|
4
15
|
### Fixed
|
|
5
16
|
- `h queue run`/`watch`: frontmatter `session_name` now directly controls which tmux session the task launches in; working directory seeded from that session's active pane when no tree is specified
|
|
6
17
|
- `h queue sadd`: now immediately launches a new tmux window in the target session after adding to queue, consistent with `hadd`/`vadd` behavior (previously just added to pending with no launch)
|
|
18
|
+
- `h pr ls`: revert `statusCheckRollup` contexts limit from 250 back to 100; GitHub's GraphQL API caps connections at 100 — exceeding it silently returns null for the entire field, causing all checks to vanish from the list
|
|
19
|
+
- `h pr update`: paginate beyond the first 100 checks for PRs that hit the limit; show `❓` status for PRs where pagination still couldn't retrieve all checks
|
|
7
20
|
|
|
8
21
|
## v0.1.288 (2026-03-26)
|
|
9
22
|
|
data/bin/h-pr
CHANGED
|
@@ -238,7 +238,9 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
|
|
|
238
238
|
flag(:verbose, short: 'v', desc: 'multi-line output per PR')
|
|
239
239
|
flag(:checks, short: 'C', desc: 'show individual check run details')
|
|
240
240
|
flag(:diff, short: 'd', desc: 'open diff for selected PR')
|
|
241
|
+
flag(:all, short: 'a', desc: 'show all tracked PRs (no filter)')
|
|
241
242
|
instance_eval(&FILTER_OPTS)
|
|
243
|
+
mutual_exclusion(:all, :active, :merged, :drafts, :red, :green, :pending, :conflicts)
|
|
242
244
|
}
|
|
243
245
|
|
|
244
246
|
if opts.help
|
data/lib/hiiro/git/pr.rb
CHANGED
|
@@ -106,7 +106,8 @@ class Hiiro
|
|
|
106
106
|
|
|
107
107
|
# Summarizes raw statusCheckRollup contexts into { total, success, pending, failed, frozen }.
|
|
108
108
|
# frozen = number of failed contexts that are specifically the ISC code freeze check.
|
|
109
|
-
|
|
109
|
+
# truncated: true is added when pagination couldn't retrieve all checks.
|
|
110
|
+
def self.summarize_checks(rollup, truncated: false)
|
|
110
111
|
return nil unless rollup
|
|
111
112
|
|
|
112
113
|
contexts = rollup.is_a?(Array) ? rollup : []
|
|
@@ -126,7 +127,9 @@ class Hiiro
|
|
|
126
127
|
(FAILED_CONCLUSIONS.include?(c['conclusion']) || %w[FAILURE ERROR].include?(c['state']))
|
|
127
128
|
end
|
|
128
129
|
|
|
129
|
-
{ 'total' => total, 'success' => success, 'pending' => pending, 'failed' => failed, 'frozen' => frozen }
|
|
130
|
+
result = { 'total' => total, 'success' => success, 'pending' => pending, 'failed' => failed, 'frozen' => frozen }
|
|
131
|
+
result['truncated'] = true if truncated
|
|
132
|
+
result
|
|
130
133
|
end
|
|
131
134
|
|
|
132
135
|
# Summarizes raw review nodes into { approved, changes_requested, commented, reviewers }.
|
data/lib/hiiro/options.rb
CHANGED
|
@@ -63,6 +63,19 @@ class Hiiro
|
|
|
63
63
|
self
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# Declare a mutual exclusion group with star topology.
|
|
67
|
+
# The first name is the hub; the rest are spokes.
|
|
68
|
+
# Setting the hub clears all spokes.
|
|
69
|
+
# Setting a spoke clears only the hub.
|
|
70
|
+
# Spokes can still be combined with each other freely.
|
|
71
|
+
# Two-member groups are fully symmetric (hub == spoke).
|
|
72
|
+
# Last flag encountered in argv always wins.
|
|
73
|
+
def mutual_exclusion(*names)
|
|
74
|
+
@mutex_groups ||= []
|
|
75
|
+
@mutex_groups << names.map(&:to_sym)
|
|
76
|
+
self
|
|
77
|
+
end
|
|
78
|
+
|
|
66
79
|
private
|
|
67
80
|
|
|
68
81
|
def deconflict_short(short)
|
|
@@ -82,7 +95,7 @@ class Hiiro
|
|
|
82
95
|
end
|
|
83
96
|
|
|
84
97
|
def parse(args)
|
|
85
|
-
Args.new(@definitions, args.flatten.compact)
|
|
98
|
+
Args.new(@definitions, args.flatten.compact, mutex_groups: @mutex_groups || [])
|
|
86
99
|
end
|
|
87
100
|
|
|
88
101
|
def parse!(args)
|
|
@@ -93,8 +106,9 @@ class Hiiro
|
|
|
93
106
|
attr_reader :remaining_args, :original_args
|
|
94
107
|
alias args remaining_args
|
|
95
108
|
|
|
96
|
-
def initialize(definitions, raw_args)
|
|
109
|
+
def initialize(definitions, raw_args, mutex_groups: [])
|
|
97
110
|
@definitions = definitions
|
|
111
|
+
@mutex_groups = mutex_groups
|
|
98
112
|
@original_args = raw_args.dup.freeze
|
|
99
113
|
@values = {}
|
|
100
114
|
@remaining_args = []
|
|
@@ -182,7 +196,7 @@ class Hiiro
|
|
|
182
196
|
return unless defn
|
|
183
197
|
|
|
184
198
|
if defn.flag? || defn.flag_active?(@values)
|
|
185
|
-
|
|
199
|
+
set_flag(defn, !defn.default)
|
|
186
200
|
else
|
|
187
201
|
value ||= args.shift
|
|
188
202
|
store_value(defn, value)
|
|
@@ -197,7 +211,7 @@ class Hiiro
|
|
|
197
211
|
next unless defn
|
|
198
212
|
|
|
199
213
|
if defn.flag? || defn.flag_active?(@values)
|
|
200
|
-
|
|
214
|
+
set_flag(defn, !defn.default)
|
|
201
215
|
elsif idx == chars.length - 1
|
|
202
216
|
store_value(defn, args.shift)
|
|
203
217
|
else
|
|
@@ -207,6 +221,28 @@ class Hiiro
|
|
|
207
221
|
end
|
|
208
222
|
end
|
|
209
223
|
|
|
224
|
+
def set_flag(defn, value)
|
|
225
|
+
# Star topology: group[0] is the hub.
|
|
226
|
+
# Setting the hub → clears all spokes (group[1..])
|
|
227
|
+
# Setting a spoke → clears only the hub (group[0])
|
|
228
|
+
# This lets spokes coexist with each other (e.g. --red --drafts is fine)
|
|
229
|
+
# while still preventing any spoke from combining with the hub (--all).
|
|
230
|
+
@mutex_groups.each do |group|
|
|
231
|
+
next unless group.include?(defn.name)
|
|
232
|
+
hub, *spokes = group
|
|
233
|
+
if defn.name == hub
|
|
234
|
+
spokes.each do |spoke|
|
|
235
|
+
spoke_defn = @definitions[spoke]
|
|
236
|
+
@values[spoke] = spoke_defn.default if spoke_defn
|
|
237
|
+
end
|
|
238
|
+
else
|
|
239
|
+
hub_defn = @definitions[hub]
|
|
240
|
+
@values[hub] = hub_defn.default if hub_defn
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
@values[defn.name] = value
|
|
244
|
+
end
|
|
245
|
+
|
|
210
246
|
def store_value(defn, value)
|
|
211
247
|
coerced = defn.coerce(value)
|
|
212
248
|
if defn.multi
|
|
@@ -262,7 +262,7 @@ class Hiiro
|
|
|
262
262
|
pr.state = info['state']
|
|
263
263
|
pr.title = info['title']
|
|
264
264
|
pr.check_runs = rollup
|
|
265
|
-
pr.checks = Hiiro::Git::Pr.summarize_checks(rollup)
|
|
265
|
+
pr.checks = Hiiro::Git::Pr.summarize_checks(rollup, truncated: info['checksTruncated'])
|
|
266
266
|
pr.reviews = Hiiro::Git::Pr.summarize_reviews(info['reviews'])
|
|
267
267
|
pr.review_decision = info['reviewDecision']
|
|
268
268
|
pr.is_draft = info['isDraft']
|
|
@@ -317,6 +317,8 @@ class Hiiro
|
|
|
317
317
|
only_frozen ? " ❄️" : " ❌"
|
|
318
318
|
elsif has_pending
|
|
319
319
|
"⏳ "
|
|
320
|
+
elsif c['truncated']
|
|
321
|
+
" ❓"
|
|
320
322
|
else
|
|
321
323
|
" ✅"
|
|
322
324
|
end
|
|
@@ -494,44 +496,24 @@ class Hiiro
|
|
|
494
496
|
def fetch_batch_for_repo(owner, name, pr_numbers)
|
|
495
497
|
return {} if pr_numbers.empty?
|
|
496
498
|
|
|
499
|
+
context_fragment = <<~GRAPHQL.strip
|
|
500
|
+
__typename
|
|
501
|
+
... on CheckRun { __typename name conclusion status detailsUrl }
|
|
502
|
+
... on StatusContext { __typename context state targetUrl }
|
|
503
|
+
GRAPHQL
|
|
504
|
+
|
|
497
505
|
pr_queries = pr_numbers.map.with_index do |num, idx|
|
|
498
506
|
<<~GRAPHQL.strip
|
|
499
507
|
pr#{idx}: pullRequest(number: #{num}) {
|
|
500
|
-
number
|
|
501
|
-
title
|
|
502
|
-
url
|
|
503
|
-
headRefName
|
|
504
|
-
state
|
|
505
|
-
isDraft
|
|
506
|
-
mergeable
|
|
507
|
-
reviewDecision
|
|
508
|
+
number title url headRefName state isDraft mergeable reviewDecision
|
|
508
509
|
statusCheckRollup {
|
|
509
|
-
contexts(last:
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
name
|
|
514
|
-
conclusion
|
|
515
|
-
status
|
|
516
|
-
detailsUrl
|
|
517
|
-
}
|
|
518
|
-
... on StatusContext {
|
|
519
|
-
__typename
|
|
520
|
-
context
|
|
521
|
-
state
|
|
522
|
-
targetUrl
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
reviews(last: 50) {
|
|
528
|
-
nodes {
|
|
529
|
-
author {
|
|
530
|
-
login
|
|
531
|
-
}
|
|
532
|
-
state
|
|
510
|
+
contexts(last: 100) {
|
|
511
|
+
totalCount
|
|
512
|
+
pageInfo { hasPreviousPage startCursor }
|
|
513
|
+
nodes { #{context_fragment} }
|
|
533
514
|
}
|
|
534
515
|
}
|
|
516
|
+
reviews(last: 50) { nodes { author { login } state } }
|
|
535
517
|
}
|
|
536
518
|
GRAPHQL
|
|
537
519
|
end
|
|
@@ -557,18 +539,36 @@ class Hiiro
|
|
|
557
539
|
pr_data = repo_data["pr#{idx}"]
|
|
558
540
|
next unless pr_data
|
|
559
541
|
|
|
542
|
+
contexts_data = pr_data.dig('statusCheckRollup', 'contexts')
|
|
543
|
+
nodes = contexts_data&.[]('nodes') || []
|
|
544
|
+
total_count = contexts_data&.[]('totalCount').to_i
|
|
545
|
+
page_info = contexts_data&.[]('pageInfo') || {}
|
|
546
|
+
|
|
547
|
+
# Paginate backwards to collect all checks beyond the first 100
|
|
548
|
+
all_nodes = nodes.dup
|
|
549
|
+
cursor = page_info['startCursor']
|
|
550
|
+
while page_info['hasPreviousPage'] && cursor
|
|
551
|
+
extra_nodes, page_info = fetch_contexts_page(owner, name, num, cursor, context_fragment)
|
|
552
|
+
break unless extra_nodes
|
|
553
|
+
all_nodes = extra_nodes + all_nodes
|
|
554
|
+
cursor = page_info&.[]('startCursor')
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
truncated = total_count > 0 && all_nodes.length < total_count
|
|
558
|
+
|
|
560
559
|
pr_info_by_key[[num, repo_path]] = {
|
|
561
|
-
'number'
|
|
562
|
-
'title'
|
|
563
|
-
'url'
|
|
564
|
-
'headRefName'
|
|
565
|
-
'state'
|
|
566
|
-
'isDraft'
|
|
567
|
-
'mergeable'
|
|
568
|
-
'reviewDecision'
|
|
569
|
-
'statusCheckRollup'
|
|
570
|
-
'
|
|
571
|
-
'
|
|
560
|
+
'number' => pr_data['number'],
|
|
561
|
+
'title' => pr_data['title'],
|
|
562
|
+
'url' => pr_data['url'],
|
|
563
|
+
'headRefName' => pr_data['headRefName'],
|
|
564
|
+
'state' => pr_data['state'],
|
|
565
|
+
'isDraft' => pr_data['isDraft'],
|
|
566
|
+
'mergeable' => pr_data['mergeable'],
|
|
567
|
+
'reviewDecision' => pr_data['reviewDecision'],
|
|
568
|
+
'statusCheckRollup'=> all_nodes.any? ? all_nodes : nil,
|
|
569
|
+
'checksTruncated' => truncated,
|
|
570
|
+
'reviews' => pr_data.dig('reviews', 'nodes') || [],
|
|
571
|
+
'repo' => repo_path
|
|
572
572
|
}
|
|
573
573
|
end
|
|
574
574
|
|
|
@@ -577,6 +577,33 @@ class Hiiro
|
|
|
577
577
|
{}
|
|
578
578
|
end
|
|
579
579
|
|
|
580
|
+
def fetch_contexts_page(owner, name, pr_number, before_cursor, context_fragment)
|
|
581
|
+
query = <<~GRAPHQL
|
|
582
|
+
query {
|
|
583
|
+
repository(owner: "#{owner}", name: "#{name}") {
|
|
584
|
+
pullRequest(number: #{pr_number}) {
|
|
585
|
+
statusCheckRollup {
|
|
586
|
+
contexts(last: 100, before: "#{before_cursor}") {
|
|
587
|
+
pageInfo { hasPreviousPage startCursor }
|
|
588
|
+
nodes { #{context_fragment} }
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
GRAPHQL
|
|
595
|
+
|
|
596
|
+
result = `gh api graphql -f query='#{query.gsub("'", "'\\''")}' 2>/dev/null`
|
|
597
|
+
return [nil, nil] if result.empty?
|
|
598
|
+
|
|
599
|
+
contexts = JSON.parse(result).dig('data', 'repository', 'pullRequest', 'statusCheckRollup', 'contexts')
|
|
600
|
+
return [nil, nil] unless contexts
|
|
601
|
+
|
|
602
|
+
[contexts['nodes'] || [], contexts['pageInfo'] || {}]
|
|
603
|
+
rescue JSON::ParserError, StandardError
|
|
604
|
+
[nil, nil]
|
|
605
|
+
end
|
|
606
|
+
|
|
580
607
|
def check_run_emoji(conclusion, status)
|
|
581
608
|
return "⏳" if %w[QUEUED IN_PROGRESS PENDING REQUESTED WAITING].include?(status) && conclusion.nil?
|
|
582
609
|
case conclusion
|
data/lib/hiiro/version.rb
CHANGED