hiiro 0.1.85 → 0.1.86
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/bin/h-branch +86 -141
- data/bin/h-pane +11 -0
- data/bin/h-pr +64 -0
- data/bin/h-session +12 -0
- data/bin/h-window +12 -0
- data/lib/hiiro/history/entry.rb +136 -0
- data/lib/hiiro/history.rb +454 -0
- data/lib/hiiro/shell.rb +19 -0
- data/lib/hiiro/sk.rb +2 -9
- data/lib/hiiro/version.rb +1 -1
- data/lib/hiiro.rb +9 -0
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2da3861933a1b2b03e5ff7cd9fa87d700e7f3f1ed8e6eb62463ec93c783e2a86
|
|
4
|
+
data.tar.gz: 9c5957829cae479530f68cccae440bc683285a93e1695db75c4a44dfa1a02503
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6c0c41355a0c33056574f1e4c61d937087e7b8fedb1db2fe864f41f571264c44c8c7c46af14b4e238d17f202f061f599fb982fa7fdcfddc7829051802462e7f8
|
|
7
|
+
data.tar.gz: 07a5888842492bf0aa9b9ed5444b9f9b12bb15ea14615b70eaeb44b4c0e41d9319e1ffdbab80eba422108a91cb68dfb5c0c20edc9f76500364eaf67ee5671d25
|
data/bin/h-branch
CHANGED
|
@@ -14,6 +14,18 @@ class BranchManager
|
|
|
14
14
|
@hiiro = hiiro
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
def help
|
|
18
|
+
puts "Usage: h branch <subcommand> [args]"
|
|
19
|
+
puts
|
|
20
|
+
puts "Subcommands:"
|
|
21
|
+
puts " save Record current branch for this task"
|
|
22
|
+
puts " history List branch history (oldest to newest)"
|
|
23
|
+
puts " history --worktree=X Filter by worktree"
|
|
24
|
+
puts " history --task=X Filter by task"
|
|
25
|
+
puts " history --session=X Filter by tmux session"
|
|
26
|
+
puts " current Show current branch info"
|
|
27
|
+
end
|
|
28
|
+
|
|
17
29
|
def save
|
|
18
30
|
branch_name = current_branch
|
|
19
31
|
unless branch_name
|
|
@@ -50,6 +62,35 @@ class BranchManager
|
|
|
50
62
|
true
|
|
51
63
|
end
|
|
52
64
|
|
|
65
|
+
def history(args = [])
|
|
66
|
+
filters = parse_filters(args)
|
|
67
|
+
data = load_data
|
|
68
|
+
branches = data['branches'] || []
|
|
69
|
+
|
|
70
|
+
if branches.empty?
|
|
71
|
+
puts "No branches recorded."
|
|
72
|
+
puts "Use 'h branch save' to record the current branch."
|
|
73
|
+
return
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Apply filters
|
|
77
|
+
branches = filter_entries(branches, filters)
|
|
78
|
+
|
|
79
|
+
if branches.empty?
|
|
80
|
+
puts "No branches match the given filters."
|
|
81
|
+
return
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Sort by created_at (oldest first)
|
|
85
|
+
branches = branches.sort_by { |b| b['created_at'] || '' }
|
|
86
|
+
|
|
87
|
+
puts "Branch history (oldest to newest):"
|
|
88
|
+
puts
|
|
89
|
+
branches.each_with_index do |branch, idx|
|
|
90
|
+
puts format_entry(branch, idx + 1)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
53
94
|
def current
|
|
54
95
|
branch_name = current_branch
|
|
55
96
|
unless branch_name
|
|
@@ -91,6 +132,48 @@ class BranchManager
|
|
|
91
132
|
}
|
|
92
133
|
end
|
|
93
134
|
|
|
135
|
+
def parse_filters(args)
|
|
136
|
+
filters = {}
|
|
137
|
+
args.each do |arg|
|
|
138
|
+
case arg
|
|
139
|
+
when /^--worktree=(.+)$/
|
|
140
|
+
filters[:worktree] = $1
|
|
141
|
+
when /^--task=(.+)$/
|
|
142
|
+
filters[:task] = $1
|
|
143
|
+
when /^--session=(.+)$/
|
|
144
|
+
filters[:session] = $1
|
|
145
|
+
when /^-w(.+)$/
|
|
146
|
+
filters[:worktree] = $1
|
|
147
|
+
when /^-t(.+)$/
|
|
148
|
+
filters[:task] = $1
|
|
149
|
+
when /^-s(.+)$/
|
|
150
|
+
filters[:session] = $1
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
filters
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def filter_entries(entries, filters)
|
|
157
|
+
entries.select do |entry|
|
|
158
|
+
next false if filters[:worktree] && !entry['worktree']&.include?(filters[:worktree])
|
|
159
|
+
next false if filters[:task] && !entry['task']&.include?(filters[:task])
|
|
160
|
+
next false if filters[:session] && entry.dig('tmux', 'session') != filters[:session]
|
|
161
|
+
true
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def format_entry(entry, num)
|
|
166
|
+
lines = []
|
|
167
|
+
lines << "#{num}. #{entry['name']}"
|
|
168
|
+
lines << " Task: #{entry['task'] || '(none)'}"
|
|
169
|
+
lines << " Worktree: #{entry['worktree'] || '(none)'}"
|
|
170
|
+
if entry['tmux']
|
|
171
|
+
lines << " Tmux: #{entry['tmux']['session']}/#{entry['tmux']['window']}"
|
|
172
|
+
end
|
|
173
|
+
lines << " Created: #{entry['created_at']}"
|
|
174
|
+
lines.join("\n")
|
|
175
|
+
end
|
|
176
|
+
|
|
94
177
|
def show_entry(entry)
|
|
95
178
|
puts " Branch: #{entry[:name]}"
|
|
96
179
|
puts " Task: #{entry[:task] || '(none)'}"
|
|
@@ -122,8 +205,7 @@ manager = BranchManager.new(hiiro)
|
|
|
122
205
|
hiiro.add_subcmd(:edit) { system(ENV['EDITOR'] || 'nvim', __FILE__) }
|
|
123
206
|
hiiro.add_subcmd(:save) { manager.save }
|
|
124
207
|
hiiro.add_subcmd(:history) { |*args| manager.history(args) }
|
|
125
|
-
hiiro.add_subcmd(:current) {
|
|
126
|
-
hiiro.add_subcmd(:info) { manager.current }
|
|
208
|
+
hiiro.add_subcmd(:current) { manager.current }
|
|
127
209
|
hiiro.add_subcmd(:select) do |*args|
|
|
128
210
|
branches = hiiro.git.branches(sort_by: 'authordate', ignore_case: true)
|
|
129
211
|
|
|
@@ -259,36 +341,6 @@ hiiro.add_subcmd(:diff) { |*args|
|
|
|
259
341
|
system('git', 'log', '--oneline', '--decorate', range)
|
|
260
342
|
}
|
|
261
343
|
|
|
262
|
-
hiiro.add_subcmd(:changed) { |*args|
|
|
263
|
-
show_all = args.delete('-a') || args.delete('--all')
|
|
264
|
-
upstream = args.first
|
|
265
|
-
|
|
266
|
-
upstream ||= %w[origin/master master origin/main main].find { |ref|
|
|
267
|
-
system("git rev-parse --verify #{ref} >/dev/null 2>&1")
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
unless upstream
|
|
271
|
-
puts "Cannot find master or origin/master"
|
|
272
|
-
next
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
forkpoint = `git merge-base --fork-point #{upstream} HEAD 2>/dev/null`.strip
|
|
276
|
-
if forkpoint.empty?
|
|
277
|
-
forkpoint = `git merge-base #{upstream} HEAD 2>/dev/null`.strip
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
if forkpoint.empty?
|
|
281
|
-
puts "Could not find fork point"
|
|
282
|
-
next
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
if show_all
|
|
286
|
-
system('git', 'diff', '--name-only', "#{forkpoint}...HEAD")
|
|
287
|
-
else
|
|
288
|
-
system('git', 'diff', '--name-only', '--relative', "#{forkpoint}...HEAD")
|
|
289
|
-
end
|
|
290
|
-
}
|
|
291
|
-
|
|
292
344
|
hiiro.add_subcmd(:ahead) { |*args|
|
|
293
345
|
case args.length
|
|
294
346
|
when 0
|
|
@@ -323,115 +375,6 @@ hiiro.add_subcmd(:behind) { |*args|
|
|
|
323
375
|
puts "#{branch} is #{count} commit(s) behind #{behind_of}"
|
|
324
376
|
}
|
|
325
377
|
|
|
326
|
-
hiiro.add_subcmd(:log) { |upstream = nil|
|
|
327
|
-
upstream ||= %w[origin/master master origin/main main].find { |ref|
|
|
328
|
-
system("git rev-parse --verify #{ref} >/dev/null 2>&1")
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
unless upstream
|
|
332
|
-
puts "Cannot find master or origin/master"
|
|
333
|
-
next
|
|
334
|
-
end
|
|
335
|
-
|
|
336
|
-
forkpoint = `git merge-base --fork-point #{upstream} HEAD 2>/dev/null`.strip
|
|
337
|
-
if forkpoint.empty?
|
|
338
|
-
forkpoint = `git merge-base #{upstream} HEAD 2>/dev/null`.strip
|
|
339
|
-
end
|
|
340
|
-
|
|
341
|
-
if forkpoint.empty?
|
|
342
|
-
puts "Could not find fork point"
|
|
343
|
-
next
|
|
344
|
-
end
|
|
345
|
-
|
|
346
|
-
system('git', 'log', '--oneline', '--decorate', "#{forkpoint}..HEAD")
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
hiiro.add_subcmd(:forkpoint) { |upstream = nil, branch = nil|
|
|
350
|
-
branch ||= 'HEAD'
|
|
351
|
-
upstream ||= %w[origin/master master origin/main main].find { |ref|
|
|
352
|
-
system("git rev-parse --verify #{ref} >/dev/null 2>&1")
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
unless upstream
|
|
356
|
-
puts "Cannot find master or origin/master"
|
|
357
|
-
next
|
|
358
|
-
end
|
|
359
|
-
|
|
360
|
-
forkpoint = `git merge-base --fork-point #{upstream} #{branch} 2>/dev/null`.strip
|
|
361
|
-
|
|
362
|
-
if forkpoint.empty?
|
|
363
|
-
# Fallback to regular merge-base if fork-point fails
|
|
364
|
-
forkpoint = `git merge-base #{upstream} #{branch} 2>/dev/null`.strip
|
|
365
|
-
end
|
|
366
|
-
|
|
367
|
-
if forkpoint.empty?
|
|
368
|
-
puts "Could not find fork point between #{upstream} and #{branch}"
|
|
369
|
-
else
|
|
370
|
-
puts forkpoint
|
|
371
|
-
end
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
hiiro.add_subcmd(:changed) { |*args|
|
|
375
|
-
show_all = args.delete('-a') || args.delete('--all')
|
|
376
|
-
upstream = args.first
|
|
377
|
-
|
|
378
|
-
upstream ||= %w[origin/master master origin/main main].find { |ref|
|
|
379
|
-
system("git rev-parse --verify #{ref} >/dev/null 2>&1")
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
unless upstream
|
|
383
|
-
puts "Cannot find master or origin/master"
|
|
384
|
-
next
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
forkpoint = `git merge-base --fork-point #{upstream} HEAD 2>/dev/null`.strip
|
|
388
|
-
if forkpoint.empty?
|
|
389
|
-
forkpoint = `git merge-base #{upstream} HEAD 2>/dev/null`.strip
|
|
390
|
-
end
|
|
391
|
-
|
|
392
|
-
if forkpoint.empty?
|
|
393
|
-
puts "Could not find fork point"
|
|
394
|
-
next
|
|
395
|
-
end
|
|
396
|
-
|
|
397
|
-
if show_all
|
|
398
|
-
system('git', 'diff', '--name-only', "#{forkpoint}...HEAD")
|
|
399
|
-
else
|
|
400
|
-
system('git', 'diff', '--name-only', '--relative', "#{forkpoint}...HEAD")
|
|
401
|
-
end
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
hiiro.add_subcmd(:changed) { |*args|
|
|
406
|
-
show_all = args.delete('-a') || args.delete('--all')
|
|
407
|
-
upstream = args.first
|
|
408
|
-
|
|
409
|
-
upstream ||= %w[origin/master master origin/main main].find { |ref|
|
|
410
|
-
system("git rev-parse --verify #{ref} >/dev/null 2>&1")
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
unless upstream
|
|
414
|
-
puts "Cannot find master or origin/master"
|
|
415
|
-
next
|
|
416
|
-
end
|
|
417
|
-
|
|
418
|
-
forkpoint = `git merge-base --fork-point #{upstream} HEAD 2>/dev/null`.strip
|
|
419
|
-
if forkpoint.empty?
|
|
420
|
-
forkpoint = `git merge-base #{upstream} HEAD 2>/dev/null`.strip
|
|
421
|
-
end
|
|
422
|
-
|
|
423
|
-
if forkpoint.empty?
|
|
424
|
-
puts "Could not find fork point"
|
|
425
|
-
next
|
|
426
|
-
end
|
|
427
|
-
|
|
428
|
-
if show_all
|
|
429
|
-
system('git', 'diff', '--name-only', "#{forkpoint}...HEAD")
|
|
430
|
-
else
|
|
431
|
-
system('git', 'diff', '--name-only', '--relative', "#{forkpoint}...HEAD")
|
|
432
|
-
end
|
|
433
|
-
}
|
|
434
|
-
|
|
435
378
|
hiiro.add_subcmd(:ancestor) { |*args|
|
|
436
379
|
case args.length
|
|
437
380
|
when 0
|
|
@@ -458,4 +401,6 @@ hiiro.add_subcmd(:ancestor) { |*args|
|
|
|
458
401
|
end
|
|
459
402
|
}
|
|
460
403
|
|
|
404
|
+
hiiro.add_default { manager.help }
|
|
405
|
+
|
|
461
406
|
hiiro.run
|
data/bin/h-pane
CHANGED
|
@@ -60,6 +60,17 @@ o.add_subcmd(:resize) { |*args|
|
|
|
60
60
|
system('tmux', 'resize-pane', *args)
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
o.add_subcmd(:history) { |pane_id=nil|
|
|
64
|
+
entries = Hiiro::History.by_pane(pane_id)
|
|
65
|
+
if entries.empty?
|
|
66
|
+
puts "No history for this pane"
|
|
67
|
+
else
|
|
68
|
+
puts "History for pane: #{pane_id || ENV['TMUX_PANE'] || 'current'}"
|
|
69
|
+
puts
|
|
70
|
+
entries.last(20).each_with_index { |e, i| puts e.oneline(i + 1) }
|
|
71
|
+
end
|
|
72
|
+
}
|
|
73
|
+
|
|
63
74
|
o.add_subcmd(:select) do |*args|
|
|
64
75
|
# Get all panes with details
|
|
65
76
|
output = `tmux list-panes -a -F '\#{session_name}:\#{window_index}.\#{pane_index} [\#{pane_current_command}] \#{pane_current_path}'`.strip
|
data/bin/h-pr
CHANGED
|
@@ -21,6 +21,10 @@ class PRManager
|
|
|
21
21
|
puts
|
|
22
22
|
puts "Subcommands:"
|
|
23
23
|
puts " save [PR_NUMBER] Record PR for this task (auto-detects if omitted)"
|
|
24
|
+
puts " history List PR history (oldest to newest)"
|
|
25
|
+
puts " history --worktree=X Filter by worktree"
|
|
26
|
+
puts " history --task=X Filter by task"
|
|
27
|
+
puts " history --session=X Filter by tmux session"
|
|
24
28
|
puts " current Show current branch's PR info"
|
|
25
29
|
puts " open [PR_NUMBER] Open PR in browser"
|
|
26
30
|
puts " view [PR_NUMBER] View PR details in terminal"
|
|
@@ -63,6 +67,35 @@ class PRManager
|
|
|
63
67
|
true
|
|
64
68
|
end
|
|
65
69
|
|
|
70
|
+
def history(args = [])
|
|
71
|
+
filters = parse_filters(args)
|
|
72
|
+
data = load_data
|
|
73
|
+
prs = data['prs'] || []
|
|
74
|
+
|
|
75
|
+
if prs.empty?
|
|
76
|
+
puts "No PRs recorded."
|
|
77
|
+
puts "Use 'h pr save' to record the current PR."
|
|
78
|
+
return
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Apply filters
|
|
82
|
+
prs = filter_entries(prs, filters)
|
|
83
|
+
|
|
84
|
+
if prs.empty?
|
|
85
|
+
puts "No PRs match the given filters."
|
|
86
|
+
return
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Sort by created_at (oldest first)
|
|
90
|
+
prs = prs.sort_by { |p| p['created_at'] || '' }
|
|
91
|
+
|
|
92
|
+
puts "PR history (oldest to newest):"
|
|
93
|
+
puts
|
|
94
|
+
prs.each_with_index do |pr, idx|
|
|
95
|
+
puts format_entry(pr, idx + 1)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
66
99
|
def current
|
|
67
100
|
pr_info = fetch_pr_info
|
|
68
101
|
unless pr_info
|
|
@@ -136,6 +169,36 @@ class PRManager
|
|
|
136
169
|
}
|
|
137
170
|
end
|
|
138
171
|
|
|
172
|
+
def parse_filters(args)
|
|
173
|
+
filters = {}
|
|
174
|
+
args.each do |arg|
|
|
175
|
+
case arg
|
|
176
|
+
when /^--worktree=(.+)$/
|
|
177
|
+
filters[:worktree] = $1
|
|
178
|
+
when /^--task=(.+)$/
|
|
179
|
+
filters[:task] = $1
|
|
180
|
+
when /^--session=(.+)$/
|
|
181
|
+
filters[:session] = $1
|
|
182
|
+
when /^-w(.+)$/
|
|
183
|
+
filters[:worktree] = $1
|
|
184
|
+
when /^-t(.+)$/
|
|
185
|
+
filters[:task] = $1
|
|
186
|
+
when /^-s(.+)$/
|
|
187
|
+
filters[:session] = $1
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
filters
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def filter_entries(entries, filters)
|
|
194
|
+
entries.select do |entry|
|
|
195
|
+
next false if filters[:worktree] && !entry['worktree']&.include?(filters[:worktree])
|
|
196
|
+
next false if filters[:task] && !entry['task']&.include?(filters[:task])
|
|
197
|
+
next false if filters[:session] && entry.dig('tmux', 'session') != filters[:session]
|
|
198
|
+
true
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
139
202
|
def format_entry(entry, num)
|
|
140
203
|
lines = []
|
|
141
204
|
lines << "#{num}. PR ##{entry['number']}: #{entry['title']}"
|
|
@@ -478,6 +541,7 @@ hiiro.add_subcmd(:link) { |*args|
|
|
|
478
541
|
|
|
479
542
|
hiiro.add_subcmd(:edit) { system(ENV['EDITOR'] || 'nvim', __FILE__) }
|
|
480
543
|
hiiro.add_subcmd(:save) { |pr_number=nil| manager.save(pr_number) }
|
|
544
|
+
hiiro.add_subcmd(:history) { |*args| manager.history(args) }
|
|
481
545
|
hiiro.add_subcmd(:current) { manager.current }
|
|
482
546
|
hiiro.add_subcmd(:open) { |pr_number=nil| manager.open(pr_number) }
|
|
483
547
|
hiiro.add_subcmd(:view) { |pr_number=nil| manager.view(pr_number) }
|
data/bin/h-session
CHANGED
|
@@ -60,4 +60,16 @@ o.add_subcmd(:info) { |*args|
|
|
|
60
60
|
system('tmux', 'display-message', '-p', '#{session_name}: #{session_windows} windows, #{session_attached} attached', *args)
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
o.add_subcmd(:history) { |session_name=nil|
|
|
64
|
+
session_name ||= `tmux display-message -p '#S'`.strip if ENV['TMUX']
|
|
65
|
+
entries = Hiiro::History.by_session(session_name)
|
|
66
|
+
if entries.empty?
|
|
67
|
+
puts "No history for this session"
|
|
68
|
+
else
|
|
69
|
+
puts "History for session: #{session_name || 'current'}"
|
|
70
|
+
puts
|
|
71
|
+
entries.last(20).each_with_index { |e, i| puts e.oneline(i + 1) }
|
|
72
|
+
end
|
|
73
|
+
}
|
|
74
|
+
|
|
63
75
|
o.run
|
data/bin/h-window
CHANGED
|
@@ -56,6 +56,18 @@ o.add_subcmd(:unlink) { |*args|
|
|
|
56
56
|
system('tmux', 'unlink-window', *args)
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
o.add_subcmd(:history) { |window_id=nil|
|
|
60
|
+
window_id ||= `tmux display-message -p '#I'`.strip if ENV['TMUX']
|
|
61
|
+
entries = Hiiro::History.by_window(window_id)
|
|
62
|
+
if entries.empty?
|
|
63
|
+
puts "No history for this window"
|
|
64
|
+
else
|
|
65
|
+
puts "History for window: #{window_id || 'current'}"
|
|
66
|
+
puts
|
|
67
|
+
entries.last(20).each_with_index { |e, i| puts e.oneline(i + 1) }
|
|
68
|
+
end
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
o.add_subcmd(:select) do |*args|
|
|
60
72
|
# Get all windows with details
|
|
61
73
|
output = `tmux list-windows -a -F '\#{session_name}:\#{window_index} \#{window_name} [\#{window_panes} panes]'`.strip
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
class Hiiro
|
|
2
|
+
class History
|
|
3
|
+
class Entry
|
|
4
|
+
FIELDS = %i[
|
|
5
|
+
id timestamp source cmd description pwd
|
|
6
|
+
tmux_session tmux_window tmux_pane
|
|
7
|
+
git_branch git_sha git_origin_sha git_worktree
|
|
8
|
+
task subtask app
|
|
9
|
+
].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader(*FIELDS)
|
|
12
|
+
|
|
13
|
+
def initialize(data)
|
|
14
|
+
data ||= {}
|
|
15
|
+
@id = data['id']
|
|
16
|
+
@timestamp = data['timestamp']
|
|
17
|
+
@source = data['source']
|
|
18
|
+
@cmd = data['cmd']
|
|
19
|
+
@description = data['description']
|
|
20
|
+
@pwd = data['pwd']
|
|
21
|
+
@tmux_session = data['tmux_session']
|
|
22
|
+
@tmux_window = data['tmux_window']
|
|
23
|
+
@tmux_pane = data['tmux_pane']
|
|
24
|
+
@git_branch = data['git_branch']
|
|
25
|
+
@git_sha = data['git_sha']
|
|
26
|
+
@git_origin_sha = data['git_origin_sha']
|
|
27
|
+
@git_worktree = data['git_worktree']
|
|
28
|
+
@task = data['task']
|
|
29
|
+
@subtask = data['subtask']
|
|
30
|
+
@app = data['app']
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_h
|
|
34
|
+
{
|
|
35
|
+
'id' => id,
|
|
36
|
+
'timestamp' => timestamp,
|
|
37
|
+
'source' => source,
|
|
38
|
+
'cmd' => cmd,
|
|
39
|
+
'description' => description,
|
|
40
|
+
'pwd' => pwd,
|
|
41
|
+
'tmux_session' => tmux_session,
|
|
42
|
+
'tmux_window' => tmux_window,
|
|
43
|
+
'tmux_pane' => tmux_pane,
|
|
44
|
+
'git_branch' => git_branch,
|
|
45
|
+
'git_sha' => git_sha,
|
|
46
|
+
'git_origin_sha' => git_origin_sha,
|
|
47
|
+
'git_worktree' => git_worktree,
|
|
48
|
+
'task' => task,
|
|
49
|
+
'subtask' => subtask,
|
|
50
|
+
'app' => app,
|
|
51
|
+
}.compact
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Data used for change detection (excludes timestamp, id, cmd)
|
|
55
|
+
def state_key
|
|
56
|
+
{
|
|
57
|
+
'pwd' => pwd,
|
|
58
|
+
'tmux_session' => tmux_session,
|
|
59
|
+
'tmux_window' => tmux_window,
|
|
60
|
+
'tmux_pane' => tmux_pane,
|
|
61
|
+
'git_branch' => git_branch,
|
|
62
|
+
'git_sha' => git_sha,
|
|
63
|
+
'git_origin_sha' => git_origin_sha,
|
|
64
|
+
'git_worktree' => git_worktree,
|
|
65
|
+
'task' => task,
|
|
66
|
+
'subtask' => subtask,
|
|
67
|
+
'app' => app,
|
|
68
|
+
}.compact
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def state_fingerprint
|
|
72
|
+
Digest::SHA256.hexdigest(state_key.to_yaml)[0..15]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def short_line(index)
|
|
76
|
+
time_str = timestamp ? Time.parse(timestamp).strftime('%Y-%m-%d %H:%M') : Time.now.strftime('%Y-%m-%d %H:%M')
|
|
77
|
+
desc = description || cmd || '(no description)'
|
|
78
|
+
desc = desc[0..50] + '...' if desc.length > 53
|
|
79
|
+
[
|
|
80
|
+
format('%3d %s %s', index, time_str, desc),
|
|
81
|
+
format(' %s @ %s', git_branch || '(no branch)', task || '(no task)'),
|
|
82
|
+
].join("\n")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def oneline(index = nil)
|
|
86
|
+
time_str = timestamp ? Time.parse(timestamp).strftime('%m/%d %H:%M') : ''
|
|
87
|
+
prefix = index ? format('%3d ', index) : ''
|
|
88
|
+
sha_str = git_sha ? git_sha[0..6] : '-------'
|
|
89
|
+
branch_str = git_branch ? "[#{git_branch}]" : ''
|
|
90
|
+
task_str = task ? "(#{task})" : ''
|
|
91
|
+
cmd_str = cmd || description || ''
|
|
92
|
+
cmd_str = cmd_str[0..35] + '...' if cmd_str.length > 38
|
|
93
|
+
|
|
94
|
+
"#{prefix}#{time_str} #{sha_str} #{branch_str.ljust(20)} #{task_str.ljust(15)} #{cmd_str}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def full_display
|
|
98
|
+
lines = []
|
|
99
|
+
lines << "ID: #{id}"
|
|
100
|
+
lines << "Timestamp: #{timestamp}"
|
|
101
|
+
lines << "Description: #{description}" if description
|
|
102
|
+
lines << "Command: #{cmd}" if cmd
|
|
103
|
+
lines << "PWD: #{pwd}" if pwd
|
|
104
|
+
lines << "Source: #{source}" if source
|
|
105
|
+
lines << ""
|
|
106
|
+
lines << "Tmux:"
|
|
107
|
+
lines << " Session: #{tmux_session}" if tmux_session
|
|
108
|
+
lines << " Window: #{tmux_window}" if tmux_window
|
|
109
|
+
lines << " Pane: #{tmux_pane}" if tmux_pane
|
|
110
|
+
lines << ""
|
|
111
|
+
lines << "Git:"
|
|
112
|
+
lines << " Worktree: #{git_worktree}" if git_worktree
|
|
113
|
+
lines << " Branch: #{git_branch}" if git_branch
|
|
114
|
+
lines << " SHA: #{git_sha}" if git_sha
|
|
115
|
+
lines << " Origin: #{git_origin_sha}" if git_origin_sha
|
|
116
|
+
lines << ""
|
|
117
|
+
lines << "Task:"
|
|
118
|
+
lines << " Task: #{task}" if task
|
|
119
|
+
lines << " Subtask: #{subtask}" if subtask
|
|
120
|
+
lines << " App: #{app}" if app
|
|
121
|
+
lines.join("\n")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Filter matching
|
|
125
|
+
def matches?(filters)
|
|
126
|
+
filters.all? do |key, value|
|
|
127
|
+
entry_value = send(key) rescue nil
|
|
128
|
+
next true if value.nil?
|
|
129
|
+
next entry_value == value if value.is_a?(String)
|
|
130
|
+
next value.include?(entry_value) if value.is_a?(Array)
|
|
131
|
+
true
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
require 'time'
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'digest'
|
|
5
|
+
require_relative 'history/entry'
|
|
6
|
+
|
|
7
|
+
class Hiiro
|
|
8
|
+
class History
|
|
9
|
+
HISTORY_DIR = File.join(Dir.home, '.config/hiiro/history')
|
|
10
|
+
HISTORY_FILE = File.join(HISTORY_DIR, 'entries.yml')
|
|
11
|
+
LAST_STATE_FILE = File.join(HISTORY_DIR, 'last_state.yml')
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def load(hiiro)
|
|
15
|
+
history = new
|
|
16
|
+
|
|
17
|
+
hiiro.add_subcmd(:history) do |*args|
|
|
18
|
+
subcmd, *rest = args
|
|
19
|
+
case subcmd
|
|
20
|
+
when 'ls', 'list', nil
|
|
21
|
+
history.list(limit: 20)
|
|
22
|
+
when 'all'
|
|
23
|
+
history.list(limit: nil)
|
|
24
|
+
when 'show'
|
|
25
|
+
history.show(rest.first)
|
|
26
|
+
when 'goto'
|
|
27
|
+
history.goto(rest.first)
|
|
28
|
+
when 'add'
|
|
29
|
+
history.add_manual(rest.join(' '), hiiro: hiiro)
|
|
30
|
+
when 'by'
|
|
31
|
+
# h history by pane|window|session|task|branch [value]
|
|
32
|
+
dimension, value = rest
|
|
33
|
+
history.list_by(dimension, value)
|
|
34
|
+
when 'clear'
|
|
35
|
+
history.clear!
|
|
36
|
+
puts "History cleared."
|
|
37
|
+
else
|
|
38
|
+
puts "Unknown history subcommand: #{subcmd}"
|
|
39
|
+
puts "Available: ls, all, show <id>, goto <id>, add <description>, by <dimension> [value], clear"
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Called automatically when a command runs (if in a task context)
|
|
46
|
+
def track(cmd:, hiiro: nil)
|
|
47
|
+
new.track_command(cmd: cmd, hiiro: hiiro)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def log(description: nil, pwd: Dir.pwd, cmd: nil, source: nil, task: nil, subtask: nil)
|
|
51
|
+
new.add(
|
|
52
|
+
description: description,
|
|
53
|
+
cmd: cmd,
|
|
54
|
+
pwd: pwd,
|
|
55
|
+
source: source,
|
|
56
|
+
task: task,
|
|
57
|
+
subtask: subtask
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Query helpers for other commands (e.g., h pane history)
|
|
62
|
+
def by_pane(pane_id = nil)
|
|
63
|
+
pane_id ||= current_tmux_pane
|
|
64
|
+
new.filter(tmux_pane: pane_id)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def by_window(window_id = nil)
|
|
68
|
+
window_id ||= current_tmux_window
|
|
69
|
+
new.filter(tmux_window: window_id)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def by_session(session_name = nil)
|
|
73
|
+
session_name ||= current_tmux_session
|
|
74
|
+
new.filter(tmux_session: session_name)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def by_task(task_name)
|
|
78
|
+
new.filter(task: task_name)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def by_branch(branch_name)
|
|
82
|
+
new.filter(git_branch: branch_name)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def by_worktree(worktree_path)
|
|
86
|
+
new.filter(git_worktree: worktree_path)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def by_app(app_name)
|
|
90
|
+
new.filter(app: app_name)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def current_tmux_pane
|
|
96
|
+
return nil unless ENV['TMUX']
|
|
97
|
+
`tmux display-message -p '#D'`.strip rescue nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def current_tmux_window
|
|
101
|
+
return nil unless ENV['TMUX']
|
|
102
|
+
`tmux display-message -p '#S:#I'`.strip rescue nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def current_tmux_session
|
|
106
|
+
return nil unless ENV['TMUX']
|
|
107
|
+
`tmux display-message -p '#S'`.strip rescue nil
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def initialize
|
|
112
|
+
ensure_history_dir
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def entries
|
|
116
|
+
@entries ||= load_entries
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def reload
|
|
120
|
+
@entries = nil
|
|
121
|
+
entries
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def filter(filters = {})
|
|
125
|
+
entries.select { |e| e.matches?(filters) }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def list(limit: 20)
|
|
129
|
+
items = entries.reverse
|
|
130
|
+
items = items.first(limit) if limit
|
|
131
|
+
|
|
132
|
+
if items.empty?
|
|
133
|
+
puts "No history entries."
|
|
134
|
+
return true
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
items.reverse.each_with_index do |entry, idx|
|
|
138
|
+
puts entry.oneline(idx + 1)
|
|
139
|
+
end
|
|
140
|
+
true
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def list_by(dimension, value = nil)
|
|
144
|
+
dimension_map = {
|
|
145
|
+
'pane' => :tmux_pane,
|
|
146
|
+
'window' => :tmux_window,
|
|
147
|
+
'session' => :tmux_session,
|
|
148
|
+
'task' => :task,
|
|
149
|
+
'subtask' => :subtask,
|
|
150
|
+
'branch' => :git_branch,
|
|
151
|
+
'worktree' => :git_worktree,
|
|
152
|
+
'app' => :app,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
field = dimension_map[dimension]
|
|
156
|
+
unless field
|
|
157
|
+
puts "Unknown dimension: #{dimension}"
|
|
158
|
+
puts "Available: #{dimension_map.keys.join(', ')}"
|
|
159
|
+
return false
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# If no value specified, use current context
|
|
163
|
+
value ||= current_value_for(field)
|
|
164
|
+
|
|
165
|
+
if value.nil?
|
|
166
|
+
puts "No current #{dimension} detected. Please specify a value."
|
|
167
|
+
return false
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
filtered = filter(field => value)
|
|
171
|
+
|
|
172
|
+
if filtered.empty?
|
|
173
|
+
puts "No history for #{dimension}=#{value}"
|
|
174
|
+
return true
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
puts "History for #{dimension}: #{value}"
|
|
178
|
+
puts
|
|
179
|
+
filtered.last(20).each_with_index do |entry, idx|
|
|
180
|
+
puts entry.oneline(idx + 1)
|
|
181
|
+
end
|
|
182
|
+
true
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def show(ref)
|
|
186
|
+
entry = find_entry(ref)
|
|
187
|
+
unless entry
|
|
188
|
+
puts "Entry not found: #{ref}"
|
|
189
|
+
return false
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
puts entry.full_display
|
|
193
|
+
true
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def goto(ref)
|
|
197
|
+
entry = find_entry(ref)
|
|
198
|
+
unless entry
|
|
199
|
+
puts "Entry not found: #{ref}"
|
|
200
|
+
return false
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
unless entry.tmux_session
|
|
204
|
+
puts "No tmux session recorded for this entry"
|
|
205
|
+
return false
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
target = entry.tmux_session
|
|
209
|
+
target += ":#{entry.tmux_window}" if entry.tmux_window
|
|
210
|
+
target += ".#{entry.tmux_pane}" if entry.tmux_pane
|
|
211
|
+
|
|
212
|
+
system('tmux', 'switch-client', '-t', target)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def add_manual(description, hiiro: nil)
|
|
216
|
+
add(
|
|
217
|
+
description: description,
|
|
218
|
+
source: 'manual',
|
|
219
|
+
cmd: hiiro&.full_command,
|
|
220
|
+
)
|
|
221
|
+
true
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Main tracking method - gathers all context and saves if changed
|
|
225
|
+
def track_command(cmd:, hiiro: nil, force: false)
|
|
226
|
+
context = gather_context
|
|
227
|
+
return nil unless context[:task] || force # Only track if in a task context (unless forced)
|
|
228
|
+
|
|
229
|
+
context[:cmd] = cmd
|
|
230
|
+
context[:source] = 'auto'
|
|
231
|
+
|
|
232
|
+
# Check if state changed
|
|
233
|
+
unless force
|
|
234
|
+
new_state = build_state_key(context)
|
|
235
|
+
return nil if state_unchanged?(new_state)
|
|
236
|
+
save_last_state(new_state)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
add(**context)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def add(
|
|
243
|
+
description: nil, cmd: nil, source: nil, pwd: nil,
|
|
244
|
+
tmux_session: nil, tmux_window: nil, tmux_pane: nil,
|
|
245
|
+
git_branch: nil, git_sha: nil, git_origin_sha: nil, git_worktree: nil,
|
|
246
|
+
task: nil, subtask: nil, app: nil
|
|
247
|
+
)
|
|
248
|
+
context = gather_context
|
|
249
|
+
|
|
250
|
+
entry_data = {
|
|
251
|
+
'id' => generate_id,
|
|
252
|
+
'timestamp' => Time.now.iso8601,
|
|
253
|
+
'description' => description,
|
|
254
|
+
'cmd' => cmd,
|
|
255
|
+
'pwd' => pwd || context[:pwd],
|
|
256
|
+
'source' => source,
|
|
257
|
+
'tmux_session' => tmux_session || context[:tmux_session],
|
|
258
|
+
'tmux_window' => tmux_window || context[:tmux_window],
|
|
259
|
+
'tmux_pane' => tmux_pane || context[:tmux_pane],
|
|
260
|
+
'git_branch' => git_branch || context[:git_branch],
|
|
261
|
+
'git_sha' => git_sha || context[:git_sha],
|
|
262
|
+
'git_origin_sha' => git_origin_sha || context[:git_origin_sha],
|
|
263
|
+
'git_worktree' => git_worktree || context[:git_worktree],
|
|
264
|
+
'task' => task || context[:task],
|
|
265
|
+
'subtask' => subtask || context[:subtask],
|
|
266
|
+
'app' => app || context[:app],
|
|
267
|
+
}.compact
|
|
268
|
+
|
|
269
|
+
all = load_raw_entries
|
|
270
|
+
all << entry_data
|
|
271
|
+
save_entries(all)
|
|
272
|
+
|
|
273
|
+
Entry.new(entry_data)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def clear!
|
|
277
|
+
save_entries([])
|
|
278
|
+
File.delete(LAST_STATE_FILE) if File.exist?(LAST_STATE_FILE)
|
|
279
|
+
@entries = nil
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
private
|
|
283
|
+
|
|
284
|
+
def gather_context
|
|
285
|
+
{
|
|
286
|
+
pwd: Dir.pwd,
|
|
287
|
+
tmux_session: current_tmux_session,
|
|
288
|
+
tmux_window: current_tmux_window,
|
|
289
|
+
tmux_pane: current_tmux_pane,
|
|
290
|
+
git_branch: current_git_branch,
|
|
291
|
+
git_sha: current_git_sha,
|
|
292
|
+
git_origin_sha: current_git_origin_sha,
|
|
293
|
+
git_worktree: current_git_worktree,
|
|
294
|
+
task: current_task_name,
|
|
295
|
+
subtask: current_subtask_name,
|
|
296
|
+
app: current_app_name,
|
|
297
|
+
}
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def build_state_key(context)
|
|
301
|
+
context.slice(
|
|
302
|
+
:pwd, :tmux_session, :tmux_window, :tmux_pane,
|
|
303
|
+
:git_branch, :git_sha, :git_origin_sha, :git_worktree,
|
|
304
|
+
:task, :subtask, :app
|
|
305
|
+
).compact
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def state_unchanged?(new_state)
|
|
309
|
+
return false unless File.exist?(LAST_STATE_FILE)
|
|
310
|
+
last_state = YAML.load_file(LAST_STATE_FILE) rescue {}
|
|
311
|
+
last_state == new_state.transform_keys(&:to_s)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def save_last_state(state)
|
|
315
|
+
File.write(LAST_STATE_FILE, state.transform_keys(&:to_s).to_yaml)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def current_value_for(field)
|
|
319
|
+
context = gather_context
|
|
320
|
+
context[field]
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def find_entry(ref)
|
|
324
|
+
return nil unless ref
|
|
325
|
+
|
|
326
|
+
if ref.to_s.match?(/^\d+$/)
|
|
327
|
+
idx = ref.to_i - 1
|
|
328
|
+
reversed = entries.reverse
|
|
329
|
+
return reversed[idx] if idx >= 0 && idx < reversed.length
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
entries.find { |e| e.id == ref.to_s }
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def generate_id
|
|
336
|
+
Time.now.strftime('%Y%m%d%H%M%S') + '-' + rand(10000).to_s.rjust(4, '0')
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def ensure_history_dir
|
|
340
|
+
FileUtils.mkdir_p(HISTORY_DIR) unless Dir.exist?(HISTORY_DIR)
|
|
341
|
+
|
|
342
|
+
unless File.exist?(HISTORY_FILE)
|
|
343
|
+
File.write(HISTORY_FILE, [].to_yaml)
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def load_raw_entries
|
|
348
|
+
YAML.load_file(HISTORY_FILE) || []
|
|
349
|
+
rescue => e
|
|
350
|
+
[]
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def load_entries
|
|
354
|
+
load_raw_entries.map { |data| Entry.new(data) }
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def save_entries(entries_data)
|
|
358
|
+
# Keep only last 1000 entries to prevent unbounded growth
|
|
359
|
+
entries_data = entries_data.last(1000) if entries_data.length > 1000
|
|
360
|
+
File.write(HISTORY_FILE, entries_data.to_yaml)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Tmux context
|
|
364
|
+
def current_tmux_session
|
|
365
|
+
return nil unless ENV['TMUX']
|
|
366
|
+
`tmux display-message -p '#S'`.strip rescue nil
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def current_tmux_window
|
|
370
|
+
return nil unless ENV['TMUX']
|
|
371
|
+
`tmux display-message -p '#I'`.strip rescue nil
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def current_tmux_pane
|
|
375
|
+
return nil unless ENV['TMUX']
|
|
376
|
+
ENV['TMUX_PANE'] || `tmux display-message -p '#P'`.strip rescue nil
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Git context
|
|
380
|
+
def current_git_branch
|
|
381
|
+
git_helper.branch_current
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def current_git_sha
|
|
385
|
+
git_helper.commit('HEAD')
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def current_git_origin_sha
|
|
389
|
+
branch = current_git_branch
|
|
390
|
+
return nil unless branch
|
|
391
|
+
git_helper.commit("origin/#{branch}")
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def current_git_worktree
|
|
395
|
+
git_helper.root
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def git_helper
|
|
399
|
+
@git_helper ||= Git.new(nil, Dir.pwd)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Task context
|
|
403
|
+
def current_task_name
|
|
404
|
+
env = environment
|
|
405
|
+
return nil unless env
|
|
406
|
+
|
|
407
|
+
task = env.task
|
|
408
|
+
return nil unless task
|
|
409
|
+
|
|
410
|
+
task.subtask? ? task.parent_name : task.name
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def current_subtask_name
|
|
414
|
+
env = environment
|
|
415
|
+
return nil unless env
|
|
416
|
+
|
|
417
|
+
task = env.task
|
|
418
|
+
return nil unless task
|
|
419
|
+
return nil unless task.subtask?
|
|
420
|
+
|
|
421
|
+
task.short_name
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def current_app_name
|
|
425
|
+
env = environment
|
|
426
|
+
return nil unless env
|
|
427
|
+
|
|
428
|
+
pwd = Dir.pwd
|
|
429
|
+
task = env.task
|
|
430
|
+
return nil unless task
|
|
431
|
+
|
|
432
|
+
tree = env.find_tree(task.tree_name)
|
|
433
|
+
return nil unless tree
|
|
434
|
+
|
|
435
|
+
# Check if pwd is in an app directory
|
|
436
|
+
env.all_apps.each do |app|
|
|
437
|
+
app_path = app.resolve(tree.path)
|
|
438
|
+
if pwd == app_path || pwd.start_with?(app_path + '/')
|
|
439
|
+
return app.name
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
nil
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def environment
|
|
447
|
+
@environment ||= begin
|
|
448
|
+
Environment.current
|
|
449
|
+
rescue NameError
|
|
450
|
+
nil
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
end
|
data/lib/hiiro/shell.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'open3'
|
|
2
|
+
|
|
3
|
+
class Hiiro
|
|
4
|
+
class Shell
|
|
5
|
+
def self.pipe_lines(lines, *command)
|
|
6
|
+
content = lines.is_a?(Array) ? lines.join("\n") : lines.to_s
|
|
7
|
+
|
|
8
|
+
pipe(content, *command)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.pipe(content, *command)
|
|
12
|
+
selected, status = Open3.capture2(*command, stdin_data: content)
|
|
13
|
+
|
|
14
|
+
return nil unless status.success?
|
|
15
|
+
|
|
16
|
+
selected&.chomp
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/hiiro/sk.rb
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
require 'open3'
|
|
2
|
+
require_relative 'shell'
|
|
2
3
|
|
|
3
4
|
class Hiiro
|
|
4
5
|
class Sk
|
|
5
6
|
def self.select(lines)
|
|
6
|
-
|
|
7
|
-
selected, status = Open3.capture2('sk', stdin_data: input)
|
|
8
|
-
|
|
9
|
-
return nil unless status.success?
|
|
10
|
-
|
|
11
|
-
result = selected.strip
|
|
12
|
-
return nil if result.empty?
|
|
13
|
-
|
|
14
|
-
result
|
|
7
|
+
Shell.pipe_lines(lines, 'sk')
|
|
15
8
|
end
|
|
16
9
|
|
|
17
10
|
def self.map_select(mapping)
|
data/lib/hiiro/version.rb
CHANGED
data/lib/hiiro.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "pry"
|
|
|
6
6
|
require_relative "hiiro/version"
|
|
7
7
|
require_relative "hiiro/prefix_matcher"
|
|
8
8
|
require_relative "hiiro/git"
|
|
9
|
+
require_relative "hiiro/history"
|
|
9
10
|
require_relative "hiiro/options"
|
|
10
11
|
require_relative "hiiro/notification"
|
|
11
12
|
require_relative "hiiro/sk"
|
|
@@ -38,6 +39,7 @@ class Hiiro
|
|
|
38
39
|
bin_name = values[:bin_name] || $0
|
|
39
40
|
|
|
40
41
|
new(bin_name, *args, logging: logging, **values).tap do |hiiro|
|
|
42
|
+
History.load(hiiro)
|
|
41
43
|
hiiro.load_plugins(*plugins)
|
|
42
44
|
|
|
43
45
|
hiiro.add_subcommand(:pry) { |*args|
|
|
@@ -102,9 +104,16 @@ class Hiiro
|
|
|
102
104
|
@todo_manager ||= TodoManager.new
|
|
103
105
|
end
|
|
104
106
|
|
|
107
|
+
def history
|
|
108
|
+
@history ||= History.new
|
|
109
|
+
end
|
|
110
|
+
|
|
105
111
|
def run
|
|
106
112
|
result = runner.run(*args)
|
|
107
113
|
|
|
114
|
+
# Track command after running (only saves if state changed and in task context)
|
|
115
|
+
History.track(cmd: full_command, hiiro: self)
|
|
116
|
+
|
|
108
117
|
handle_result(result)
|
|
109
118
|
|
|
110
119
|
exit 1
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hiiro
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.86
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joshua Toyota
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: pry
|
|
@@ -101,9 +100,12 @@ files:
|
|
|
101
100
|
- lib/hiiro/git/remote.rb
|
|
102
101
|
- lib/hiiro/git/worktree.rb
|
|
103
102
|
- lib/hiiro/git/worktrees.rb
|
|
103
|
+
- lib/hiiro/history.rb
|
|
104
|
+
- lib/hiiro/history/entry.rb
|
|
104
105
|
- lib/hiiro/notification.rb
|
|
105
106
|
- lib/hiiro/options.rb
|
|
106
107
|
- lib/hiiro/prefix_matcher.rb
|
|
108
|
+
- lib/hiiro/shell.rb
|
|
107
109
|
- lib/hiiro/sk.rb
|
|
108
110
|
- lib/hiiro/todo.rb
|
|
109
111
|
- lib/hiiro/version.rb
|
|
@@ -126,7 +128,6 @@ metadata:
|
|
|
126
128
|
homepage_uri: https://github.com/unixsuperhero/hiiro
|
|
127
129
|
source_code_uri: https://github.com/unixsuperhero/hiiro
|
|
128
130
|
changelog_uri: https://github.com/unixsuperhero/hiiro/blob/main/CHANGELOG.md
|
|
129
|
-
post_install_message:
|
|
130
131
|
rdoc_options: []
|
|
131
132
|
require_paths:
|
|
132
133
|
- lib
|
|
@@ -141,8 +142,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
141
142
|
- !ruby/object:Gem::Version
|
|
142
143
|
version: '0'
|
|
143
144
|
requirements: []
|
|
144
|
-
rubygems_version:
|
|
145
|
-
signing_key:
|
|
145
|
+
rubygems_version: 4.0.3
|
|
146
146
|
specification_version: 4
|
|
147
147
|
summary: A lightweight CLI framework for Ruby
|
|
148
148
|
test_files: []
|