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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +58 -0
- data/docs/usage.md +62 -10
- data/lib/ace/git/worktree/commands/cleanup_command.rb +49 -25
- data/lib/ace/git/worktree/commands/create_command.rb +67 -154
- data/lib/ace/git/worktree/molecules/cleanup_pr_resolver.rb +193 -132
- data/lib/ace/git/worktree/molecules/cleanup_reporter.rb +49 -11
- data/lib/ace/git/worktree/molecules/pull_request_checkout_preparer.rb +100 -0
- data/lib/ace/git/worktree/molecules/pull_request_creator.rb +67 -0
- data/lib/ace/git/worktree/molecules/pull_request_evidence_resolver.rb +40 -0
- data/lib/ace/git/worktree/molecules/worktree_creator.rb +52 -20
- data/lib/ace/git/worktree/organisms/task_worktree_orchestrator.rb +43 -9
- data/lib/ace/git/worktree/organisms/worktree_manager.rb +55 -11
- data/lib/ace/git/worktree/version.rb +1 -1
- data/lib/ace/git/worktree.rb +3 -1
- metadata +5 -17
- data/lib/ace/git/worktree/molecules/pr_creator.rb +0 -253
|
@@ -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
|