ace-git-worktree 0.19.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 +7 -0
- data/.ace-defaults/git/worktree.yml +250 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-git-worktree.yml +19 -0
- data/CHANGELOG.md +957 -0
- data/LICENSE +21 -0
- data/README.md +40 -0
- data/Rakefile +14 -0
- data/docs/demo/ace-git-worktree-getting-started.gif +0 -0
- data/docs/demo/ace-git-worktree-getting-started.tape.yml +28 -0
- data/docs/demo/fixtures/README.md +3 -0
- data/docs/demo/fixtures/sample.txt +1 -0
- data/docs/getting-started.md +114 -0
- data/docs/handbook.md +38 -0
- data/docs/usage.md +334 -0
- data/exe/ace-git-worktree +24 -0
- data/handbook/agents/worktree.ag.md +189 -0
- data/handbook/skills/as-git-worktree/SKILL.md +27 -0
- data/handbook/skills/as-git-worktree-create/SKILL.md +21 -0
- data/handbook/skills/as-git-worktree-manage/SKILL.md +20 -0
- data/handbook/workflow-instructions/git/worktree-create.wf.md +262 -0
- data/handbook/workflow-instructions/git/worktree-manage.wf.md +384 -0
- data/handbook/workflow-instructions/git/worktree.wf.md +224 -0
- data/lib/ace/git/worktree/atoms/git_command.rb +121 -0
- data/lib/ace/git/worktree/atoms/path_expander.rb +189 -0
- data/lib/ace/git/worktree/atoms/slug_generator.rb +235 -0
- data/lib/ace/git/worktree/atoms/task_id_extractor.rb +91 -0
- data/lib/ace/git/worktree/cli/commands/config.rb +50 -0
- data/lib/ace/git/worktree/cli/commands/create.rb +80 -0
- data/lib/ace/git/worktree/cli/commands/list.rb +76 -0
- data/lib/ace/git/worktree/cli/commands/prune.rb +43 -0
- data/lib/ace/git/worktree/cli/commands/remove.rb +48 -0
- data/lib/ace/git/worktree/cli/commands/shared_helpers.rb +66 -0
- data/lib/ace/git/worktree/cli/commands/switch.rb +44 -0
- data/lib/ace/git/worktree/cli.rb +103 -0
- data/lib/ace/git/worktree/commands/config_command.rb +351 -0
- data/lib/ace/git/worktree/commands/create_command.rb +961 -0
- data/lib/ace/git/worktree/commands/list_command.rb +247 -0
- data/lib/ace/git/worktree/commands/prune_command.rb +260 -0
- data/lib/ace/git/worktree/commands/remove_command.rb +522 -0
- data/lib/ace/git/worktree/commands/switch_command.rb +249 -0
- data/lib/ace/git/worktree/configuration.rb +167 -0
- data/lib/ace/git/worktree/models/worktree_config.rb +502 -0
- data/lib/ace/git/worktree/models/worktree_info.rb +303 -0
- data/lib/ace/git/worktree/models/worktree_metadata.rb +294 -0
- data/lib/ace/git/worktree/molecules/config_loader.rb +125 -0
- data/lib/ace/git/worktree/molecules/current_task_linker.rb +136 -0
- data/lib/ace/git/worktree/molecules/hook_executor.rb +361 -0
- data/lib/ace/git/worktree/molecules/parent_task_resolver.rb +186 -0
- data/lib/ace/git/worktree/molecules/pr_creator.rb +253 -0
- data/lib/ace/git/worktree/molecules/task_committer.rb +329 -0
- data/lib/ace/git/worktree/molecules/task_fetcher.rb +244 -0
- data/lib/ace/git/worktree/molecules/task_pusher.rb +183 -0
- data/lib/ace/git/worktree/molecules/task_status_updater.rb +447 -0
- data/lib/ace/git/worktree/molecules/worktree_creator.rb +832 -0
- data/lib/ace/git/worktree/molecules/worktree_lister.rb +337 -0
- data/lib/ace/git/worktree/molecules/worktree_remover.rb +416 -0
- data/lib/ace/git/worktree/organisms/task_worktree_orchestrator.rb +906 -0
- data/lib/ace/git/worktree/organisms/worktree_manager.rb +714 -0
- data/lib/ace/git/worktree/version.rb +9 -0
- data/lib/ace/git/worktree.rb +215 -0
- metadata +218 -0
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../atoms/task_id_extractor"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Git
|
|
7
|
+
module Worktree
|
|
8
|
+
module Molecules
|
|
9
|
+
# Worktree creator molecule
|
|
10
|
+
#
|
|
11
|
+
# Creates git worktrees with proper validation, naming, and error handling.
|
|
12
|
+
# Integrates with git commands and provides task-aware creation capabilities.
|
|
13
|
+
#
|
|
14
|
+
# @example Create a task-aware worktree
|
|
15
|
+
# creator = WorktreeCreator.new
|
|
16
|
+
# task_data = fetch_task_data("081")
|
|
17
|
+
# config = WorktreeConfig.new
|
|
18
|
+
# result = creator.create_for_task(task_data, config)
|
|
19
|
+
#
|
|
20
|
+
# @example Create a traditional worktree
|
|
21
|
+
# result = creator.create_traditional("feature-branch", "/path/to/worktree")
|
|
22
|
+
class WorktreeCreator
|
|
23
|
+
# Default timeout for git commands
|
|
24
|
+
DEFAULT_TIMEOUT = 60
|
|
25
|
+
|
|
26
|
+
# Initialize a new WorktreeCreator
|
|
27
|
+
#
|
|
28
|
+
# @param config [WorktreeConfig, nil] Worktree configuration
|
|
29
|
+
# @param timeout [Integer] Command timeout in seconds
|
|
30
|
+
def initialize(config: nil, timeout: DEFAULT_TIMEOUT)
|
|
31
|
+
@config = config
|
|
32
|
+
@timeout = timeout
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Create a worktree for a specific task
|
|
36
|
+
#
|
|
37
|
+
# @param task_data [Hash] Task data hash from ace-task
|
|
38
|
+
# @param config [WorktreeConfig] Worktree configuration
|
|
39
|
+
# @param counter [Integer, nil] Counter for multiple worktrees of same task
|
|
40
|
+
# @param git_root [String, nil] Git repository root (auto-detected if nil)
|
|
41
|
+
# @param source [String, nil] Git ref to use as start-point for the new branch
|
|
42
|
+
# If nil, uses current branch (default behavior - fixes the branch source bug)
|
|
43
|
+
# @param target_branch [String, nil] PR target branch (for subtasks)
|
|
44
|
+
# @return [Hash] Result with :success, :worktree_path, :branch, :error
|
|
45
|
+
#
|
|
46
|
+
# @example
|
|
47
|
+
# creator = WorktreeCreator.new
|
|
48
|
+
# task_data = fetch_task_data("081")
|
|
49
|
+
# config = ConfigLoader.new.load
|
|
50
|
+
# result = creator.create_for_task(task_data, config)
|
|
51
|
+
# # => { success: true, worktree_path: "/project/.ace-wt/task.081", branch: "081-fix-auth", error: nil }
|
|
52
|
+
#
|
|
53
|
+
# @example With explicit source
|
|
54
|
+
# result = creator.create_for_task(task_data, config, source: "main")
|
|
55
|
+
# # => Creates branch based on 'main' instead of current branch
|
|
56
|
+
#
|
|
57
|
+
# @example Subtask with target branch
|
|
58
|
+
# result = creator.create_for_task(subtask_data, config, target_branch: "202-orchestrator")
|
|
59
|
+
# # => { success: true, target_branch: "202-orchestrator", ... }
|
|
60
|
+
def create_for_task(task_data, config, counter: nil, git_root: nil, source: nil, target_branch: nil)
|
|
61
|
+
return error_result("Task data is required") unless task_data
|
|
62
|
+
return error_result("Configuration is required") unless config
|
|
63
|
+
|
|
64
|
+
begin
|
|
65
|
+
# Determine git repository root
|
|
66
|
+
git_root ||= detect_git_root
|
|
67
|
+
return error_result("Not in a git repository") unless git_root
|
|
68
|
+
|
|
69
|
+
# Generate names based on configuration
|
|
70
|
+
directory_name = config.format_directory(task_data, counter)
|
|
71
|
+
branch_name = config.format_branch(task_data)
|
|
72
|
+
|
|
73
|
+
# Build full path
|
|
74
|
+
worktree_path = File.join(config.absolute_root_path, directory_name)
|
|
75
|
+
|
|
76
|
+
# Ensure parent directory exists before validation
|
|
77
|
+
# (PathExpander rejects paths whose parent doesn't exist yet)
|
|
78
|
+
parent_dir = File.dirname(worktree_path)
|
|
79
|
+
FileUtils.mkdir_p(parent_dir) unless File.exist?(parent_dir)
|
|
80
|
+
|
|
81
|
+
# Validate worktree path
|
|
82
|
+
validation = validate_worktree_path(worktree_path, git_root)
|
|
83
|
+
return error_result(validation[:error]) unless validation[:valid]
|
|
84
|
+
|
|
85
|
+
# Create the worktree with source as start-point
|
|
86
|
+
result = create_worktree(worktree_path, branch_name, git_root, start_point: source)
|
|
87
|
+
return result unless result[:success]
|
|
88
|
+
|
|
89
|
+
# Success - return worktree information
|
|
90
|
+
{
|
|
91
|
+
success: true,
|
|
92
|
+
worktree_path: worktree_path,
|
|
93
|
+
branch: branch_name,
|
|
94
|
+
start_point: result[:start_point],
|
|
95
|
+
directory_name: directory_name,
|
|
96
|
+
task_id: extract_task_id_from_data(task_data),
|
|
97
|
+
git_root: git_root,
|
|
98
|
+
target_branch: target_branch,
|
|
99
|
+
error: nil
|
|
100
|
+
}
|
|
101
|
+
rescue => e
|
|
102
|
+
error_result("Unexpected error: #{e.message}")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Create a worktree for a Pull Request
|
|
107
|
+
#
|
|
108
|
+
# @param pr_data [Hash] PR data hash from PrFetcher
|
|
109
|
+
# @param config [WorktreeConfig] Worktree configuration
|
|
110
|
+
# @param git_root [String, nil] Git repository root (auto-detected if nil)
|
|
111
|
+
# @return [Hash] Result with :success, :worktree_path, :branch, :error
|
|
112
|
+
#
|
|
113
|
+
# @example
|
|
114
|
+
# 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)
|
|
118
|
+
return error_result("PR data is required") unless pr_data
|
|
119
|
+
return error_result("Configuration is required") unless config
|
|
120
|
+
|
|
121
|
+
begin
|
|
122
|
+
# Determine git repository root
|
|
123
|
+
git_root ||= detect_git_root
|
|
124
|
+
return error_result("Not in a git repository") unless git_root
|
|
125
|
+
|
|
126
|
+
# Get PR-specific configuration (fallback to defaults)
|
|
127
|
+
pr_config = config.pr_config || {}
|
|
128
|
+
remote_name = pr_config[:remote_name] || "origin"
|
|
129
|
+
directory_format = pr_config[:directory_format] || "ace-pr-{number}"
|
|
130
|
+
branch_format = pr_config[:branch_format] || "pr-{number}-{slug}"
|
|
131
|
+
|
|
132
|
+
# Format directory and branch names
|
|
133
|
+
directory_name = format_pr_name(directory_format, pr_data)
|
|
134
|
+
local_branch_name = format_pr_name(branch_format, pr_data)
|
|
135
|
+
|
|
136
|
+
# Build full path
|
|
137
|
+
worktree_path = File.join(config.absolute_root_path, directory_name)
|
|
138
|
+
|
|
139
|
+
# Validate worktree path
|
|
140
|
+
validation = validate_worktree_path(worktree_path, git_root)
|
|
141
|
+
return error_result(validation[:error]) unless validation[:valid]
|
|
142
|
+
|
|
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
|
+
)
|
|
156
|
+
return result unless result[:success]
|
|
157
|
+
|
|
158
|
+
# Success - return worktree information
|
|
159
|
+
{
|
|
160
|
+
success: true,
|
|
161
|
+
worktree_path: worktree_path,
|
|
162
|
+
branch: local_branch_name,
|
|
163
|
+
tracking: "#{remote_name}/#{head_branch}",
|
|
164
|
+
directory_name: directory_name,
|
|
165
|
+
pr_number: pr_data[:number],
|
|
166
|
+
pr_title: pr_data[:title],
|
|
167
|
+
git_root: git_root,
|
|
168
|
+
error: nil
|
|
169
|
+
}
|
|
170
|
+
rescue => e
|
|
171
|
+
error_result("Unexpected error: #{e.message}")
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Create a worktree for a specific branch (local or remote)
|
|
176
|
+
#
|
|
177
|
+
# @param branch_name [String] Branch name (e.g., "feature" or "origin/feature")
|
|
178
|
+
# @param config [WorktreeConfig] Worktree configuration
|
|
179
|
+
# @param git_root [String, nil] Git repository root (auto-detected if nil)
|
|
180
|
+
# @return [Hash] Result with :success, :worktree_path, :branch, :error
|
|
181
|
+
#
|
|
182
|
+
# @example Remote branch
|
|
183
|
+
# result = creator.create_for_branch("origin/feature/auth", config)
|
|
184
|
+
# # => { success: true, worktree_path: "/project/.ace-wt/feature-auth", branch: "feature/auth", tracking: "origin/feature/auth" }
|
|
185
|
+
#
|
|
186
|
+
# @example Local branch
|
|
187
|
+
# result = creator.create_for_branch("local-feature", config)
|
|
188
|
+
# # => { success: true, worktree_path: "/project/.ace-wt/local-feature", branch: "local-feature", tracking: nil }
|
|
189
|
+
def create_for_branch(branch_name, config, git_root: nil)
|
|
190
|
+
return error_result("Branch name is required") if branch_name.nil? || branch_name.empty?
|
|
191
|
+
return error_result("Configuration is required") unless config
|
|
192
|
+
|
|
193
|
+
begin
|
|
194
|
+
# Determine git repository root
|
|
195
|
+
git_root ||= detect_git_root
|
|
196
|
+
return error_result("Not in a git repository") unless git_root
|
|
197
|
+
|
|
198
|
+
# Detect if this is a remote branch
|
|
199
|
+
remote_info = detect_remote_branch(branch_name)
|
|
200
|
+
|
|
201
|
+
if remote_info
|
|
202
|
+
# Remote branch - create with tracking
|
|
203
|
+
create_for_remote_branch(branch_name, remote_info, config, git_root)
|
|
204
|
+
else
|
|
205
|
+
# Local branch - create without tracking
|
|
206
|
+
create_for_local_branch(branch_name, config, git_root)
|
|
207
|
+
end
|
|
208
|
+
rescue => e
|
|
209
|
+
error_result("Unexpected error: #{e.message}")
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Create a traditional worktree (not task-aware)
|
|
214
|
+
#
|
|
215
|
+
# @param branch_name [String] Branch name
|
|
216
|
+
# @param worktree_path [String, nil] Worktree path (auto-generated if nil)
|
|
217
|
+
# @param git_root [String, nil] Git repository root (auto-detected if nil)
|
|
218
|
+
# @param source [String, nil] Git ref to use as start-point for the new branch
|
|
219
|
+
# If nil, uses current branch (default behavior)
|
|
220
|
+
# @return [Hash] Result with :success, :worktree_path, :branch, :error
|
|
221
|
+
#
|
|
222
|
+
# @example
|
|
223
|
+
# result = creator.create_traditional("feature-branch", "/tmp/worktree")
|
|
224
|
+
#
|
|
225
|
+
# @example With explicit source
|
|
226
|
+
# result = creator.create_traditional("feature-branch", nil, source: "main")
|
|
227
|
+
def create_traditional(branch_name, worktree_path = nil, git_root: nil, source: nil)
|
|
228
|
+
return error_result("Branch name is required") if branch_name.nil? || branch_name.empty?
|
|
229
|
+
|
|
230
|
+
begin
|
|
231
|
+
# Determine git repository root
|
|
232
|
+
git_root ||= detect_git_root
|
|
233
|
+
return error_result("Not in a git repository") unless git_root
|
|
234
|
+
|
|
235
|
+
# Auto-generate worktree path if not provided
|
|
236
|
+
if worktree_path.nil?
|
|
237
|
+
worktree_path = generate_default_worktree_path(branch_name, git_root)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Validate worktree path
|
|
241
|
+
validation = validate_worktree_path(worktree_path, git_root)
|
|
242
|
+
return error_result(validation[:error]) unless validation[:valid]
|
|
243
|
+
|
|
244
|
+
# Validate branch name
|
|
245
|
+
return error_result("Invalid branch name") unless valid_branch_name?(branch_name)
|
|
246
|
+
|
|
247
|
+
# Check if branch already exists (locally or remotely)
|
|
248
|
+
if branch_exists?(branch_name)
|
|
249
|
+
# Branch exists - create worktree for existing branch
|
|
250
|
+
create_worktree_for_existing_branch(worktree_path, branch_name, git_root)
|
|
251
|
+
else
|
|
252
|
+
# Branch doesn't exist - create new branch with worktree
|
|
253
|
+
create_worktree(worktree_path, branch_name, git_root, start_point: source)
|
|
254
|
+
end
|
|
255
|
+
rescue => e
|
|
256
|
+
error_result("Unexpected error: #{e.message}")
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Check if a worktree already exists for the given criteria
|
|
261
|
+
#
|
|
262
|
+
# @param task_data [Hash, nil] Task data hash from ace-task
|
|
263
|
+
# @param branch_name [String, nil] Branch name
|
|
264
|
+
# @param worktree_path [String, nil] Worktree path
|
|
265
|
+
# @return [WorktreeInfo, nil] Existing worktree info or nil
|
|
266
|
+
#
|
|
267
|
+
# @example
|
|
268
|
+
# existing = creator.worktree_exists_for_task?(task_data)
|
|
269
|
+
# existing = creator.worktree_exists_for_branch?("feature-branch")
|
|
270
|
+
def worktree_exists?(task_data: nil, branch_name: nil, worktree_path: nil)
|
|
271
|
+
require_relative "worktree_lister"
|
|
272
|
+
lister = WorktreeLister.new
|
|
273
|
+
worktrees = lister.list_all
|
|
274
|
+
|
|
275
|
+
# Check by task ID
|
|
276
|
+
if task_data
|
|
277
|
+
task_id = extract_task_id_from_data(task_data)
|
|
278
|
+
existing = Models::WorktreeInfo.find_by_task_id(worktrees, task_id)
|
|
279
|
+
return existing if existing
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Check by branch name
|
|
283
|
+
if branch_name
|
|
284
|
+
existing = Models::WorktreeInfo.find_by_branch(worktrees, branch_name)
|
|
285
|
+
return existing if existing
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Check by path
|
|
289
|
+
if worktree_path
|
|
290
|
+
expanded_path = File.expand_path(worktree_path)
|
|
291
|
+
existing = worktrees.find { |wt| File.expand_path(wt.path) == expanded_path }
|
|
292
|
+
return existing if existing
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
nil
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Generate a unique worktree path for a task (handles conflicts)
|
|
299
|
+
#
|
|
300
|
+
# @param task_data [Hash] Task data hash from ace-task
|
|
301
|
+
# @param config [WorktreeConfig] Worktree configuration
|
|
302
|
+
# @param git_root [String] Git repository root
|
|
303
|
+
# @return [String] Unique worktree path
|
|
304
|
+
#
|
|
305
|
+
# @example
|
|
306
|
+
# path = creator.generate_unique_path(task_data, config, git_root)
|
|
307
|
+
# # => "/project/.ace-wt/task.081-2"
|
|
308
|
+
def generate_unique_path(task_data, config, git_root)
|
|
309
|
+
counter = 1
|
|
310
|
+
loop do
|
|
311
|
+
directory_name = config.format_directory(task_data, (counter > 1) ? counter : nil)
|
|
312
|
+
worktree_path = File.join(config.absolute_root_path, directory_name)
|
|
313
|
+
|
|
314
|
+
# Check if path already exists
|
|
315
|
+
existing = worktree_exists?(worktree_path: worktree_path)
|
|
316
|
+
break worktree_path unless existing
|
|
317
|
+
|
|
318
|
+
counter += 1
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Validate a worktree path for creation
|
|
323
|
+
#
|
|
324
|
+
# @param worktree_path [String] Path to validate
|
|
325
|
+
# @param git_root [String] Git repository root
|
|
326
|
+
# @return [Hash] Validation result with :valid, :error, :expanded_path
|
|
327
|
+
#
|
|
328
|
+
# @example
|
|
329
|
+
# validation = creator.validate_worktree_path("/tmp/worktree", "/project")
|
|
330
|
+
# # => { valid: true, error: nil, expanded_path: "/tmp/worktree" }
|
|
331
|
+
def validate_worktree_path(worktree_path, git_root)
|
|
332
|
+
require_relative "../atoms/path_expander"
|
|
333
|
+
Atoms::PathExpander.validate_for_worktree(worktree_path, git_root)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
private
|
|
337
|
+
|
|
338
|
+
# Create a worktree using git commands
|
|
339
|
+
#
|
|
340
|
+
# @param worktree_path [String] Path for the worktree
|
|
341
|
+
# @param branch_name [String] Branch name
|
|
342
|
+
# @param git_root [String] Git repository root
|
|
343
|
+
# @param start_point [String, nil] Git ref to use as start-point for the new branch
|
|
344
|
+
# If nil, uses current branch (or commit SHA if in detached HEAD state)
|
|
345
|
+
# @return [Hash] Result with :success, :worktree_path, :branch, :error
|
|
346
|
+
def create_worktree(worktree_path, branch_name, git_root, start_point: nil)
|
|
347
|
+
require_relative "../atoms/git_command"
|
|
348
|
+
|
|
349
|
+
# Ensure parent directory exists
|
|
350
|
+
parent_dir = File.dirname(worktree_path)
|
|
351
|
+
FileUtils.mkdir_p(parent_dir) unless File.exist?(parent_dir)
|
|
352
|
+
|
|
353
|
+
# Default to current branch if no start_point provided
|
|
354
|
+
# This ensures new branches are based on the current branch, not main worktree HEAD
|
|
355
|
+
start_point ||= Atoms::GitCommand.current_branch
|
|
356
|
+
return error_result("Cannot determine current branch for start-point") unless start_point
|
|
357
|
+
|
|
358
|
+
# Validate start_point exists
|
|
359
|
+
unless Atoms::GitCommand.ref_exists?(start_point)
|
|
360
|
+
return error_result("Source ref '#{start_point}' does not exist")
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Create the worktree with explicit start-point
|
|
364
|
+
result = Atoms::GitCommand.worktree("add", worktree_path, "-b", branch_name, start_point, timeout: @timeout)
|
|
365
|
+
|
|
366
|
+
if result[:success]
|
|
367
|
+
{
|
|
368
|
+
success: true,
|
|
369
|
+
worktree_path: worktree_path,
|
|
370
|
+
branch: branch_name,
|
|
371
|
+
start_point: start_point,
|
|
372
|
+
git_root: git_root,
|
|
373
|
+
error: nil
|
|
374
|
+
}
|
|
375
|
+
else
|
|
376
|
+
error_result("Failed to create worktree: #{result[:error]}")
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Detect the git repository root
|
|
381
|
+
#
|
|
382
|
+
# @return [String, nil] Git repository root or nil
|
|
383
|
+
def detect_git_root
|
|
384
|
+
require_relative "../atoms/git_command"
|
|
385
|
+
Atoms::GitCommand.git_root
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Check if a branch exists locally or as a remote-tracking branch
|
|
389
|
+
#
|
|
390
|
+
# Checks local and remote refs separately since git show-ref --verify
|
|
391
|
+
# requires ALL refs to exist when given multiple refs. This ensures we
|
|
392
|
+
# correctly detect local-only branches (which have no remote tracking ref).
|
|
393
|
+
#
|
|
394
|
+
# @param branch_name [String] Branch name to check
|
|
395
|
+
# @return [Boolean] true if branch exists locally or as origin remote-tracking ref
|
|
396
|
+
def branch_exists?(branch_name)
|
|
397
|
+
require_relative "../atoms/git_command"
|
|
398
|
+
|
|
399
|
+
# Check local branch first (short-circuit if found)
|
|
400
|
+
local_result = Atoms::GitCommand.execute(
|
|
401
|
+
"show-ref", "--verify", "--quiet",
|
|
402
|
+
"refs/heads/#{branch_name}",
|
|
403
|
+
timeout: 5
|
|
404
|
+
)
|
|
405
|
+
return true if local_result[:success]
|
|
406
|
+
|
|
407
|
+
# Check remote tracking branch
|
|
408
|
+
remote_result = Atoms::GitCommand.execute(
|
|
409
|
+
"show-ref", "--verify", "--quiet",
|
|
410
|
+
"refs/remotes/origin/#{branch_name}",
|
|
411
|
+
timeout: 5
|
|
412
|
+
)
|
|
413
|
+
remote_result[:success]
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Create a worktree for an existing branch
|
|
417
|
+
#
|
|
418
|
+
# @param worktree_path [String] Path for the worktree
|
|
419
|
+
# @param branch_name [String] Existing branch name
|
|
420
|
+
# @param git_root [String] Git repository root
|
|
421
|
+
# @return [Hash] Result with :success, :worktree_path, :branch, :error
|
|
422
|
+
def create_worktree_for_existing_branch(worktree_path, branch_name, git_root)
|
|
423
|
+
require_relative "../atoms/git_command"
|
|
424
|
+
|
|
425
|
+
# Ensure parent directory exists
|
|
426
|
+
parent_dir = File.dirname(worktree_path)
|
|
427
|
+
FileUtils.mkdir_p(parent_dir) unless File.exist?(parent_dir)
|
|
428
|
+
|
|
429
|
+
# Create the worktree without -b flag (uses existing branch)
|
|
430
|
+
result = Atoms::GitCommand.worktree("add", worktree_path, branch_name, timeout: @timeout)
|
|
431
|
+
|
|
432
|
+
if result[:success]
|
|
433
|
+
{
|
|
434
|
+
success: true,
|
|
435
|
+
worktree_path: worktree_path,
|
|
436
|
+
branch: branch_name,
|
|
437
|
+
start_point: nil,
|
|
438
|
+
git_root: git_root,
|
|
439
|
+
error: nil
|
|
440
|
+
}
|
|
441
|
+
else
|
|
442
|
+
error_result("Failed to create worktree: #{result[:error]}")
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Generate a default worktree path based on branch name
|
|
447
|
+
#
|
|
448
|
+
# @param branch_name [String] Branch name
|
|
449
|
+
# @param git_root [String] Git repository root
|
|
450
|
+
# @return [String] Generated worktree path
|
|
451
|
+
def generate_default_worktree_path(branch_name, git_root)
|
|
452
|
+
require_relative "../atoms/slug_generator"
|
|
453
|
+
# Sanitize branch name for directory use
|
|
454
|
+
sanitized_branch = Atoms::SlugGenerator.to_directory_name(branch_name)
|
|
455
|
+
|
|
456
|
+
# Use config's root_path if available, otherwise default to .ace-wt
|
|
457
|
+
if @config
|
|
458
|
+
File.join(@config.absolute_root_path, sanitized_branch)
|
|
459
|
+
else
|
|
460
|
+
File.join(git_root, ".ace-wt", sanitized_branch)
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# Validate if a branch name is valid for git
|
|
465
|
+
#
|
|
466
|
+
# @param branch_name [String] Branch name to validate
|
|
467
|
+
# @return [Boolean] true if valid
|
|
468
|
+
def valid_branch_name?(branch_name)
|
|
469
|
+
return false if branch_name.nil? || branch_name.empty?
|
|
470
|
+
return false if branch_name.length > 255
|
|
471
|
+
|
|
472
|
+
# Git branch name restrictions (following git's actual rules)
|
|
473
|
+
invalid_patterns = [
|
|
474
|
+
/\.\./, # Cannot contain ..
|
|
475
|
+
/^@{/, # Cannot start with @{
|
|
476
|
+
/\s/, # Cannot contain whitespace
|
|
477
|
+
/[~^:?*\[\]]/, # Cannot contain these special characters
|
|
478
|
+
/\.$/, # Cannot end with .
|
|
479
|
+
/^\.$/, # Cannot be just .
|
|
480
|
+
/\.lock$/, # Cannot end with .lock
|
|
481
|
+
/^$/, # Cannot be empty
|
|
482
|
+
/^\. / # Cannot start with dot followed by space
|
|
483
|
+
]
|
|
484
|
+
|
|
485
|
+
# Check for invalid patterns
|
|
486
|
+
return false if invalid_patterns.any? { |pattern| branch_name.match?(pattern) }
|
|
487
|
+
|
|
488
|
+
# Cannot be HEAD or other reserved names that conflict with git's internal refs
|
|
489
|
+
reserved_names = %w[HEAD]
|
|
490
|
+
return false if reserved_names.include?(branch_name)
|
|
491
|
+
|
|
492
|
+
# Additional validation: branch name cannot contain sequences that would be invalid
|
|
493
|
+
# in file system paths (since git stores branches as files)
|
|
494
|
+
return false if branch_name.include?(".git")
|
|
495
|
+
|
|
496
|
+
true
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Extract task ID from task data
|
|
500
|
+
#
|
|
501
|
+
# @param task_data [Hash] Task data hash from ace-task
|
|
502
|
+
# @return [String] Task ID (e.g., "094")
|
|
503
|
+
def extract_task_id_from_data(task_data)
|
|
504
|
+
# Use shared extractor that preserves subtask IDs (e.g., "121.01")
|
|
505
|
+
Atoms::TaskIDExtractor.extract(task_data)
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Create an error result hash
|
|
509
|
+
#
|
|
510
|
+
# @param message [String] Error message
|
|
511
|
+
# @return [Hash] Error result hash
|
|
512
|
+
def error_result(message)
|
|
513
|
+
{
|
|
514
|
+
success: false,
|
|
515
|
+
worktree_path: nil,
|
|
516
|
+
branch: nil,
|
|
517
|
+
error: message
|
|
518
|
+
}
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
# Detect if a branch name refers to a remote branch
|
|
522
|
+
#
|
|
523
|
+
# Only returns a remote/branch hash if the first part is actually a configured
|
|
524
|
+
# git remote. This prevents branches like "feature/login" from being incorrectly
|
|
525
|
+
# treated as remote branches (where "feature" would be the remote).
|
|
526
|
+
#
|
|
527
|
+
# @param branch_name [String] Branch name to check
|
|
528
|
+
# @return [Hash, nil] { remote: "origin", branch: "feature/auth" } or nil if local
|
|
529
|
+
#
|
|
530
|
+
# @example
|
|
531
|
+
# detect_remote_branch("origin/feature/auth")
|
|
532
|
+
# # => { remote: "origin", branch: "feature/auth" }
|
|
533
|
+
#
|
|
534
|
+
# detect_remote_branch("feature/login")
|
|
535
|
+
# # => nil (when "feature" is not a configured remote)
|
|
536
|
+
#
|
|
537
|
+
# detect_remote_branch("local-branch")
|
|
538
|
+
# # => nil
|
|
539
|
+
def detect_remote_branch(branch_name)
|
|
540
|
+
# Check if branch name contains a slash (remote/branch pattern)
|
|
541
|
+
return nil unless branch_name.include?("/")
|
|
542
|
+
|
|
543
|
+
# Split on first slash only
|
|
544
|
+
parts = branch_name.split("/", 2)
|
|
545
|
+
return nil if parts.length != 2
|
|
546
|
+
|
|
547
|
+
potential_remote = parts[0]
|
|
548
|
+
branch = parts[1]
|
|
549
|
+
|
|
550
|
+
# Basic validation
|
|
551
|
+
return nil if potential_remote.empty? || branch.empty?
|
|
552
|
+
# Invalid if branch starts with / or ends with /
|
|
553
|
+
return nil if branch.start_with?("/") || branch.end_with?("/")
|
|
554
|
+
# Invalid if remote ends with / or starts with /
|
|
555
|
+
return nil if potential_remote.start_with?("/") || potential_remote.end_with?("/")
|
|
556
|
+
|
|
557
|
+
# Verify the potential remote is actually configured
|
|
558
|
+
remote_check = validate_remote_exists(potential_remote, Dir.pwd)
|
|
559
|
+
return nil unless remote_check[:exists]
|
|
560
|
+
|
|
561
|
+
{remote: potential_remote, branch: branch}
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
# Validate that a git remote exists
|
|
565
|
+
#
|
|
566
|
+
# @param remote [String] Remote name (e.g., "origin")
|
|
567
|
+
# @param git_root [String] Git repository root
|
|
568
|
+
# @return [Hash] Result with :exists (Boolean) and :remotes (Array) for helpful error messages
|
|
569
|
+
def validate_remote_exists(remote, git_root)
|
|
570
|
+
require_relative "../atoms/git_command"
|
|
571
|
+
|
|
572
|
+
# Get list of remotes
|
|
573
|
+
result = Atoms::GitCommand.execute(
|
|
574
|
+
"remote",
|
|
575
|
+
timeout: 5
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
if result[:success]
|
|
579
|
+
remotes = result[:output].strip.split("\n")
|
|
580
|
+
exists = remotes.include?(remote)
|
|
581
|
+
{exists: exists, remotes: remotes}
|
|
582
|
+
else
|
|
583
|
+
# If we can't list remotes, assume it doesn't exist
|
|
584
|
+
{exists: false, remotes: []}
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
# Fetch a remote branch
|
|
589
|
+
#
|
|
590
|
+
# @param remote [String] Remote name (e.g., "origin")
|
|
591
|
+
# @param branch [String] Branch name
|
|
592
|
+
# @param git_root [String] Git repository root
|
|
593
|
+
# @return [Hash] Result with :success, :error
|
|
594
|
+
def fetch_remote_branch(remote, branch, git_root)
|
|
595
|
+
require_relative "../atoms/git_command"
|
|
596
|
+
|
|
597
|
+
# Validate remote exists first
|
|
598
|
+
validation = validate_remote_exists(remote, git_root)
|
|
599
|
+
unless validation[:exists]
|
|
600
|
+
available = validation[:remotes].empty? ? "no remotes configured" : validation[:remotes].join(", ")
|
|
601
|
+
return {
|
|
602
|
+
success: false,
|
|
603
|
+
error: "Remote '#{remote}' not found. Available remotes: #{available}"
|
|
604
|
+
}
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
result = Atoms::GitCommand.execute(
|
|
608
|
+
"fetch", remote, branch,
|
|
609
|
+
timeout: @timeout
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
if result[:success]
|
|
613
|
+
{success: true, error: nil}
|
|
614
|
+
else
|
|
615
|
+
{success: false, error: "Failed to fetch #{remote}/#{branch}: #{result[:error]}"}
|
|
616
|
+
end
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
# Create a worktree with remote tracking
|
|
620
|
+
#
|
|
621
|
+
# @param worktree_path [String] Path for the worktree
|
|
622
|
+
# @param local_branch_name [String] Local branch name
|
|
623
|
+
# @param remote_branch [String] Remote branch reference (e.g., "origin/feature")
|
|
624
|
+
# @param git_root [String] Git repository root
|
|
625
|
+
# @param configure_push [Boolean] Whether to configure push behavior for branch name mismatches
|
|
626
|
+
# @return [Hash] Result with :success, :worktree_path, :branch, :error
|
|
627
|
+
def create_worktree_with_tracking(worktree_path, local_branch_name, remote_branch, git_root, configure_push: true)
|
|
628
|
+
require_relative "../atoms/git_command"
|
|
629
|
+
|
|
630
|
+
# Ensure parent directory exists
|
|
631
|
+
parent_dir = File.dirname(worktree_path)
|
|
632
|
+
FileUtils.mkdir_p(parent_dir) unless File.exist?(parent_dir)
|
|
633
|
+
|
|
634
|
+
# Create worktree with tracking: git worktree add <path> -b <local> <remote>
|
|
635
|
+
result = Atoms::GitCommand.worktree(
|
|
636
|
+
"add", worktree_path, "-b", local_branch_name, remote_branch,
|
|
637
|
+
timeout: @timeout
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
unless result[:success]
|
|
641
|
+
return error_result("Failed to create worktree: #{result[:error]}")
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
# Configure push behavior if local and remote branch names differ
|
|
645
|
+
if configure_push && local_branch_name != extract_remote_branch_name(remote_branch)
|
|
646
|
+
configure_push_for_worktree(worktree_path, local_branch_name, remote_branch)
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
{
|
|
650
|
+
success: true,
|
|
651
|
+
worktree_path: worktree_path,
|
|
652
|
+
branch: local_branch_name,
|
|
653
|
+
tracking: remote_branch,
|
|
654
|
+
git_root: git_root,
|
|
655
|
+
error: nil
|
|
656
|
+
}
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
# Create a worktree for a remote branch
|
|
660
|
+
#
|
|
661
|
+
# @param branch_name [String] Full branch name (e.g., "origin/feature/auth")
|
|
662
|
+
# @param remote_info [Hash] Remote info from detect_remote_branch
|
|
663
|
+
# @param config [WorktreeConfig] Configuration
|
|
664
|
+
# @param git_root [String] Git repository root
|
|
665
|
+
# @return [Hash] Result hash
|
|
666
|
+
def create_for_remote_branch(branch_name, remote_info, config, git_root)
|
|
667
|
+
remote = remote_info[:remote]
|
|
668
|
+
branch = remote_info[:branch]
|
|
669
|
+
|
|
670
|
+
# Fetch the remote branch
|
|
671
|
+
fetch_result = fetch_remote_branch(remote, branch, git_root)
|
|
672
|
+
return error_result(fetch_result[:error]) unless fetch_result[:success]
|
|
673
|
+
|
|
674
|
+
# Generate local branch name (use full branch path to avoid collisions)
|
|
675
|
+
# For "feature/auth/v1" -> keep as "feature/auth/v1"
|
|
676
|
+
# For "feature/auth" -> keep as "feature/auth"
|
|
677
|
+
local_branch_name = branch
|
|
678
|
+
|
|
679
|
+
# Generate directory name by sanitizing branch for directory use
|
|
680
|
+
# "feature/auth/v1" -> "feature-auth-v1"
|
|
681
|
+
require_relative "../atoms/slug_generator"
|
|
682
|
+
directory_name = Atoms::SlugGenerator.to_directory_name(branch)
|
|
683
|
+
|
|
684
|
+
# Build worktree path
|
|
685
|
+
worktree_path = File.join(config.absolute_root_path, directory_name)
|
|
686
|
+
|
|
687
|
+
# Validate worktree path
|
|
688
|
+
validation = validate_worktree_path(worktree_path, git_root)
|
|
689
|
+
return error_result(validation[:error]) unless validation[:valid]
|
|
690
|
+
|
|
691
|
+
# Create worktree with tracking
|
|
692
|
+
# Check if we should configure push for branch name mismatches
|
|
693
|
+
configure_push = if config.respond_to?(:configure_push_for_mismatch?)
|
|
694
|
+
config.configure_push_for_mismatch?
|
|
695
|
+
else
|
|
696
|
+
# For backward compatibility or when config is not available
|
|
697
|
+
# Default to true for branch creation
|
|
698
|
+
true
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
result = create_worktree_with_tracking(
|
|
702
|
+
worktree_path,
|
|
703
|
+
local_branch_name,
|
|
704
|
+
branch_name,
|
|
705
|
+
git_root,
|
|
706
|
+
configure_push: configure_push
|
|
707
|
+
)
|
|
708
|
+
return result unless result[:success]
|
|
709
|
+
|
|
710
|
+
# Success
|
|
711
|
+
{
|
|
712
|
+
success: true,
|
|
713
|
+
worktree_path: worktree_path,
|
|
714
|
+
branch: local_branch_name,
|
|
715
|
+
tracking: branch_name,
|
|
716
|
+
directory_name: directory_name,
|
|
717
|
+
git_root: git_root,
|
|
718
|
+
error: nil
|
|
719
|
+
}
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
# Create a worktree for a local branch
|
|
723
|
+
#
|
|
724
|
+
# @param branch_name [String] Local branch name
|
|
725
|
+
# @param config [WorktreeConfig] Configuration
|
|
726
|
+
# @param git_root [String] Git repository root
|
|
727
|
+
# @return [Hash] Result hash
|
|
728
|
+
def create_for_local_branch(branch_name, config, git_root)
|
|
729
|
+
# Verify branch exists locally
|
|
730
|
+
require_relative "../atoms/git_command"
|
|
731
|
+
check_result = Atoms::GitCommand.execute(
|
|
732
|
+
"show-ref", "--verify", "--quiet", "refs/heads/#{branch_name}",
|
|
733
|
+
timeout: @timeout
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
unless check_result[:success]
|
|
737
|
+
return error_result("Local branch '#{branch_name}' not found")
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
# Generate directory name
|
|
741
|
+
require_relative "../atoms/slug_generator"
|
|
742
|
+
directory_name = Atoms::SlugGenerator.to_directory_name(branch_name)
|
|
743
|
+
|
|
744
|
+
# Build worktree path
|
|
745
|
+
worktree_path = File.join(config.absolute_root_path, directory_name)
|
|
746
|
+
|
|
747
|
+
# Validate worktree path
|
|
748
|
+
validation = validate_worktree_path(worktree_path, git_root)
|
|
749
|
+
return error_result(validation[:error]) unless validation[:valid]
|
|
750
|
+
|
|
751
|
+
# Create worktree (no tracking for local branches)
|
|
752
|
+
result = create_worktree(worktree_path, branch_name, git_root)
|
|
753
|
+
return result unless result[:success]
|
|
754
|
+
|
|
755
|
+
# Success
|
|
756
|
+
{
|
|
757
|
+
success: true,
|
|
758
|
+
worktree_path: worktree_path,
|
|
759
|
+
branch: branch_name,
|
|
760
|
+
tracking: nil,
|
|
761
|
+
directory_name: directory_name,
|
|
762
|
+
git_root: git_root,
|
|
763
|
+
error: nil
|
|
764
|
+
}
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
# Format PR name using template
|
|
768
|
+
#
|
|
769
|
+
# @param template [String] Template string with {variables}
|
|
770
|
+
# @param pr_data [Hash] PR data hash
|
|
771
|
+
# @return [String] Formatted string
|
|
772
|
+
#
|
|
773
|
+
# @example
|
|
774
|
+
# format_pr_name("pr-{number}-{slug}", { number: 26, title: "Add Feature" })
|
|
775
|
+
# # => "pr-26-add-feature"
|
|
776
|
+
def format_pr_name(template, pr_data)
|
|
777
|
+
require_relative "../atoms/slug_generator"
|
|
778
|
+
|
|
779
|
+
result = template.dup
|
|
780
|
+
|
|
781
|
+
# Replace {number}
|
|
782
|
+
result.gsub!("{number}", pr_data[:number].to_s)
|
|
783
|
+
|
|
784
|
+
# Replace {slug} with slugified title
|
|
785
|
+
if pr_data[:title]
|
|
786
|
+
slug = Atoms::SlugGenerator.from_title(pr_data[:title])
|
|
787
|
+
result.gsub!("{slug}", slug)
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
# Replace {title_slug} (alias for slug)
|
|
791
|
+
if pr_data[:title]
|
|
792
|
+
slug = Atoms::SlugGenerator.from_title(pr_data[:title])
|
|
793
|
+
result.gsub!("{title_slug}", slug)
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
# Replace {base_branch}
|
|
797
|
+
result.gsub!("{base_branch}", pr_data[:base_branch].to_s) if pr_data[:base_branch]
|
|
798
|
+
|
|
799
|
+
result
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
# Extract just the branch name from a remote branch reference
|
|
803
|
+
#
|
|
804
|
+
# @param remote_branch [String] Remote branch reference (e.g., "origin/feature")
|
|
805
|
+
# @return [String] Branch name (e.g., "feature")
|
|
806
|
+
def extract_remote_branch_name(remote_branch)
|
|
807
|
+
remote_info = detect_remote_branch(remote_branch)
|
|
808
|
+
remote_info ? remote_info[:branch] : remote_branch
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
# Configure push behavior for a worktree when local and remote branch names differ
|
|
812
|
+
#
|
|
813
|
+
# @param worktree_path [String] Path to the worktree
|
|
814
|
+
# @param local_branch_name [String] Local branch name
|
|
815
|
+
# @param remote_branch [String] Remote branch reference
|
|
816
|
+
def configure_push_for_worktree(worktree_path, local_branch_name, remote_branch)
|
|
817
|
+
require_relative "../atoms/git_command"
|
|
818
|
+
|
|
819
|
+
# Run git config commands within the worktree
|
|
820
|
+
Dir.chdir(worktree_path) do
|
|
821
|
+
# Set push.default to "upstream" to push to the configured upstream regardless of name
|
|
822
|
+
Atoms::GitCommand.execute("config", "push.default", "upstream", timeout: 5)
|
|
823
|
+
|
|
824
|
+
# Set push.autoSetupRemote to true for convenience
|
|
825
|
+
Atoms::GitCommand.execute("config", "push.autoSetupRemote", "true", timeout: 5)
|
|
826
|
+
end
|
|
827
|
+
end
|
|
828
|
+
end
|
|
829
|
+
end
|
|
830
|
+
end
|
|
831
|
+
end
|
|
832
|
+
end
|