hiiro 0.1.233 → 0.1.234

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 +258 -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: 8460afefae6d7bd3f6ba3d2fb8f71a11b6c78afab3056bc103e3ed3c7a20eba1
4
+ data.tar.gz: ec62d0166213c073e62040fa456732e1b75cd3060a0d76ee2061bd921fa69047
5
5
  SHA512:
6
- metadata.gz: b9c0145e7765252e6d77f17cfd787d152caa7dc0e29d33b5759ba7960a58100c9ba3bb8b3a9e94a5b31cf8b70170546107a3da195797f68546bdb3e42c2c423f
7
- data.tar.gz: 5dd5b16bee94ad2d442ed42ba4cf8869d33e6a4ca4d357172f1958620860a46029c3f64eb82bb5b0890954357de0818d0f7a9747557ad1bc11027f5967467210
6
+ metadata.gz: 211bafbcf686e09654a3b983cb99fb8327f43273b89bb0e9a9c1c52971572f17eb8fafc4b95bcb96b3ff182b83e65218a3aa62df05b2ef75e2b3be390c1adfc8
7
+ data.tar.gz: feaa58f7885e1b8ecc0fc3f06f4b8e40b4f1f1b319ae37110772544f96abfb6ba1197d18966f6af4540dafadf3122fdcea2abcb0409470e514291ec1c23e49f7
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
+ open: ->(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,62 @@ 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')
727
+ opts = Hiiro::Options.parse(ls_args) {
728
+ flag(:update, short: 'u', desc: 'update status before listing')
729
+ instance_eval(&FILTER_OPTS)
730
+ }
696
731
 
697
732
  pinned = pinned_manager.load_pinned
733
+ next puts "No tracked PRs" if pinned.empty?
698
734
 
699
- if pinned.empty?
700
- puts "No tracked PRs"
701
- next
702
- end
703
-
704
- if update
735
+ if opts.update
705
736
  puts "Updating status for #{pinned.length} PR(s)..."
706
737
  pinned_manager.refresh_all_status(pinned, force: true)
707
738
  pinned_manager.save_pinned(pinned)
708
739
  puts
709
740
  end
710
741
 
711
- pinned.each_with_index do |pr, idx|
712
- puts pinned_manager.display_pinned(pr, idx)
742
+ results = pinned_manager.apply_filters(pinned, opts)
743
+
744
+ if results.empty?
745
+ puts pinned_manager.filter_active?(opts) ? "No PRs match filters." : "No tracked PRs."
746
+ next
713
747
  end
714
748
 
715
- last_checked = pinned.filter_map { |pr| pr['last_checked'] }.max
716
- puts "---"
717
- puts "Last updated: #{last_checked || 'never'}"
749
+ if opts.numbers
750
+ results.each { |pr| puts pr['number'] }
751
+ else
752
+ results.each_with_index { |pr, idx| puts pinned_manager.display_pinned(pr, idx) }
753
+ last_checked = pinned.filter_map { |pr| pr['last_checked'] }.max
754
+ puts "---"
755
+ puts "Last updated: #{last_checked || 'never'}"
756
+ end
718
757
  end
719
758
 
720
759
  add_subcmd(:status) do |*status_args|
760
+ opts = Hiiro::Options.parse(status_args, &FILTER_OPTS)
761
+
721
762
  pinned = pinned_manager.load_pinned
763
+ next puts "No tracked PRs" if pinned.empty?
722
764
 
723
- if pinned.empty?
724
- puts "No tracked PRs"
765
+ results = pinned_manager.apply_filters(pinned, opts)
766
+
767
+ if results.empty?
768
+ puts pinned_manager.filter_active?(opts) ? "No PRs match filters." : "No tracked PRs."
725
769
  next
726
770
  end
727
771
 
728
- pinned.each_with_index do |pr, idx|
729
- puts pinned_manager.display_detailed(pr, idx)
730
- puts
772
+ if opts.numbers
773
+ results.each { |pr| puts pr['number'] }
774
+ else
775
+ results.each_with_index do |pr, idx|
776
+ puts pinned_manager.display_detailed(pr, idx)
777
+ puts
778
+ end
779
+ last_checked = pinned.filter_map { |pr| pr['last_checked'] }.max
780
+ puts "---"
781
+ puts "Last updated: #{last_checked || 'never'}"
731
782
  end
732
-
733
- last_checked = pinned.filter_map { |pr| pr['last_checked'] }.max
734
- puts "---"
735
- puts "Last updated: #{last_checked || 'never'}"
736
783
  end
737
784
 
738
785
  add_subcmd(:update) do |*update_args|
@@ -756,54 +803,54 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
756
803
  end
757
804
 
758
805
  add_subcmd(:green) do |*green_args|
806
+ opts = Hiiro::Options.parse(green_args, &FILTER_OPTS)
759
807
  pinned = pinned_manager.load_pinned
808
+ results = pinned_manager.apply_filters(pinned, opts, forced: [:green])
760
809
 
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"
810
+ if results.empty?
811
+ puts "No PRs match."
768
812
  next
769
813
  end
770
814
 
771
- puts "PRs with passing checks:"
772
- puts
773
- filtered.each_with_index { |pr, i| puts pinned_manager.display_pinned(pr, i) }
815
+ if opts.numbers
816
+ results.each { |pr| puts pr['number'] }
817
+ else
818
+ results.each_with_index { |pr, i| puts pinned_manager.display_pinned(pr, i) }
819
+ end
774
820
  end
775
821
 
776
822
  add_subcmd(:red) do |*red_args|
823
+ opts = Hiiro::Options.parse(red_args, &FILTER_OPTS)
777
824
  pinned = pinned_manager.load_pinned
825
+ results = pinned_manager.apply_filters(pinned, opts, forced: [:red])
778
826
 
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"
827
+ if results.empty?
828
+ puts "No PRs match."
786
829
  next
787
830
  end
788
831
 
789
- puts "PRs with failing checks:"
790
- puts
791
- filtered.each_with_index { |pr, i| puts pinned_manager.display_pinned(pr, i) }
832
+ if opts.numbers
833
+ results.each { |pr| puts pr['number'] }
834
+ else
835
+ results.each_with_index { |pr, i| puts pinned_manager.display_pinned(pr, i) }
836
+ end
792
837
  end
793
838
 
794
839
  add_subcmd(:old) do |*old_args|
840
+ opts = Hiiro::Options.parse(old_args, &FILTER_OPTS)
795
841
  pinned = pinned_manager.load_pinned
842
+ results = pinned_manager.apply_filters(pinned, opts, forced: [:merged])
796
843
 
797
- filtered = pinned.select { |pr| pr['state'] == 'MERGED' }
798
-
799
- if filtered.empty?
800
- puts "No merged PRs"
844
+ if results.empty?
845
+ puts "No PRs match."
801
846
  next
802
847
  end
803
848
 
804
- puts "Merged PRs:"
805
- puts
806
- filtered.each_with_index { |pr, i| puts pinned_manager.display_pinned(pr, i) }
849
+ if opts.numbers
850
+ results.each { |pr| puts pr['number'] }
851
+ else
852
+ results.each_with_index { |pr, i| puts pinned_manager.display_pinned(pr, i) }
853
+ end
807
854
  end
808
855
 
809
856
  add_subcmd(:prune) do |*args|
@@ -834,18 +881,20 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
834
881
  end
835
882
 
836
883
  add_subcmd(:draft) do |*draft_args|
884
+ opts = Hiiro::Options.parse(draft_args, &FILTER_OPTS)
837
885
  pinned = pinned_manager.load_pinned
886
+ results = pinned_manager.apply_filters(pinned, opts, forced: [:drafts])
838
887
 
839
- filtered = pinned.select { |pr| pr['is_draft'] == true }
840
-
841
- if filtered.empty?
842
- puts "No draft PRs"
888
+ if results.empty?
889
+ puts "No PRs match."
843
890
  next
844
891
  end
845
892
 
846
- puts "Draft PRs:"
847
- puts
848
- filtered.each_with_index { |pr, i| puts pinned_manager.display_pinned(pr, i) }
893
+ if opts.numbers
894
+ results.each { |pr| puts pr['number'] }
895
+ else
896
+ results.each_with_index { |pr, i| puts pinned_manager.display_pinned(pr, i) }
897
+ end
849
898
  end
850
899
 
851
900
  add_subcmd(:assigned) do
@@ -1263,6 +1312,159 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
1263
1312
  matches.each_with_index { |pr, i| puts pinned_manager.display_pinned(pr, i) }
1264
1313
  end
1265
1314
 
1315
+ # === Multi-PR action commands (mCMD) ===
1316
+ # Each accepts composable filter flags and applies the action to all matching PRs.
1317
+ # Without filters, falls back to fuzzy-select.
1318
+
1319
+ resolve_multi = ->(pinned, opts) {
1320
+ if pinned_manager.filter_active?(opts)
1321
+ results = pinned_manager.apply_filters(pinned, opts)
1322
+ if results.empty?
1323
+ puts "No PRs match filters."
1324
+ return nil
1325
+ end
1326
+ results
1327
+ else
1328
+ lines = pinned.each_with_index.each_with_object({}) { |(pr, i), h| h[pinned_manager.display_pinned(pr, i)] = pr }
1329
+ sel = fuzzyfind_from_map(lines)
1330
+ sel ? [sel] : nil
1331
+ end
1332
+ }
1333
+
1334
+ add_subcmd(:mfix) do |*args|
1335
+ opts = Hiiro::Options.parse(args) {
1336
+ instance_eval(&FILTER_OPTS)
1337
+ flag(:run, short: 'R', desc: 'launch tasks immediately after queuing')
1338
+ }
1339
+ pinned = pinned_manager.load_pinned
1340
+ next puts "No tracked PRs" if pinned.empty?
1341
+
1342
+ prs = resolve_multi.call(pinned, opts)
1343
+ next unless prs
1344
+
1345
+ q = Hiiro::Queue.current(self)
1346
+ queued_names = []
1347
+
1348
+ prs.each do |pr|
1349
+ task_info = { task_name: pr['task'], tree_name: pr['worktree'], session_name: pr['tmux_session'] }.compact
1350
+ task_info = nil if task_info.empty?
1351
+ result = q.add_with_frontmatter("/pr:fix #{pr['number']}", task_info: task_info)
1352
+ if result
1353
+ puts "Queued fix for ##{pr['number']}: #{pr['title']}"
1354
+ queued_names << result[:name]
1355
+ else
1356
+ STDERR.puts "Failed to queue fix for ##{pr['number']}"
1357
+ end
1358
+ end
1359
+
1360
+ if queued_names.any?
1361
+ puts
1362
+ puts "Queued #{queued_names.length} fix task(s)."
1363
+ if opts.run
1364
+ puts "Launching..."
1365
+ queued_names.each { |name| q.launch_task(name) }
1366
+ else
1367
+ puts "Run 'h queue run' to start."
1368
+ end
1369
+ end
1370
+ end
1371
+
1372
+ add_subcmd(:mopen) do |*args|
1373
+ opts = Hiiro::Options.parse(args, &FILTER_OPTS)
1374
+ pinned = pinned_manager.load_pinned
1375
+ next puts "No tracked PRs" if pinned.empty?
1376
+
1377
+ prs = resolve_multi.call(pinned, opts)
1378
+ next unless prs
1379
+
1380
+ prs.each { |pr| system('gh', 'pr', 'view', pr['number'].to_s, '--web') }
1381
+ end
1382
+
1383
+ add_subcmd(:mmerge) do |*args|
1384
+ opts = Hiiro::Options.parse(args) {
1385
+ instance_eval(&FILTER_OPTS)
1386
+ option(:strategy, short: 's', desc: 'merge strategy (merge, squash, rebase)')
1387
+ }
1388
+ pinned = pinned_manager.load_pinned
1389
+ next puts "No tracked PRs" if pinned.empty?
1390
+
1391
+ prs = resolve_multi.call(pinned, opts)
1392
+ next unless prs
1393
+
1394
+ merge_args = opts.strategy ? ["--#{opts.strategy}"] : []
1395
+ prs.each do |pr|
1396
+ puts "Merging ##{pr['number']}: #{pr['title']}"
1397
+ system('gh', 'pr', 'merge', pr['number'].to_s, *merge_args)
1398
+ end
1399
+ end
1400
+
1401
+ add_subcmd(:mready) do |*args|
1402
+ opts = Hiiro::Options.parse(args, &FILTER_OPTS)
1403
+ pinned = pinned_manager.load_pinned
1404
+ next puts "No tracked PRs" if pinned.empty?
1405
+
1406
+ prs = resolve_multi.call(pinned, opts)
1407
+ next unless prs
1408
+
1409
+ prs.each do |pr|
1410
+ puts "Marking ##{pr['number']} as ready: #{pr['title']}"
1411
+ system('gh', 'pr', 'ready', pr['number'].to_s)
1412
+ end
1413
+ end
1414
+
1415
+ add_subcmd(:'mto-draft') do |*args|
1416
+ opts = Hiiro::Options.parse(args, &FILTER_OPTS)
1417
+ pinned = pinned_manager.load_pinned
1418
+ next puts "No tracked PRs" if pinned.empty?
1419
+
1420
+ prs = resolve_multi.call(pinned, opts)
1421
+ next unless prs
1422
+
1423
+ prs.each do |pr|
1424
+ puts "Converting ##{pr['number']} to draft: #{pr['title']}"
1425
+ system('gh', 'pr', 'ready', '--draft', pr['number'].to_s)
1426
+ end
1427
+ end
1428
+
1429
+ add_subcmd(:mrm) do |*args|
1430
+ opts = Hiiro::Options.parse(args, &FILTER_OPTS)
1431
+ pinned = pinned_manager.load_pinned
1432
+ next puts "No tracked PRs" if pinned.empty?
1433
+
1434
+ prs = resolve_multi.call(pinned, opts)
1435
+ next unless prs
1436
+
1437
+ prs.each do |pr|
1438
+ pinned_manager.unpin(pr['number'])
1439
+ puts "Removed ##{pr['number']}: #{pr['title']}"
1440
+ end
1441
+ end
1442
+
1443
+ add_subcmd(:mcomment) do |*args|
1444
+ opts = Hiiro::Options.parse(args, &FILTER_OPTS)
1445
+ pinned = pinned_manager.load_pinned
1446
+ next puts "No tracked PRs" if pinned.empty?
1447
+
1448
+ prs = resolve_multi.call(pinned, opts)
1449
+ next unless prs
1450
+
1451
+ tmpfile = Tempfile.new(['pr-comment-', '.md'])
1452
+ tmpfile.close
1453
+ edit_files(tmpfile.path)
1454
+ body = File.read(tmpfile.path).strip
1455
+ tmpfile.unlink
1456
+
1457
+ if body.empty?
1458
+ puts "Aborted: empty comment"
1459
+ next
1460
+ end
1461
+
1462
+ prs.each do |pr|
1463
+ puts "Commenting on ##{pr['number']}: #{pr['title']}"
1464
+ system('gh', 'pr', 'comment', pr['number'].to_s, '--body', body)
1465
+ end
1466
+ end
1467
+
1266
1468
  add_subcmd(:config) do |*args|
1267
1469
  make_child do
1268
1470
  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.234"
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.234
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota