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.
@@ -103,20 +103,27 @@ module Ace
103
103
  end
104
104
  end
105
105
 
106
- # Create a worktree for a Pull Request
106
+ # Create a worktree for a Pull Request from an already-verified
107
+ # source ref (see PullRequestCheckoutPreparer).
107
108
  #
108
- # @param pr_data [Hash] PR data hash from PrFetcher
109
+ # @param pr_data [Hash] normalized PR data: :number, :title,
110
+ # :head_branch, :base_branch
109
111
  # @param config [WorktreeConfig] Worktree configuration
110
112
  # @param git_root [String, nil] Git repository root (auto-detected if nil)
113
+ # @param source_ref [String, nil] verified local ref (e.g. FETCH_HEAD)
114
+ # the PR head was proven against; required
115
+ # @param remote_tracking [String, nil] "remote/branch" tracking ref
116
+ # when the source is reachable via a configured local remote
111
117
  # @return [Hash] Result with :success, :worktree_path, :branch, :error
112
118
  #
113
119
  # @example
114
120
  # pr_data = { number: 26, title: "Add feature", head_branch: "feature/auth", base_branch: "main" }
115
- # result = creator.create_for_pr(pr_data, config)
116
- # # => { success: true, worktree_path: "/project/.ace-wt/pr-26", branch: "pr-26", tracking: "origin/feature/auth" }
117
- def create_for_pr(pr_data, config, git_root: nil)
121
+ # result = creator.create_for_pr(pr_data, config, source_ref: "FETCH_HEAD")
122
+ # # => { success: true, worktree_path: ".../ace-pr-26", branch: "pr-26-add-feature", tracking: nil }
123
+ def create_for_pr(pr_data, config, git_root: nil, source_ref: nil, remote_tracking: nil)
118
124
  return error_result("PR data is required") unless pr_data
119
125
  return error_result("Configuration is required") unless config
126
+ return error_result("Verified source ref is required for PR checkout") if source_ref.nil? || source_ref.empty?
120
127
 
121
128
  begin
122
129
  # Determine git repository root
@@ -125,7 +132,6 @@ module Ace
125
132
 
126
133
  # Get PR-specific configuration (fallback to defaults)
127
134
  pr_config = config.pr_config || {}
128
- remote_name = pr_config[:remote_name] || "origin"
129
135
  directory_format = pr_config[:directory_format] || "ace-pr-{number}"
130
136
  branch_format = pr_config[:branch_format] || "pr-{number}-{slug}"
131
137
 
@@ -140,27 +146,28 @@ module Ace
140
146
  validation = validate_worktree_path(worktree_path, git_root)
141
147
  return error_result(validation[:error]) unless validation[:valid]
142
148
 
143
- # Fetch the remote branch
144
- head_branch = pr_data[:head_branch]
145
- fetch_result = fetch_remote_branch(remote_name, head_branch, git_root)
146
- return error_result(fetch_result[:error]) unless fetch_result[:success]
147
-
148
- # Create worktree with remote tracking
149
- result = create_worktree_with_tracking(
150
- worktree_path,
151
- local_branch_name,
152
- "#{remote_name}/#{head_branch}",
153
- git_root,
154
- configure_push: config.configure_push_for_mismatch?
155
- )
149
+ # Create the worktree from the proven source ref
150
+ result = create_worktree(worktree_path, local_branch_name, git_root, start_point: source_ref)
156
151
  return result unless result[:success]
157
152
 
153
+ # Configure push behavior when tracking a differently-named branch
154
+ if remote_tracking && local_branch_name != extract_remote_branch_name(remote_tracking)
155
+ configure_push_for_worktree(worktree_path, local_branch_name, remote_tracking)
156
+ end
157
+
158
+ # Bind the reported tracking ref as the branch's real upstream so
159
+ # `@{upstream}` resolves; advisory when the tracking ref is not a
160
+ # local remote-tracking ref (fork URL fetches).
161
+ upstream = remote_tracking ? set_and_verify_upstream(local_branch_name, remote_tracking) : nil
162
+
158
163
  # Success - return worktree information
159
164
  {
160
165
  success: true,
161
166
  worktree_path: worktree_path,
162
167
  branch: local_branch_name,
163
- tracking: "#{remote_name}/#{head_branch}",
168
+ tracking: remote_tracking,
169
+ upstream: upstream,
170
+ warnings: remote_tracking && !upstream ? ["Could not set upstream to #{remote_tracking}"] : nil,
164
171
  directory_name: directory_name,
165
172
  pr_number: pr_data[:number],
166
173
  pr_title: pr_data[:title],
@@ -764,6 +771,31 @@ module Ace
764
771
  }
765
772
  end
766
773
 
774
+ # Set and verify upstream for a PR worktree branch.
775
+ #
776
+ # @param local_branch [String] the worktree's local branch
777
+ # @param remote_tracking [String] e.g. "origin/feature"
778
+ # @return [String, nil] the verified upstream, or nil when it could
779
+ # not be established (advisory)
780
+ def set_and_verify_upstream(local_branch, remote_tracking)
781
+ require_relative "../atoms/git_command"
782
+
783
+ exists = Atoms::GitCommand.execute(
784
+ "show-ref", "--verify", "--quiet", "refs/remotes/#{remote_tracking}", timeout: 5
785
+ )
786
+ return nil unless exists[:success]
787
+
788
+ result = Atoms::GitCommand.execute(
789
+ "branch", "--set-upstream-to=#{remote_tracking}", local_branch, timeout: 10
790
+ )
791
+ return nil unless result[:success]
792
+
793
+ verify = Atoms::GitCommand.execute(
794
+ "rev-parse", "--abbrev-ref", "#{local_branch}@{upstream}", timeout: 5
795
+ )
796
+ verify[:success] && verify[:output].to_s.strip == remote_tracking ? remote_tracking : nil
797
+ end
798
+
767
799
  # Format PR name using template
768
800
  #
769
801
  # @param template [String] Template string with {variables}
@@ -31,7 +31,6 @@ module Ace
31
31
  @task_committer = Molecules::TaskCommitter.new(project_root: project_root)
32
32
  @task_pusher = Molecules::TaskPusher.new
33
33
  @worktree_creator = Molecules::WorktreeCreator.new
34
- @pr_creator = Molecules::PrCreator.new
35
34
  @parent_task_resolver = Molecules::ParentTaskResolver.new(project_root: project_root)
36
35
  end
37
36
 
@@ -861,14 +860,14 @@ module Ace
861
860
  result[:success] && result[:output]&.include?(branch)
862
861
  end
863
862
 
864
- # Create draft PR for task
863
+ # Create draft PR for task through the forge-neutral provider contract.
865
864
  #
866
- # Creates a draft PR targeting the source branch (start_point) from which
867
- # the worktree branch was created.
865
+ # The exact pushed head SHA is proven to the provider (expected_head);
866
+ # PR identity is never inferred from branch names alone.
868
867
  #
869
868
  # @param task_data [Hash] Task data hash from ace-task
870
869
  # @param worktree_result [Hash] Worktree creation result with :branch, :start_point
871
- # @param options [Hash] Options (may include :source for base branch override)
870
+ # @param options [Hash] Options (may include :source, :server, :default_server)
872
871
  # @return [Hash] Result with :success, :pr_number, :pr_url, :existing, :error
873
872
  def create_pr_for_task(task_data, worktree_result, options)
874
873
  branch = worktree_result[:branch]
@@ -880,14 +879,49 @@ module Ace
880
879
  # Resolve base branch - handle SHA vs branch name
881
880
  base = resolve_pr_base(start_point, options)
882
881
 
883
- # Create draft PR
884
- @pr_creator.create_draft(
882
+ push_remote = options[:push_remote] || @config.push_remote || "origin"
883
+ creator = Molecules::PullRequestCreator.new(
884
+ server_name: options[:server],
885
+ use_default: options[:default_server] == true,
886
+ remote_name: push_remote
887
+ )
888
+ creator.create_draft(
885
889
  branch: branch,
886
890
  base: base,
887
- title: title
891
+ title: title,
892
+ expected_head: pushed_branch_sha(branch, worktree_result[:worktree_path]),
893
+ head_repository_url: remote_url(push_remote)
888
894
  )
889
895
  end
890
896
 
897
+ # Exact SHA of the pushed worktree branch, proven locally before any
898
+ # provider call.
899
+ #
900
+ # @param branch [String] worktree branch name
901
+ # @param worktree_path [String, nil] worktree directory to resolve in
902
+ # @return [String] branch head SHA
903
+ def pushed_branch_sha(branch, worktree_path)
904
+ result = if worktree_path && Dir.exist?(worktree_path)
905
+ Dir.chdir(worktree_path) do
906
+ Atoms::GitCommand.execute("rev-parse", "--verify", "#{branch}^{commit}", timeout: 10)
907
+ end
908
+ else
909
+ Atoms::GitCommand.execute("rev-parse", "--verify", "#{branch}^{commit}", timeout: 10)
910
+ end
911
+ return result[:output].to_s.strip if result[:success]
912
+
913
+ raise Ace::Git::Error, "Cannot prove pushed head SHA for branch '#{branch}': #{result[:error]}"
914
+ end
915
+
916
+ # Local URL of a configured git remote (no network).
917
+ #
918
+ # @param remote [String] remote name
919
+ # @return [String, nil] remote URL
920
+ def remote_url(remote)
921
+ result = Atoms::GitCommand.execute("remote", "get-url", remote, timeout: 10)
922
+ result[:success] ? result[:output].to_s.strip : nil
923
+ end
924
+
891
925
  # Resolve PR base branch from start_point
892
926
  #
893
927
  # If start_point is a commit SHA (not a branch name), creates a branch
@@ -948,7 +982,7 @@ module Ace
948
982
  # Add started_at timestamp to task file IN WORKTREE
949
983
  #
950
984
  # This creates an initial commit in the worktree branch, enabling PR creation
951
- # (GitHub requires at least one commit difference between branches for a PR).
985
+ # (Forges require at least one commit difference between branches for a PR).
952
986
  #
953
987
  # @param task_data [Hash] Task data hash from ace-task
954
988
  # @param worktree_result [Hash] Worktree creation result with :worktree_path
@@ -121,25 +121,31 @@ module Ace
121
121
  end
122
122
  end
123
123
 
124
- # Create a worktree for a Pull Request
124
+ # Create a worktree for a Pull Request from normalized evidence.
125
125
  #
126
- # @param pr_number [Integer] PR number
127
- # @param pr_data [Hash] PR data from Ace::Git::Github::PrFetcher
126
+ # @param evidence [Ace::Git::ProviderPullRequest] normalized PR
127
+ # evidence with exact source/base provenance
128
+ # @param checkout [Hash, nil] verified checkout from
129
+ # PullRequestCheckoutPreparer ({local_ref:, remote_tracking:});
130
+ # nil is valid only for dry runs
128
131
  # @param options [Hash] Options for creation
129
132
  # @return [Hash] Creation result
130
133
  #
131
134
  # @example
132
135
  # manager = WorktreeManager.new
133
- # pr_data = { number: 26, title: "Add feature", head_branch: "feature/auth" }
134
- # result = manager.create_pr(26, pr_data)
135
- def create_pr(pr_number, pr_data, options = {})
136
- return error_result("PR number is required") if pr_number.nil?
137
- return error_result("PR data is required") if pr_data.nil?
136
+ # checkout = preparer.prepare(evidence)
137
+ # result = manager.create_pr(evidence, checkout)
138
+ def create_pr(evidence, checkout, options = {})
139
+ return error_result("PR evidence is required") if evidence.nil?
140
+
141
+ pr_number = evidence.number
142
+ pr_data = normalized_pr_data(evidence)
143
+ local_branch = preview_pr_branch(pr_data)
138
144
 
139
145
  # Check if worktree already exists for this PR's branch
140
- head_branch = pr_data[:head_branch]
141
146
  existing = @worktree_lister.find_by_branch("pr-#{pr_number}") ||
142
- @worktree_lister.find_by_branch(head_branch)
147
+ @worktree_lister.find_by_branch(local_branch) ||
148
+ @worktree_lister.find_by_branch(evidence.head_ref)
143
149
 
144
150
  if existing && !options[:force]
145
151
  return error_result("Worktree already exists at: #{existing.path}")
@@ -150,11 +156,15 @@ module Ace
150
156
  return dry_run_pr_creation(pr_number, pr_data, options)
151
157
  end
152
158
 
159
+ return error_result("Verified checkout is required for PR worktree creation") unless checkout
160
+
153
161
  # Create the worktree
154
162
  result = @worktree_creator.create_for_pr(
155
163
  pr_data,
156
164
  @config,
157
- git_root: @project_root
165
+ git_root: @project_root,
166
+ source_ref: checkout[:local_ref],
167
+ remote_tracking: checkout[:remote_tracking]
158
168
  )
159
169
 
160
170
  if result[:success]
@@ -554,6 +564,40 @@ module Ace
554
564
 
555
565
  # Create error result
556
566
  #
567
+ # Convert normalized PR evidence into the internal pr_data naming
568
+ # format used by configuration templates.
569
+ #
570
+ # @param evidence [Ace::Git::ProviderPullRequest]
571
+ # @return [Hash] {number:, title:, head_branch:, base_branch:}
572
+ def normalized_pr_data(evidence)
573
+ {
574
+ number: evidence.number,
575
+ title: evidence.title,
576
+ head_branch: evidence.head_ref,
577
+ base_branch: evidence.base_ref
578
+ }
579
+ end
580
+
581
+ # Preview the configured local branch name for a PR worktree.
582
+ #
583
+ # @param pr_data [Hash] normalized PR data
584
+ # @return [String] formatted branch name
585
+ def preview_pr_branch(pr_data)
586
+ pr_config = @config.pr_config || {}
587
+ branch_format = pr_config[:branch_format] || "pr-{number}-{slug}"
588
+ require_relative "../atoms/slug_generator"
589
+
590
+ result = branch_format.dup
591
+ result.gsub!("{number}", pr_data[:number].to_s)
592
+ if pr_data[:title]
593
+ slug = Atoms::SlugGenerator.from_title(pr_data[:title])
594
+ result.gsub!("{slug}", slug)
595
+ result.gsub!("{title_slug}", slug)
596
+ end
597
+ result.gsub!("{base_branch}", pr_data[:base_branch].to_s) if pr_data[:base_branch]
598
+ result
599
+ end
600
+
557
601
  # Dry run PR worktree creation
558
602
  #
559
603
  # @param pr_number [Integer] PR number
@@ -3,7 +3,7 @@
3
3
  module Ace
4
4
  module Git
5
5
  module Worktree
6
- VERSION = "0.23.0"
6
+ VERSION = "0.24.0"
7
7
  end
8
8
  end
9
9
  end
@@ -190,7 +190,9 @@ module Ace
190
190
 
191
191
  require_relative "worktree/molecules/config_loader"
192
192
  require_relative "worktree/molecules/task_fetcher"
193
- require_relative "worktree/molecules/pr_creator"
193
+ require_relative "worktree/molecules/pull_request_creator"
194
+ require_relative "worktree/molecules/pull_request_evidence_resolver"
195
+ require_relative "worktree/molecules/pull_request_checkout_preparer"
194
196
  require_relative "worktree/molecules/task_committer"
195
197
  require_relative "worktree/molecules/task_pusher"
196
198
  require_relative "worktree/molecules/task_status_updater"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ace-git-worktree
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.23.0
4
+ version: 0.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-09-27 00:00:00.000000000 Z
10
+ date: 2026-09-29 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ace-support-cli
@@ -65,20 +65,6 @@ dependencies:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0.24'
68
- - !ruby/object:Gem::Dependency
69
- name: ace-git-github
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - "~>"
73
- - !ruby/object:Gem::Version
74
- version: 0.1.1
75
- type: :runtime
76
- prerelease: false
77
- version_requirements: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - "~>"
80
- - !ruby/object:Gem::Version
81
- version: 0.1.1
82
68
  - !ruby/object:Gem::Dependency
83
69
  name: ace-task
84
70
  requirement: !ruby/object:Gem::Requirement
@@ -205,7 +191,9 @@ files:
205
191
  - lib/ace/git/worktree/molecules/current_task_linker.rb
206
192
  - lib/ace/git/worktree/molecules/hook_executor.rb
207
193
  - lib/ace/git/worktree/molecules/parent_task_resolver.rb
208
- - lib/ace/git/worktree/molecules/pr_creator.rb
194
+ - lib/ace/git/worktree/molecules/pull_request_checkout_preparer.rb
195
+ - lib/ace/git/worktree/molecules/pull_request_creator.rb
196
+ - lib/ace/git/worktree/molecules/pull_request_evidence_resolver.rb
209
197
  - lib/ace/git/worktree/molecules/task_committer.rb
210
198
  - lib/ace/git/worktree/molecules/task_fetcher.rb
211
199
  - lib/ace/git/worktree/molecules/task_pusher.rb
@@ -1,253 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "open3"
5
-
6
- module Ace
7
- module Git
8
- module Worktree
9
- module Molecules
10
- # PR creator molecule
11
- #
12
- # Creates draft pull requests on GitHub using the gh CLI.
13
- # Provides a simple interface for creating PRs with graceful degradation
14
- # when gh CLI is unavailable or not authenticated.
15
- #
16
- # @example Create a draft PR
17
- # creator = PrCreator.new
18
- # result = creator.create_draft(
19
- # branch: "125-upstream-setup",
20
- # base: "main",
21
- # title: "125 - upstream-setup-and-pr-creation"
22
- # )
23
- # result[:pr_number] # => 456
24
- # result[:pr_url] # => "https://github.com/owner/repo/pull/456"
25
- #
26
- # @example Handle unavailable gh CLI
27
- # creator = PrCreator.new
28
- # unless creator.gh_available?
29
- # puts "gh CLI not available"
30
- # end
31
- class PrCreator
32
- # Error raised when gh CLI is not available
33
- class GhNotAvailableError < StandardError; end
34
-
35
- # Error raised when gh CLI is not authenticated
36
- class GhNotAuthenticatedError < StandardError; end
37
-
38
- # Error raised on network/timeout issues
39
- class NetworkError < StandardError; end
40
-
41
- # Error raised when PR already exists
42
- class PrAlreadyExistsError < StandardError; end
43
-
44
- # Initialize a new PrCreator
45
- #
46
- # @param timeout [Integer] Timeout in seconds (default: 30)
47
- def initialize(timeout: 30)
48
- @timeout = timeout
49
- end
50
-
51
- # Create a draft pull request
52
- #
53
- # @param branch [String] Head branch for the PR
54
- # @param base [String] Base branch to merge into
55
- # @param title [String] PR title
56
- # @param body [String, nil] PR body/description
57
- # @return [Hash] Result hash with :success, :pr_number, :pr_url, :error
58
- #
59
- # @example
60
- # result = creator.create_draft(branch: "feature", base: "main", title: "Add feature")
61
- # result[:success] # => true
62
- # result[:pr_number] # => 123
63
- # result[:pr_url] # => "https://github.com/owner/repo/pull/123"
64
- def create_draft(branch:, base:, title:, body: nil)
65
- # Check gh availability and authentication
66
- unless gh_available?
67
- return error_result("gh CLI is not installed")
68
- end
69
-
70
- unless gh_authenticated?
71
- return error_result("gh CLI is not authenticated. Run: gh auth login")
72
- end
73
-
74
- # Check if PR already exists for this branch
75
- existing_pr = find_existing_pr(branch: branch)
76
- if existing_pr
77
- return {
78
- success: true,
79
- pr_number: existing_pr[:number],
80
- pr_url: existing_pr[:url],
81
- existing: true,
82
- message: "PR already exists for branch"
83
- }
84
- end
85
-
86
- # Build gh command
87
- cmd = ["gh", "pr", "create", "--draft", "--head", branch, "--base", base, "--title", title]
88
- cmd += ["--body", body || title]
89
-
90
- # Execute command
91
- stdout, stderr, status = execute_with_timeout(cmd, @timeout)
92
-
93
- if status.success?
94
- # Parse PR URL from output
95
- pr_url = stdout.strip
96
- pr_number = extract_pr_number(pr_url)
97
-
98
- {
99
- success: true,
100
- pr_number: pr_number,
101
- pr_url: pr_url,
102
- existing: false,
103
- error: nil
104
- }
105
- else
106
- handle_creation_error(stderr)
107
- end
108
- rescue => e
109
- error_result("Unexpected error: #{e.message}")
110
- end
111
-
112
- # Check if gh CLI is available (cached)
113
- #
114
- # @return [Boolean] true if gh is installed and accessible
115
- def gh_available?
116
- return @gh_available unless @gh_available.nil?
117
-
118
- @gh_available = system("which gh > /dev/null 2>&1")
119
- end
120
-
121
- # Check if gh CLI is authenticated (cached)
122
- #
123
- # @return [Boolean] true if gh is authenticated
124
- def gh_authenticated?
125
- return @gh_authenticated unless @gh_authenticated.nil?
126
-
127
- _, _stderr, status = Open3.capture3("gh", "auth", "status")
128
- @gh_authenticated = status.success?
129
- rescue
130
- @gh_authenticated = false
131
- end
132
-
133
- # Find an existing PR for a branch
134
- #
135
- # @param branch [String] Branch name to search for
136
- # @return [Hash, nil] PR info hash or nil if not found
137
- #
138
- # @example
139
- # pr = creator.find_existing_pr(branch: "feature-branch")
140
- # pr[:number] # => 123
141
- # pr[:url] # => "https://github.com/owner/repo/pull/123"
142
- def find_existing_pr(branch:)
143
- return nil unless gh_available?
144
-
145
- # Search for open PRs with this head branch
146
- cmd = [
147
- "gh", "pr", "list",
148
- "--head", branch,
149
- "--state", "open",
150
- "--json", "number,url",
151
- "--limit", "1"
152
- ]
153
-
154
- stdout, _stderr, status = execute_with_timeout(cmd, @timeout)
155
- return nil unless status.success?
156
-
157
- prs = JSON.parse(stdout)
158
- return nil if prs.empty?
159
-
160
- pr = prs.first
161
- {
162
- number: pr["number"],
163
- url: pr["url"]
164
- }
165
- rescue JSON::ParserError
166
- nil
167
- rescue
168
- nil
169
- end
170
-
171
- # Get helpful error message when gh CLI is unavailable
172
- #
173
- # @return [String] User-friendly error message with installation guidance
174
- def gh_not_available_message
175
- <<~MESSAGE
176
- gh CLI is required for PR creation but is not installed.
177
-
178
- Install gh CLI:
179
- - macOS: brew install gh
180
- - Linux: See https://github.com/cli/cli#installation
181
- - Windows: See https://github.com/cli/cli#installation
182
-
183
- After installation, authenticate with: gh auth login
184
- MESSAGE
185
- end
186
-
187
- private
188
-
189
- # Execute command with timeout
190
- #
191
- # @param cmd [Array<String>] Command and arguments
192
- # @param timeout [Integer] Timeout in seconds
193
- # @return [Array<String, String, Process::Status>] stdout, stderr, status
194
- def execute_with_timeout(cmd, timeout)
195
- require "timeout"
196
-
197
- Timeout.timeout(timeout) do
198
- Open3.capture3(*cmd)
199
- end
200
- rescue Timeout::Error
201
- raise NetworkError, "Request timed out after #{timeout} seconds"
202
- end
203
-
204
- # Extract PR number from URL
205
- #
206
- # @param url [String] PR URL
207
- # @return [Integer, nil] PR number or nil if not parseable
208
- def extract_pr_number(url)
209
- return nil unless url
210
-
211
- match = url.match(%r{/pull/(\d+)})
212
- match ? match[1].to_i : nil
213
- end
214
-
215
- # Handle PR creation error
216
- #
217
- # @param stderr [String] Error output from gh
218
- # @return [Hash] Error result hash
219
- def handle_creation_error(stderr)
220
- error_msg = stderr.downcase
221
-
222
- if error_msg.include?("already exists") || error_msg.include?("pull request already exists")
223
- error_result("A PR already exists for this branch")
224
- elsif error_msg.include?("authentication") || error_msg.include?("not logged in")
225
- error_result("GitHub CLI not authenticated. Run: gh auth login")
226
- elsif error_msg.include?("network") || error_msg.include?("connection")
227
- error_result("Network error. Check your connection and try again.")
228
- elsif error_msg.include?("repository not found") || error_msg.include?("not a git repository")
229
- error_result("Not in a git repository or repository not found on GitHub")
230
- elsif error_msg.include?("branch") && error_msg.include?("not found")
231
- error_result("Branch not found on remote. Push the branch first.")
232
- else
233
- error_result("GitHub CLI error: #{stderr.strip}")
234
- end
235
- end
236
-
237
- # Create an error result hash
238
- #
239
- # @param message [String] Error message
240
- # @return [Hash] Error result
241
- def error_result(message)
242
- {
243
- success: false,
244
- pr_number: nil,
245
- pr_url: nil,
246
- error: message
247
- }
248
- end
249
- end
250
- end
251
- end
252
- end
253
- end