ace-git-worktree 0.23.0 → 0.24.1

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.
@@ -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