rails-worktree 0.2.0 → 0.3.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.
@@ -2,20 +2,18 @@ require "fileutils"
2
2
 
3
3
  module RailsWorktree
4
4
  module Commands
5
+ # Drops the worktree-local databases recorded in .env.*.local, removes the
6
+ # checkout and deletes its branch.
5
7
  class Close
6
8
  def initialize(args)
7
9
  @worktree_name = args[0]
10
+ @named = !@worktree_name.nil?
8
11
  @main_worktree = get_main_worktree
9
12
  @current_dir = Dir.pwd
10
13
  end
11
14
 
12
15
  def run
13
16
  detect_worktree_name unless @worktree_name
14
-
15
- @db_prefix = get_db_prefix
16
- @dev_database_name = "#{@db_prefix}_#{@worktree_name}_development"
17
- @test_database_name = "#{@db_prefix}_#{@worktree_name}_test"
18
-
19
17
  detect_paths
20
18
 
21
19
  puts "Closing worktree '#{@worktree_name}'..."
@@ -33,12 +31,10 @@ module RailsWorktree
33
31
 
34
32
  puts ""
35
33
  puts "✓ Worktree '#{@worktree_name}' closed successfully!"
36
- puts " Databases dropped: #{@dev_database_name}, #{@test_database_name}"
37
- puts " node_modules removed"
38
34
  puts " Worktree removed from #{@worktree_path}"
39
- puts " Branch #{@worktree_name} deleted"
35
+ puts " Branch #{@branch} deleted" if @branch_deleted
40
36
 
41
- unless @in_main_repo
37
+ if @worktree_path == @current_dir
42
38
  puts ""
43
39
  puts "Run: cd #{@main_worktree}"
44
40
  end
@@ -53,88 +49,107 @@ module RailsWorktree
53
49
 
54
50
  def detect_worktree_name
55
51
  if @current_dir == @main_worktree
56
- puts "Error: You must specify a worktree name when running from the main repository"
57
- puts "Usage: worktree --close <worktree-name>"
58
- puts " or: cd to the worktree and run: worktree --close"
59
- exit 1
52
+ raise Error, "Specify a worktree name when closing from the main repository: bin/worktree close NAME"
60
53
  else
61
54
  @worktree_name = File.basename(@current_dir)
62
55
  puts "Detected worktree name: #{@worktree_name}"
63
56
  end
64
57
  end
65
58
 
66
- def get_db_prefix
67
- # Try to get prefix from database.yml first
68
- database_yml = File.join(@main_worktree, "config/database.yml")
69
- if File.exist?(database_yml)
70
- content = File.read(database_yml)
71
- match = content.match(/database:\s*(\w+)_development/)
72
- return match[1] if match
59
+ # Finds the checkout in git's worktree list by directory name or branch, so
60
+ # worktrees outside .worktrees/ (git worktree add, herdr, Claude Code) close too.
61
+ # An explicit name wins over the current directory, so closing another
62
+ # worktree from inside one leaves the current checkout alone.
63
+ def detect_paths
64
+ @in_main_repo = @current_dir == @main_worktree
65
+ entries = worktree_entries.reject { |entry| entry[:path] == @main_worktree }
66
+ entry = if @named
67
+ entries.find { |candidate| candidate[:path] == File.join(@main_worktree, ".worktrees", @worktree_name) } ||
68
+ entries.find { |candidate| File.basename(candidate[:path]) == @worktree_name || candidate[:branch] == @worktree_name }
69
+ else
70
+ entries.find { |candidate| candidate[:path] == @current_dir }
73
71
  end
72
+ raise Error, "No worktree named #{@worktree_name}; see git worktree list." if @named && !entry
74
73
 
75
- # Fall back to using the Rails app name (directory name of main worktree)
76
- File.basename(@main_worktree)
74
+ @worktree_path = entry ? entry[:path] : @current_dir
75
+ @branch = entry ? entry[:branch] : @worktree_name
77
76
  end
78
77
 
79
- def detect_paths
80
- if @current_dir == @main_worktree
81
- @in_main_repo = true
82
- @worktree_path = File.join(@main_worktree, ".worktrees", @worktree_name)
83
- @worktree_dir = @worktree_path
84
- else
85
- @in_main_repo = false
86
- @worktree_path = @current_dir
87
- @worktree_dir = "."
78
+ def worktree_entries
79
+ `git worktree list --porcelain`.split("\n\n").map do |block|
80
+ lines = block.lines.map(&:strip)
81
+ path = lines.find { |line| line.start_with?("worktree ") }&.delete_prefix("worktree ")
82
+ branch = lines.find { |line| line.start_with?("branch ") }&.delete_prefix("branch refs/heads/")
83
+ {path: path, branch: branch}
84
+ end
85
+ end
86
+
87
+ def config
88
+ @config ||= Config.load(File.exist?(File.join(@worktree_path, Config::FILE)) ? @worktree_path : @main_worktree)
89
+ end
90
+
91
+ # Databases named in the worktree's .env.development.local and .env.test.local.
92
+ # Without those files the checkout shares the main databases, which stay untouched.
93
+ def worktree_databases
94
+ Init::ENV_FILES.filter_map do |environment, file|
95
+ path = File.join(@worktree_path, file)
96
+ next unless File.file?(path)
97
+
98
+ variable = environment == "test" ? config.test_variable : config.development_variable
99
+ match = File.read(path).match(/^#{Regexp.escape(variable)}=(.+)$/)
100
+ [environment, variable, match[1].strip] if match
88
101
  end
89
102
  end
90
103
 
91
104
  def drop_databases
92
- puts "Dropping databases..."
105
+ databases = worktree_databases
106
+ if databases.empty?
107
+ puts "No worktree-local databases in .env.development.local or .env.test.local; nothing to drop"
108
+ return
109
+ end
110
+ unless File.executable?(File.join(@worktree_path, "bin/rails"))
111
+ puts "Warning: bin/rails not found in worktree, skipping database drop"
112
+ return
113
+ end
93
114
 
94
115
  Dir.chdir(@worktree_path) do
95
- if File.executable?("bin/rails")
96
- system({ "DISABLE_DATABASE_ENVIRONMENT_CHECK" => "1" }, "bin/rails", "db:drop") ||
97
- puts("Warning: Could not drop databases")
98
- else
99
- puts "Warning: bin/rails not found in worktree, skipping database drop"
116
+ databases.each do |environment, variable, database|
117
+ puts "Dropping #{database}..."
118
+ env = {"RAILS_ENV" => environment, variable => database, "DISABLE_DATABASE_ENVIRONMENT_CHECK" => "1"}
119
+ system(env, "bin/rails", "db:drop") || puts("Warning: could not drop #{database}")
100
120
  end
101
121
  end
102
122
  end
103
123
 
104
124
  def remove_node_modules
105
- puts "Removing node_modules..."
125
+ node_modules_path = File.join(@worktree_path, "node_modules")
126
+ return unless File.exist?(node_modules_path) || File.symlink?(node_modules_path)
106
127
 
107
- node_modules_path = File.join(@worktree_dir, "node_modules")
108
- if Dir.exist?(node_modules_path)
109
- FileUtils.rm_rf(node_modules_path)
110
- puts "node_modules removed"
111
- else
112
- puts "No node_modules to remove"
113
- end
128
+ puts "Removing node_modules..."
129
+ FileUtils.rm_rf(node_modules_path)
114
130
  end
115
131
 
116
132
  def remove_worktree
117
133
  puts "Removing worktree..."
118
-
119
- if system("git worktree remove #{@worktree_path} --force 2>/dev/null")
134
+ if system(Context::GIT_ENVIRONMENT, "git", "worktree", "remove", "--force", @worktree_path, err: File::NULL)
120
135
  puts "Worktree removed successfully via git"
121
- else
136
+ elsif Dir.exist?(@worktree_path)
122
137
  puts "Git worktree remove failed, deleting directory manually..."
123
- if Dir.exist?(@worktree_path)
124
- FileUtils.rm_rf(@worktree_path)
125
- puts "Directory deleted: #{@worktree_path}"
126
- end
138
+ FileUtils.rm_rf(@worktree_path)
139
+ puts "Directory deleted: #{@worktree_path}"
127
140
  end
128
141
  end
129
142
 
130
143
  def prune_worktrees
131
- system("git worktree prune")
144
+ system("git", "worktree", "prune")
132
145
  end
133
146
 
134
147
  def delete_branch
135
- puts "Deleting branch #{@worktree_name}..."
136
- system("git branch -D #{@worktree_name} 2>/dev/null") ||
137
- puts("Warning: Could not delete branch #{@worktree_name}")
148
+ return puts("No branch to delete (detached HEAD)") unless @branch
149
+
150
+ puts "Deleting branch #{@branch}..."
151
+ @branch_deleted = system("git", "branch", "-D", @branch, err: File::NULL)
152
+ puts("Warning: Could not delete branch #{@branch}") unless @branch_deleted
138
153
  end
139
154
  end
140
155
  end
@@ -2,20 +2,19 @@ require "fileutils"
2
2
 
3
3
  module RailsWorktree
4
4
  module Commands
5
+ # Creates a worktree under .worktrees/ from an explicit base, then provisions it.
5
6
  class Create
6
- def initialize(args, skip_seeds: false)
7
+ def initialize(args, level: nil, seed: false, force: false, skip_seeds: nil)
7
8
  @worktree_name = args[0]
8
9
  @base_branch = args[1]
9
- @skip_seeds = skip_seeds
10
+ @level = level
11
+ @seed = seed
10
12
  end
11
13
 
12
14
  def run
13
- unless @worktree_name
14
- puts "Error: Worktree name is required"
15
- exit 1
16
- end
15
+ raise Error, "Worktree name is required: bin/worktree create NAME BASE" unless @worktree_name
16
+ raise Error, "Base branch required: bin/worktree create #{@worktree_name} BASE (for example origin/main)." unless @base_branch
17
17
 
18
- raise Error, "Base branch required: bin/worktree #{@worktree_name} BASE (for example origin/main)." unless @base_branch
19
18
  context = Context.new
20
19
  Dir.chdir(context.root) { create }
21
20
  end
@@ -37,32 +36,18 @@ module RailsWorktree
37
36
  Installer.ignore_worktrees(Dir.pwd)
38
37
  FileUtils.mkdir_p(worktree_dir)
39
38
 
40
- puts "Creating worktree '#{@worktree_name}' from branch '#{@base_branch}' at #{worktree_path}..."
41
-
39
+ puts "Creating worktree '#{@worktree_name}' from #{@base_branch} at #{worktree_path}..."
42
40
  unless system(Context::GIT_ENVIRONMENT, "git", "worktree", "add", "-b", @worktree_name, worktree_path, base_commit)
43
- puts "Failed to create worktree"
44
- exit 1
41
+ raise Error, "Failed to create worktree"
45
42
  end
46
43
 
47
- puts ""
48
- puts "✓ Worktree created at #{worktree_path}"
49
- puts ""
50
- puts "Initializing worktree..."
51
-
52
44
  Dir.chdir(worktree_path) do
53
- created = Context.new
54
- created.record(@base_branch, base_commit, @worktree_name)
55
- created.verify
56
- Installer.install(created.root, replace: true)
57
- PushHook.install(created.root)
58
- Init.new([@worktree_name], skip_seeds: @skip_seeds).run
45
+ Init.new([@worktree_name], level: @level, seed: @seed, base: @base_branch).run
59
46
  end
60
47
 
61
48
  puts ""
62
49
  puts "To switch to the new worktree:"
63
50
  puts " cd #{absolute_path}"
64
- puts ""
65
- puts "To start the development server: bin/dev"
66
51
  end
67
52
  end
68
53
  end