rails-worktree 0.1.6 → 0.2.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 +4 -4
- data/README.md +66 -3
- data/lib/rails-worktree.rb +9 -4
- data/lib/worktree/cli.rb +19 -5
- data/lib/worktree/commands/close.rb +11 -14
- data/lib/worktree/commands/create.rb +28 -9
- data/lib/worktree/commands/exec.rb +19 -0
- data/lib/worktree/commands/init.rb +31 -16
- data/lib/worktree/context.rb +132 -0
- data/lib/worktree/installer.rb +19 -0
- data/lib/worktree/launcher.rb +22 -0
- data/lib/worktree/push_hook.rb +48 -0
- data/lib/worktree/railtie.rb +1 -17
- data/lib/worktree/tasks.rb +4 -20
- data/lib/worktree/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cd8698c3bbb798a625214a84c2f0e6cc585b4759cd9ef6ae9d3806eb6e6c572a
|
|
4
|
+
data.tar.gz: 98611afb823f7b713a6c12412232d1cc6e85ef023eaad9b0518bb211aa41e628
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 71ce31463125cb9756e54f0b66f0b153f39cc8658a9af88c6d8a2a726c22521b5ac5649d5b0a204bfc9775f4c925bfe77648120c734a13953b77eff3781345d2
|
|
7
|
+
data.tar.gz: 96f6e366bdd38b682df8735b368ac26c2b56398bb750d278b0d1556af99cdb7cd97ff811628ce6ed53aaf8d9f061717488d8a84757f354b2d4d03f67802aaed0
|
data/README.md
CHANGED
|
@@ -4,7 +4,9 @@ Git worktree management for Rails projects with isolated databases and configura
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- Create git worktrees with automatic branch creation
|
|
7
|
+
- Create git worktrees from an explicit base with automatic branch creation
|
|
8
|
+
- Run commands with worktree-local Bundler and binstubs
|
|
9
|
+
- Verify the recorded base before execution and push
|
|
8
10
|
- Isolated databases per worktree (separate development and test databases)
|
|
9
11
|
- Automatic configuration file copying
|
|
10
12
|
- Copy node_modules from main worktree
|
|
@@ -21,6 +23,8 @@ end
|
|
|
21
23
|
```
|
|
22
24
|
|
|
23
25
|
Run `bundle install`. A binstub will be automatically created at `bin/worktree`.
|
|
26
|
+
When Rails loads in development, installation also adds `/.worktrees/` to the project's
|
|
27
|
+
`.gitignore`. The manual installer does the same, including when the binstub already exists.
|
|
24
28
|
|
|
25
29
|
Manual binstub installation (if needed):
|
|
26
30
|
```bash
|
|
@@ -32,16 +36,75 @@ bundle exec rake worktree:install
|
|
|
32
36
|
### Create a worktree
|
|
33
37
|
|
|
34
38
|
```bash
|
|
35
|
-
|
|
36
|
-
bin/worktree feature-branch main
|
|
39
|
+
git fetch origin
|
|
40
|
+
bin/worktree feature-branch origin/main # Explicit base required
|
|
37
41
|
```
|
|
38
42
|
|
|
43
|
+
The base is required: omitting it fails before creating a branch or directory. The exact local
|
|
44
|
+
commit resolved from that reference is recorded; creation does not fetch or change your base branch.
|
|
45
|
+
Worktrees are created under `.worktrees/` in the repository root, including when invoked from a subdirectory.
|
|
46
|
+
|
|
39
47
|
This creates a new worktree with:
|
|
40
48
|
- Separate databases: `myapp_feature-branch_development` and `myapp_feature-branch_test`
|
|
41
49
|
- Copied configuration files (`.env`, `database.yml`, `Procfile.dev`, credentials)
|
|
42
50
|
- Copied `node_modules`
|
|
43
51
|
- Migrated and seeded databases
|
|
44
52
|
|
|
53
|
+
### Run commands in the worktree
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cd .worktrees/feature-branch
|
|
57
|
+
bin/worktree exec -- bundle exec rspec
|
|
58
|
+
bin/worktree exec -- bin/rails db:migrate
|
|
59
|
+
bin/worktree verify
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`exec` runs from the Git worktree root, including when called from a subdirectory. It checks the
|
|
63
|
+
recorded branch and base ancestry, verifies Bundler's effective root in a fresh Ruby process, and
|
|
64
|
+
pins `BUNDLE_GEMFILE` to this worktree. It clears inherited Bundler state and Git directory overrides,
|
|
65
|
+
removes other repositories' `bin` directories from PATH, and puts this checkout's `bin` first.
|
|
66
|
+
An explicit `bundle` command uses Bundler's gem executable rather than an inherited project binstub.
|
|
67
|
+
Initialization commands use the same isolated environment.
|
|
68
|
+
|
|
69
|
+
Direct commands, `bundle exec` commands, and Ruby script invocations reject binstubs that resolve
|
|
70
|
+
into another repository, including symlinks. Arguments are passed directly without shell expansion;
|
|
71
|
+
command exit status is preserved. Use an explicit `sh -c` when shell syntax is intended. This wrapper
|
|
72
|
+
is an execution-context guard, not a sandbox: it does not inspect shell scripts or prevent application
|
|
73
|
+
code from spawning commands elsewhere. Commands run outside the wrapper are not intercepted.
|
|
74
|
+
|
|
75
|
+
Generated `bin/worktree` launchers are shell scripts that set `BUNDLE_GEMFILE` before starting Ruby,
|
|
76
|
+
so an inherited `RUBYOPT=-rbundler/setup` cannot activate the previous checkout's bundle first.
|
|
77
|
+
Both automatic installation and the Rake installer use the same generator. To replace an existing
|
|
78
|
+
launcher in a project after updating the gem:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
bundle exec rake worktree:install FORCE=1
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Creation replaces the new worktree's launcher with the current generator. Symlinked launchers
|
|
85
|
+
are refused rather than followed. Existing worktrees without recorded metadata cannot use `exec`
|
|
86
|
+
or `verify`; create a managed worktree from an explicit base to use these checks.
|
|
87
|
+
|
|
88
|
+
### Verify before pushing
|
|
89
|
+
|
|
90
|
+
Creation stores the base reference, its resolved commit, and the feature branch in the private
|
|
91
|
+
`rails-worktree.json` file under `git rev-parse --git-dir`. It does not add this metadata to tracked
|
|
92
|
+
project files. The recorded commit remains fixed even if the base reference advances.
|
|
93
|
+
|
|
94
|
+
Creation also installs a pre-push wrapper at Git's configured hook location, including a custom
|
|
95
|
+
`core.hooksPath`. An existing hook is preserved as `pre-push.before-rails-worktree` and runs after
|
|
96
|
+
verification with the same arguments and input. Its exit status is preserved. Repeated installation
|
|
97
|
+
is idempotent; an unexpected existing backup causes an actionable error instead of overwriting it.
|
|
98
|
+
|
|
99
|
+
In managed worktrees the hook checks the branch, base ancestry, and bundle root, then prints the
|
|
100
|
+
change summary against the recorded base. Non-deletion updates must push the current HEAD; push
|
|
101
|
+
other branches from their own checkout. In checkouts without metadata, the wrapper only runs the
|
|
102
|
+
previous hook. Git hooks are shared by worktrees unless configured otherwise.
|
|
103
|
+
|
|
104
|
+
A branch mismatch tells you to switch back; a base mismatch tells you to rebase onto the recorded
|
|
105
|
+
base. These checks cannot decide whether you chose the right base or whether a change belongs in
|
|
106
|
+
the feature. Review the printed diff before publishing. Git's `--no-verify` still bypasses hooks.
|
|
107
|
+
|
|
45
108
|
### Close a worktree
|
|
46
109
|
|
|
47
110
|
```bash
|
data/lib/rails-worktree.rb
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
+
module RailsWorktree
|
|
2
|
+
class Error < StandardError; end
|
|
3
|
+
end
|
|
4
|
+
|
|
1
5
|
require_relative "worktree/version"
|
|
6
|
+
require_relative "worktree/context"
|
|
7
|
+
require_relative "worktree/launcher"
|
|
8
|
+
require_relative "worktree/installer"
|
|
9
|
+
require_relative "worktree/push_hook"
|
|
2
10
|
require_relative "worktree/cli"
|
|
3
11
|
require_relative "worktree/commands/create"
|
|
4
12
|
require_relative "worktree/commands/init"
|
|
5
13
|
require_relative "worktree/commands/close"
|
|
14
|
+
require_relative "worktree/commands/exec"
|
|
6
15
|
|
|
7
16
|
# Load Railtie only if Rails is available
|
|
8
17
|
require_relative "worktree/railtie" if defined?(Rails::Railtie)
|
|
9
|
-
|
|
10
|
-
module RailsWorktree
|
|
11
|
-
class Error < StandardError; end
|
|
12
|
-
end
|
data/lib/worktree/cli.rb
CHANGED
|
@@ -5,7 +5,7 @@ module RailsWorktree
|
|
|
5
5
|
end
|
|
6
6
|
|
|
7
7
|
def initialize(args)
|
|
8
|
-
@args = args
|
|
8
|
+
@args = args.dup
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def run
|
|
@@ -14,6 +14,13 @@ module RailsWorktree
|
|
|
14
14
|
exit 1
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
case @args.first
|
|
18
|
+
when "exec"
|
|
19
|
+
return Commands::Exec.new(@args.drop(1)).run
|
|
20
|
+
when "verify"
|
|
21
|
+
return Context.new.verify(push: @args.include?("--push"))
|
|
22
|
+
end
|
|
23
|
+
|
|
17
24
|
# Extract flags
|
|
18
25
|
@skip_seeds = @args.delete("--skip-seeds")
|
|
19
26
|
|
|
@@ -30,20 +37,27 @@ module RailsWorktree
|
|
|
30
37
|
# Default: create worktree
|
|
31
38
|
Commands::Create.new(@args, skip_seeds: @skip_seeds).run
|
|
32
39
|
end
|
|
40
|
+
rescue Error => error
|
|
41
|
+
warn "Error: #{error.message}"
|
|
42
|
+
exit 1
|
|
33
43
|
end
|
|
34
44
|
|
|
35
45
|
private
|
|
36
46
|
|
|
37
47
|
def print_usage
|
|
38
48
|
puts <<~USAGE
|
|
39
|
-
Usage: worktree <name>
|
|
49
|
+
Usage: worktree <name> <base-branch> [options]
|
|
40
50
|
worktree --close [worktree-name]
|
|
51
|
+
worktree exec -- <command> [arguments]
|
|
52
|
+
worktree verify [--push]
|
|
41
53
|
worktree --init <worktree-name> [options]
|
|
42
54
|
|
|
43
55
|
Creates a new git worktree and initializes it with configuration
|
|
44
56
|
|
|
45
57
|
Commands:
|
|
46
58
|
<name> Create a new worktree with the given name
|
|
59
|
+
exec -- COMMAND Run a command with verified worktree context
|
|
60
|
+
verify Check the recorded base and bundle root
|
|
47
61
|
--close [name] Close and remove a worktree
|
|
48
62
|
--init <name> Initialize a worktree (usually called automatically)
|
|
49
63
|
--help, -h Show this help message
|
|
@@ -53,11 +67,11 @@ module RailsWorktree
|
|
|
53
67
|
|
|
54
68
|
Arguments:
|
|
55
69
|
<name> Name of the worktree (required)
|
|
56
|
-
|
|
70
|
+
<base-branch> Explicit branch or commit to create worktree from (required)
|
|
57
71
|
|
|
58
72
|
Examples:
|
|
59
|
-
worktree feature-branch
|
|
60
|
-
worktree feature-branch --skip-seeds # Create without seeding database
|
|
73
|
+
worktree feature-branch origin/main # Create from an explicit base
|
|
74
|
+
worktree feature-branch origin/main --skip-seeds # Create without seeding database
|
|
61
75
|
worktree --close feature-branch # Close worktree from main repo
|
|
62
76
|
worktree --close # Close worktree from within it
|
|
63
77
|
USAGE
|
|
@@ -22,11 +22,11 @@ module RailsWorktree
|
|
|
22
22
|
puts "Main worktree: #{@main_worktree}"
|
|
23
23
|
puts ""
|
|
24
24
|
|
|
25
|
-
# Move to main worktree early so we're not inside the directory we're about to delete
|
|
26
|
-
Dir.chdir(@main_worktree) unless @in_main_repo
|
|
27
|
-
|
|
28
25
|
drop_databases
|
|
29
26
|
remove_node_modules
|
|
27
|
+
|
|
28
|
+
# Move to main worktree so we're not inside the directory we're about to delete
|
|
29
|
+
Dir.chdir(@main_worktree) unless @in_main_repo
|
|
30
30
|
remove_worktree
|
|
31
31
|
prune_worktrees
|
|
32
32
|
delete_branch
|
|
@@ -79,7 +79,7 @@ module RailsWorktree
|
|
|
79
79
|
def detect_paths
|
|
80
80
|
if @current_dir == @main_worktree
|
|
81
81
|
@in_main_repo = true
|
|
82
|
-
@worktree_path = File.join(
|
|
82
|
+
@worktree_path = File.join(@main_worktree, ".worktrees", @worktree_name)
|
|
83
83
|
@worktree_dir = @worktree_path
|
|
84
84
|
else
|
|
85
85
|
@in_main_repo = false
|
|
@@ -91,16 +91,13 @@ module RailsWorktree
|
|
|
91
91
|
def drop_databases
|
|
92
92
|
puts "Dropping databases..."
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
puts "Dropped database '#{@test_database_name}'"
|
|
102
|
-
else
|
|
103
|
-
puts "Warning: Could not drop test database #{@test_database_name}"
|
|
94
|
+
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"
|
|
100
|
+
end
|
|
104
101
|
end
|
|
105
102
|
end
|
|
106
103
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
1
3
|
module RailsWorktree
|
|
2
4
|
module Commands
|
|
3
5
|
class Create
|
|
@@ -13,13 +15,31 @@ module RailsWorktree
|
|
|
13
15
|
exit 1
|
|
14
16
|
end
|
|
15
17
|
|
|
16
|
-
@
|
|
17
|
-
|
|
18
|
+
raise Error, "Base branch required: bin/worktree #{@worktree_name} BASE (for example origin/main)." unless @base_branch
|
|
19
|
+
context = Context.new
|
|
20
|
+
Dir.chdir(context.root) { create }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def create
|
|
26
|
+
_output, error, status = Open3.capture3(Context::GIT_ENVIRONMENT, "git", "check-ref-format", "--branch", @worktree_name)
|
|
27
|
+
raise Error, "Invalid worktree branch: #{error.strip}" unless status.success?
|
|
28
|
+
output, error, status = Open3.capture3(Context::GIT_ENVIRONMENT, "git", "rev-parse", "--verify", "--end-of-options", "#{@base_branch}^{commit}")
|
|
29
|
+
raise Error, "Invalid base #{@base_branch}: #{error.strip}" unless status.success?
|
|
30
|
+
base_commit = output.strip
|
|
31
|
+
worktree_dir = ".worktrees"
|
|
32
|
+
worktree_path = "#{worktree_dir}/#{@worktree_name}"
|
|
18
33
|
absolute_path = File.expand_path(worktree_path)
|
|
19
34
|
|
|
35
|
+
raise Error, "Worktree name must stay inside .worktrees" unless absolute_path.start_with?(File.expand_path(worktree_dir) + File::SEPARATOR)
|
|
36
|
+
|
|
37
|
+
Installer.ignore_worktrees(Dir.pwd)
|
|
38
|
+
FileUtils.mkdir_p(worktree_dir)
|
|
39
|
+
|
|
20
40
|
puts "Creating worktree '#{@worktree_name}' from branch '#{@base_branch}' at #{worktree_path}..."
|
|
21
41
|
|
|
22
|
-
unless system("git worktree add -b
|
|
42
|
+
unless system(Context::GIT_ENVIRONMENT, "git", "worktree", "add", "-b", @worktree_name, worktree_path, base_commit)
|
|
23
43
|
puts "Failed to create worktree"
|
|
24
44
|
exit 1
|
|
25
45
|
end
|
|
@@ -30,6 +50,11 @@ module RailsWorktree
|
|
|
30
50
|
puts "Initializing worktree..."
|
|
31
51
|
|
|
32
52
|
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)
|
|
33
58
|
Init.new([@worktree_name], skip_seeds: @skip_seeds).run
|
|
34
59
|
end
|
|
35
60
|
|
|
@@ -39,12 +64,6 @@ module RailsWorktree
|
|
|
39
64
|
puts ""
|
|
40
65
|
puts "To start the development server: bin/dev"
|
|
41
66
|
end
|
|
42
|
-
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
def current_branch
|
|
46
|
-
`git branch --show-current`.strip
|
|
47
|
-
end
|
|
48
67
|
end
|
|
49
68
|
end
|
|
50
69
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module RailsWorktree
|
|
2
|
+
module Commands
|
|
3
|
+
class Exec
|
|
4
|
+
def initialize(args)
|
|
5
|
+
@args = args.dup
|
|
6
|
+
@args.shift if @args.first == "--"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def run
|
|
10
|
+
context = Context.new
|
|
11
|
+
context.verify
|
|
12
|
+
command = context.command(@args)
|
|
13
|
+
system(context.environment, [command.first, command.first], *command.drop(1), chdir: context.root, unsetenv_others: true)
|
|
14
|
+
status = $?
|
|
15
|
+
exit(status.exitstatus || 128 + status.termsig)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -25,6 +25,9 @@ module RailsWorktree
|
|
|
25
25
|
puts "Development database: #{@dev_database_name}"
|
|
26
26
|
puts "Test database: #{@test_database_name}"
|
|
27
27
|
|
|
28
|
+
@context = Context.new
|
|
29
|
+
@context_environment = @context.environment
|
|
30
|
+
|
|
28
31
|
copy_config_files
|
|
29
32
|
set_database_names
|
|
30
33
|
update_database_yml
|
|
@@ -65,9 +68,11 @@ module RailsWorktree
|
|
|
65
68
|
".env",
|
|
66
69
|
"config/database.yml",
|
|
67
70
|
"Procfile.dev",
|
|
68
|
-
"config/
|
|
71
|
+
"config/master.key"
|
|
69
72
|
]
|
|
70
73
|
|
|
74
|
+
files_to_copy += Dir.glob("config/credentials/*", base: @main_worktree)
|
|
75
|
+
|
|
71
76
|
files_to_copy.each do |file|
|
|
72
77
|
source = File.join(@main_worktree, file)
|
|
73
78
|
if File.exist?(source)
|
|
@@ -112,13 +117,17 @@ module RailsWorktree
|
|
|
112
117
|
|
|
113
118
|
content = File.read(database_yml)
|
|
114
119
|
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
# Use a single-pass replacement to avoid one substitution creating
|
|
121
|
+
# a substring that matches the other pattern (e.g., replacing
|
|
122
|
+
# "app_development" first produces "app_test-branch_development",
|
|
123
|
+
# which contains "app_test" and would be wrongly matched by a
|
|
124
|
+
# second gsub for the test pattern).
|
|
125
|
+
replacements = {
|
|
126
|
+
"#{@db_prefix}_development" => @dev_database_name,
|
|
127
|
+
"#{@db_prefix}_test" => @test_database_name
|
|
128
|
+
}
|
|
129
|
+
pattern = Regexp.union(replacements.keys)
|
|
130
|
+
content.gsub!(pattern) { |match| replacements[match] }
|
|
122
131
|
|
|
123
132
|
File.write(database_yml, content)
|
|
124
133
|
end
|
|
@@ -135,28 +144,34 @@ module RailsWorktree
|
|
|
135
144
|
end
|
|
136
145
|
|
|
137
146
|
def database_env
|
|
138
|
-
{
|
|
147
|
+
(@context_environment || {}).merge(
|
|
148
|
+
"BUNDLE_GEMFILE" => File.expand_path("Gemfile"),
|
|
139
149
|
"DATABASE_NAME_DEVELOPMENT" => @dev_database_name,
|
|
140
150
|
"DATABASE_NAME_TEST" => @test_database_name
|
|
141
|
-
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def run_command(environment, *arguments)
|
|
155
|
+
command = @context ? @context.command(arguments) : arguments
|
|
156
|
+
system(environment, *command, unsetenv_others: !!@context_environment)
|
|
142
157
|
end
|
|
143
158
|
|
|
144
159
|
def setup_database
|
|
145
160
|
if File.executable?("bin/setup")
|
|
146
161
|
puts "Running bin/setup..."
|
|
147
|
-
|
|
162
|
+
run_command(database_env, "bin/setup") || puts("Warning: bin/setup failed")
|
|
148
163
|
else
|
|
149
164
|
puts "Creating databases..."
|
|
150
|
-
|
|
151
|
-
|
|
165
|
+
run_command(database_env.merge("RAILS_ENV" => "development"), "bin/rails", "db:create") || puts("Warning: Could not create development database")
|
|
166
|
+
run_command(database_env.merge("RAILS_ENV" => "test"), "bin/rails", "db:create") || puts("Warning: Could not create test database")
|
|
152
167
|
|
|
153
168
|
puts "Running migrations..."
|
|
154
|
-
|
|
155
|
-
|
|
169
|
+
run_command(database_env.merge("RAILS_ENV" => "development"), "bin/rails", "db:migrate") || puts("Warning: Could not run migrations")
|
|
170
|
+
run_command(database_env.merge("RAILS_ENV" => "test"), "bin/rails", "db:migrate") || puts("Warning: Could not run test migrations")
|
|
156
171
|
|
|
157
172
|
unless @skip_seeds
|
|
158
173
|
puts "Seeding development database..."
|
|
159
|
-
|
|
174
|
+
run_command(database_env.merge("RAILS_ENV" => "development"), "bin/rails", "db:seed") || puts("Warning: Could not seed database")
|
|
160
175
|
else
|
|
161
176
|
puts "Skipping database seeding..."
|
|
162
177
|
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "open3"
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
require "bundler"
|
|
5
|
+
|
|
6
|
+
module RailsWorktree
|
|
7
|
+
class Context
|
|
8
|
+
GIT_ENVIRONMENT = {"GIT_DIR" => nil, "GIT_WORK_TREE" => nil, "GIT_INDEX_FILE" => nil, "GIT_COMMON_DIR" => nil}.freeze
|
|
9
|
+
|
|
10
|
+
attr_reader :root
|
|
11
|
+
|
|
12
|
+
def initialize(directory = Dir.pwd)
|
|
13
|
+
@root = File.realpath(git(directory, "rev-parse", "--show-toplevel").strip)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def record(base, commit, branch)
|
|
17
|
+
File.write(metadata_path, JSON.pretty_generate({"base" => base, "commit" => commit, "branch" => branch}) + "\n")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def verify(push: false)
|
|
21
|
+
data = JSON.parse(File.read(metadata_path))
|
|
22
|
+
raise Error, "Invalid worktree context in #{metadata_path}; expected an object with base, commit and branch." unless data.is_a?(Hash)
|
|
23
|
+
verify_branch(data)
|
|
24
|
+
verify_base(data)
|
|
25
|
+
verify_bundle
|
|
26
|
+
verify_push(data) if push
|
|
27
|
+
data
|
|
28
|
+
rescue Errno::ENOENT, JSON::ParserError, KeyError => error
|
|
29
|
+
raise Error, "Missing or invalid worktree context. Recreate with an explicit base using worktree NAME BASE. #{error.message}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def environment
|
|
33
|
+
env = Bundler.respond_to?(:unbundled_env) ? Bundler.unbundled_env : Bundler.clean_env
|
|
34
|
+
env["PATH"] = executable_path(env.fetch("PATH", ""))
|
|
35
|
+
env["BUNDLE_GEMFILE"] = File.join(root, "Gemfile")
|
|
36
|
+
env.delete("BUNDLE_BIN_PATH")
|
|
37
|
+
GIT_ENVIRONMENT.each_key { |key| env.delete(key) }
|
|
38
|
+
env
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def command(arguments)
|
|
42
|
+
raise Error, "Command required: bin/worktree exec -- bundle exec rspec" if arguments.empty?
|
|
43
|
+
arguments = arguments.dup
|
|
44
|
+
invoked = arguments[0, 2] == ["bundle", "exec"] ? arguments.drop(2) : arguments
|
|
45
|
+
candidates = invoked.first == "bundle" ? [] : [invoked.first]
|
|
46
|
+
candidates << invoked[1] if invoked.first && File.basename(invoked.first) == "ruby"
|
|
47
|
+
candidates.compact.each { |path| verify_binstub(path) }
|
|
48
|
+
if arguments.first == "bundle"
|
|
49
|
+
arguments.shift
|
|
50
|
+
[RbConfig.ruby, Gem.bin_path("bundler", "bundle"), *arguments]
|
|
51
|
+
else
|
|
52
|
+
arguments
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def metadata_path
|
|
57
|
+
File.expand_path(git(root, "rev-parse", "--git-path", "rails-worktree.json").strip, root)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def executable_path(path)
|
|
63
|
+
worktree_bins = git(root, "worktree", "list", "--porcelain").lines.grep(/^worktree /).map do |line|
|
|
64
|
+
File.join(line.delete_prefix("worktree ").strip, "bin")
|
|
65
|
+
end
|
|
66
|
+
paths = path.split(File::PATH_SEPARATOR).reject do |directory|
|
|
67
|
+
worktree_bins.include?(directory) || foreign_bin_directory?(directory)
|
|
68
|
+
end
|
|
69
|
+
([File.join(root, "bin")] + paths).uniq.join(File::PATH_SEPARATOR)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def verify_branch(data)
|
|
73
|
+
branch = git(root, "branch", "--show-current").strip
|
|
74
|
+
raise Error, "Worktree branch differs from recorded #{data.fetch("branch")}; switch back before continuing." unless branch == data.fetch("branch")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def verify_base(data)
|
|
78
|
+
git(root, "merge-base", "--is-ancestor", data.fetch("commit"), "HEAD")
|
|
79
|
+
rescue Error
|
|
80
|
+
raise Error, "Recorded base #{data.fetch("base")} is no longer an ancestor of HEAD. Rebase onto that base before continuing."
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def verify_bundle
|
|
84
|
+
gemfile = File.join(root, "Gemfile")
|
|
85
|
+
raise Error, "Missing worktree Gemfile: #{gemfile}" unless File.file?(gemfile) && File.realpath(gemfile) == gemfile
|
|
86
|
+
|
|
87
|
+
output, error, status = Open3.capture3(environment, RbConfig.ruby, "-rbundler", "-e", "puts Bundler.root.realpath", chdir: root, unsetenv_others: true)
|
|
88
|
+
unless status.success? && output.strip == root
|
|
89
|
+
raise Error, "Bundle resolves outside this worktree or cannot load. Run through bin/worktree exec. #{error.strip}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def verify_push(data)
|
|
94
|
+
unless $stdin.tty?
|
|
95
|
+
head = git(root, "rev-parse", "HEAD").strip
|
|
96
|
+
$stdin.each_line do |line|
|
|
97
|
+
_local_ref, sha, _remote_ref, _remote_sha = line.split
|
|
98
|
+
next if sha && sha.match?(/\A0+\z/)
|
|
99
|
+
raise Error, "Push includes a commit other than this worktree's HEAD. Push the current branch separately." unless sha == head
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
puts "Worktree base: #{data.fetch("base")} (#{data.fetch("commit")})"
|
|
103
|
+
puts git(root, "diff", "--stat", "#{data.fetch("commit")}...HEAD", "--")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def foreign_bin_directory?(path)
|
|
107
|
+
return false unless File.basename(path) == "bin" && File.directory?(path)
|
|
108
|
+
output, _error, status = Open3.capture3(GIT_ENVIRONMENT, "git", "-C", path, "rev-parse", "--show-toplevel")
|
|
109
|
+
status.success? && File.realpath(output.strip) != root
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def verify_binstub(path)
|
|
113
|
+
resolved = if path.include?(File::SEPARATOR)
|
|
114
|
+
File.expand_path(path, root)
|
|
115
|
+
else
|
|
116
|
+
environment.fetch("PATH").split(File::PATH_SEPARATOR).map { |dir| File.join(dir, path) }.find { |file| File.executable?(file) && !File.directory?(file) }
|
|
117
|
+
end
|
|
118
|
+
return unless resolved && File.exist?(resolved)
|
|
119
|
+
resolved = File.realpath(resolved)
|
|
120
|
+
return if resolved.start_with?(root + File::SEPARATOR)
|
|
121
|
+
return unless resolved.include?("/bin/")
|
|
122
|
+
_output, _error, status = Open3.capture3(GIT_ENVIRONMENT, "git", "-C", File.dirname(resolved), "rev-parse", "--show-toplevel")
|
|
123
|
+
raise Error, "Binstub resolves outside this worktree: #{resolved}. Use this checkout's binstub through bin/worktree exec." if status.success?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def git(directory, *arguments)
|
|
127
|
+
output, error, status = Open3.capture3(GIT_ENVIRONMENT, "git", "-C", directory, *arguments)
|
|
128
|
+
raise Error, "Git context check failed (#{arguments.join(" ")}): #{error.strip}" unless status.success?
|
|
129
|
+
output
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module RailsWorktree
|
|
2
|
+
class Installer
|
|
3
|
+
def self.install(root, replace: false)
|
|
4
|
+
Launcher.install(root, replace: replace)
|
|
5
|
+
ignore_worktrees(root)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def self.ignore_worktrees(root)
|
|
9
|
+
path = File.join(root, ".gitignore")
|
|
10
|
+
content = File.exist?(path) ? File.read(path) : ""
|
|
11
|
+
return if content.lines.any? { |line| %w[ .worktrees .worktrees/ /.worktrees /.worktrees/ ].include?(line.strip) }
|
|
12
|
+
|
|
13
|
+
File.open(path, "a") do |file|
|
|
14
|
+
file.puts unless content.empty? || content.end_with?("\n")
|
|
15
|
+
file.puts "/.worktrees/"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module RailsWorktree
|
|
4
|
+
class Launcher
|
|
5
|
+
def self.install(root, replace: false)
|
|
6
|
+
path = File.join(root, "bin/worktree")
|
|
7
|
+
return if File.exist?(path) && !replace
|
|
8
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
9
|
+
raise Error, "Launcher points outside this worktree: #{path}" unless File.realpath(File.dirname(path)).start_with?(File.realpath(root) + File::SEPARATOR)
|
|
10
|
+
raise Error, "Refusing to replace a symlinked launcher: #{path}" if File.symlink?(path)
|
|
11
|
+
File.write(path, <<~'SH')
|
|
12
|
+
#!/bin/sh
|
|
13
|
+
# Set the bundle before Ruby processes an inherited -rbundler/setup.
|
|
14
|
+
worktree_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd -P) || exit 1
|
|
15
|
+
export BUNDLE_GEMFILE="$worktree_root/Gemfile"
|
|
16
|
+
unset BUNDLE_BIN_PATH
|
|
17
|
+
exec ruby -e 'require "bundler/setup"; require "rails-worktree"; RailsWorktree::CLI.run(ARGV)' -- "$@"
|
|
18
|
+
SH
|
|
19
|
+
File.chmod(0755, path)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "open3"
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
|
|
5
|
+
module RailsWorktree
|
|
6
|
+
class PushHook
|
|
7
|
+
MARKER = "# rails-worktree context hook"
|
|
8
|
+
|
|
9
|
+
def self.install(root)
|
|
10
|
+
output, error, status = Open3.capture3(Context::GIT_ENVIRONMENT, "git", "-C", root, "rev-parse", "--git-path", "hooks/pre-push")
|
|
11
|
+
raise Error, "Cannot locate pre-push hook: #{error}" unless status.success?
|
|
12
|
+
path = File.expand_path(output.strip, root)
|
|
13
|
+
return if File.file?(path) && File.read(path).include?(MARKER)
|
|
14
|
+
previous = "#{path}.before-rails-worktree"
|
|
15
|
+
raise Error, "Hook backup already exists: #{previous}; restore it before installing the worktree hook." if File.exist?(previous)
|
|
16
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
17
|
+
FileUtils.mv(path, previous) if File.exist?(path)
|
|
18
|
+
File.write(path, <<~RUBY)
|
|
19
|
+
#!#{RbConfig.ruby}
|
|
20
|
+
#{MARKER}
|
|
21
|
+
require "open3"
|
|
22
|
+
require "tempfile"
|
|
23
|
+
root, _, status = Open3.capture3("git", "rev-parse", "--show-toplevel")
|
|
24
|
+
exit 1 unless status.success?
|
|
25
|
+
root = root.strip
|
|
26
|
+
metadata, _, status = Open3.capture3("git", "rev-parse", "--git-path", "rails-worktree.json")
|
|
27
|
+
exit 1 unless status.success?
|
|
28
|
+
Tempfile.create("worktree-push") do |input|
|
|
29
|
+
input.write($stdin.read)
|
|
30
|
+
input.flush
|
|
31
|
+
input.rewind
|
|
32
|
+
if File.file?(File.expand_path(metadata.strip, root))
|
|
33
|
+
launcher = File.join(root, "bin/worktree")
|
|
34
|
+
abort "Missing bin/worktree; reinstall the worktree launcher before pushing." unless File.executable?(launcher)
|
|
35
|
+
exit 1 unless system({"BUNDLE_GEMFILE" => File.join(root, "Gemfile")}, launcher, "verify", "--push", in: input)
|
|
36
|
+
end
|
|
37
|
+
previous = __FILE__ + ".before-rails-worktree"
|
|
38
|
+
if File.executable?(previous)
|
|
39
|
+
input.rewind
|
|
40
|
+
system([previous, previous], *ARGV, in: input)
|
|
41
|
+
exit($?.exitstatus || 128 + $?.termsig)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
RUBY
|
|
45
|
+
File.chmod(0755, path)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/worktree/railtie.rb
CHANGED
|
@@ -11,23 +11,7 @@ module RailsWorktree
|
|
|
11
11
|
initializer "worktree.install_binstub" do
|
|
12
12
|
# Install binstub automatically when Rails loads in development
|
|
13
13
|
if Rails.env.development?
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
unless File.exist?(binstub_path)
|
|
17
|
-
Rails.logger.info "Installing worktree binstub to bin/worktree..."
|
|
18
|
-
|
|
19
|
-
File.write(binstub_path, <<~RUBY)
|
|
20
|
-
#!/usr/bin/env ruby
|
|
21
|
-
|
|
22
|
-
require "bundler/setup"
|
|
23
|
-
require "rails-worktree"
|
|
24
|
-
|
|
25
|
-
RailsWorktree::CLI.run(ARGV)
|
|
26
|
-
RUBY
|
|
27
|
-
|
|
28
|
-
FileUtils.chmod("+x", binstub_path)
|
|
29
|
-
Rails.logger.info "✓ Worktree binstub installed! Use: bin/worktree <name>"
|
|
30
|
-
end
|
|
14
|
+
Installer.install(Rails.root.to_s)
|
|
31
15
|
end
|
|
32
16
|
end
|
|
33
17
|
end
|
data/lib/worktree/tasks.rb
CHANGED
|
@@ -1,26 +1,10 @@
|
|
|
1
1
|
namespace :worktree do
|
|
2
|
-
desc "Install worktree
|
|
2
|
+
desc "Install bin/worktree and ignore local .worktrees"
|
|
3
3
|
task :install do
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
if File.exist?(binstub_path)
|
|
7
|
-
puts "Binstub already exists at bin/worktree"
|
|
8
|
-
exit 0
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
File.write(binstub_path, <<~RUBY)
|
|
12
|
-
#!/usr/bin/env ruby
|
|
13
|
-
|
|
14
|
-
require "bundler/setup"
|
|
15
|
-
require "rails-worktree"
|
|
16
|
-
|
|
17
|
-
RailsWorktree::CLI.run(ARGV)
|
|
18
|
-
RUBY
|
|
19
|
-
|
|
20
|
-
File.chmod(0755, binstub_path)
|
|
4
|
+
RailsWorktree::Installer.install(Dir.pwd, replace: ENV["FORCE"] == "1")
|
|
21
5
|
|
|
22
|
-
puts "✓
|
|
23
|
-
puts "Usage: bin/worktree <name>"
|
|
6
|
+
puts "✓ bin/worktree is installed and .worktrees is ignored"
|
|
7
|
+
puts "Usage: bin/worktree <name> <base-branch>"
|
|
24
8
|
end
|
|
25
9
|
|
|
26
10
|
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.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Martin Ulleberg
|
|
@@ -24,7 +24,12 @@ files:
|
|
|
24
24
|
- lib/worktree/cli.rb
|
|
25
25
|
- lib/worktree/commands/close.rb
|
|
26
26
|
- lib/worktree/commands/create.rb
|
|
27
|
+
- lib/worktree/commands/exec.rb
|
|
27
28
|
- lib/worktree/commands/init.rb
|
|
29
|
+
- lib/worktree/context.rb
|
|
30
|
+
- lib/worktree/installer.rb
|
|
31
|
+
- lib/worktree/launcher.rb
|
|
32
|
+
- lib/worktree/push_hook.rb
|
|
28
33
|
- lib/worktree/railtie.rb
|
|
29
34
|
- lib/worktree/tasks.rb
|
|
30
35
|
- lib/worktree/version.rb
|