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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1 -72
- data/bin/h-branch +458 -44
- data/bin/h-pr +25 -0
- 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: e00081cfbdc892d4c4e799cbeeb927b6d70139ad18e0b5d6abca846fe3fb1a99
|
|
4
|
+
data.tar.gz: 57a103e3413c9370cb3de8968d73aa451fa36f4bcea6ea2d5d1834a56c3fd7c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c832c6372c95f54e41e0f4932ac404d85495adb1724838e8b5aed601567b5519a7b9c82e1e60ce095ca8d02963e855d631c8b99ae42cdb785007ea275f5e8635
|
|
7
|
+
data.tar.gz: f062a27380003af64dedaf9c4a950744073c255221de17a84b4d01b2879c2bd9697811799e997803c0b347232aab5928b7eaabf1b7c5449e0cd0a52b2deadef1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,72 +1 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
73
|
+
def branch_names(all: false)
|
|
74
|
+
branch_entries(all: all).map { |e| e['name'] }
|
|
75
|
+
end
|
|
71
76
|
|
|
72
|
-
def
|
|
73
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
|
164
|
-
|
|
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
|
|
298
|
+
tags_all = tag_store.all
|
|
167
299
|
|
|
168
300
|
tag_filter = Array(opts.tag).reject(&:empty?)
|
|
169
301
|
if tag_filter.any?
|
|
170
|
-
|
|
302
|
+
entries = entries.select { |b| (Array(tags_all[b['name']]) & tag_filter).any? }
|
|
171
303
|
end
|
|
172
304
|
|
|
173
|
-
if
|
|
174
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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}#{
|
|
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
|
-
|
|
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
|
|
270
|
-
lines = hash_matches?(lines, *
|
|
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
|
-
|
|
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
|
|
289
|
-
lines = hash_matches?(lines, *
|
|
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)
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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