Class: Ace::Git::Worktree::Molecules::WorktreeRemover

Inherits:
Object
  • Object
show all
Defined in:
lib/ace/git/worktree/molecules/worktree_remover.rb

Overview

Worktree remover molecule

Removes git worktrees with proper cleanup, validation, and error handling. Provides options for force removal and handles various edge cases.

Examples:

Remove a worktree

remover = WorktreeRemover.new
success = remover.remove("/path/to/worktree")

Force remove with changes

success = remover.remove("/path/to/worktree", force: true)

Constant Summary collapse

FALLBACK_TIMEOUT =

Fallback timeout for git commands Used only when config is unavailable

30

Instance Method Summary collapse

Constructor Details

#initialize(timeout: nil) ⇒ WorktreeRemover

Initialize a new WorktreeRemover

Parameters:

  • timeout (Integer, nil) (defaults to: nil)

    Command timeout in seconds (uses config default if nil)



26
27
28
# File 'lib/ace/git/worktree/molecules/worktree_remover.rb', line 26

def initialize(timeout: nil)
  @timeout = timeout || config_timeout
end

Instance Method Details

#check_removal_safety(worktree_path) ⇒ Hash

Check if a worktree can be safely removed

Examples:

check = remover.check_removal_safety("/path/to/worktree")
if check[:safe]
  remover.remove("/path/to/worktree")
else
  puts "Cannot remove: #{check[:errors].join(', ')}"
end

Parameters:

  • worktree_path (String)

    Path to the worktree

Returns:

  • (Hash)

    Safety check result with :safe, :warnings, :errors



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/ace/git/worktree/molecules/worktree_remover.rb', line 218

def check_removal_safety(worktree_path)
  expanded_path = File.expand_path(worktree_path)

  result = {
    safe: true,
    warnings: [],
    errors: []
  }

  # Check if worktree exists
  worktree_info = find_worktree_info(expanded_path)
  unless worktree_info
    result[:safe] = false
    result[:errors] << "Worktree not found"
    return result
  end

  # Check for uncommitted changes
  if has_uncommitted_changes?(expanded_path)
    result[:safe] = false
    result[:errors] << "Worktree has uncommitted changes"
  end

  # Check if it's the current worktree
  current_dir = Dir.pwd
  if File.expand_path(current_dir) == expanded_path
    result[:warnings] << "Currently in this worktree"
  end

  # Check if it's the main worktree
  if worktree_info.branch.nil || worktree_info.detached || worktree_info.bare
    result[:warnings] << "This might be the main worktree"
  end

  result
end

#delete_branch_if_safe(branch_name, force) ⇒ Hash

Delete a branch if it’s safe to do so

Parameters:

  • branch_name (String)

    Branch name to delete

  • force (Boolean)

    Force deletion even if not merged

Returns:

  • (Hash)

    Result with :success, :message, :error



389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
# File 'lib/ace/git/worktree/molecules/worktree_remover.rb', line 389

def delete_branch_if_safe(branch_name, force)
  require_relative "../atoms/git_command"

  # Check if branch is already merged (unless forcing)
  unless force
    # Check if branch is merged into current branch
    result = Atoms::GitCommand.execute("branch", "--merged", timeout: @timeout)
    if result[:success]
      merged_branches = result[:output].split("\n").map(&:strip).map { |b| b.gsub(/^\*?\s*/, "") }
      unless merged_branches.include?(branch_name)
        # Branch is not merged, don't delete unless forced
        warn "Warning: Branch #{branch_name} is not merged. Skipping deletion. Use --force to delete anyway."
        return {success: false, message: "Branch not merged", error: nil}
      end
    end
  end

  # Delete the branch
  delete_flag = force ? "-D" : "-d"
  result = Atoms::GitCommand.execute("branch", delete_flag, branch_name, timeout: @timeout)

  if result[:success]
    {success: true, message: "Branch #{branch_name} deleted", error: nil}
  else
    {success: false, message: nil, error: "Failed to delete branch: #{result[:error]}"}
  end
end

#pruneHash

Prune deleted worktrees (cleanup git metadata)

Examples:

result = remover.prune
# => { success: true, message: "Pruned 2 worktrees", pruned_count: 2 }

Returns:

  • (Hash)

    Result with :success, :message, :pruned_count



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/ace/git/worktree/molecules/worktree_remover.rb', line 187

def prune
  result = execute_git_worktree_prune
  if result[:success]
    # Parse output to count pruned worktrees
    pruned_count = parse_prune_output(result[:output])

    {
      success: true,
      message: "Pruned #{pruned_count} worktree(s)",
      pruned_count: pruned_count,
      error: nil
    }
  else
    error_result("Failed to prune worktrees: #{result[:error]}")
  end
rescue => e
  error_result("Unexpected error during prune: #{e.message}")
end

#remove(worktree_path, force: false, remove_directory: true, delete_branch: false, ignore_untracked: false) ⇒ Hash

Remove a worktree by path

Examples:

remover = WorktreeRemover.new
result = remover.remove("/project/.ace-wt/task.081")
# => { success: true, message: "Worktree removed successfully", error: nil }

Parameters:

  • worktree_path (String)

    Path to the worktree directory

  • force (Boolean) (defaults to: false)

    Force removal even if there are uncommitted changes

  • remove_directory (Boolean) (defaults to: true)

    Also remove the worktree directory

  • delete_branch (Boolean) (defaults to: false)

    Also delete the associated branch

  • ignore_untracked (Boolean) (defaults to: false)

    Treat untracked files as clean for removal checks

Returns:

  • (Hash)

    Result with :success, :message, :error



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/ace/git/worktree/molecules/worktree_remover.rb', line 55

def remove(
  worktree_path,
  force: false,
  remove_directory: true,
  delete_branch: false,
  ignore_untracked: false
)
  return error_result("Worktree path is required") if worktree_path.nil? || worktree_path.empty?

  begin
    expanded_path = File.expand_path(worktree_path)

    # Check if worktree exists
    worktree_info = find_worktree_info(expanded_path)
    return error_result("Worktree not found at #{expanded_path}") unless worktree_info

    # Check for uncommitted changes
    if !force && has_uncommitted_changes?(expanded_path, ignore_untracked: ignore_untracked)
      return error_result("Worktree has uncommitted changes. Use --force to remove anyway.")
    end

    # Store branch name before removal
    branch_name = worktree_info.branch

    # Remove the worktree using git
    # When ignore_untracked is true, we've already verified there are no tracked changes,
    # so pass force: true to skip git's own untracked-file check.
    result = remove_git_worktree(expanded_path, force: force || ignore_untracked)
    return result unless result[:success]

    # Optionally remove the directory
    if remove_directory && File.exist?(expanded_path)
      directory_result = remove_worktree_directory(expanded_path)
      return directory_result unless directory_result[:success]
    end

    # Optionally delete the branch
    branch_deleted = false
    if delete_branch && branch_name && !branch_name.empty?
      delete_result = delete_branch_if_safe(branch_name, force)
      branch_deleted = delete_result[:success]
    end

    {
      success: true,
      message: "Worktree removed successfully",
      path: expanded_path,
      branch: branch_name,
      branch_deleted: branch_deleted,
      error: nil
    }
  rescue => e
    error_result("Unexpected error: #{e.message}")
  end
end

#remove_by_branch(branch_name, force: false) ⇒ Hash

Remove a worktree by branch name

Examples:

result = remover.remove_by_branch("081-fix-auth")

Parameters:

  • branch_name (String)

    Branch name of the worktree

  • force (Boolean) (defaults to: false)

    Force removal even if there are uncommitted changes

Returns:

  • (Hash)

    Result with :success, :message, :error



119
120
121
122
123
124
125
126
127
# File 'lib/ace/git/worktree/molecules/worktree_remover.rb', line 119

def remove_by_branch(branch_name, force: false)
  return error_result("Branch name is required") if branch_name.nil? || branch_name.empty?

  # Find worktree by branch
  worktree_info = find_worktree_by_branch(branch_name)
  return error_result("No worktree found for branch: #{branch_name}") unless worktree_info

  remove(worktree_info.path, force: force)
end

#remove_by_task_id(task_id, force: false) ⇒ Hash

Remove a worktree by task ID

Examples:

result = remover.remove_by_task_id("081")

Parameters:

  • task_id (String)

    Task ID

  • force (Boolean) (defaults to: false)

    Force removal even if there are uncommitted changes

Returns:

  • (Hash)

    Result with :success, :message, :error



137
138
139
140
141
142
143
144
145
# File 'lib/ace/git/worktree/molecules/worktree_remover.rb', line 137

def remove_by_task_id(task_id, force: false)
  return error_result("Task ID is required") if task_id.nil? || task_id.empty?

  # Find worktree by task ID
  worktree_info = find_worktree_by_task_id(task_id)
  return error_result("No worktree found for task: #{task_id}") unless worktree_info

  remove(worktree_info.path, force: force)
end

#remove_multiple(worktree_paths, force: false) ⇒ Hash

Remove multiple worktrees

Examples:

result = remover.remove_multiple(["/path1", "/path2"], force: true)
# => { success: true, removed: ["/path1"], failed: ["/path2"], errors: {...} }

Parameters:

  • worktree_paths (Array<String>)

    Array of worktree paths

  • force (Boolean) (defaults to: false)

    Force removal even if there are uncommitted changes

Returns:

  • (Hash)

    Result with :success, :removed, :failed, :errors



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/ace/git/worktree/molecules/worktree_remover.rb', line 156

def remove_multiple(worktree_paths, force: false)
  return error_result("Worktree paths array is required") if worktree_paths.nil? || worktree_paths.empty?

  results = {
    success: true,
    removed: [],
    failed: [],
    errors: {}
  }

  Array(worktree_paths).each do |path|
    result = remove(path, force: force)
    if result[:success]
      results[:removed] << path
    else
      results[:success] = false
      results[:failed] << path
      results[:errors][path] = result[:error]
    end
  end

  results
end