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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37466eb01d7284c838b3ba958f35a41e0f33f4f4fd5c22036ce11f2daa13bfb4
4
- data.tar.gz: 55b421c1b3c124cc265d14c93f7709d0e2c894d4497125884306fbe53fe29762
3
+ metadata.gz: 03a9ad967ffc6e338367882dd85b3305099e521dc7a0a2341acf71257ca22bde
4
+ data.tar.gz: 80f4746d3cdb168b9275e8ca4e55822d356966f4467f147b68741cde3045cfa6
5
5
  SHA512:
6
- metadata.gz: dc41d3e7f2fc019d20336f0890dcff641d545a3bb25a6b83147fc1067b3a139a2548c498b8f9e825cbe7096c2a4d2d77706ed9e21d79b36b26459db54a2ea3f4
7
- data.tar.gz: 403890caf1a18fe7b5c231b1fd12c2e4d07897867c8e46620441923f2860b4e5c0f7ab848105edb4c773a7d2724f5a4be4048e37155ec31bce78ea387f07fc1c
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
- def self.summarize_checks(rollup)
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
- @values[defn.name] = !defn.default
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
- @values[defn.name] = !defn.default
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: 250) {
510
- nodes {
511
- ... on CheckRun {
512
- __typename
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' => pr_data['number'],
562
- 'title' => pr_data['title'],
563
- 'url' => pr_data['url'],
564
- 'headRefName' => pr_data['headRefName'],
565
- 'state' => pr_data['state'],
566
- 'isDraft' => pr_data['isDraft'],
567
- 'mergeable' => pr_data['mergeable'],
568
- 'reviewDecision' => pr_data['reviewDecision'],
569
- 'statusCheckRollup' => pr_data.dig('statusCheckRollup', 'contexts', 'nodes'),
570
- 'reviews' => pr_data.dig('reviews', 'nodes') || [],
571
- 'repo' => repo_path
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
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.289"
2
+ VERSION = "0.1.291"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hiiro
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.289
4
+ version: 0.1.291
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota