hiiro 0.1.306 → 0.1.307

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 (6) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -72
  3. data/bin/h-branch +458 -44
  4. data/bin/h-pr +25 -0
  5. data/lib/hiiro/version.rb +1 -1
  6. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c183cf3e7fe7b583062f06764f09217bf361884d993bf9e50ce57ea8244a82e
4
- data.tar.gz: d93c261665ed81c3dcfee23fd3fe885b2ed5e8b980a23075486b1d9de35cfe11
3
+ metadata.gz: e00081cfbdc892d4c4e799cbeeb927b6d70139ad18e0b5d6abca846fe3fb1a99
4
+ data.tar.gz: 57a103e3413c9370cb3de8968d73aa451fa36f4bcea6ea2d5d1834a56c3fd7c1
5
5
  SHA512:
6
- metadata.gz: f64a5fb53855bb8968b48598224ad5b6a11ec18f64a6985141b8fb547a54aad9be36ed4b64c6f815eb7d5fbfdcda7d57f168d9754840f06632e2b9343a5f2b50
7
- data.tar.gz: 85f1544552f1137f8222ae2fe8bbf48c61a79838f76352743ad367941175216ee9945f2b55cd2007c5ad58045a165d978d7cdbf1c79d090d104ff5f9f4090a7d
6
+ metadata.gz: c832c6372c95f54e41e0f4932ac404d85495adb1724838e8b5aed601567b5519a7b9c82e1e60ce095ca8d02963e855d631c8b99ae42cdb785007ea275f5e8635
7
+ data.tar.gz: f062a27380003af64dedaf9c4a950744073c255221de17a84b4d01b2879c2bd9697811799e997803c0b347232aab5928b7eaabf1b7c5449e0cd0a52b2deadef1
data/CHANGELOG.md CHANGED
@@ -1,72 +1 @@
1
- ```markdown
2
- # Changelog
3
-
4
- ## [0.1.306] - 2026-03-30
5
-
6
- ### Changed
7
- - Increase delayed_update sleep duration from 5s to 15s
8
- - Add logging for delayed_update invocation in publish script
9
-
10
- ## [0.1.305] - 2026-03-30
11
-
12
- ### Changed
13
- - Refactor: use delayed_update subcommand instead of direct update call
14
- - Improve gem version matching regex in version check
15
-
16
- ## [0.1.304] - 2026-03-30
17
-
18
- ### Changed
19
- - h-notify: use universal log instead of per-session logging
20
- - Todo output simplified
21
-
22
- ## [0.1.302] - 2026-03-30
23
-
24
- ### Fixed
25
- - Truncate output lines to terminal width in tasks plugin
26
-
27
- ## [0.1.301]
28
-
29
- ### Added
30
- - Check version delayed update functionality
31
-
32
- ### Changed
33
- - h-claude: add verbose flags and refactor glob_path handling
34
-
35
- ### Fixed
36
- - Use exact session matching to prevent tmux prefix ambiguity
37
-
38
- ## [0.1.300]
39
-
40
- ### Added
41
- - h-claude: fulltext search option for agents/commands/skills
42
-
43
- ### Changed
44
- - Refactor h-claude directory traversal and file globbing
45
-
46
- ## [0.1.299]
47
-
48
- ### Added
49
- - h-pr open: support opening multiple PRs
50
-
51
- ## [0.1.298]
52
-
53
- ### Changed
54
- - Use Pathname to walk up directory tree
55
- - h-claude agents/commands/skills walk from pwd up to home
56
-
57
- ## [0.1.297]
58
-
59
- ### Added
60
- - h rnext subcommand
61
-
62
- ## [0.1.296]
63
-
64
- ### Changed
65
- - Refactor PR filter logic to pinned_pr_manager
66
- - Move PR filter logic to Pr#matches_filters?
67
-
68
- ## [0.1.295]
69
-
70
- ### Changed
71
- - Filter logic changes for PR management
72
- ```
1
+ Done. CHANGELOG.md has been updated with v0.1.307 entry at the top, documenting the two recent commits.
data/bin/h-branch CHANGED
@@ -28,18 +28,15 @@ class BranchManager
28
28
  data = load_data
29
29
  data['branches'] ||= []
30
30
 
31
- # Check if this branch is already recorded for this task
32
31
  existing = data['branches'].find do |b|
33
32
  b['name'] == branch_name && b['task'] == entry[:task]
34
33
  end
35
34
 
36
35
  if existing
37
- # Update existing entry
38
36
  existing.merge!(entry.transform_keys(&:to_s))
39
37
  existing['updated_at'] = Time.now.iso8601
40
38
  puts "Updated branch '#{branch_name}' for task '#{entry[:task]}'"
41
39
  else
42
- # Add new entry
43
40
  data['branches'] << entry.transform_keys(&:to_s).merge('created_at' => Time.now.iso8601)
44
41
  puts "Saved branch '#{branch_name}' for task '#{entry[:task]}'"
45
42
  end
@@ -62,15 +59,73 @@ class BranchManager
62
59
  show_entry(entry)
63
60
  end
64
61
 
65
- def load_data
66
- return {} unless File.exist?(data_file)
67
- YAML.safe_load_file(data_file) || {}
62
+ # Returns entries as uniform hashes: [{name, task, ...}]
63
+ # all: false → saved branches only; all: true → all local git branches
64
+ def branch_entries(all: false)
65
+ if all
66
+ hiiro.git.branches(sort_by: 'authordate', ignore_case: true)
67
+ .map { |name| {'name' => name, 'task' => nil} }
68
+ else
69
+ load_data['branches'] || []
70
+ end
68
71
  end
69
72
 
70
- private
73
+ def branch_names(all: false)
74
+ branch_entries(all: all).map { |e| e['name'] }
75
+ end
71
76
 
72
- def current_branch
73
- hiiro.git.branch
77
+ def saved_entry(branch_name)
78
+ (load_data['branches'] || []).find { |b| b['name'] == branch_name }
79
+ end
80
+
81
+ def rename_saved(old_name, new_name)
82
+ data = load_data
83
+ changed = false
84
+ (data['branches'] || []).each do |b|
85
+ if b['name'] == old_name
86
+ b['name'] = new_name
87
+ b['updated_at'] = Time.now.iso8601
88
+ changed = true
89
+ end
90
+ end
91
+ save_data(data) if changed
92
+ changed
93
+ end
94
+
95
+ def remove_saved(branch_name)
96
+ data = load_data
97
+ before = (data['branches'] || []).length
98
+ data['branches'] = (data['branches'] || []).reject { |b| b['name'] == branch_name }
99
+ save_data(data) if data['branches'].length < before
100
+ end
101
+
102
+ def get_note(branch_name = nil)
103
+ branch_name ||= current_branch
104
+ saved_entry(branch_name)&.[]('note')
105
+ end
106
+
107
+ def set_note(text, branch_name = nil)
108
+ branch_name ||= current_branch
109
+ data = load_data
110
+ data['branches'] ||= []
111
+ entry = data['branches'].find { |b| b['name'] == branch_name }
112
+ unless entry
113
+ new_entry = build_entry(branch_name).transform_keys(&:to_s).merge('created_at' => Time.now.iso8601)
114
+ data['branches'] << new_entry
115
+ entry = data['branches'].last
116
+ end
117
+ if text.nil?
118
+ entry.delete('note')
119
+ else
120
+ entry['note'] = text
121
+ end
122
+ entry['updated_at'] = Time.now.iso8601
123
+ save_data(data)
124
+ end
125
+
126
+ def load_data
127
+ return {} unless File.exist?(data_file)
128
+ YAML.safe_load_file(data_file) || {}
74
129
  end
75
130
 
76
131
  def build_entry(branch_name)
@@ -87,6 +142,12 @@ class BranchManager
87
142
  }
88
143
  end
89
144
 
145
+ private
146
+
147
+ def current_branch
148
+ hiiro.git.branch
149
+ end
150
+
90
151
  def capture_tmux_info
91
152
  return nil unless ENV['TMUX']
92
153
 
@@ -119,14 +180,35 @@ class BranchManager
119
180
  end
120
181
  end
121
182
 
122
- BRANCH_TAG_OPTS = Proc.new {
123
- option(:tag, short: 't', desc: 'filter by tag (OR when multiple)', multi: true)
183
+ # Helper: find main or master branch name
184
+ FIND_BASE = -> {
185
+ %w[main master].find { |b| system("git rev-parse --verify #{b} >/dev/null 2>&1") }
124
186
  }
125
187
 
126
188
  Hiiro.run(*ARGV) do
127
189
  manager = BranchManager.new(self)
128
190
  tag_store = Hiiro::Tags.new(:branch)
129
191
 
192
+ # Shared lambda: find a pinned PR by arg (number, URL, or fuzzy select)
193
+ find_pr_by_arg = ->(arg, pinned) {
194
+ if arg.nil?
195
+ return nil if pinned.empty?
196
+ lines = pinned.each_with_object({}) do |pr, h|
197
+ h["##{pr.number} [#{pr.head_branch}] #{pr.title}"] = pr
198
+ end
199
+ require 'open3'
200
+ selected, status = Open3.capture2('sk', '--no-sort', stdin_data: lines.keys.join("\n"))
201
+ return nil unless status.success? && !selected.strip.empty?
202
+ lines[selected.chomp]
203
+ elsif arg.to_s =~ %r{/pull/(\d+)}
204
+ num = $1
205
+ pinned.find { |pr| pr.number.to_s == num }
206
+ elsif arg.to_s =~ /^#?(\d+)$/
207
+ num = $1
208
+ pinned.find { |pr| pr.number.to_s == num }
209
+ end
210
+ }
211
+
130
212
  add_subcmd(:edit) { edit_files(__FILE__) }
131
213
  add_subcmd(:save) { |branch_name = nil| manager.save(branch_name) }
132
214
 
@@ -157,29 +239,123 @@ Hiiro.run(*ARGV) do
157
239
  end
158
240
 
159
241
  add_subcmd(:current) { print `git branch --show-current` }
160
- add_subcmd(:info) { manager.current }
242
+
243
+ add_subcmd(:info) do
244
+ branch_name = git.branch
245
+ unless branch_name
246
+ puts "ERROR: Not in a git repository"
247
+ next
248
+ end
249
+
250
+ entry = manager.build_entry(branch_name)
251
+ saved = manager.saved_entry(branch_name)
252
+
253
+ puts "Current branch info:"
254
+ puts
255
+ puts " Branch: #{entry[:name]}"
256
+ puts " SHA: #{entry[:sha] || '(unknown)'}"
257
+ puts " Task: #{entry[:task] || '(none)'}"
258
+ puts " Worktree: #{entry[:worktree] || '(none)'}"
259
+ if entry[:tmux]
260
+ puts " Tmux session: #{entry[:tmux]['session']}"
261
+ puts " Tmux window: #{entry[:tmux]['window']}"
262
+ puts " Tmux pane: #{entry[:tmux]['pane']}"
263
+ end
264
+
265
+ base = FIND_BASE.call
266
+ if base
267
+ ahead = `git rev-list --count #{base}..HEAD 2>/dev/null`.strip.to_i
268
+ behind = `git rev-list --count HEAD..#{base} 2>/dev/null`.strip.to_i
269
+ ahead_str = ahead > 0 ? "\e[32m↑#{ahead}\e[0m" : "↑0"
270
+ behind_str = behind > 0 ? "\e[31m↓#{behind}\e[0m" : "↓0"
271
+ puts " vs #{base}: #{ahead_str} #{behind_str}"
272
+ end
273
+
274
+ if saved && saved['note']
275
+ puts " Note: #{saved['note']}"
276
+ end
277
+
278
+ begin
279
+ pinned_prs = Hiiro::PinnedPRManager.new.load_pinned
280
+ pr = pinned_prs.find { |p| p.head_branch == branch_name }
281
+ if pr
282
+ puts " PR: ##{pr.number} (#{pr.state}) #{pr.title}"
283
+ puts " URL: #{pr.url}" if pr.url
284
+ end
285
+ rescue
286
+ # silently skip if PR data unavailable
287
+ end
288
+ end
161
289
 
162
290
  add_subcmd(:ls) do |*ls_args|
163
- opts = Hiiro::Options.parse(ls_args, &BRANCH_TAG_OPTS)
164
- branches = git.branches(sort_by: 'authordate', ignore_case: true)
291
+ opts = Hiiro::Options.parse(ls_args) do
292
+ option(:tag, short: 't', desc: 'filter by tag (OR when multiple)', multi: true)
293
+ flag(:all, short: 'a', desc: 'Show all local branches instead of just saved')
294
+ end
295
+
296
+ entries = manager.branch_entries(all: opts.all)
165
297
  current = git.branch
166
- tags_all = tag_store.all # { branch_name => [tags] }
298
+ tags_all = tag_store.all
167
299
 
168
300
  tag_filter = Array(opts.tag).reject(&:empty?)
169
301
  if tag_filter.any?
170
- branches = branches.select { |b| (Array(tags_all[b]) & tag_filter).any? }
302
+ entries = entries.select { |b| (Array(tags_all[b['name']]) & tag_filter).any? }
171
303
  end
172
304
 
173
- if branches.empty?
174
- puts tag_filter.any? ? "No branches match tags: #{tag_filter.join(', ')}" : "No branches found."
305
+ if entries.empty?
306
+ msg = if tag_filter.any?
307
+ "No #{opts.all ? '' : 'saved '}branches match tags: #{tag_filter.join(', ')}"
308
+ elsif opts.all
309
+ "No branches found."
310
+ else
311
+ "No saved branches. Use 'h branch save' or pass -a for all local branches."
312
+ end
313
+ puts msg
175
314
  next
176
315
  end
177
316
 
178
- branches.each do |b|
179
- marker = b == current ? "* " : " "
180
- tags = Array(tags_all[b])
317
+ entries.each do |b|
318
+ name = b['name']
319
+ marker = name == current ? "* " : " "
320
+ tags = Array(tags_all[name])
321
+ task_str = b['task'] ? " \e[90m[#{b['task']}]\e[0m" : ""
181
322
  tag_str = tags.any? ? " " + Hiiro::Tags.badges(tags) : ""
182
- puts "#{marker}#{b}#{tag_str}"
323
+ puts "#{marker}#{name}#{task_str}#{tag_str}"
324
+ end
325
+ end
326
+
327
+ add_subcmd(:search) do |*raw_args|
328
+ opts = Hiiro::Options.parse(raw_args) { flag(:all, short: 'a', desc: 'Search all local branches') }
329
+ terms = opts.args
330
+
331
+ if terms.empty?
332
+ puts "Usage: h branch search TERM [TERM2 ...]"
333
+ next
334
+ end
335
+
336
+ entries = manager.branch_entries(all: opts.all)
337
+ current = git.branch
338
+ tags_all = tag_store.all
339
+
340
+ matched = entries.select do |b|
341
+ terms.any? { |t|
342
+ b['name'].match?(/#{Regexp.escape(t)}/i) ||
343
+ Array(tags_all[b['name']]).any? { |tag| tag.match?(/#{Regexp.escape(t)}/i) }
344
+ }
345
+ end
346
+
347
+ if matched.empty?
348
+ puts "No #{opts.all ? '' : 'saved '}branches match: #{terms.join(', ')}"
349
+ next
350
+ end
351
+
352
+ matched.each do |b|
353
+ name = b['name']
354
+ marker = name == current ? "* " : " "
355
+ tags = Array(tags_all[name])
356
+ task_str = b['task'] ? " \e[90m[#{b['task']}]\e[0m" : ""
357
+ tag_str = tags.any? ? " " + Hiiro::Tags.badges(tags) : ""
358
+ puts "#{marker}#{name}#{task_str}#{tag_str}"
183
359
  end
184
360
  end
185
361
 
@@ -260,14 +436,15 @@ Hiiro.run(*ARGV) do
260
436
  end
261
437
 
262
438
  add_subcmd(:select) do |*select_args|
263
- branches = git.branches(sort_by: 'authordate', ignore_case: true)
439
+ opts = Hiiro::Options.parse(select_args) { flag(:all, short: 'a', desc: 'Select from all local branches') }
440
+ branches = manager.branch_names(all: opts.all)
264
441
 
265
442
  lines = branches.each_with_object({}) do |name, h|
266
443
  h[" #{name}"] = name
267
444
  end
268
445
 
269
- if select_args.any?
270
- lines = hash_matches?(lines, *select_args)
446
+ if opts.args.any?
447
+ lines = hash_matches?(lines, *opts.args)
271
448
  end
272
449
 
273
450
  require 'open3'
@@ -279,14 +456,15 @@ Hiiro.run(*ARGV) do
279
456
  end
280
457
 
281
458
  add_subcmd(:copy) do |*copy_args|
282
- branches = git.branches(sort_by: 'authordate', ignore_case: true)
459
+ opts = Hiiro::Options.parse(copy_args) { flag(:all, short: 'a', desc: 'Select from all local branches') }
460
+ branches = manager.branch_names(all: opts.all)
283
461
 
284
462
  lines = branches.each_with_object({}) do |name, h|
285
463
  h[" #{name}"] = name
286
464
  end
287
465
 
288
- if copy_args.any?
289
- lines = hash_matches?(lines, *copy_args)
466
+ if opts.args.any?
467
+ lines = hash_matches?(lines, *opts.args)
290
468
  end
291
469
 
292
470
  require 'open3'
@@ -299,10 +477,12 @@ Hiiro.run(*ARGV) do
299
477
  end
300
478
  end
301
479
 
302
- add_subcmd(:co, :checkout) { |branch = nil, *checkout_args|
480
+ add_subcmd(:co, :checkout) do |*co_args|
481
+ opts = Hiiro::Options.parse(co_args) { flag(:all, short: 'a', desc: 'Select from all local branches') }
482
+ branch = opts.args.first
483
+
303
484
  unless branch
304
- # Use sk to select a branch
305
- branches = git.branches(sort_by: 'authordate', ignore_case: true)
485
+ branches = manager.branch_names(all: opts.all)
306
486
  require 'open3'
307
487
  selected, status = Open3.capture2('sk', '--no-sort', '--tac', stdin_data: branches.join("\n"))
308
488
  unless status.success? && !selected.strip.empty?
@@ -312,13 +492,15 @@ Hiiro.run(*ARGV) do
312
492
  branch = selected.strip
313
493
  end
314
494
 
315
- system('git', 'checkout', branch, *checkout_args)
316
- }
495
+ system('git', 'checkout', branch)
496
+ end
497
+
498
+ add_subcmd(:rm, :remove) do |*rm_args|
499
+ opts = Hiiro::Options.parse(rm_args) { flag(:all, short: 'a', desc: 'Select from all local branches') }
500
+ branch = opts.args.first
317
501
 
318
- add_subcmd(:rm, :remove) { |branch = nil, *remove_args|
319
502
  unless branch
320
- # Use sk to select a branch
321
- branches = git.branches(sort_by: 'authordate', ignore_case: true)
503
+ branches = manager.branch_names(all: opts.all)
322
504
  require 'open3'
323
505
  selected, status = Open3.capture2('sk', '--no-sort', '--tac', stdin_data: branches.join("\n"))
324
506
  unless status.success? && !selected.strip.empty?
@@ -328,8 +510,246 @@ Hiiro.run(*ARGV) do
328
510
  branch = selected.strip
329
511
  end
330
512
 
331
- system('git', 'branch', '-d', branch, *remove_args)
332
- }
513
+ system('git', 'branch', '-d', branch)
514
+ end
515
+
516
+ add_subcmd(:rename) do |new_name = nil, old_name = nil|
517
+ unless new_name
518
+ puts "Usage: h branch rename <new_name> [old_name]"
519
+ next
520
+ end
521
+
522
+ old_name ||= git.branch
523
+
524
+ unless system('git', 'branch', '-m', old_name, new_name)
525
+ puts "ERROR: Could not rename branch"
526
+ next
527
+ end
528
+ puts "Renamed '#{old_name}' → '#{new_name}'"
529
+
530
+ remote = `git config branch.#{old_name}.remote 2>/dev/null`.strip
531
+ if !remote.empty?
532
+ if system('git', 'push', remote, ":#{old_name}", "#{new_name}")
533
+ system('git', 'branch', '--set-upstream-to', "#{remote}/#{new_name}", new_name)
534
+ puts "Updated remote #{remote}: deleted #{old_name}, pushed #{new_name}"
535
+ else
536
+ puts "WARNING: Could not update remote. Renamed locally only."
537
+ end
538
+ end
539
+
540
+ if manager.rename_saved(old_name, new_name)
541
+ puts "Updated saved branch record"
542
+ end
543
+ end
544
+
545
+ add_subcmd(:status) do |*args|
546
+ opts = Hiiro::Options.parse(args) { flag(:all, short: 'a', desc: 'Show all local branches') }
547
+ entries = manager.branch_entries(all: opts.all)
548
+ current = git.branch
549
+ base = FIND_BASE.call || 'HEAD'
550
+
551
+ if entries.empty?
552
+ puts opts.all ? "No branches found." : "No saved branches. Use -a to show all."
553
+ next
554
+ end
555
+
556
+ pinned_prs = begin
557
+ Hiiro::PinnedPRManager.new.load_pinned
558
+ rescue
559
+ []
560
+ end
561
+ pr_by_branch = pinned_prs.each_with_object({}) { |pr, h| h[pr.head_branch] = pr }
562
+
563
+ name_width = [entries.map { |b| b['name'].length }.max, 20].max
564
+
565
+ entries.each do |b|
566
+ name = b['name']
567
+ marker = name == current ? "* " : " "
568
+
569
+ ahead = `git rev-list --count #{base}..#{name} 2>/dev/null`.strip.to_i
570
+ behind = `git rev-list --count #{name}..#{base} 2>/dev/null`.strip.to_i
571
+
572
+ ahead_str = ahead > 0 ? "\e[32m↑#{ahead}\e[0m" : "\e[90m↑0\e[0m"
573
+ behind_str = behind > 0 ? "\e[31m↓#{behind}\e[0m" : "\e[90m↓0\e[0m"
574
+
575
+ pr = pr_by_branch[name]
576
+ pr_str = pr ? " \e[36m##{pr.number} #{pr.state}\e[0m" : ""
577
+ task_str = b['task'] ? " \e[90m[#{b['task']}]\e[0m" : ""
578
+
579
+ puts "#{marker}#{name.ljust(name_width)} #{ahead_str} #{behind_str}#{pr_str}#{task_str}"
580
+ end
581
+ end
582
+
583
+ add_subcmd(:merged) do |*args|
584
+ opts = Hiiro::Options.parse(args) { flag(:all, short: 'a', desc: 'Show all merged branches, not just saved') }
585
+ base = FIND_BASE.call || 'main'
586
+
587
+ all_merged = `git branch --merged #{base} 2>/dev/null`
588
+ .lines.map { |b| b.strip.sub(/^\* /, '') }
589
+ .reject { |b| b.empty? || %w[main master].include?(b) }
590
+
591
+ if all_merged.empty?
592
+ puts "No merged branches."
593
+ next
594
+ end
595
+
596
+ displayed = if opts.all
597
+ all_merged
598
+ else
599
+ saved_names = manager.branch_names.to_set
600
+ filtered = all_merged.select { |b| saved_names.include?(b) }
601
+ if filtered.empty?
602
+ puts "No saved merged branches (use -a to show all)."
603
+ next
604
+ end
605
+ filtered
606
+ end
607
+
608
+ current = git.branch
609
+ displayed.each do |b|
610
+ marker = b == current ? "* " : " "
611
+ puts "#{marker}#{b}"
612
+ end
613
+ end
614
+
615
+ add_subcmd(:clean) do |*args|
616
+ opts = Hiiro::Options.parse(args) do
617
+ flag(:all, short: 'a', desc: 'Include all merged branches, not just saved')
618
+ flag(:force, short: 'f', desc: 'Delete all without confirmation prompt')
619
+ end
620
+ base = FIND_BASE.call || 'main'
621
+
622
+ all_merged = `git branch --merged #{base} 2>/dev/null`
623
+ .lines.map { |b| b.strip.sub(/^\* /, '') }
624
+ .reject { |b| b.empty? || %w[main master].include?(b) }
625
+
626
+ to_consider = if opts.all
627
+ all_merged
628
+ else
629
+ saved_names = manager.branch_names.to_set
630
+ all_merged.select { |b| saved_names.include?(b) }
631
+ end
632
+
633
+ if to_consider.empty?
634
+ puts "No #{opts.all ? '' : 'saved '}merged branches to clean."
635
+ next
636
+ end
637
+
638
+ to_delete = if opts.force
639
+ to_consider
640
+ else
641
+ require 'open3'
642
+ selected, status = Open3.capture2('sk', '--multi', '--no-sort', stdin_data: to_consider.join("\n"))
643
+ if !status.success? || selected.strip.empty?
644
+ puts "Nothing selected."
645
+ next
646
+ end
647
+ selected.lines.map(&:strip).reject(&:empty?)
648
+ end
649
+
650
+ to_delete.each do |b|
651
+ if system('git', 'branch', '-d', b)
652
+ puts "Deleted #{b}"
653
+ manager.remove_saved(b)
654
+ else
655
+ puts "Could not delete #{b} (may need -D for unmerged; skipping)"
656
+ end
657
+ end
658
+ end
659
+
660
+ add_subcmd(:recent) do |n = nil|
661
+ n = (n || 10).to_i
662
+
663
+ recent_branches = `git reflog --format="%D" 2>/dev/null`
664
+ .lines
665
+ .flat_map { |line| line.scan(/HEAD -> ([^,\n]+)/) }
666
+ .flatten
667
+ .uniq
668
+ .reject { |b| b.strip.empty? }
669
+ .first(n)
670
+
671
+ if recent_branches.empty?
672
+ puts "No recent branches found in reflog."
673
+ next
674
+ end
675
+
676
+ current = git.branch
677
+ tags_all = tag_store.all
678
+ saved_names = manager.branch_names.to_set
679
+
680
+ recent_branches.each do |name|
681
+ marker = name == current ? "* " : " "
682
+ tags = Array(tags_all[name])
683
+ saved_str = saved_names.include?(name) ? " \e[90m[saved]\e[0m" : ""
684
+ tag_str = tags.any? ? " " + Hiiro::Tags.badges(tags) : ""
685
+ puts "#{marker}#{name}#{saved_str}#{tag_str}"
686
+ end
687
+ end
688
+
689
+ add_subcmd(:note) do |*note_args|
690
+ opts = Hiiro::Options.parse(note_args) { flag(:clear, desc: 'Clear the note for this branch') }
691
+
692
+ if opts.clear
693
+ manager.set_note(nil)
694
+ puts "Note cleared."
695
+ next
696
+ end
697
+
698
+ text = opts.args.join(' ')
699
+
700
+ if text.empty?
701
+ note = manager.get_note
702
+ puts note ? note : "(no note)"
703
+ else
704
+ manager.set_note(text)
705
+ puts "Note saved: #{text}"
706
+ end
707
+ end
708
+
709
+ add_subcmd(:'for-task') do |task_name = nil|
710
+ task_name ||= begin
711
+ Hiiro::Environment.current&.task&.name
712
+ rescue
713
+ nil
714
+ end
715
+
716
+ unless task_name
717
+ puts "Usage: h branch for-task <task_name>"
718
+ puts " (or run from a task session to use current task)"
719
+ next
720
+ end
721
+
722
+ data = manager.load_data
723
+ branches = (data['branches'] || []).select { |b| b['task'] == task_name }
724
+
725
+ if branches.empty?
726
+ puts "No saved branches for task '#{task_name}'"
727
+ next
728
+ end
729
+
730
+ current = git.branch
731
+ puts "Branches for task '#{task_name}':"
732
+ branches.each do |b|
733
+ marker = b['name'] == current ? "* " : " "
734
+ puts "#{marker}#{b['name']}"
735
+ end
736
+ end
737
+
738
+ add_subcmd(:'for-pr', :pr) do |arg = nil|
739
+ pinned_prs = Hiiro::PinnedPRManager.new.load_pinned rescue []
740
+ pr = find_pr_by_arg.call(arg, pinned_prs)
741
+
742
+ if pr.nil?
743
+ puts arg ? "No tracked PR found for: #{arg}" : "No PR selected."
744
+ next
745
+ end
746
+
747
+ if pr.head_branch
748
+ puts pr.head_branch
749
+ else
750
+ puts "PR ##{pr.number} has no branch info"
751
+ end
752
+ end
333
753
 
334
754
  add_subcmd(:duplicate) { |new_name = nil, source = nil|
335
755
  unless new_name
@@ -394,14 +814,11 @@ Hiiro.run(*ARGV) do
394
814
  add_subcmd(:diff) { |*diff_args|
395
815
  case diff_args.length
396
816
  when 0
397
- # Compare current branch to main/master
398
817
  base = %w[main master].find { |b| system("git rev-parse --verify #{b} >/dev/null 2>&1") } || 'HEAD~1'
399
818
  range = "#{base}..HEAD"
400
819
  when 1
401
- # Compare current branch to specified ref
402
820
  range = "#{diff_args[0]}..HEAD"
403
821
  else
404
- # Compare two refs (from..to or from...to based on args)
405
822
  from, to = diff_args[0..1]
406
823
  range = "#{from}..#{to}"
407
824
  end
@@ -510,7 +927,6 @@ Hiiro.run(*ARGV) do
510
927
  forkpoint = `git merge-base --fork-point #{upstream} #{branch} 2>/dev/null`.strip
511
928
 
512
929
  if forkpoint.empty?
513
- # Fallback to regular merge-base if fork-point fails
514
930
  forkpoint = `git merge-base #{upstream} #{branch} 2>/dev/null`.strip
515
931
  end
516
932
 
@@ -524,7 +940,6 @@ Hiiro.run(*ARGV) do
524
940
  add_subcmd(:ancestor) { |*ancestor_args|
525
941
  case ancestor_args.length
526
942
  when 0
527
- # Check if main/master is ancestor of HEAD
528
943
  base = %w[main master].find { |b| system("git rev-parse --verify #{b} >/dev/null 2>&1") }
529
944
  unless base
530
945
  puts "Cannot find main or master branch"
@@ -533,7 +948,6 @@ Hiiro.run(*ARGV) do
533
948
  ancestor = base
534
949
  descendant = 'HEAD'
535
950
  when 1
536
- # Check if arg is ancestor of HEAD
537
951
  ancestor = ancestor_args[0]
538
952
  descendant = 'HEAD'
539
953
  else
data/bin/h-pr CHANGED
@@ -829,6 +829,31 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
829
829
 
830
830
  # === PR List by Context ===
831
831
 
832
+ add_subcmd(:branch) do |arg = nil|
833
+ pr_arg = if arg.to_s =~ %r{/pull/(\d+)}
834
+ $1
835
+ else
836
+ arg
837
+ end
838
+
839
+ pr_number = resolve(:pr, pr_arg)
840
+ next unless pr_number
841
+
842
+ pinned = pinned_manager.load_pinned
843
+ pr = pinned.find { |p| p.number.to_s == pr_number.to_s }
844
+
845
+ unless pr
846
+ puts "PR ##{pr_number} not in tracked list"
847
+ next
848
+ end
849
+
850
+ if pr.head_branch
851
+ puts pr.head_branch
852
+ else
853
+ puts "PR ##{pr.number} has no branch info"
854
+ end
855
+ end
856
+
832
857
  add_subcmd(:'for-task') do |task_name = nil|
833
858
  tracked = pinned_manager.load_pinned
834
859
 
data/lib/hiiro/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.306"
2
+ VERSION = "0.1.307"
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.306
4
+ version: 0.1.307
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota