ace-git-worktree 0.23.0 → 0.24.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 657c7b373d89fe83a9950e50fe62beff7a1c2610951e8d714420bbb42d54da5e
4
- data.tar.gz: aaa908624ab0191dfbad907e24308a0922bd299ad24179c5b5897dcbbb37d9ca
3
+ metadata.gz: 440da5b336b58817b1e6bbc1b8d57de396e78848b9e9c289d1492ca85fc7065a
4
+ data.tar.gz: 432c570346044a2351c72355b729501cef4723507fb64f0298307f7790edf289
5
5
  SHA512:
6
- metadata.gz: 8ca4f8f31064a54255b854a7f9477f227d1429bc44516ed88a73b8ee92717425179aa58a5e5cc0dabb8e30cdb60d3dc7cd0245c78574b4ef8106414edab23991
7
- data.tar.gz: 329727b4de42a420b1e6c4aaa060e45e24cc6d21c97c787a873f4da333ed22dc8056bed32d229c13e9991068550945e87f573d4d760c4bb06b241771fc7a07c6
6
+ metadata.gz: c4c1f7d225354653e5d6d4c3ff8e88c0c8b4cee1142707e5d51181569b961b58e81fd1f64bca355a78a6675205429654420ed9776481470204342f70e7bfc9a5
7
+ data.tar.gz: 46e6f7112657db3d867a69db98dacbe429883672beb37b83fa35bda5b059d071a8a318fe5d26adc90835da05a3b4f74d105b096244be92beb61a904007652358
data/CHANGELOG.md CHANGED
@@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.24.0] - 2026-09-28
11
+
12
+ ### Added
13
+ - Forge-neutral PR worktree checkout: the exact declared source
14
+ repository/ref is fetched and its head SHA verified against normalized PR
15
+ evidence before any worktree is created (fork and canonical sources are
16
+ equally valid).
17
+ - `--server` / `--default-server` selection for PR creation, task-PR
18
+ creation, and cleanup; `--remote` remains a local Git remote name only.
19
+ - Task PR creation proves the pushed branch head SHA to the provider
20
+ (`expected_head`); PR identity is never inferred from branch names.
21
+ - Cleanup report digests now bind the resolved server identity and each
22
+ item's provider proof (status, PR, proven head, merge commit); `--apply`
23
+ recomputes the full report immediately before executing and refuses any
24
+ drift from the approved digest.
25
+
26
+ ### Removed
27
+ - Direct GitHub coupling from worktree correctness paths: `ace/git/github`
28
+ requires, `Github::PrFetcher` usage, `gh` CLI checks and troubleshooting,
29
+ the raw gh-based `PrCreator` molecule, and the `ace-git-github` gemspec
30
+ dependency. Provider execution lives behind the neutral ace-git provider
31
+ contract.
32
+
33
+
34
+
35
+ ### Added
36
+ - Forge-neutral PR worktree checkout: the exact declared source
37
+ repository/ref is fetched and its head SHA verified against normalized PR
38
+ evidence before any worktree is created (fork and canonical sources are
39
+ equally valid).
40
+ - `--server` / `--default-server` selection for PR creation, task-PR
41
+ creation, and cleanup; `--remote` remains a local Git remote name only.
42
+ - Task PR creation proves the pushed branch head SHA to the provider
43
+ (`expected_head`); PR identity is never inferred from branch names.
44
+ - Cleanup report digests now bind the resolved server identity and each
45
+ item's provider proof (status, PR, proven head, merge commit); `--apply`
46
+ recomputes the full report immediately before executing and refuses any
47
+ drift from the approved digest.
48
+
49
+ ### Removed
50
+ - Direct GitHub coupling from worktree correctness paths: `ace/git/github`
51
+ requires, `Github::PrFetcher` usage, `gh` CLI checks and troubleshooting,
52
+ the raw gh-based `PrCreator` molecule, and the `ace-git-github` gemspec
53
+ dependency. Provider execution lives behind the neutral ace-git provider
54
+ contract.
55
+
56
+
10
57
 
11
58
  ## [0.23.0] - 2026-09-27
12
59
 
data/docs/usage.md CHANGED
@@ -76,12 +76,18 @@ Created task worktree and branch for 081
76
76
 
77
77
  ### Scenario 2: Review a Pull Request in Its Own Worktree
78
78
 
79
- **Goal:** Create a dedicated workspace for PR review or patching.
79
+ **Goal:** Create a dedicated workspace for PR review or patching, on any
80
+ configured forge (GitHub, default Forgejo, or a named Forgejo).
80
81
 
81
82
  **Commands:**
82
83
 
83
84
  ```bash
84
- ace-git-worktree create --pr 26
85
+ # Preview provenance first (no mutation): source/base repositories, exact head
86
+ ace-git-worktree create --pr 26 --server forgejo-lab --dry-run
87
+
88
+ # Create the worktree; the declared source ref is fetched and its SHA is
89
+ # verified against the PR evidence before anything is created
90
+ ace-git-worktree create --pr 26 --server forgejo-lab
85
91
  ace-git-worktree list
86
92
  ```
87
93
 
@@ -187,6 +193,8 @@ ace-git-worktree create --task 081 --dry-run
187
193
 
188
194
  * `--task` - Task ID for a task-aware worktree
189
195
  * `--pr`, `--pull-request` - PR number for a PR-aware worktree
196
+ * `--server <name>` - Named configured forge server for PR and task-PR operations
197
+ * `--default-server` - Use the configured default forge server
190
198
  * `--from`, `-b` - Create from a specific local or remote branch
191
199
  * `--path` - Override the destination worktree path
192
200
  * `--source` - Override the git ref used as the start point
@@ -195,7 +203,8 @@ ace-git-worktree create --task 081 --dry-run
195
203
  * `--no-commit` - Skip committing task metadata changes
196
204
  * `--no-push` - Skip pushing task changes
197
205
  * `--no-upstream` - Skip pushing the new branch with upstream tracking
198
- * `--no-pr` - Skip automatic draft-PR creation when enabled
206
+ * `--no-pr` - Skip automatic draft-PR creation; keeps task creation fully
207
+ local and provider-free
199
208
  * `--push-remote` - Override the remote used for task-related pushes
200
209
  * `--no-auto-navigate` - Stay in the current directory after creation
201
210
  * `--commit-message` - Use a custom commit message for task metadata updates
@@ -295,6 +304,42 @@ ace-git-worktree prune --cleanup-directories
295
304
  * `-v`, `--verbose` - Show verbose output
296
305
  * `-d`, `--debug` - Show debug output
297
306
 
307
+ ### `ace-git-worktree cleanup`
308
+
309
+ Build a complete, deterministic, report-only cleanup inventory for merged
310
+ worktrees and refs. Local ancestry is the first safe proof; provider PR
311
+ evidence (from the selected forge server) proves candidates whose local
312
+ ancestry is unproven. Unavailable or failed provider proof is never relabeled
313
+ as merged.
314
+
315
+ ```bash
316
+ ace-git-worktree cleanup --target main
317
+ ace-git-worktree cleanup --target main --format json
318
+ ace-git-worktree cleanup --target main --server forgejo-lab
319
+ ace-git-worktree cleanup --target main --offline
320
+ ```
321
+
322
+ **Options:**
323
+
324
+ * `--target <ref>` - Target ref for ancestry proof (required)
325
+ * `--remote <name>` - Local Git remote name (default: `origin`); never forge selection
326
+ * `--server <name>` - Named configured forge server for PR proof
327
+ * `--default-server` - Use the configured default forge server
328
+ * `--offline` - Skip remote refresh and provider proof
329
+ * `--format <type>` - Output format: `table` (default), `json`
330
+ * `--apply` - Apply a reviewed plan (see below)
331
+ * `--approved-digest <sha256>` - Digest of the reviewed plan; required with `--apply`
332
+ * `--require-only-target` - Fail if the final rescan retains non-target state
333
+
334
+ **Approval binding:** the report carries a canonical SHA-256 digest over the
335
+ inventory, decisions, the resolved server identity, and each item's provider
336
+ proof (status, PR number, proven head, merge commit). `--apply` recomputes
337
+ the full report immediately before executing and refuses anything whose
338
+ digest differs from the approved one — changed repo state, changed server
339
+ configuration, or changed PR/head proof all invalidate consent. Only
340
+ confirmed merged evidence permits remote-merge cleanup; `--force` does not
341
+ exist and dirty, locked, and in-use worktrees are always retained.
342
+
298
343
  ### `ace-git-worktree bootstrap [IDENTIFIER]`
299
344
 
300
345
  Rerun preparation phases (toolchain trust and bootstrap policy) for an existing worktree in place.
@@ -399,16 +444,23 @@ data is available.
399
444
 
400
445
  ### Problem: PR worktree creation fails
401
446
 
402
- **Symptom:** `create --pr` reports a GitHub or authentication error.
447
+ **Symptom:** `create --pr` reports a classified provider error (missing
448
+ provider CLI, unauthenticated, unreachable, or identity mismatch).
403
449
 
404
450
  **Solution:**
405
451
 
406
- ```bash
407
- gh auth status
408
- gh auth login
409
- ```
410
-
411
- PR-based creation depends on `gh` being installed and authenticated.
452
+ 1. Configure the forge server in `git.servers` (see `ace-git` docs) and
453
+ select it with `--server` / `--default-server`, or make the repository
454
+ remote resolvable.
455
+ 2. Install and authenticate the provider CLI for that server's provider
456
+ type (`gh` for GitHub, `fj` for Forgejo; check `gh auth status` or
457
+ `fj auth list`).
458
+ 3. Preview with `--dry-run` to inspect the exact source/base provenance
459
+ before creating anything.
460
+
461
+ PR creation is forge-neutral: the exact declared source repository/ref is
462
+ fetched and its head SHA is verified against the PR evidence. Failures never
463
+ fall back to another provider.
412
464
 
413
465
  ### Problem: `switch` returns nothing useful
414
466
 
@@ -30,28 +30,16 @@ module Ace
30
30
 
31
31
  validate_options(options)
32
32
 
33
- reporter = Molecules::CleanupReporter.new(
34
- target: options[:target],
35
- remote: options[:remote],
36
- offline: options[:offline]
37
- )
38
-
39
- result = reporter.report
33
+ result = build_reporter(options).report
40
34
 
41
35
  if result[:success]
42
36
  if options[:apply]
43
- applier = Molecules::CleanupApplier.new(result, options[:approved_digest])
44
- apply_result = applier.apply
37
+ apply_result = apply_fresh_report(options)
45
38
 
46
39
  if apply_result[:success]
47
- # Strict rescan
48
- final_reporter = Molecules::CleanupReporter.new(
49
- target: options[:target],
50
- remote: options[:remote],
51
- offline: options[:offline]
52
- )
53
- final_result = final_reporter.report
54
-
40
+ # Strict rescan with the same selection
41
+ final_result = build_reporter(options).report
42
+
55
43
  if final_result[:success]
56
44
  # Check require_only_target if needed
57
45
  if options[:require_only_target] && has_non_target_state?(final_result)
@@ -59,7 +47,7 @@ module Ace
59
47
  display_result(final_result, options)
60
48
  return 1
61
49
  end
62
-
50
+
63
51
  display_apply_result(apply_result, final_result, options)
64
52
  return 0
65
53
  else
@@ -93,6 +81,35 @@ module Ace
93
81
  1
94
82
  end
95
83
 
84
+ private
85
+
86
+ # Build a reporter with the command's server selection; --remote
87
+ # stays a local Git remote name and is never forge selection.
88
+ def build_reporter(options)
89
+ Molecules::CleanupReporter.new(
90
+ target: options[:target],
91
+ remote: options[:remote],
92
+ offline: options[:offline],
93
+ server_name: options[:server],
94
+ use_default: options[:default_server] == true,
95
+ remote_name: options[:remote]
96
+ )
97
+ end
98
+
99
+ # Recompute the complete report immediately before apply; approval
100
+ # is bound to the reviewed digest over the exact evidence. Any
101
+ # drift (repo state, server identity, PR/head proof) invalidates
102
+ # the approved digest and refuses the apply.
103
+ def apply_fresh_report(options)
104
+ fresh = build_reporter(options).report
105
+ unless fresh[:success]
106
+ return {success: false, error: "Report recomputation failed before apply: #{fresh[:error]}", ledger: [], partial_failure: false}
107
+ end
108
+
109
+ applier = Molecules::CleanupApplier.new(fresh, options[:approved_digest])
110
+ applier.apply
111
+ end
112
+
96
113
  # Show help
97
114
  #
98
115
  # @return [Integer] Exit code
@@ -103,8 +120,10 @@ module Ace
103
120
 
104
121
  OPTIONS
105
122
  --target <ref> Target ref for ancestry proof (required)
106
- --remote <name> Remote name (default: origin)
107
- --offline Skip remote evidence refresh
123
+ --remote <name> Local Git remote name (default: origin); never forge selection
124
+ --server <name> Named configured forge server for PR proof
125
+ --default-server Use the configured default forge server
126
+ --offline Skip remote evidence refresh and provider proof
108
127
  --apply Apply a reviewed plan
109
128
  --approved-digest <sha256> Approved plan digest to apply
110
129
  --require-only-target Fail if final rescan has retained non-target state
@@ -128,12 +147,11 @@ module Ace
128
147
  0
129
148
  end
130
149
 
131
- private
132
-
133
150
  def parse_arguments(args)
134
151
  options = {
135
152
  target: nil, remote: "origin", offline: false, format: "table", help: false,
136
- apply: false, approved_digest: nil, require_only_target: false
153
+ apply: false, approved_digest: nil, require_only_target: false,
154
+ server: nil, default_server: false
137
155
  }
138
156
  i = 0
139
157
  while i < args.length
@@ -146,6 +164,11 @@ module Ace
146
164
  options[:remote] = args[i]
147
165
  when "--offline"
148
166
  options[:offline] = true
167
+ when "--server"
168
+ i += 1
169
+ options[:server] = args[i]
170
+ when "--default-server"
171
+ options[:default_server] = true
149
172
  when "--apply"
150
173
  options[:apply] = true
151
174
  when "--approved-digest"
@@ -169,16 +192,17 @@ module Ace
169
192
  def validate_options(options)
170
193
  raise ArgumentError, "--target is required" unless options[:target]
171
194
  raise ArgumentError, "--remote is required" unless options[:remote]
195
+ raise ArgumentError, "--server and --default-server are mutually exclusive" if options[:server] && options[:default_server]
172
196
 
173
197
  valid_formats = %w[table json]
174
198
  unless valid_formats.include?(options[:format])
175
199
  raise ArgumentError, "Invalid format '#{options[:format]}'. Use: #{valid_formats.join(", ")}"
176
200
  end
177
-
201
+
178
202
  if options[:apply] && !options[:approved_digest]
179
203
  raise ArgumentError, "--approved-digest is required when --apply is used"
180
204
  end
181
-
205
+
182
206
  if options[:approved_digest] && !options[:apply]
183
207
  raise ArgumentError, "--apply is required when --approved-digest is used"
184
208
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ace/git/github"
4
-
5
3
  module Ace
6
4
  module Git
7
5
  module Worktree
@@ -60,6 +58,7 @@ module Ace
60
58
  rescue Ace::Git::ProviderObjectNotFoundError,
61
59
  Ace::Git::ProviderAuthenticationError,
62
60
  Ace::Git::ProviderCliMissingError,
61
+ Ace::Git::ProviderIdentityMismatchError,
63
62
  Ace::Git::TimeoutError => e
64
63
  puts "Error: #{e.message}"
65
64
  1
@@ -88,9 +87,12 @@ module Ace
88
87
  Task ID formats: 081, task.081, v.0.9.0+081
89
88
 
90
89
  PR-AWARE CREATION:
91
- --pr <number> Create worktree for a GitHub pull request
90
+ --pr <number> Create worktree for a pull request
92
91
  --pull-request <number> (alias for --pr)
93
- Requires gh CLI to be installed and authenticated
92
+ Requires a configured forge server and its
93
+ provider package; fetches the exact declared
94
+ source repository/ref and verifies the head
95
+ SHA against the PR evidence.
94
96
 
95
97
  BRANCH-AWARE CREATION:
96
98
  -b <branch> Create worktree from a branch (local or remote)
@@ -102,12 +104,14 @@ module Ace
102
104
  --path <path> Custom worktree path (default: from config)
103
105
  --source <ref> Git ref to use as branch start-point (default: current branch)
104
106
  Examples: main, origin/develop, HEAD~3, commit-sha
107
+ --server <name> Use the named configured forge server (PR mode)
108
+ --default-server Use the configured default forge server
105
109
  --dry-run Show what would be created without creating
106
110
  --no-status-update Skip marking task as in-progress (task mode only)
107
111
  --no-commit Skip committing task changes (task mode only)
108
112
  --no-push Skip pushing task changes to remote (task mode only)
109
113
  --no-upstream Skip pushing worktree branch with upstream tracking (task mode only)
110
- --no-pr Skip creating draft PR (task mode only)
114
+ --no-pr Skip creating draft PR; keeps task creation local and provider-free
111
115
  --no-bootstrap Skip worktree bootstrap preparation
112
116
  --push-remote <name> Remote to push to (default: origin) (task mode only)
113
117
  --no-auto-navigate Stay in current directory (default: navigate to worktree)
@@ -122,8 +126,8 @@ module Ace
122
126
  # Create task worktree based on main instead of current branch
123
127
  ace-git-worktree create --task 081 --source main
124
128
 
125
- # Create PR worktree
126
- ace-git-worktree create --pr 26
129
+ # Create PR worktree on a named forge server (no mutation)
130
+ ace-git-worktree create --pr 26 --server forgejo-lab --dry-run
127
131
 
128
132
  # Create worktree from remote branch
129
133
  ace-git-worktree create -b origin/feature/auth
@@ -150,9 +154,10 @@ module Ace
150
154
  - hooks.after_create: Commands to run after worktree creation
151
155
 
152
156
  REQUIREMENTS:
153
- PR-aware creation requires GitHub CLI (gh):
154
- - Install: brew install gh
155
- - Authenticate: gh auth login
157
+ PR-aware creation requires a configured forge server
158
+ (git.servers) and the provider package for its provider
159
+ type. Task and branch creation are fully local and never
160
+ touch a forge.
156
161
  HELP
157
162
  0
158
163
  end
@@ -170,6 +175,8 @@ module Ace
170
175
  branch: nil,
171
176
  path: nil,
172
177
  source: nil,
178
+ server: nil,
179
+ default_server: false,
173
180
  dry_run: false,
174
181
  no_status_update: false,
175
182
  no_commit: false,
@@ -205,6 +212,11 @@ module Ace
205
212
  when "--source"
206
213
  i += 1
207
214
  options[:source] = args[i]
215
+ when "--server"
216
+ i += 1
217
+ options[:server] = args[i]
218
+ when "--default-server"
219
+ options[:default_server] = true
208
220
  when "--dry-run"
209
221
  options[:dry_run] = true
210
222
  when "--no-status-update"
@@ -377,6 +389,8 @@ module Ace
377
389
  push_remote: options[:push_remote],
378
390
  commit_message: options[:commit_message],
379
391
  target_branch: options[:target_branch],
392
+ server: options[:server],
393
+ default_server: options[:default_server],
380
394
  no_mise_trust: options[:no_mise_trust],
381
395
  force: options[:force]
382
396
  }.compact
@@ -410,7 +424,7 @@ module Ace
410
424
  end
411
425
  end
412
426
 
413
- # Create a PR worktree
427
+ # Create a PR worktree from normalized provider evidence.
414
428
  #
415
429
  # @param options [Hash] Command options
416
430
  # @return [Integer] Exit code
@@ -434,47 +448,28 @@ module Ace
434
448
 
435
449
  puts "Creating worktree for PR ##{pr_number}..."
436
450
 
437
- # Check gh CLI availability via the GitHub provider (ace-git-github)
438
- unless Ace::Git::Github::PrFetcher.installed?
439
- puts "Error: gh CLI is required for PR worktree creation."
440
- puts
441
- puts gh_not_available_message
442
- return 1
443
- end
444
-
445
- unless Ace::Git::Github::PrFetcher.authenticated?
446
- puts "Error: gh CLI is not authenticated."
447
- puts
448
- puts "Authenticate with: gh auth login"
449
- return 1
450
- end
451
-
452
- # Fetch PR data
453
- puts "Fetching PR information..."
454
451
  begin
455
- result = Ace::Git::Github::PrFetcher.fetch_metadata(pr_number.to_s)
456
-
457
- unless result[:success]
458
- puts "Error: #{result[:error]}"
459
- return 1
460
- end
461
-
462
- metadata = result[:metadata]
463
-
464
- # Convert to pr_data format expected by worktree creator
465
- pr_data = pr_data_from_metadata(metadata)
466
-
467
- # Show PR details
468
- puts "PR ##{pr_data[:number]}: #{pr_data[:title]}"
469
- puts "Branch: #{pr_data[:head_branch]} -> #{pr_data[:base_branch]}"
470
-
471
- # Warn about fork PRs
472
- if pr_data[:is_cross_repository]
473
- puts
474
- puts "Warning: This PR is from a fork (#{pr_data[:head_repository_owner]})."
475
- puts "You will not be able to push to the PR branch directly."
452
+ # Fetch normalized evidence through the selected forge server.
453
+ puts "Fetching PR information..."
454
+ resolver = Molecules::PullRequestEvidenceResolver.new(
455
+ server_name: options[:server],
456
+ use_default: options[:default_server] == true
457
+ )
458
+ resolved = resolver.resolve(pr_number)
459
+ evidence = resolved[:evidence]
460
+
461
+ display_pr_evidence(evidence)
462
+
463
+ # Fetch and verify the exact declared source (skipped for dry
464
+ # runs, which may read evidence but never create anything).
465
+ checkout = nil
466
+ unless options[:dry_run]
467
+ checkout = Molecules::PullRequestCheckoutPreparer.new.prepare(evidence)
468
+ unless checkout[:success]
469
+ puts "Error: #{checkout[:error]}"
470
+ return 1
471
+ end
476
472
  end
477
- puts
478
473
 
479
474
  # Prepare creation options
480
475
  creation_options = {
@@ -485,7 +480,7 @@ module Ace
485
480
  }.compact
486
481
 
487
482
  # Create the worktree
488
- result = @manager.create_pr(pr_number, pr_data, creation_options)
483
+ result = @manager.create_pr(evidence, checkout, creation_options)
489
484
 
490
485
  if result[:success]
491
486
  display_pr_creation_result(result, options[:dry_run])
@@ -495,14 +490,30 @@ module Ace
495
490
  display_warnings(result[:warnings]) if result[:warnings]
496
491
  1
497
492
  end
498
- rescue Ace::Git::ProviderObjectNotFoundError, Ace::Git::ProviderAuthenticationError,
499
- Ace::Git::ProviderCliMissingError => e
500
- # Let specific ace-git errors be handled by handle_pr_fetch_error
501
- # Other errors will bubble up to the top-level rescue in run()
502
- handle_pr_fetch_error(e, pr_number)
493
+ rescue Ace::Git::Error => e
494
+ puts "Error: #{e.message}"
495
+ puts "Debug: #{e.class}" if ENV["DEBUG"]
496
+ 1
503
497
  end
504
498
  end
505
499
 
500
+ # Display normalized PR evidence before checkout.
501
+ #
502
+ # @param evidence [Ace::Git::ProviderPullRequest] normalized evidence
503
+ def display_pr_evidence(evidence)
504
+ puts "PR ##{evidence.number}: #{evidence.title}"
505
+ puts "Branch: #{evidence.head_ref} -> #{evidence.base_ref}"
506
+ puts "Source: #{evidence.head_repository_url}@#{evidence.head_ref}" \
507
+ " (head #{evidence.head_sha.to_s[0, 8] || "unknown"})"
508
+ puts "Server: #{evidence.server_name}"
509
+
510
+ return unless evidence.head_repository_url != evidence.base_repository_url
511
+
512
+ puts
513
+ puts "Warning: This PR is from a fork (#{evidence.head_repository_url})."
514
+ puts "You will not be able to push to the PR branch directly."
515
+ end
516
+
506
517
  # Create a branch worktree
507
518
  #
508
519
  # @param options [Hash] Command options
@@ -865,104 +876,6 @@ module Ace
865
876
  system("which ace-tmux > /dev/null 2>&1")
866
877
  end
867
878
 
868
- # Convert PR metadata from gh CLI to internal pr_data format
869
- #
870
- # Anti-corruption layer that translates gh CLI JSON output to internal format.
871
- # This isolates worktree creation from gh CLI output structure changes.
872
- #
873
- # Expected metadata schema from the GitHub provider PrFetcher (via gh pr view --json):
874
- # {
875
- # "number" => Integer,
876
- # "title" => String,
877
- # "headRefName" => String (PR source branch),
878
- # "baseRefName" => String (PR target branch),
879
- # "isCrossRepository" => Boolean (true for fork PRs),
880
- # "headRepositoryOwner" => { "login" => String } (fork owner info)
881
- # }
882
- #
883
- # @param metadata [Hash] PR metadata from gh CLI
884
- # @return [Hash] pr_data format expected by worktree creator
885
- def pr_data_from_metadata(metadata)
886
- {
887
- number: metadata["number"],
888
- title: metadata["title"],
889
- head_branch: metadata["headRefName"],
890
- base_branch: metadata["baseRefName"],
891
- is_cross_repository: metadata["isCrossRepository"] || false,
892
- head_repository_owner: metadata.dig("headRepositoryOwner", "login") || "unknown"
893
- }
894
- end
895
-
896
- # Handle errors during PR metadata fetch
897
- #
898
- # @param error [Exception] The error that occurred
899
- # @param pr_number [Integer] PR number for context in error messages
900
- # @return [Integer] Exit code (always 1 for errors)
901
- def handle_pr_fetch_error(error, pr_number)
902
- case error
903
- when Ace::Git::ProviderObjectNotFoundError
904
- handle_pr_not_found(error, pr_number)
905
- when Ace::Git::ProviderAuthenticationError
906
- handle_gh_auth_error(error)
907
- when Ace::Git::ProviderCliMissingError
908
- handle_gh_not_installed(error)
909
- else
910
- handle_unknown_error(error)
911
- end
912
- 1
913
- end
914
-
915
- # Handle PR not found error with helpful suggestions
916
- def handle_pr_not_found(error, pr_number)
917
- puts "Error: #{error.message}"
918
- puts
919
- puts "Suggestions:"
920
- puts " 1. Verify the PR number is correct"
921
- puts " 2. Check if the PR exists: gh pr view #{pr_number}"
922
- puts " 3. Ensure you're in the correct repository"
923
- end
924
-
925
- # Handle GitHub authentication error with troubleshooting steps
926
- def handle_gh_auth_error(error)
927
- puts "Error: #{error.message}"
928
- puts
929
- puts "Troubleshooting:"
930
- puts " 1. Verify GitHub authentication: gh auth status"
931
- puts " 2. Re-authenticate if needed: gh auth login"
932
- puts " 3. Check repository access permissions"
933
- end
934
-
935
- # Handle gh CLI not installed error with installation guidance
936
- def handle_gh_not_installed(error)
937
- puts "Error: #{error.message}"
938
- puts
939
- puts gh_not_available_message
940
- end
941
-
942
- # Handle unknown/unexpected errors with debug info
943
- def handle_unknown_error(error)
944
- puts "Error: #{error.message}"
945
- if ENV["DEBUG"]
946
- puts "Debug: #{error.class}"
947
- puts "Debug: #{error.backtrace&.first}" if error.backtrace
948
- end
949
- end
950
-
951
- # Get helpful error message when gh CLI is unavailable
952
- #
953
- # @return [String] User-friendly error message with installation guidance
954
- def gh_not_available_message
955
- <<~MESSAGE
956
- gh CLI is required for PR worktrees but is not installed.
957
-
958
- Install gh CLI:
959
- - macOS: brew install gh
960
- - Linux: See https://github.com/cli/cli#installation
961
- - Windows: See https://github.com/cli/cli#installation
962
-
963
- After installation, authenticate with: gh auth login
964
- MESSAGE
965
- end
966
879
  end
967
880
  end
968
881
  end