hiiro 0.1.386 → 0.1.387.pre.2
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/AGENTS.md +144 -2
- data/CHANGELOG.md +12 -1
- data/bin/h-branch +29 -105
- data/bin/h-date +7 -39
- data/bin/h-link +5 -58
- data/bin/h-pr +7 -16
- data/bin/h-remind +3 -27
- data/bin/h-time +7 -39
- data/bin/h-todo +1 -1
- data/bin/h-wtree +18 -30
- data/lib/hiiro/check_run.rb +1 -8
- data/lib/hiiro/check_summary.rb +141 -0
- data/lib/hiiro/current_task.rb +4 -11
- data/lib/hiiro/duration.rb +61 -0
- data/lib/hiiro/extractor.rb +22 -0
- data/lib/hiiro/git/branch_comparison.rb +62 -0
- data/lib/hiiro/git/pr.rb +20 -45
- data/lib/hiiro/git/worktree.rb +8 -0
- data/lib/hiiro/git.rb +31 -0
- data/lib/hiiro/herdr.rb +73 -0
- data/lib/hiiro/input_file.rb +52 -7
- data/lib/hiiro/link.rb +37 -0
- data/lib/hiiro/matcher.rb +20 -74
- data/lib/hiiro/options.rb +70 -46
- data/lib/hiiro/pinned_pr.rb +13 -3
- data/lib/hiiro/pinned_pr_manager.rb +24 -49
- data/lib/hiiro/ps_process.rb +82 -54
- data/lib/hiiro/pull_request_reference.rb +77 -0
- data/lib/hiiro/queue.rb +147 -87
- data/lib/hiiro/service_definition.rb +135 -0
- data/lib/hiiro/service_manager.rb +41 -78
- data/lib/hiiro/shell.rb +50 -22
- data/lib/hiiro/task_cli.rb +18 -20
- data/lib/hiiro/task_location.rb +59 -0
- data/lib/hiiro/task_sessions.rb +7 -8
- data/lib/hiiro/time_input.rb +42 -0
- data/lib/hiiro/todo.rb +5 -11
- data/lib/hiiro/version.rb +1 -1
- data/lib/hiiro.rb +8 -0
- data/script/publish +1 -1
- metadata +10 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d3ede89762b00a0016a246a8dab64fcd6e74c28e72ca81a3ee3a3b87d851d88e
|
|
4
|
+
data.tar.gz: 005d776898b48607a94853906af0dff588706ed47fc945a031e39f7650aef786
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0d4a558ace716c259a5cfc156b5649954be5972774494b625c4da698b8e84385158c54c00f4d1ae91b7831e33f5bbb783b1d2e851deb84bba77f454ac2cd2893
|
|
7
|
+
data.tar.gz: e42589fde56d8011c5ae722ab8c79fb5b8dd5b3da2cb30482604ca19d64a458a8483c427a822d6efc2c45e3f95f889c2ed02d70e163e421918afd3f6b705fb02
|
data/AGENTS.md
CHANGED
|
@@ -187,6 +187,8 @@ Hiiro::Matcher.by_prefix(items, "pre", key: :name)
|
|
|
187
187
|
Hiiro::Matcher.by_substring(items, "abc", key: :name)
|
|
188
188
|
```
|
|
189
189
|
|
|
190
|
+
`Result` and `PathResult` share their cardinality/resolution answers through `Hiiro::Matcher::ResultQueries` (`count`, `one?`, `ambiguous?`, `match?`, `exact_match`, `exact?`, `match`, `resolved`, `first`); both expose the original query as `query`.
|
|
191
|
+
|
|
190
192
|
Note: `Hiiro::PrefixMatcher` is aliased to `Hiiro::Matcher` for backward compatibility.
|
|
191
193
|
|
|
192
194
|
### Hiiro::Git (lib/hiiro/git.rb)
|
|
@@ -198,7 +200,8 @@ git = hiiro.git
|
|
|
198
200
|
git.root # Repository root path
|
|
199
201
|
git.branch # Current branch name
|
|
200
202
|
git.branches # List all branches
|
|
201
|
-
git.worktrees # List all worktrees
|
|
203
|
+
git.worktrees # List all worktrees (Git::Worktree: path, head, branch, bare?, detached?, name, to_line)
|
|
204
|
+
Hiiro::Git::Worktrees.fetch # Porcelain-backed collection; bins should use this instead of splitting `git worktree list`
|
|
202
205
|
git.add_worktree(path, branch:) # Create worktree
|
|
203
206
|
git.move_worktree(from, to) # Rename worktree
|
|
204
207
|
git.current_pr # Get current PR info
|
|
@@ -281,6 +284,130 @@ Or via `h db q`:
|
|
|
281
284
|
h db q "SELECT bin_name, argv_json, invoked_at FROM invocations ORDER BY invoked_at DESC LIMIT 10"
|
|
282
285
|
```
|
|
283
286
|
|
|
287
|
+
### Hiiro::InputFile / Hiiro::EditedDocument (lib/hiiro/input_file.rb)
|
|
288
|
+
|
|
289
|
+
Tempfile + editor lifecycle for YAML/markdown input. After `edit`, the result is retained as an `EditedDocument`:
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
input = yaml_input_file(content: yaml, prefix: 'todo-edit-') # or InputFile.yaml_file(hiiro:, ...) / .md_file
|
|
293
|
+
doc = input.edit.document # EditedDocument: original, result (alias contents), parsed (alias data), error
|
|
294
|
+
doc.changed? / doc.empty? / doc.valid?
|
|
295
|
+
input.parsed_file # still available; nil on YAML parse error
|
|
296
|
+
input.cleanup # unlink the tempfile when done
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Use this instead of a hand-rolled Tempfile/system/YAML.load/unlink cycle (TodoManager#edit_items now does).
|
|
300
|
+
|
|
301
|
+
### Hiiro::CheckSummary / Hiiro::CheckContext (lib/hiiro/check_summary.rb)
|
|
302
|
+
|
|
303
|
+
One interpretation of a PR's `statusCheckRollup`, shared by `Git::Pr`, `PinnedPr`, `PinnedPRManager` display, `CheckRun` persistence, and `h pr status`:
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
s = Hiiro::CheckSummary.from_rollup(rollup, truncated: false) # nil when rollup nil/empty
|
|
307
|
+
s = pr.check_summary # from stored counts (+ raw contexts when present)
|
|
308
|
+
s.total / s.success / s.pending / s.failed / s.frozen / s.complete?
|
|
309
|
+
s.red? / s.green? / s.pending? # the -r / -g / -p filter predicates (pr.red? etc. delegate here)
|
|
310
|
+
s.any_pending? / s.only_frozen? # display distinctions
|
|
311
|
+
s.counts_by_outcome # { 'SUCCESS' => 12, 'FAILURE' => 1 } (missing keys read 0)
|
|
312
|
+
s.contexts.each { |c| c.kind (:check_run|:status_context) ; c.name ; c.url ; c.outcome ; c.raw }
|
|
313
|
+
s.to_h # the stored { 'total','success','pending','failed','frozen'[,'truncated'] } hash
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Hiiro::PullRequestReference / Hiiro::RepositoryIdentity (lib/hiiro/pull_request_reference.rb)
|
|
317
|
+
|
|
318
|
+
```ruby
|
|
319
|
+
ref = Hiiro::PullRequestReference.parse(input) # URL | '#123' | '123' | 123 → ref, else nil
|
|
320
|
+
ref.number / ref.number_string / ref.repo # repo: RepositoryIdentity or nil for a bare number
|
|
321
|
+
ref.key # [number, 'owner/name'] — the key PinnedPRManager batches by
|
|
322
|
+
ref.repo_args # ['-R', 'owner/name'] or []
|
|
323
|
+
ref.matches?(pr) # same number; same repo when both sides know it
|
|
324
|
+
pr.reference # Git::Pr / PinnedPr → reference
|
|
325
|
+
Hiiro::PinnedPr.find_by_reference(ref) # repo-qualified lookup; legacy rows without repo still match
|
|
326
|
+
Hiiro::RepositoryIdentity.parse('owner/name' | github URL) # .host .owner .name .path .url
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Hiiro::Git::BranchComparison (lib/hiiro/git/branch_comparison.rb)
|
|
330
|
+
|
|
331
|
+
Two refs plus an explicit base policy. Used by `h branch diff/changed/ahead/behind/log/forkpoint/ancestor`:
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
cmp = git.compare(source: 'HEAD', target: nil, base_policy: :local_main, fallback: 'HEAD~1')
|
|
335
|
+
# base_policy :local_main → main, master ; :remote_first → origin/master, master, origin/main, main
|
|
336
|
+
cmp.target / cmp.target_source (:explicit|:policy|:fallback|:none) / cmp.resolved?
|
|
337
|
+
cmp.ahead / cmp.behind / cmp.ancestor? / cmp.merge_base / cmp.fork_point (--fork-point, then merge-base)
|
|
338
|
+
cmp.two_dot_range / cmp.three_dot_range / cmp.fork_range / cmp.changed_paths(relative: true)
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
`Hiiro::Git` gained the underlying queries: `ref_exists?`, `rev_list_count`, `merge_base`, `ancestor?`, `changed_files`.
|
|
342
|
+
|
|
343
|
+
### Hiiro::TaskLocation (lib/hiiro/task_location.rb)
|
|
344
|
+
|
|
345
|
+
Where a task's directories are. `CurrentTask` and `TaskCli` delegate `code_directory`, `workspace_label`, `tree_path`, `start_directory`, and `app_directory` to it:
|
|
346
|
+
|
|
347
|
+
```ruby
|
|
348
|
+
loc = Hiiro::TaskLocation.for(task, override: opts[:directory], app: Hiiro::App.new(name:, path:))
|
|
349
|
+
loc.home # TaskRecord.home_for(task.name); never creates it
|
|
350
|
+
loc.worktree_path # task.tree, absolute or under WORK_DIR; nil without a tree
|
|
351
|
+
loc.code_directory # task.primary_directory || worktree_path
|
|
352
|
+
loc.app_directory # app.resolve(code_directory); nil unless both known
|
|
353
|
+
loc.start_directory # override || app_directory || code_directory || home
|
|
354
|
+
loc.source # :override | :app | :primary | :tree | :home (callers run ensure_home only for :home)
|
|
355
|
+
loc.workspace_label # (task.session || task.name).tr('.', '_')
|
|
356
|
+
Hiiro::TaskLocation.contains?(root, path) # realpath containment
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Hiiro::Herdr::TabCreation / Hiiro::Herdr::Location (lib/hiiro/herdr.rb)
|
|
360
|
+
|
|
361
|
+
`tab create` responses are read through one value instead of `result.dig('tab','tab_id')` at every call site:
|
|
362
|
+
|
|
363
|
+
```ruby
|
|
364
|
+
created = Hiiro::Herdr::TabCreation.create(client, name:, workspace:, start_directory:, command:, focus:) # or herdr.create_tab(**req)
|
|
365
|
+
created.tab_id / created.pane_id / created.tab / created.root_pane
|
|
366
|
+
created.created? # tab id present created.complete? # tab + root pane present
|
|
367
|
+
created.errors # [] | ['no tab'] | ['no root pane']
|
|
368
|
+
created.location(workspace_id: ws.id) # Herdr::Location
|
|
369
|
+
loc = Hiiro::Herdr::Location.from_meta(meta) # reads herdr_workspace / herdr_tab / herdr_pane
|
|
370
|
+
loc.target # pane_id || tab_id || workspace_id
|
|
371
|
+
loc.to_meta # inverse of from_meta (queue/service metadata)
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Hiiro::Link / Hiiro::URLTemplate (lib/hiiro/link.rb)
|
|
375
|
+
|
|
376
|
+
`bin/h-link` uses `Hiiro::Link` directly (its inline `LinkManager::Link` copy is gone). `Link.from_hash(yaml_row)` builds an unsaved instance; `to_h` / `to_yaml` give the links.yml shape.
|
|
377
|
+
|
|
378
|
+
```ruby
|
|
379
|
+
t = Hiiro::URLTemplate.new('https://x/{org}/{repo}', values: { 'org' => 'a b' })
|
|
380
|
+
t.placeholders # ['org', 'repo'] t.placeholders? # true
|
|
381
|
+
t.missing_values # ['repo'] t.complete? # false
|
|
382
|
+
t.url # 'https://x/a+b/{repo}' (spaces → '+', nothing else is encoded)
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Hiiro::PsProcess::Snapshot / OpenFiles (lib/hiiro/ps_process.rb)
|
|
386
|
+
|
|
387
|
+
```ruby
|
|
388
|
+
snap = Hiiro::PsProcess::Snapshot.capture # one `ps awwux`; .processes .captured_at
|
|
389
|
+
snap.by_pid(pid) / snap.with_pids(set) / snap.matching(pattern) / snap.parent_of(pid) / snap.children_of(pid)
|
|
390
|
+
of = Hiiro::PsProcess::OpenFiles.for_pid(pid) # one lsof; also .for_port(port), .in_dir(path), for_pid(pid, network_only: true)
|
|
391
|
+
of.pids / of.cwd / of.files / of.sockets / of.failed # failed = lsof exited non-zero (not the same as empty)
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
`PsProcess.by_port`, `.in_dirs`, `#files`, `#ports`, `#dir`, `#parent`, `#children` keep their shapes and read through these.
|
|
395
|
+
|
|
396
|
+
### Hiiro::Duration / Hiiro::TimeInput (lib/hiiro/duration.rb, lib/hiiro/time_input.rb)
|
|
397
|
+
|
|
398
|
+
Value objects behind `h date`, `h time`, and `h remind` (formerly duplicated helpers in each bin):
|
|
399
|
+
|
|
400
|
+
```ruby
|
|
401
|
+
Hiiro::Duration.parse('15m') # => Duration(900s); nil when not <digits><s|m|h|d|w>
|
|
402
|
+
Hiiro::Duration.seconds(90_061).compact # => "1d1h1m1s" (.clock => "25:01:01", .components => {days:,hours:,...})
|
|
403
|
+
Hiiro::Duration.milliseconds(1500) # Rational-precise seconds
|
|
404
|
+
|
|
405
|
+
t = Hiiro::TimeInput.parse('1726000000123') # digits → unix; ms auto-detected past 11 digits (or ms: true)
|
|
406
|
+
t.unit / t.time / t.unix_seconds / t.unix_milliseconds / t.utc / t.iso8601(3)
|
|
407
|
+
Hiiro::TimeInput.parse('now') ; Hiiro::TimeInput.parse('2026-09-22 10:00') # 'now'/blank → now ; else Time.parse
|
|
408
|
+
a.difference(b) # => Duration
|
|
409
|
+
```
|
|
410
|
+
|
|
284
411
|
### Hiiro::Shell (lib/hiiro/shell.rb)
|
|
285
412
|
|
|
286
413
|
Utility for piping content to external commands:
|
|
@@ -288,8 +415,16 @@ Utility for piping content to external commands:
|
|
|
288
415
|
```ruby
|
|
289
416
|
Hiiro::Shell.pipe("content", "pbcopy") # Pipe string to command
|
|
290
417
|
Hiiro::Shell.pipe_lines(["a", "b"], "command") # Join array with newlines and pipe
|
|
418
|
+
result = Hiiro::Shell.run('cmd', 'arg', SOME_ENV: 'x') # Result: stdout, stderr, status, success?, lines, plain_text
|
|
419
|
+
result.spec # Hiiro::CommandSpecification — argv, env (normalized to strings), cwd, preview, open3_args
|
|
420
|
+
result.command # spec.preview, e.g. "cmd arg"
|
|
421
|
+
content = Hiiro::Shell.read_input(*args) # files named in args ('-' = stdin), else piped stdin
|
|
291
422
|
```
|
|
292
423
|
|
|
424
|
+
`read_input` keeps only args that are readable paths (regular files, `/dev/fd/N`, `<(cmd)`) or `-`, concatenates them, and falls back to stdin when none remain. A named file wins over piped stdin unless `-` places it. At a terminal with no input it aborts with `no input: pipe data in or pass a file`.
|
|
425
|
+
|
|
426
|
+
`run`, `run_combined`, `run3`, `stream`, and `stream_combined` all build one `CommandSpecification` and attach it to the `Result`.
|
|
427
|
+
|
|
293
428
|
### Hiiro::Options (lib/hiiro/options.rb)
|
|
294
429
|
|
|
295
430
|
Argument parsing with flag and option support:
|
|
@@ -304,6 +439,8 @@ opts.verbose # true if --verbose or -v was passed
|
|
|
304
439
|
opts.args # Remaining non-option arguments
|
|
305
440
|
```
|
|
306
441
|
|
|
442
|
+
The declared options live in a `Hiiro::OptionSet` (`options.option_set`), which both `Options` and the parsed `Args` use for `help_text`, `hint`, `definition_for('--out' | '-o' | :out)`, `defaults`, `subset(names)`, and `conflicts_for(name)` (mutual-exclusion star topology).
|
|
443
|
+
|
|
307
444
|
### Hiiro::Notification (lib/hiiro/notification.rb)
|
|
308
445
|
|
|
309
446
|
macOS notification wrapper using terminal-notifier:
|
|
@@ -334,7 +471,9 @@ Subcommands (`h queue <subcmd>`):
|
|
|
334
471
|
Config: `~/.config/hiiro/queue/{wip,pending,running,done,failed}/`
|
|
335
472
|
|
|
336
473
|
Key internals:
|
|
337
|
-
- `Queue::
|
|
474
|
+
- `Queue::Entry` - `Entry.find(dirs, name)` / `Entry.in(dirs, status)`; answers `prompt_path`, `meta_path`, `launcher_path`, `body_path`, `companion_files`, `modified_at`, `meta`, `preview`, `prompt`, `execution`, `moved_to(status)`, `to_h` ({ name:, status: }). `Queue#entry_for(name, status)` builds one; `tasks_in`, `all_tasks`, `meta_for`, `task_preview`, `find_task` are thin wrappers over it.
|
|
475
|
+
- `Queue::Execution` - snapshot of a running entry's `.meta`: `started_at`, `elapsed` (Duration), `elapsed_minutes`, `working_directory`, `location` (Herdr::Location), `attach_target`
|
|
476
|
+
- `Queue::Prompt` - Parses frontmatter to resolve task/tree/session for working directory; retains `path`; `body` strips frontmatter; `Prompt.frontmatter_lines(task_info:, ignore:, hints:)` is the editor template shared by add/wip
|
|
338
477
|
- Tasks launch in tmux windows within the task's session (from frontmatter) or default `hq` session
|
|
339
478
|
- Launcher script runs `cat prompt | claude`, then moves files to done/failed based on exit code
|
|
340
479
|
|
|
@@ -386,6 +525,9 @@ my-stack:
|
|
|
386
525
|
```
|
|
387
526
|
|
|
388
527
|
Key internals:
|
|
528
|
+
- `Hiiro::ServiceDefinition` (lib/hiiro/service_definition.rb) - `sm.definition_for(name)`; answers `base_directory`, `host`, `port`, `url`, `init_commands`, `start_command`, `stop_command`, `cleanup_commands`, `environment_files`
|
|
529
|
+
- `Hiiro::EnvironmentFile` - one env file of a service: `template_path`, `destination_path`, `variables` ({ VAR => { variation => value } }), `selected_variation(var, overrides)` (default `local`), `effective_values(overrides)`, `desired_content(existing_lines, overrides)`. `h service env` displays it and `prepare_env` writes it; both read the same object.
|
|
530
|
+
- `Hiiro::ServiceGroup` - `sm.group_definition_for(name)` / `ServiceGroup.from_config(name, cfg)`; `service_names`, `overrides_for(service_name)`
|
|
389
531
|
- `prepare_env(svc_name, variation_overrides:)` - Copies base_env template from `~/.config/hiiro/env_templates/` to `base_dir/env_file`, then injects variation values
|
|
390
532
|
- `find_group(name)` / `start_group(name, ...)` - Detect and start service groups, applying per-member `use:` overrides
|
|
391
533
|
- Default variation is `local` when not specified
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.387.pre.2] - 2026-10-01
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- publish script now uses the `sonnet` model alias instead of `claude-haiku-4-5` for commit plan and changelog generation
|
|
7
|
+
|
|
8
|
+
## [0.1.387.pre.1] - 2026-10-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `Hiiro::Extractor` value object for data extraction
|
|
12
|
+
- Support for Integer return values as exit codes from subcommand handlers
|
|
13
|
+
|
|
3
14
|
## Unreleased
|
|
4
15
|
|
|
5
16
|
### Added
|
|
@@ -155,4 +166,4 @@
|
|
|
155
166
|
|
|
156
167
|
### Removed
|
|
157
168
|
- Remove tmux paste-buffer, jumplist, task-color, focus-hook, and arbitrary-layout features that Herdr 0.8.2 does not support.
|
|
158
|
-
- Remove the old `Hiiro::Tmux` adapter and tmux-specific command tests.
|
|
169
|
+
- Remove the old `Hiiro::Tmux` adapter and tmux-specific command tests.
|
data/bin/h-branch
CHANGED
|
@@ -235,12 +235,8 @@ Hiiro.run(*ARGV) do
|
|
|
235
235
|
selected, status = Open3.capture2('sk', '--no-sort', stdin_data: lines.keys.join("\n"))
|
|
236
236
|
return nil unless status.success? && !selected.strip.empty?
|
|
237
237
|
lines[selected.chomp]
|
|
238
|
-
elsif
|
|
239
|
-
|
|
240
|
-
pinned.find { |pr| pr.number.to_s == num }
|
|
241
|
-
elsif arg.to_s =~ /^#?(\d+)$/
|
|
242
|
-
num = $1
|
|
243
|
-
pinned.find { |pr| pr.number.to_s == num }
|
|
238
|
+
elsif (ref = Hiiro::PullRequestReference.parse(arg))
|
|
239
|
+
pinned.find { |pr| ref.matches?(pr) }
|
|
244
240
|
end
|
|
245
241
|
}
|
|
246
242
|
|
|
@@ -857,105 +853,51 @@ Hiiro.run(*ARGV) do
|
|
|
857
853
|
}
|
|
858
854
|
|
|
859
855
|
add_subcmd(:diff) { |*diff_args|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
base = %w[main master].find { |b| system("git rev-parse --verify #{b} >/dev/null 2>&1") } || 'HEAD~1'
|
|
863
|
-
range = "#{base}..HEAD"
|
|
864
|
-
when 1
|
|
865
|
-
range = "#{diff_args[0]}..HEAD"
|
|
866
|
-
else
|
|
867
|
-
from, to = diff_args[0..1]
|
|
868
|
-
range = "#{from}..#{to}"
|
|
869
|
-
end
|
|
870
|
-
|
|
871
|
-
system('git', 'log', '--oneline', '--decorate', range)
|
|
856
|
+
cmp = git.compare(source: diff_args[1] || 'HEAD', target: diff_args[0], base_policy: :local_main, fallback: 'HEAD~1')
|
|
857
|
+
system('git', 'log', '--oneline', '--decorate', cmp.two_dot_range)
|
|
872
858
|
}
|
|
873
859
|
|
|
874
860
|
add_subcmd(:changed) { |*changed_args|
|
|
875
861
|
show_all = changed_args.delete('-a') || changed_args.delete('--all')
|
|
876
|
-
|
|
862
|
+
cmp = git.compare(target: changed_args.first, base_policy: :remote_first)
|
|
877
863
|
|
|
878
|
-
|
|
879
|
-
system("git rev-parse --verify #{ref} >/dev/null 2>&1")
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
unless upstream
|
|
864
|
+
unless cmp.resolved?
|
|
883
865
|
puts "Cannot find master or origin/master"
|
|
884
866
|
next
|
|
885
867
|
end
|
|
886
868
|
|
|
887
|
-
|
|
888
|
-
if forkpoint.empty?
|
|
889
|
-
forkpoint = `git merge-base #{upstream} HEAD 2>/dev/null`.strip
|
|
890
|
-
end
|
|
891
|
-
|
|
892
|
-
if forkpoint.empty?
|
|
869
|
+
unless cmp.fork_point
|
|
893
870
|
puts "Could not find fork point"
|
|
894
871
|
next
|
|
895
872
|
end
|
|
896
873
|
|
|
897
|
-
|
|
898
|
-
system('git', 'diff', '--name-only', "#{forkpoint}...HEAD")
|
|
899
|
-
else
|
|
900
|
-
system('git', 'diff', '--name-only', '--relative', "#{forkpoint}...HEAD")
|
|
901
|
-
end
|
|
874
|
+
puts cmp.changed_paths(relative: !show_all)
|
|
902
875
|
}
|
|
903
876
|
|
|
904
877
|
add_subcmd(:ahead) { |*ahead_args|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
base = %w[main master].find { |b| system("git rev-parse --verify #{b} >/dev/null 2>&1") } || 'HEAD~1'
|
|
908
|
-
ahead_of = base
|
|
909
|
-
branch = 'HEAD'
|
|
910
|
-
when 1
|
|
911
|
-
ahead_of = ahead_args[0]
|
|
912
|
-
branch = 'HEAD'
|
|
913
|
-
else
|
|
914
|
-
ahead_of, branch = ahead_args[0..1]
|
|
915
|
-
end
|
|
916
|
-
|
|
917
|
-
count = `git rev-list --count #{ahead_of}..#{branch}`.strip.to_i
|
|
918
|
-
puts "#{branch} is #{count} commit(s) ahead of #{ahead_of}"
|
|
878
|
+
cmp = git.compare(source: ahead_args[1] || 'HEAD', target: ahead_args[0], base_policy: :local_main, fallback: 'HEAD~1')
|
|
879
|
+
puts "#{cmp.source} is #{cmp.ahead} commit(s) ahead of #{cmp.target}"
|
|
919
880
|
}
|
|
920
881
|
|
|
921
882
|
add_subcmd(:behind) { |*behind_args|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
base = %w[main master].find { |b| system("git rev-parse --verify #{b} >/dev/null 2>&1") } || 'HEAD~1'
|
|
925
|
-
behind_of = base
|
|
926
|
-
branch = 'HEAD'
|
|
927
|
-
when 1
|
|
928
|
-
behind_of = behind_args[0]
|
|
929
|
-
branch = 'HEAD'
|
|
930
|
-
else
|
|
931
|
-
behind_of, branch = behind_args[0..1]
|
|
932
|
-
end
|
|
933
|
-
|
|
934
|
-
count = `git rev-list --count #{branch}..#{behind_of}`.strip.to_i
|
|
935
|
-
puts "#{branch} is #{count} commit(s) behind #{behind_of}"
|
|
883
|
+
cmp = git.compare(source: behind_args[1] || 'HEAD', target: behind_args[0], base_policy: :local_main, fallback: 'HEAD~1')
|
|
884
|
+
puts "#{cmp.source} is #{cmp.behind} commit(s) behind #{cmp.target}"
|
|
936
885
|
}
|
|
937
886
|
|
|
938
887
|
add_subcmd(:log) { |upstream = nil|
|
|
939
|
-
|
|
940
|
-
system("git rev-parse --verify #{ref} >/dev/null 2>&1")
|
|
941
|
-
}
|
|
888
|
+
cmp = git.compare(target: upstream, base_policy: :remote_first)
|
|
942
889
|
|
|
943
|
-
unless
|
|
890
|
+
unless cmp.resolved?
|
|
944
891
|
puts "Cannot find master or origin/master"
|
|
945
892
|
next
|
|
946
893
|
end
|
|
947
894
|
|
|
948
|
-
|
|
949
|
-
if forkpoint.empty?
|
|
950
|
-
forkpoint = `git merge-base #{upstream} HEAD 2>/dev/null`.strip
|
|
951
|
-
end
|
|
952
|
-
|
|
953
|
-
if forkpoint.empty?
|
|
895
|
+
unless cmp.fork_point
|
|
954
896
|
puts "Could not find fork point"
|
|
955
897
|
next
|
|
956
898
|
end
|
|
957
899
|
|
|
958
|
-
system('git', 'log', '--oneline', '--decorate',
|
|
900
|
+
system('git', 'log', '--oneline', '--decorate', cmp.fork_range)
|
|
959
901
|
}
|
|
960
902
|
|
|
961
903
|
add_subcmd(:q, :query) { |*args|
|
|
@@ -1001,50 +943,32 @@ Hiiro.run(*ARGV) do
|
|
|
1001
943
|
}
|
|
1002
944
|
|
|
1003
945
|
add_subcmd(:forkpoint) { |upstream = nil, branch = nil|
|
|
1004
|
-
branch
|
|
1005
|
-
upstream ||= %w[origin/master master origin/main main].find { |ref|
|
|
1006
|
-
system("git rev-parse --verify #{ref} >/dev/null 2>&1")
|
|
1007
|
-
}
|
|
946
|
+
cmp = git.compare(source: branch || 'HEAD', target: upstream, base_policy: :remote_first)
|
|
1008
947
|
|
|
1009
|
-
unless
|
|
948
|
+
unless cmp.resolved?
|
|
1010
949
|
puts "Cannot find master or origin/master"
|
|
1011
950
|
next
|
|
1012
951
|
end
|
|
1013
952
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
if forkpoint.empty?
|
|
1017
|
-
forkpoint = `git merge-base #{upstream} #{branch} 2>/dev/null`.strip
|
|
1018
|
-
end
|
|
1019
|
-
|
|
1020
|
-
if forkpoint.empty?
|
|
1021
|
-
puts "Could not find fork point between #{upstream} and #{branch}"
|
|
953
|
+
if cmp.fork_point
|
|
954
|
+
puts cmp.fork_point
|
|
1022
955
|
else
|
|
1023
|
-
puts
|
|
956
|
+
puts "Could not find fork point between #{cmp.target} and #{cmp.source}"
|
|
1024
957
|
end
|
|
1025
958
|
}
|
|
1026
959
|
|
|
1027
960
|
add_subcmd(:ancestor) { |*ancestor_args|
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
next
|
|
1034
|
-
end
|
|
1035
|
-
ancestor = base
|
|
1036
|
-
descendant = 'HEAD'
|
|
1037
|
-
when 1
|
|
1038
|
-
ancestor = ancestor_args[0]
|
|
1039
|
-
descendant = 'HEAD'
|
|
1040
|
-
else
|
|
1041
|
-
ancestor, descendant = ancestor_args[0..1]
|
|
961
|
+
cmp = git.compare(source: ancestor_args[1] || 'HEAD', target: ancestor_args[0], base_policy: :local_main)
|
|
962
|
+
|
|
963
|
+
unless cmp.resolved?
|
|
964
|
+
puts "Cannot find main or master branch"
|
|
965
|
+
next
|
|
1042
966
|
end
|
|
1043
967
|
|
|
1044
|
-
if
|
|
1045
|
-
puts "Yes, #{
|
|
968
|
+
if cmp.ancestor?
|
|
969
|
+
puts "Yes, #{cmp.target} is an ancestor of #{cmp.source}"
|
|
1046
970
|
else
|
|
1047
|
-
puts "No, #{
|
|
971
|
+
puts "No, #{cmp.target} is NOT an ancestor of #{cmp.source}"
|
|
1048
972
|
end
|
|
1049
973
|
}
|
|
1050
974
|
end
|
data/bin/h-date
CHANGED
|
@@ -3,52 +3,20 @@
|
|
|
3
3
|
require 'hiiro'
|
|
4
4
|
require 'time'
|
|
5
5
|
|
|
6
|
-
# Parse a timestamp string as unix seconds (10-digit) or milliseconds (13-digit)
|
|
7
|
-
def parse_unix(str, ms: nil)
|
|
8
|
-
n = Rational(str.to_s)
|
|
9
|
-
ms = str.to_s.sub(/\..*/, '').length > 11 if ms.nil?
|
|
10
|
-
Time.at(ms ? n / 1000 : n)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def parse_date(str)
|
|
14
|
-
str.nil? || str.empty? || str == 'now' ? Time.now : Time.parse(str)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# Parse a value that is either a unix timestamp (digits) or a date string.
|
|
18
|
-
def parse_date_or_unix(str, ms: false)
|
|
19
|
-
str.to_s =~ /^-?\d+(\.\d+)?$/ ? parse_unix(str, ms: ms ? true : nil) : parse_date(str)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Format a number of seconds as a compact human duration: 1d23h12m10s
|
|
23
|
-
def human_duration(total_secs)
|
|
24
|
-
total = total_secs.to_f
|
|
25
|
-
sign = total.negative? ? '-' : ''
|
|
26
|
-
total = total.abs.round
|
|
27
|
-
days, rem = total.divmod(86_400)
|
|
28
|
-
hours, rem = rem.divmod(3_600)
|
|
29
|
-
minutes, secs = rem.divmod(60)
|
|
30
|
-
parts = []
|
|
31
|
-
parts << "#{days}d" if days > 0
|
|
32
|
-
parts << "#{hours}h" if hours > 0
|
|
33
|
-
parts << "#{minutes}m" if minutes > 0
|
|
34
|
-
parts << "#{secs}s" if secs > 0
|
|
35
|
-
sign + (parts.empty? ? '0s' : parts.join)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
6
|
Hiiro.run(*ARGV) do
|
|
39
7
|
add_flag(:ms, short: :m, desc: 'Use milliseconds instead of seconds')
|
|
40
8
|
add_flag(:utc, short: :u, desc: 'Output in UTC')
|
|
41
9
|
|
|
42
10
|
# h date to_unix [date-string] -> unix seconds (or ms with --ms)
|
|
43
11
|
add_cmd(:to_unix, :unix, :ts, opts: %i[ms]) do
|
|
44
|
-
t =
|
|
45
|
-
puts @opts.ms ?
|
|
12
|
+
t = Hiiro::TimeInput.parse(@opts.args.join(' '))
|
|
13
|
+
puts @opts.ms ? t.unix_milliseconds : t.unix_seconds
|
|
46
14
|
end
|
|
47
15
|
|
|
48
16
|
# h date from_unix <timestamp> -> human date (auto-detects ms by digit count)
|
|
49
17
|
add_cmd(:from_unix, :parse, opts: %i[ms utc]) do
|
|
50
18
|
raw = @opts.args.first or abort 'usage: h date from_unix <timestamp> [--ms] [--utc]'
|
|
51
|
-
t =
|
|
19
|
+
t = Hiiro::TimeInput.parse(raw, ms: @opts.ms ? true : nil)
|
|
52
20
|
t = t.utc if @opts.utc
|
|
53
21
|
puts t.iso8601(3)
|
|
54
22
|
end
|
|
@@ -71,15 +39,15 @@ Hiiro.run(*ARGV) do
|
|
|
71
39
|
add_cmd(:dur, :duration, :human, opts: %i[ms]) do
|
|
72
40
|
raw = @opts.args.first or abort 'usage: h date dur <seconds> [--ms]'
|
|
73
41
|
n = Rational(raw.to_s)
|
|
74
|
-
puts
|
|
42
|
+
puts (@opts.ms ? Hiiro::Duration.milliseconds(n) : Hiiro::Duration.seconds(n)).compact
|
|
75
43
|
end
|
|
76
44
|
|
|
77
45
|
# h date diff <a> <b> -> how far apart two dates are (dates or unix timestamps)
|
|
78
46
|
add_cmd(:diff, opts: %i[ms]) do
|
|
79
47
|
a, b = @opts.args
|
|
80
48
|
abort 'usage: h date diff <date-a> <date-b> (each: date string or unix timestamp)' unless a && b
|
|
81
|
-
ta =
|
|
82
|
-
tb =
|
|
83
|
-
puts
|
|
49
|
+
ta = Hiiro::TimeInput.parse(a, ms: @opts.ms ? true : nil)
|
|
50
|
+
tb = Hiiro::TimeInput.parse(b, ms: @opts.ms ? true : nil)
|
|
51
|
+
puts ta.difference(tb).compact
|
|
84
52
|
end
|
|
85
53
|
end
|
data/bin/h-link
CHANGED
|
@@ -22,55 +22,8 @@ class LinkManager
|
|
|
22
22
|
'created_at' => Time.now.iso8601,
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def initialize(url:, description: '', shorthand: nil, created_at: Time.now.iso8601)
|
|
29
|
-
@url = url
|
|
30
|
-
@description = description
|
|
31
|
-
@shorthand = shorthand
|
|
32
|
-
@created_at = created_at
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def self.from_hash(hash)
|
|
36
|
-
new(
|
|
37
|
-
url: hash['url'],
|
|
38
|
-
description: hash['description'] || '',
|
|
39
|
-
shorthand: hash['shorthand'],
|
|
40
|
-
created_at: hash['created_at'] || Time.now.iso8601
|
|
41
|
-
)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def to_h
|
|
45
|
-
{
|
|
46
|
-
'url' => @url,
|
|
47
|
-
'description' => @description,
|
|
48
|
-
'shorthand' => @shorthand,
|
|
49
|
-
'created_at' => @created_at
|
|
50
|
-
}
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def to_yaml
|
|
54
|
-
to_h.to_yaml
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def matches?(*terms)
|
|
58
|
-
searchable = [
|
|
59
|
-
@url,
|
|
60
|
-
@description,
|
|
61
|
-
@shorthand
|
|
62
|
-
].compact.join(' ').downcase
|
|
63
|
-
|
|
64
|
-
terms.all? { |term| searchable.downcase.include?(term.downcase) }
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def display_string(index = nil)
|
|
68
|
-
num = index ? "#{(index + 1).to_s.rjust(3)}." : ""
|
|
69
|
-
shorthand_str = @shorthand ? " [#{@shorthand}]" : ""
|
|
70
|
-
desc_str = @description.to_s.empty? ? "" : " - #{@description}"
|
|
71
|
-
"#{num}#{shorthand_str} #{@url}#{desc_str}".strip
|
|
72
|
-
end
|
|
73
|
-
end
|
|
25
|
+
# Shared representation with the DB model; YAML rows load as unsaved instances.
|
|
26
|
+
Link = Hiiro::Link
|
|
74
27
|
|
|
75
28
|
attr_reader :links_file
|
|
76
29
|
|
|
@@ -172,11 +125,11 @@ class LinkManager
|
|
|
172
125
|
end
|
|
173
126
|
|
|
174
127
|
def extract_placeholders(url)
|
|
175
|
-
|
|
128
|
+
Hiiro::URLTemplate.new(url).placeholders
|
|
176
129
|
end
|
|
177
130
|
|
|
178
131
|
def has_placeholders?(url)
|
|
179
|
-
|
|
132
|
+
Hiiro::URLTemplate.new(url).placeholders?
|
|
180
133
|
end
|
|
181
134
|
|
|
182
135
|
def prompt_for_placeholder_values(url, link=nil)
|
|
@@ -212,13 +165,7 @@ class LinkManager
|
|
|
212
165
|
end
|
|
213
166
|
|
|
214
167
|
def substitute_placeholders(url, values)
|
|
215
|
-
|
|
216
|
-
values.each do |key, value|
|
|
217
|
-
# Replace spaces with + for URL encoding
|
|
218
|
-
encoded_value = value.to_s.gsub(' ', '+')
|
|
219
|
-
result.gsub!("{#{key}}", encoded_value)
|
|
220
|
-
end
|
|
221
|
-
result
|
|
168
|
+
Hiiro::URLTemplate.new(url, values: values).url
|
|
222
169
|
end
|
|
223
170
|
|
|
224
171
|
def find_link_by_ref(ref, links)
|
data/bin/h-pr
CHANGED
|
@@ -321,13 +321,7 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
|
|
|
321
321
|
end
|
|
322
322
|
|
|
323
323
|
pr = JSON.parse(raw)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
counts = Hash.new(0)
|
|
327
|
-
checks.each do |c|
|
|
328
|
-
result = c['__typename'] == 'CheckRun' ? (c['conclusion'] || c['status']) : c['state']
|
|
329
|
-
counts[result.to_s.upcase] += 1
|
|
330
|
-
end
|
|
324
|
+
counts = Hiiro::CheckSummary.from_rollup(pr['statusCheckRollup'])&.counts_by_outcome || {}
|
|
331
325
|
|
|
332
326
|
if counts.empty?
|
|
333
327
|
summary = 'no checks'
|
|
@@ -714,7 +708,7 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
|
|
|
714
708
|
pinned = pinned_manager.load_pinned
|
|
715
709
|
|
|
716
710
|
prs_to_fix = if opts.red
|
|
717
|
-
failing = pinned.select
|
|
711
|
+
failing = pinned.select(&:red?)
|
|
718
712
|
if failing.empty?
|
|
719
713
|
puts "No PRs with failing checks"
|
|
720
714
|
next
|
|
@@ -722,7 +716,7 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
|
|
|
722
716
|
failing
|
|
723
717
|
elsif opts.args.empty?
|
|
724
718
|
# Default: fuzzy-select from failing PRs, fall back to all tracked
|
|
725
|
-
failing = pinned.select
|
|
719
|
+
failing = pinned.select(&:red?)
|
|
726
720
|
candidates = failing.empty? ? pinned : failing
|
|
727
721
|
if candidates.empty?
|
|
728
722
|
puts "No tracked PRs"
|
|
@@ -867,17 +861,14 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
|
|
|
867
861
|
# === PR List by Context ===
|
|
868
862
|
|
|
869
863
|
add_subcmd(:branch) do |arg = nil|
|
|
870
|
-
|
|
871
|
-
$1
|
|
872
|
-
else
|
|
873
|
-
arg
|
|
874
|
-
end
|
|
864
|
+
ref = Hiiro::PullRequestReference.parse(arg)
|
|
875
865
|
|
|
876
|
-
pr_number = resolve(:pr,
|
|
866
|
+
pr_number = resolve(:pr, ref ? ref.number_string : arg)
|
|
877
867
|
next unless pr_number
|
|
878
868
|
|
|
879
869
|
pinned = pinned_manager.load_pinned
|
|
880
|
-
|
|
870
|
+
ref ||= Hiiro::PullRequestReference.parse(pr_number)
|
|
871
|
+
pr = pinned.find { |p| ref.matches?(p) }
|
|
881
872
|
|
|
882
873
|
unless pr
|
|
883
874
|
puts "PR ##{pr_number} not in tracked list"
|