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.
- checksums.yaml +4 -4
- data/README.md +113 -98
- data/lib/rails-worktree.rb +2 -0
- data/lib/worktree/cli.rb +76 -55
- data/lib/worktree/clone_database.rb +23 -0
- data/lib/worktree/commands/close.rb +71 -56
- data/lib/worktree/commands/create.rb +9 -24
- data/lib/worktree/commands/init.rb +240 -123
- data/lib/worktree/config.rb +114 -0
- data/lib/worktree/context.rb +7 -6
- data/lib/worktree/copy_sqlite.rb +7 -0
- data/lib/worktree/database_probe.rb +27 -0
- data/lib/worktree/installer.rb +151 -4
- data/lib/worktree/managed_block.rb +32 -0
- data/lib/worktree/push_hook.rb +15 -2
- data/lib/worktree/railtie.rb +3 -5
- data/lib/worktree/tasks.rb +5 -4
- data/lib/worktree/version.rb +1 -1
- metadata +7 -2
data/lib/worktree/installer.rb
CHANGED
|
@@ -1,12 +1,45 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "open3"
|
|
3
|
+
|
|
1
4
|
module RailsWorktree
|
|
5
|
+
# Installs the launcher and the repository integration files:
|
|
6
|
+
# .gitignore entry, .worktree.yml, .worktreeinclude (Claude Code), the
|
|
7
|
+
# AGENTS.md / CLAUDE.md block and the herdr plugin under .worktree/herdr.
|
|
2
8
|
class Installer
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
9
|
+
AGENTS_START = "<!-- rails-worktree:start -->".freeze
|
|
10
|
+
AGENTS_FINISH = "<!-- rails-worktree:end -->".freeze
|
|
11
|
+
INCLUDE_START = "# rails-worktree:start".freeze
|
|
12
|
+
INCLUDE_FINISH = "# rails-worktree:end".freeze
|
|
13
|
+
HERDR_DIR = ".worktree/herdr".freeze
|
|
14
|
+
|
|
15
|
+
def self.install(root, replace: false, quiet: true)
|
|
16
|
+
new(root, replace: replace, quiet: quiet).install
|
|
6
17
|
end
|
|
7
18
|
|
|
8
19
|
def self.ignore_worktrees(root)
|
|
9
|
-
|
|
20
|
+
new(root).ignore_worktrees
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(root, replace: false, quiet: true)
|
|
24
|
+
@root = File.expand_path(root)
|
|
25
|
+
@replace = replace
|
|
26
|
+
@quiet = quiet
|
|
27
|
+
@changes = []
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def install
|
|
31
|
+
Launcher.install(@root, replace: @replace)
|
|
32
|
+
ignore_worktrees
|
|
33
|
+
write_config
|
|
34
|
+
write_worktreeinclude
|
|
35
|
+
write_agents_block
|
|
36
|
+
write_herdr_plugin
|
|
37
|
+
report
|
|
38
|
+
@changes
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ignore_worktrees
|
|
42
|
+
path = File.join(@root, ".gitignore")
|
|
10
43
|
content = File.exist?(path) ? File.read(path) : ""
|
|
11
44
|
return if content.lines.any? { |line| %w[ .worktrees .worktrees/ /.worktrees /.worktrees/ ].include?(line.strip) }
|
|
12
45
|
|
|
@@ -14,6 +47,120 @@ module RailsWorktree
|
|
|
14
47
|
file.puts unless content.empty? || content.end_with?("\n")
|
|
15
48
|
file.puts "/.worktrees/"
|
|
16
49
|
end
|
|
50
|
+
@changes << ".gitignore"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def config
|
|
56
|
+
@config ||= Config.load(@root)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def write_config
|
|
60
|
+
path = File.join(@root, Config::FILE)
|
|
61
|
+
return if File.exist?(path)
|
|
62
|
+
|
|
63
|
+
File.write(path, Config::TEMPLATE)
|
|
64
|
+
@changes << Config::FILE
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def write_worktreeinclude
|
|
68
|
+
body = ["# Gitignored files Claude Code copies into every worktree it creates.",
|
|
69
|
+
"# Generated from the copy list in #{Config::FILE}; rerun bin/worktree install to update."] + config.copy_patterns
|
|
70
|
+
@changes << ".worktreeinclude" if ManagedBlock.write(File.join(@root, ".worktreeinclude"), body.join("\n"), start: INCLUDE_START, finish: INCLUDE_FINISH)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def write_agents_block
|
|
74
|
+
body = <<~MARKDOWN
|
|
75
|
+
## Worktrees
|
|
76
|
+
|
|
77
|
+
A fresh checkout (git worktree, herdr, Claude Code or Codex) has no gitignored
|
|
78
|
+
configuration. Run `bin/worktree init` in it before starting the app or the specs.
|
|
79
|
+
It copies the files listed in `#{Config::FILE}` from the main checkout and is safe to repeat.
|
|
80
|
+
|
|
81
|
+
- `bin/worktree init --level minimal`: configuration files only, shares the main development database.
|
|
82
|
+
- `bin/worktree init --level test`: minimal plus an isolated test setup.
|
|
83
|
+
- `bin/worktree init --level full`: minimal plus own development and test databases.
|
|
84
|
+
- `bin/worktree create <name> <base>`: create a worktree under `.worktrees/` and provision it.
|
|
85
|
+
MARKDOWN
|
|
86
|
+
agents_files.each do |path|
|
|
87
|
+
@changes << File.basename(path) if ManagedBlock.write(path, body, start: AGENTS_START, finish: AGENTS_FINISH)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# AGENTS.md and CLAUDE.md when they exist; a symlink to the other file counts once.
|
|
92
|
+
# Without either file, AGENTS.md is created.
|
|
93
|
+
def agents_files
|
|
94
|
+
candidates = %w[AGENTS.md CLAUDE.md].map { |name| File.join(@root, name) }.select { |path| File.exist?(path) }
|
|
95
|
+
return [File.join(@root, "AGENTS.md")] if candidates.empty?
|
|
96
|
+
|
|
97
|
+
candidates.uniq { |path| File.realpath(path) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def write_herdr_plugin
|
|
101
|
+
directory = File.join(@root, HERDR_DIR)
|
|
102
|
+
FileUtils.mkdir_p(directory)
|
|
103
|
+
manifest = File.join(directory, "herdr-plugin.toml")
|
|
104
|
+
script = File.join(directory, "provision")
|
|
105
|
+
app = File.basename(main_checkout).gsub(/[^A-Za-z0-9_.-]/, "-")
|
|
106
|
+
manifest_body = <<~TOML
|
|
107
|
+
# herdr plugin generated by rails-worktree. Link it once per machine:
|
|
108
|
+
# herdr plugin link #{HERDR_DIR}
|
|
109
|
+
# It provisions every worktree herdr creates for this repository.
|
|
110
|
+
id = "rails-worktree.#{app}"
|
|
111
|
+
name = "rails-worktree (#{app})"
|
|
112
|
+
version = "#{VERSION}"
|
|
113
|
+
min_herdr_version = "0.8.0"
|
|
114
|
+
description = "Provision new herdr worktrees with bin/worktree init"
|
|
115
|
+
platforms = ["linux", "macos"]
|
|
116
|
+
|
|
117
|
+
[[events]]
|
|
118
|
+
on = "worktree.created"
|
|
119
|
+
command = ["./provision"]
|
|
120
|
+
TOML
|
|
121
|
+
script_body = <<~'SH'
|
|
122
|
+
#!/bin/sh
|
|
123
|
+
# Runs bin/worktree init in the worktree that herdr just created.
|
|
124
|
+
# Skips checkouts of other repositories (no bin/worktree launcher).
|
|
125
|
+
set -eu
|
|
126
|
+
# herdr wraps the payload: {"event": "worktree_created", "data": {"worktree": {"path": ...}}}.
|
|
127
|
+
path=$(ruby -rjson -e 'event = JSON.parse(ENV.fetch("HERDR_PLUGIN_EVENT_JSON", "{}")); event = event["data"] || event; puts(event.dig("worktree", "path") || event["path"] || "")')
|
|
128
|
+
[ -n "$path" ] || { echo "rails-worktree: no worktree path in the event; skipped" >&2; exit 0; }
|
|
129
|
+
[ -x "$path/bin/worktree" ] || exit 0
|
|
130
|
+
here=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd -P)
|
|
131
|
+
main_of() { git -C "$1" worktree list --porcelain | head -n 1 | sed 's/^worktree //'; }
|
|
132
|
+
[ "$(main_of "$path")" = "$(main_of "$here")" ] || exit 0
|
|
133
|
+
cd "$path"
|
|
134
|
+
# herdr inherits the PATH of the shell that started it, which can put another Ruby first.
|
|
135
|
+
# mise exec selects the Ruby that the checkout's version file asks for.
|
|
136
|
+
if command -v mise >/dev/null 2>&1; then
|
|
137
|
+
exec mise exec -- bin/worktree init --level "${RAILS_WORKTREE_HERDR_LEVEL:-minimal}"
|
|
138
|
+
fi
|
|
139
|
+
exec bin/worktree init --level "${RAILS_WORKTREE_HERDR_LEVEL:-minimal}"
|
|
140
|
+
SH
|
|
141
|
+
write_if_changed(manifest, manifest_body)
|
|
142
|
+
write_if_changed(script, script_body, mode: 0755)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The main checkout's directory, so a worktree installs the same plugin id as the main checkout.
|
|
146
|
+
def main_checkout
|
|
147
|
+
output, _error, status = Open3.capture3(Context::GIT_ENVIRONMENT, "git", "-C", @root, "worktree", "list", "--porcelain")
|
|
148
|
+
first = status.success? && output.lines.grep(/^worktree /).first
|
|
149
|
+
first ? first.delete_prefix("worktree ").strip : @root
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def write_if_changed(path, body, mode: nil)
|
|
153
|
+
return if File.file?(path) && File.read(path) == body && (mode.nil? || File.executable?(path))
|
|
154
|
+
|
|
155
|
+
File.write(path, body)
|
|
156
|
+
File.chmod(mode, path) if mode
|
|
157
|
+
@changes << path.delete_prefix(@root + File::SEPARATOR)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def report
|
|
161
|
+
return if @quiet || @changes.empty?
|
|
162
|
+
|
|
163
|
+
@changes.each { |change| puts " wrote #{change}" }
|
|
17
164
|
end
|
|
18
165
|
end
|
|
19
166
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module RailsWorktree
|
|
2
|
+
# Writes a marked block into a text file. Repeated writes replace the block in
|
|
3
|
+
# place and leave the rest of the file untouched.
|
|
4
|
+
class ManagedBlock
|
|
5
|
+
def self.write(path, body, start:, finish:)
|
|
6
|
+
new(path, start: start, finish: finish).write(body)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def initialize(path, start:, finish:)
|
|
10
|
+
@path = path
|
|
11
|
+
@start = start
|
|
12
|
+
@finish = finish
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def write(body)
|
|
16
|
+
block = "#{@start}\n#{body.chomp}\n#{@finish}\n"
|
|
17
|
+
content = File.file?(@path) ? File.read(@path) : ""
|
|
18
|
+
updated =
|
|
19
|
+
if content.include?(@start) && content.include?(@finish)
|
|
20
|
+
content.sub(/#{Regexp.escape(@start)}\n.*?#{Regexp.escape(@finish)}\n?/m) { block }
|
|
21
|
+
elsif content.empty?
|
|
22
|
+
block
|
|
23
|
+
else
|
|
24
|
+
content + (content.end_with?("\n") ? "\n" : "\n\n") + block
|
|
25
|
+
end
|
|
26
|
+
return false if updated == content
|
|
27
|
+
|
|
28
|
+
File.write(@path, updated)
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/worktree/push_hook.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
require "fileutils"
|
|
2
2
|
require "open3"
|
|
3
|
-
require "rbconfig"
|
|
4
3
|
|
|
5
4
|
module RailsWorktree
|
|
6
5
|
class PushHook
|
|
@@ -10,13 +9,14 @@ module RailsWorktree
|
|
|
10
9
|
output, error, status = Open3.capture3(Context::GIT_ENVIRONMENT, "git", "-C", root, "rev-parse", "--git-path", "hooks/pre-push")
|
|
11
10
|
raise Error, "Cannot locate pre-push hook: #{error}" unless status.success?
|
|
12
11
|
path = File.expand_path(output.strip, root)
|
|
12
|
+
return warn_tracked(path) if tracked?(File.dirname(path))
|
|
13
13
|
return if File.file?(path) && File.read(path).include?(MARKER)
|
|
14
14
|
previous = "#{path}.before-rails-worktree"
|
|
15
15
|
raise Error, "Hook backup already exists: #{previous}; restore it before installing the worktree hook." if File.exist?(previous)
|
|
16
16
|
FileUtils.mkdir_p(File.dirname(path))
|
|
17
17
|
FileUtils.mv(path, previous) if File.exist?(path)
|
|
18
18
|
File.write(path, <<~RUBY)
|
|
19
|
-
|
|
19
|
+
#!/usr/bin/env ruby
|
|
20
20
|
#{MARKER}
|
|
21
21
|
require "open3"
|
|
22
22
|
require "tempfile"
|
|
@@ -44,5 +44,18 @@ module RailsWorktree
|
|
|
44
44
|
RUBY
|
|
45
45
|
File.chmod(0755, path)
|
|
46
46
|
end
|
|
47
|
+
|
|
48
|
+
def self.tracked?(directory)
|
|
49
|
+
return false unless File.directory?(directory)
|
|
50
|
+
inside, _, status = Open3.capture3(Context::GIT_ENVIRONMENT, "git", "-C", directory, "rev-parse", "--is-inside-work-tree")
|
|
51
|
+
return false unless status.success? && inside.strip == "true"
|
|
52
|
+
output, _, status = Open3.capture3(Context::GIT_ENVIRONMENT, "git", "-C", directory, "ls-files", "--", ".")
|
|
53
|
+
status.success? && !output.empty?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.warn_tracked(path)
|
|
57
|
+
warn "Skipped the pre-push hook: #{path} is in a directory that Git tracks. " \
|
|
58
|
+
"Run `bin/worktree verify --push` from that hook instead and pass it the hook's standard input."
|
|
59
|
+
end
|
|
47
60
|
end
|
|
48
61
|
end
|
data/lib/worktree/railtie.rb
CHANGED
|
@@ -8,11 +8,9 @@ module RailsWorktree
|
|
|
8
8
|
load "worktree/tasks.rb"
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
initializer "worktree.
|
|
12
|
-
# Install
|
|
13
|
-
if Rails.env.development?
|
|
14
|
-
Installer.install(Rails.root.to_s)
|
|
15
|
-
end
|
|
11
|
+
initializer "worktree.install" do
|
|
12
|
+
# Install the launcher and repository integration files when Rails loads in development.
|
|
13
|
+
Installer.install(Rails.root.to_s) if Rails.env.development?
|
|
16
14
|
end
|
|
17
15
|
end
|
|
18
16
|
end
|
data/lib/worktree/tasks.rb
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
namespace :worktree do
|
|
2
|
-
desc "Install bin/worktree and
|
|
2
|
+
desc "Install bin/worktree, .worktree.yml, .worktreeinclude, the AGENTS.md block and the herdr plugin"
|
|
3
3
|
task :install do
|
|
4
|
-
RailsWorktree::Installer.install(Dir.pwd, replace: ENV["FORCE"] == "1")
|
|
4
|
+
RailsWorktree::Installer.install(Dir.pwd, replace: ENV["FORCE"] == "1", quiet: false)
|
|
5
5
|
|
|
6
|
-
puts "✓
|
|
7
|
-
puts "Usage: bin/worktree <name> <base
|
|
6
|
+
puts "✓ rails-worktree is installed"
|
|
7
|
+
puts "Usage: bin/worktree create <name> <base>"
|
|
8
|
+
puts "Link the herdr plugin once with: herdr plugin link #{RailsWorktree::Installer::HERDR_DIR}"
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
desc "Uninstall worktree binstub from bin/worktree"
|
data/lib/worktree/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-worktree
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Martin Ulleberg
|
|
@@ -22,13 +22,18 @@ files:
|
|
|
22
22
|
- exe/worktree
|
|
23
23
|
- lib/rails-worktree.rb
|
|
24
24
|
- lib/worktree/cli.rb
|
|
25
|
+
- lib/worktree/clone_database.rb
|
|
25
26
|
- lib/worktree/commands/close.rb
|
|
26
27
|
- lib/worktree/commands/create.rb
|
|
27
28
|
- lib/worktree/commands/exec.rb
|
|
28
29
|
- lib/worktree/commands/init.rb
|
|
30
|
+
- lib/worktree/config.rb
|
|
29
31
|
- lib/worktree/context.rb
|
|
32
|
+
- lib/worktree/copy_sqlite.rb
|
|
33
|
+
- lib/worktree/database_probe.rb
|
|
30
34
|
- lib/worktree/installer.rb
|
|
31
35
|
- lib/worktree/launcher.rb
|
|
36
|
+
- lib/worktree/managed_block.rb
|
|
32
37
|
- lib/worktree/push_hook.rb
|
|
33
38
|
- lib/worktree/railtie.rb
|
|
34
39
|
- lib/worktree/tasks.rb
|
|
@@ -46,7 +51,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
46
51
|
requirements:
|
|
47
52
|
- - ">="
|
|
48
53
|
- !ruby/object:Gem::Version
|
|
49
|
-
version:
|
|
54
|
+
version: 3.0.0
|
|
50
55
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
51
56
|
requirements:
|
|
52
57
|
- - ">="
|