hiiro 0.1.233 → 0.1.235

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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/bin/h-pr +263 -56
  3. data/lib/hiiro/version.rb +1 -1
  4. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 998294642c1d69a195fe30bdc06fb48b26825bcf47454dd6b57748877f819dac
4
- data.tar.gz: 5e74e47755731485eeab5340e09f673aed515d5fe10f87c46283ad88ae945063
3
+ metadata.gz: '082d552b0f5f6b39721147e1f6bfd2967c8119ce2e46d544f7567c1dc60d26e2'
4
+ data.tar.gz: 64658ea73dbf23e723ad51ec3c0b68b9a7452c8d33a3402c97bd77427089f21a
5
5
  SHA512:
6
- metadata.gz: b9c0145e7765252e6d77f17cfd787d152caa7dc0e29d33b5759ba7960a58100c9ba3bb8b3a9e94a5b31cf8b70170546107a3da195797f68546bdb3e42c2c423f
7
- data.tar.gz: 5dd5b16bee94ad2d442ed42ba4cf8869d33e6a4ca4d357172f1958620860a46029c3f64eb82bb5b0890954357de0818d0f7a9747557ad1bc11027f5967467210
6
+ metadata.gz: 1677a7f3648f91d7ad6345d1e04f80684f7869763adb313f6fb47320b4f8f43c9abdf7678c5493ded5e990c91bd8131420183c7c50300537cfc64fa412fb4e92
7
+ data.tar.gz: 5d48770e81a3a8e44080a8dd5afd411f8891a146730d94f0c8db921bc0a75016bbe88b1b36f3fa9b9597ccaa32d20de3001836378d119d2f3f1dc6ff2fb60fd6
data/bin/h-pr CHANGED
@@ -288,6 +288,16 @@ class PinnedPRManager
288
288
 
289
289
  FAILED_CONCLUSIONS = %w[FAILURE ERROR TIMED_OUT STALE STARTUP_FAILURE ACTION_REQUIRED].freeze
290
290
 
291
+ FILTER_PREDICATES = {
292
+ red: ->(pr) { (c = pr['checks']) && c['failed'].to_i > 0 },
293
+ green: ->(pr) { (c = pr['checks']) && c['failed'].to_i == 0 && c['pending'].to_i == 0 && c['success'].to_i > 0 },
294
+ conflicts: ->(pr) { pr['mergeable'] == 'CONFLICTING' },
295
+ drafts: ->(pr) { pr['is_draft'] == true },
296
+ pending: ->(pr) { (c = pr['checks']) && c['pending'].to_i > 0 && c['failed'].to_i == 0 },
297
+ merged: ->(pr) { pr['state'] == 'MERGED' },
298
+ active: ->(pr) { pr['state'] != 'MERGED' && pr['state'] != 'CLOSED' },
299
+ }.freeze
300
+
291
301
  def summarize_checks(rollup)
292
302
  return nil unless rollup
293
303
 
@@ -368,7 +378,7 @@ class PinnedPRManager
368
378
 
369
379
  as_str = as > 0 ? "\e[30;102m#{as}\e[0m" : '-'
370
380
  crs_str = crs > 0 ? "\e[30;103m#{crs}\e[0m" : '-'
371
- conflict_str = pr['mergeable'] == 'CONFLICTING' ? " \e[30;101m[conflicts]\e[0m" : ""
381
+ conflict_str = pr['mergeable'] == 'CONFLICTING' ? " \e[30;101mC\e[0m" : ""
372
382
  reviews_str = "#{as_str}a/#{crs_str}cr#{conflict_str}"
373
383
 
374
384
  repo = pr_repo(pr)
@@ -377,6 +387,17 @@ class PinnedPRManager
377
387
  "#{num} #{state_icon} #{reviews_str} ##{pr['number']}#{repo_label} #{pr['title']}".strip
378
388
  end
379
389
 
390
+ def filter_active?(opts)
391
+ FILTER_PREDICATES.keys.any? { |f| opts.respond_to?(f) && opts.send(f) }
392
+ end
393
+
394
+ def apply_filters(prs, opts, forced: [])
395
+ active = FILTER_PREDICATES.keys.select { |f| opts.respond_to?(f) && opts.send(f) }
396
+ active = (active + forced).uniq
397
+ return prs if active.empty?
398
+ prs.select { |pr| active.any? { |f| FILTER_PREDICATES[f]&.call(pr) } }
399
+ end
400
+
380
401
  def display_detailed(pr, idx = nil)
381
402
  lines = []
382
403
  num = idx ? "#{idx + 1}." : ""
@@ -446,6 +467,17 @@ class PinnedPRManager
446
467
  end
447
468
  end
448
469
 
470
+ FILTER_OPTS = Proc.new {
471
+ flag(:red, short: 'r', desc: 'filter: failing checks')
472
+ flag(:green, short: 'g', desc: 'filter: passing checks')
473
+ flag(:conflicts, short: 'c', desc: 'filter: merge conflicts')
474
+ flag(:drafts, short: 'd', desc: 'filter: draft PRs')
475
+ flag(:pending, short: 'p', desc: 'filter: pending checks')
476
+ flag(:merged, short: 'm', desc: 'filter: merged PRs')
477
+ flag(:open, short: 'o', desc: 'filter: open (non-merged) PRs')
478
+ flag(:numbers, short: 'n', desc: 'output PR numbers only (no #)')
479
+ }
480
+
449
481
  Hiiro.run(*ARGV, plugins: [Pins]) do
450
482
  pinned_manager = PinnedPRManager.new
451
483
 
@@ -692,47 +724,67 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
692
724
  end
693
725
 
694
726
  add_subcmd(:ls) do |*ls_args|
695
- update = ls_args.include?('-u') || ls_args.include?('--update')
696
-
697
- pinned = pinned_manager.load_pinned
727
+ opts = Hiiro::Options.parse(ls_args) {
728
+ flag(:update, short: 'u', desc: 'update status before listing')
729
+ instance_eval(&FILTER_OPTS)
730
+ }
698
731
 
699
- if pinned.empty?
700
- puts "No tracked PRs"
701
- next
732
+ if opts.help
733
+ puts opts.help_text
734
+ exit 1
702
735
  end
703
736
 
704
- if update
737
+ pinned = pinned_manager.load_pinned
738
+ next puts "No tracked PRs" if pinned.empty?
739
+
740
+ if opts.update
705
741
  puts "Updating status for #{pinned.length} PR(s)..."
706
742
  pinned_manager.refresh_all_status(pinned, force: true)
707
743
  pinned_manager.save_pinned(pinned)
708
744
  puts
709
745
  end
710
746
 
711
- pinned.each_with_index do |pr, idx|
712
- puts pinned_manager.display_pinned(pr, idx)
747
+ results = pinned_manager.apply_filters(pinned, opts)
748
+
749
+ if results.empty?
750
+ puts pinned_manager.filter_active?(opts) ? "No PRs match filters." : "No tracked PRs."
751
+ next
713
752
  end
714
753
 
715
- last_checked = pinned.filter_map { |pr| pr['last_checked'] }.max
716
- puts "---"
717
- puts "Last updated: #{last_checked || 'never'}"
754
+ if opts.numbers
755
+ results.each { |pr| puts pr['number'] }
756
+ else
757
+ results.each_with_index { |pr, idx| puts pinned_manager.display_pinned(pr, idx) }
758
+ last_checked = pinned.filter_map { |pr| pr['last_checked'] }.max
759
+ puts "---"
760
+ puts "Last updated: #{last_checked || 'never'}"
761
+ end
718
762
  end
719
763
 
720
764
  add_subcmd(:status) do |*status_args|
765
+ opts = Hiiro::Options.parse(status_args, &FILTER_OPTS)
766
+
721
767
  pinned = pinned_manager.load_pinned
768
+ next puts "No tracked PRs" if pinned.empty?
722
769
 
723
- if pinned.empty?
724
- puts "No tracked PRs"
770
+ results = pinned_manager.apply_filters(pinned, opts)
771
+
772
+ if results.empty?
773
+ puts pinned_manager.filter_active?(opts) ? "No PRs match filters." : "No tracked PRs."
725
774
  next
726
775
  end
727
776
 
728
- pinned.each_with_index do |pr, idx|
729
- puts pinned_manager.display_detailed(pr, idx)
730
- puts
777
+ if opts.numbers
778
+ results.each { |pr| puts pr['number'] }
779
+ else
780
+ results.each_with_index do |pr, idx|
781
+ puts pinned_manager.display_detailed(pr, idx)
782
+ puts
783
+ end
784
+ last_checked = pinned.filter_map { |pr| pr['last_checked'] }.max
785
+ puts "---"
786
+ puts "Last updated: #{last_checked || 'never'}"
731
787
  end
732
-
733
- last_checked = pinned.filter_map { |pr| pr['last_checked'] }.max
734
- puts "---"
735
- puts "Last updated: #{last_checked || 'never'}"
736
788
  end
737
789
 
738
790
  add_subcmd(:update) do |*update_args|
@@ -756,54 +808,54 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
756
808
  end
757
809
 
758
810
  add_subcmd(:green) do |*green_args|
811
+ opts = Hiiro::Options.parse(green_args, &FILTER_OPTS)
759
812
  pinned = pinned_manager.load_pinned
813
+ results = pinned_manager.apply_filters(pinned, opts, forced: [:green])
760
814
 
761
- filtered = pinned.select { |pr|
762
- c = pr['checks']
763
- c && c['failed'] == 0 && c['pending'] == 0 && c['success'] > 0
764
- }
765
-
766
- if filtered.empty?
767
- puts "No PRs with passing checks"
815
+ if results.empty?
816
+ puts "No PRs match."
768
817
  next
769
818
  end
770
819
 
771
- puts "PRs with passing checks:"
772
- puts
773
- filtered.each_with_index { |pr, i| puts pinned_manager.display_pinned(pr, i) }
820
+ if opts.numbers
821
+ results.each { |pr| puts pr['number'] }
822
+ else
823
+ results.each_with_index { |pr, i| puts pinned_manager.display_pinned(pr, i) }
824
+ end
774
825
  end
775
826
 
776
827
  add_subcmd(:red) do |*red_args|
828
+ opts = Hiiro::Options.parse(red_args, &FILTER_OPTS)
777
829
  pinned = pinned_manager.load_pinned
830
+ results = pinned_manager.apply_filters(pinned, opts, forced: [:red])
778
831
 
779
- filtered = pinned.select { |pr|
780
- c = pr['checks']
781
- c && c['failed'] > 0
782
- }
783
-
784
- if filtered.empty?
785
- puts "No PRs with failing checks"
832
+ if results.empty?
833
+ puts "No PRs match."
786
834
  next
787
835
  end
788
836
 
789
- puts "PRs with failing checks:"
790
- puts
791
- filtered.each_with_index { |pr, i| puts pinned_manager.display_pinned(pr, i) }
837
+ if opts.numbers
838
+ results.each { |pr| puts pr['number'] }
839
+ else
840
+ results.each_with_index { |pr, i| puts pinned_manager.display_pinned(pr, i) }
841
+ end
792
842
  end
793
843
 
794
844
  add_subcmd(:old) do |*old_args|
845
+ opts = Hiiro::Options.parse(old_args, &FILTER_OPTS)
795
846
  pinned = pinned_manager.load_pinned
847
+ results = pinned_manager.apply_filters(pinned, opts, forced: [:merged])
796
848
 
797
- filtered = pinned.select { |pr| pr['state'] == 'MERGED' }
798
-
799
- if filtered.empty?
800
- puts "No merged PRs"
849
+ if results.empty?
850
+ puts "No PRs match."
801
851
  next
802
852
  end
803
853
 
804
- puts "Merged PRs:"
805
- puts
806
- filtered.each_with_index { |pr, i| puts pinned_manager.display_pinned(pr, i) }
854
+ if opts.numbers
855
+ results.each { |pr| puts pr['number'] }
856
+ else
857
+ results.each_with_index { |pr, i| puts pinned_manager.display_pinned(pr, i) }
858
+ end
807
859
  end
808
860
 
809
861
  add_subcmd(:prune) do |*args|
@@ -834,18 +886,20 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
834
886
  end
835
887
 
836
888
  add_subcmd(:draft) do |*draft_args|
889
+ opts = Hiiro::Options.parse(draft_args, &FILTER_OPTS)
837
890
  pinned = pinned_manager.load_pinned
891
+ results = pinned_manager.apply_filters(pinned, opts, forced: [:drafts])
838
892
 
839
- filtered = pinned.select { |pr| pr['is_draft'] == true }
840
-
841
- if filtered.empty?
842
- puts "No draft PRs"
893
+ if results.empty?
894
+ puts "No PRs match."
843
895
  next
844
896
  end
845
897
 
846
- puts "Draft PRs:"
847
- puts
848
- filtered.each_with_index { |pr, i| puts pinned_manager.display_pinned(pr, i) }
898
+ if opts.numbers
899
+ results.each { |pr| puts pr['number'] }
900
+ else
901
+ results.each_with_index { |pr, i| puts pinned_manager.display_pinned(pr, i) }
902
+ end
849
903
  end
850
904
 
851
905
  add_subcmd(:assigned) do
@@ -1263,6 +1317,159 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
1263
1317
  matches.each_with_index { |pr, i| puts pinned_manager.display_pinned(pr, i) }
1264
1318
  end
1265
1319
 
1320
+ # === Multi-PR action commands (mCMD) ===
1321
+ # Each accepts composable filter flags and applies the action to all matching PRs.
1322
+ # Without filters, falls back to fuzzy-select.
1323
+
1324
+ resolve_multi = ->(pinned, opts) {
1325
+ if pinned_manager.filter_active?(opts)
1326
+ results = pinned_manager.apply_filters(pinned, opts)
1327
+ if results.empty?
1328
+ puts "No PRs match filters."
1329
+ return nil
1330
+ end
1331
+ results
1332
+ else
1333
+ lines = pinned.each_with_index.each_with_object({}) { |(pr, i), h| h[pinned_manager.display_pinned(pr, i)] = pr }
1334
+ sel = fuzzyfind_from_map(lines)
1335
+ sel ? [sel] : nil
1336
+ end
1337
+ }
1338
+
1339
+ add_subcmd(:mfix) do |*args|
1340
+ opts = Hiiro::Options.parse(args) {
1341
+ instance_eval(&FILTER_OPTS)
1342
+ flag(:run, short: 'R', desc: 'launch tasks immediately after queuing')
1343
+ }
1344
+ pinned = pinned_manager.load_pinned
1345
+ next puts "No tracked PRs" if pinned.empty?
1346
+
1347
+ prs = resolve_multi.call(pinned, opts)
1348
+ next unless prs
1349
+
1350
+ q = Hiiro::Queue.current(self)
1351
+ queued_names = []
1352
+
1353
+ prs.each do |pr|
1354
+ task_info = { task_name: pr['task'], tree_name: pr['worktree'], session_name: pr['tmux_session'] }.compact
1355
+ task_info = nil if task_info.empty?
1356
+ result = q.add_with_frontmatter("/pr:fix #{pr['number']}", task_info: task_info)
1357
+ if result
1358
+ puts "Queued fix for ##{pr['number']}: #{pr['title']}"
1359
+ queued_names << result[:name]
1360
+ else
1361
+ STDERR.puts "Failed to queue fix for ##{pr['number']}"
1362
+ end
1363
+ end
1364
+
1365
+ if queued_names.any?
1366
+ puts
1367
+ puts "Queued #{queued_names.length} fix task(s)."
1368
+ if opts.run
1369
+ puts "Launching..."
1370
+ queued_names.each { |name| q.launch_task(name) }
1371
+ else
1372
+ puts "Run 'h queue run' to start."
1373
+ end
1374
+ end
1375
+ end
1376
+
1377
+ add_subcmd(:mopen) do |*args|
1378
+ opts = Hiiro::Options.parse(args, &FILTER_OPTS)
1379
+ pinned = pinned_manager.load_pinned
1380
+ next puts "No tracked PRs" if pinned.empty?
1381
+
1382
+ prs = resolve_multi.call(pinned, opts)
1383
+ next unless prs
1384
+
1385
+ prs.each { |pr| system('gh', 'pr', 'view', pr['number'].to_s, '--web') }
1386
+ end
1387
+
1388
+ add_subcmd(:mmerge) do |*args|
1389
+ opts = Hiiro::Options.parse(args) {
1390
+ instance_eval(&FILTER_OPTS)
1391
+ option(:strategy, short: 's', desc: 'merge strategy (merge, squash, rebase)')
1392
+ }
1393
+ pinned = pinned_manager.load_pinned
1394
+ next puts "No tracked PRs" if pinned.empty?
1395
+
1396
+ prs = resolve_multi.call(pinned, opts)
1397
+ next unless prs
1398
+
1399
+ merge_args = opts.strategy ? ["--#{opts.strategy}"] : []
1400
+ prs.each do |pr|
1401
+ puts "Merging ##{pr['number']}: #{pr['title']}"
1402
+ system('gh', 'pr', 'merge', pr['number'].to_s, *merge_args)
1403
+ end
1404
+ end
1405
+
1406
+ add_subcmd(:mready) do |*args|
1407
+ opts = Hiiro::Options.parse(args, &FILTER_OPTS)
1408
+ pinned = pinned_manager.load_pinned
1409
+ next puts "No tracked PRs" if pinned.empty?
1410
+
1411
+ prs = resolve_multi.call(pinned, opts)
1412
+ next unless prs
1413
+
1414
+ prs.each do |pr|
1415
+ puts "Marking ##{pr['number']} as ready: #{pr['title']}"
1416
+ system('gh', 'pr', 'ready', pr['number'].to_s)
1417
+ end
1418
+ end
1419
+
1420
+ add_subcmd(:'mto-draft') do |*args|
1421
+ opts = Hiiro::Options.parse(args, &FILTER_OPTS)
1422
+ pinned = pinned_manager.load_pinned
1423
+ next puts "No tracked PRs" if pinned.empty?
1424
+
1425
+ prs = resolve_multi.call(pinned, opts)
1426
+ next unless prs
1427
+
1428
+ prs.each do |pr|
1429
+ puts "Converting ##{pr['number']} to draft: #{pr['title']}"
1430
+ system('gh', 'pr', 'ready', '--draft', pr['number'].to_s)
1431
+ end
1432
+ end
1433
+
1434
+ add_subcmd(:mrm) do |*args|
1435
+ opts = Hiiro::Options.parse(args, &FILTER_OPTS)
1436
+ pinned = pinned_manager.load_pinned
1437
+ next puts "No tracked PRs" if pinned.empty?
1438
+
1439
+ prs = resolve_multi.call(pinned, opts)
1440
+ next unless prs
1441
+
1442
+ prs.each do |pr|
1443
+ pinned_manager.unpin(pr['number'])
1444
+ puts "Removed ##{pr['number']}: #{pr['title']}"
1445
+ end
1446
+ end
1447
+
1448
+ add_subcmd(:mcomment) do |*args|
1449
+ opts = Hiiro::Options.parse(args, &FILTER_OPTS)
1450
+ pinned = pinned_manager.load_pinned
1451
+ next puts "No tracked PRs" if pinned.empty?
1452
+
1453
+ prs = resolve_multi.call(pinned, opts)
1454
+ next unless prs
1455
+
1456
+ tmpfile = Tempfile.new(['pr-comment-', '.md'])
1457
+ tmpfile.close
1458
+ edit_files(tmpfile.path)
1459
+ body = File.read(tmpfile.path).strip
1460
+ tmpfile.unlink
1461
+
1462
+ if body.empty?
1463
+ puts "Aborted: empty comment"
1464
+ next
1465
+ end
1466
+
1467
+ prs.each do |pr|
1468
+ puts "Commenting on ##{pr['number']}: #{pr['title']}"
1469
+ system('gh', 'pr', 'comment', pr['number'].to_s, '--body', body)
1470
+ end
1471
+ end
1472
+
1266
1473
  add_subcmd(:config) do |*args|
1267
1474
  make_child do
1268
1475
  add_subcmd(:path) do
data/lib/hiiro/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.233"
2
+ VERSION = "0.1.235"
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.233
4
+ version: 0.1.235
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota