Class: ClaudeSwarm::WorktreeManager

Inherits:
Object
  • Object
show all
Includes:
SystemUtils
Defined in:
lib/claude_swarm/worktree_manager.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from SystemUtils

#last_status, #system!, #system_with_pid!

Constructor Details

#initialize(cli_worktree_option = nil, session_id: nil) ⇒ WorktreeManager

Returns a new instance of WorktreeManager.



9
10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/claude_swarm/worktree_manager.rb', line 9

def initialize(cli_worktree_option = nil, session_id: nil)
  @cli_worktree_option = cli_worktree_option
  @session_id = session_id
  # Generate a name based on session ID if no option given, empty string, or default "worktree" from Thor
  @shared_worktree_name = if cli_worktree_option.nil? || cli_worktree_option.empty? || cli_worktree_option == "worktree"
    generate_worktree_name
  else
    cli_worktree_option
  end
  @created_worktrees = {} # Maps "repo_root:worktree_name" to worktree_path
  @instance_worktree_configs = {} # Stores per-instance worktree settings
  @instance_directories = {} # Stores original resolved directories for each instance
  @instance_worktree_paths = {} # Stores worktree paths for each instance
end

Instance Attribute Details

#created_worktreesObject (readonly)

Returns the value of attribute created_worktrees.



7
8
9
# File 'lib/claude_swarm/worktree_manager.rb', line 7

def created_worktrees
  @created_worktrees
end

#shared_worktree_nameObject (readonly)

Returns the value of attribute shared_worktree_name.



7
8
9
# File 'lib/claude_swarm/worktree_manager.rb', line 7

def shared_worktree_name
  @shared_worktree_name
end

Instance Method Details

#cleanup_external_directoriesObject



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/claude_swarm/worktree_manager.rb', line 158

def cleanup_external_directories
  # Remove session-specific worktree directory if it exists and is empty
  return unless @session_id

  session_worktree_dir = ClaudeSwarm.joined_worktrees_dir(@session_id)
  return unless File.exist?(session_worktree_dir)

  # Try to remove the directory tree
  begin
    # Remove all empty directories recursively
    Dir.glob(File.join(session_worktree_dir, "**/*"), File::FNM_DOTMATCH).reverse_each do |path|
      next if path.end_with?("/.", "/..")

      FileUtils.rmdir(path) if File.directory?(path) && Dir.empty?(path)
    end
    # Finally try to remove the session directory itself
    FileUtils.rmdir(session_worktree_dir) if Dir.empty?(session_worktree_dir)
  rescue Errno::ENOTEMPTY
    puts "Note: Session worktree directory not empty, leaving in place: #{session_worktree_dir}" unless ENV["CLAUDE_SWARM_PROMPT"]
  rescue StandardError => e
    puts "Warning: Error cleaning up worktree directories: #{e.message}" unless ENV["CLAUDE_SWARM_PROMPT"]
  end
end

#cleanup_worktreesObject



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/claude_swarm/worktree_manager.rb', line 121

def cleanup_worktrees
  @created_worktrees.each do |worktree_key, worktree_path|
    repo_root = worktree_key.split(":", 2).first
    next unless File.exist?(worktree_path)

    # Check for uncommitted changes
    if has_uncommitted_changes?(worktree_path)
      puts "⚠️  Warning: Worktree has uncommitted changes, skipping cleanup: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
      next
    end

    # Check for unpushed commits
    has_unpushed = has_unpushed_commits?(worktree_path)

    if has_unpushed
      puts "⚠️  Warning: Worktree has unpushed commits, skipping cleanup: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
      next
    end

    puts "Removing worktree: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]

    # Remove the worktree
    output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "remove", worktree_path)
    next if status.success?

    puts "Warning: Failed to remove worktree: #{output}"
    # Try force remove
    output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "remove", "--force", worktree_path)
    puts "Force remove result: #{output}" unless status.success?
  end

  # Clean up external worktree directories
  cleanup_external_directories
rescue StandardError => e
  puts "Error during worktree cleanup: #{e.message}"
end

#map_to_worktree_path(original_path, worktree_name) ⇒ Object



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
110
111
112
113
114
115
116
117
118
119
# File 'lib/claude_swarm/worktree_manager.rb', line 80

def map_to_worktree_path(original_path, worktree_name)
  return original_path unless original_path

  expanded_path = File.expand_path(original_path)
  repo_root = find_git_root(expanded_path)

  if ENV["CLAUDE_SWARM_DEBUG"]
    puts "Debug [map_to_worktree_path]: Original path: #{original_path}"
    puts "Debug [map_to_worktree_path]: Expanded path: #{expanded_path}"
    puts "Debug [map_to_worktree_path]: Repo root: #{repo_root}"
  end

  return original_path unless repo_root

  # Check if we have a worktree for this repo and name
  worktree_key = "#{repo_root}:#{worktree_name}"
  worktree_path = @created_worktrees[worktree_key]

  if ENV["CLAUDE_SWARM_DEBUG"]
    puts "Debug [map_to_worktree_path]: Worktree key: #{worktree_key}"
    puts "Debug [map_to_worktree_path]: Worktree path: #{worktree_path}"
    puts "Debug [map_to_worktree_path]: Created worktrees: #{@created_worktrees.inspect}"
  end

  return original_path unless worktree_path

  # Calculate relative path from repo root
  relative_path = Pathname.new(expanded_path).relative_path_from(Pathname.new(repo_root)).to_s

  # Return the equivalent path in the worktree
  result = if relative_path == "."
    worktree_path
  else
    File.join(worktree_path, relative_path)
  end

  puts "Debug [map_to_worktree_path]: Result: #{result}" if ENV["CLAUDE_SWARM_DEBUG"]

  result
end

#session_metadataObject



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/claude_swarm/worktree_manager.rb', line 182

def 
  # Build instance details with resolved paths and worktree mappings
  instance_details = {}

  @instance_worktree_configs.each do |instance_name, worktree_config|
    instance_details[instance_name] = {
      worktree_config: worktree_config,
      directories: @instance_directories[instance_name] || {},
      worktree_paths: @instance_worktree_paths[instance_name] || {},
    }
  end

  {
    enabled: true,
    shared_name: @shared_worktree_name,
    created_paths: @created_worktrees.dup,
    instance_configs: instance_details,
  }
end

#setup_worktrees(instances) ⇒ Object



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/claude_swarm/worktree_manager.rb', line 24

def setup_worktrees(instances)
  # First pass: determine worktree configuration for each instance
  instances.each do |instance|
    worktree_config = determine_worktree_config(instance)
    @instance_worktree_configs[instance[:name]] = worktree_config
  end

  # Second pass: create necessary worktrees
  worktrees_to_create = collect_worktrees_to_create(instances)
  worktrees_to_create.each do |repo_root, worktree_name|
    create_worktree(repo_root, worktree_name)
  end

  # Third pass: map instance directories to worktree paths
  instances.each do |instance|
    worktree_config = @instance_worktree_configs[instance[:name]]

    if ENV["CLAUDE_SWARM_DEBUG"]
      puts "Debug [WorktreeManager]: Processing instance #{instance[:name]}"
      puts "Debug [WorktreeManager]: Worktree config: #{worktree_config.inspect}"
    end

    # Store original directories (resolved)
    original_dirs = instance[:directories] || [instance[:directory]]
    resolved_dirs = original_dirs.map { |dir| dir ? File.expand_path(dir) : nil }.compact
    @instance_directories[instance[:name]] = resolved_dirs

    if worktree_config[:skip]
      # No worktree, paths remain the same
      @instance_worktree_paths[instance[:name]] = resolved_dirs
      next
    end

    worktree_name = worktree_config[:name]
    mapped_dirs = original_dirs.map { |dir| map_to_worktree_path(dir, worktree_name) }

    # Store the worktree paths
    @instance_worktree_paths[instance[:name]] = mapped_dirs

    if ENV["CLAUDE_SWARM_DEBUG"]
      puts "Debug [WorktreeManager]: Original dirs: #{original_dirs.inspect}"
      puts "Debug [WorktreeManager]: Mapped dirs: #{mapped_dirs.inspect}"
    end

    if instance[:directories]
      instance[:directories] = mapped_dirs
      # Also update the single directory field for backward compatibility
      instance[:directory] = mapped_dirs.first
    else
      instance[:directory] = mapped_dirs.first
    end

    puts "Debug [WorktreeManager]: Updated instance[:directory] to: #{instance[:directory]}" if ENV["CLAUDE_SWARM_DEBUG"]
  end
end

#worktree_nameObject

Deprecated method for backward compatibility



203
204
205
# File 'lib/claude_swarm/worktree_manager.rb', line 203

def worktree_name
  @shared_worktree_name
end