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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd8698c3bbb798a625214a84c2f0e6cc585b4759cd9ef6ae9d3806eb6e6c572a
4
- data.tar.gz: 98611afb823f7b713a6c12412232d1cc6e85ef023eaad9b0518bb211aa41e628
3
+ metadata.gz: 2513c2eb6cd2e0d754ecdfb27482038c94204584a4b7edf076c15f36e9e7c9d8
4
+ data.tar.gz: cde0e84643776ce5e030c62ae4f6d5fd59635b6923b1401abddd660747c4dfdd
5
5
  SHA512:
6
- metadata.gz: 71ce31463125cb9756e54f0b66f0b153f39cc8658a9af88c6d8a2a726c22521b5ac5649d5b0a204bfc9775f4c925bfe77648120c734a13953b77eff3781345d2
7
- data.tar.gz: 96f6e366bdd38b682df8735b368ac26c2b56398bb750d278b0d1556af99cdb7cd97ff811628ce6ed53aaf8d9f061717488d8a84757f354b2d4d03f67802aaed0
6
+ metadata.gz: 6b54cce1aea73f3e20c7c7e560395121fd984e2e848b671ffd7385291e566bc825e95eb7880c1d438b30a8fddb6cec59fb9b370e7f91b617972c6d616157a56b
7
+ data.tar.gz: ec8c82d608897e3addafec37509703a6a4529eb7edd65017d5383c355c98db66d3a7278533e0cf8ca979019b5e871f0c768f80731fa88ce5266e75204207175d
data/README.md CHANGED
@@ -1,16 +1,20 @@
1
1
  # Rails Worktree
2
2
 
3
- Git worktree management for Rails projects with isolated databases and configurations.
3
+ Provisions git worktrees for Rails projects: configuration files, node_modules and optional
4
+ isolated databases, in a checkout created by `bin/worktree`, plain git, herdr, Claude Code or Codex.
4
5
 
5
6
  ## Features
6
7
 
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
10
- - Isolated databases per worktree (separate development and test databases)
11
- - Automatic configuration file copying
12
- - Copy node_modules from main worktree
13
- - Easy cleanup with database dropping and directory removal
8
+ - `bin/worktree init` prepares any linked worktree at one of three levels, and is safe to repeat:
9
+ - `minimal`: copies gitignored configuration from the main checkout, creates directories, links `node_modules`.
10
+ - `test`: minimal plus your isolated test setup command, or an own test database.
11
+ - `full`: minimal plus own development and test databases. The development database is cloned
12
+ from the main one with `CREATE DATABASE ... TEMPLATE` (PostgreSQL), so it is ready in seconds.
13
+ - `bin/worktree create NAME BASE` creates a worktree under `.worktrees/` from an explicit base and provisions it.
14
+ - `bin/worktree install` writes the integration files other tools read: `.worktreeinclude` for Claude Code,
15
+ a `## Worktrees` block in `AGENTS.md`/`CLAUDE.md` for coding agents, and a herdr plugin.
16
+ - `exec` and `verify` run commands with worktree-local Bundler and binstubs and check the recorded base.
17
+ - `close` drops the worktree-local databases, removes the checkout and deletes its branch.
14
18
 
15
19
  ## Installation
16
20
 
@@ -22,126 +26,137 @@ group :development do
22
26
  end
23
27
  ```
24
28
 
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.
29
+ Run `bundle install`. When Rails loads in development the gem installs `bin/worktree` and the
30
+ integration files below. The same happens with `bin/worktree install` or `bundle exec rake worktree:install`
31
+ (`--force` / `FORCE=1` also replaces an existing launcher).
28
32
 
29
- Manual binstub installation (if needed):
30
- ```bash
31
- bundle exec rake worktree:install
32
- ```
33
+ | File | Purpose |
34
+ | --- | --- |
35
+ | `bin/worktree` | Launcher that pins `BUNDLE_GEMFILE` to its own checkout before Ruby starts |
36
+ | `.gitignore` | Adds `/.worktrees/` |
37
+ | `.worktree.yml` | Project configuration (written once with the defaults, then yours) |
38
+ | `.worktreeinclude` | Generated from the `copy` list; Claude Code copies these files into every worktree it creates |
39
+ | `AGENTS.md`, `CLAUDE.md` | A marked `## Worktrees` block that tells agents to run `bin/worktree init`; a symlinked pair is updated once |
40
+ | `.worktree/herdr/` | A herdr plugin that runs `bin/worktree init --level minimal` on `worktree.created`; link it once with `herdr plugin link .worktree/herdr` |
33
41
 
34
- ## Usage
42
+ Commit `.worktree.yml`, `.worktreeinclude`, the agents block and `.worktree/herdr/`. They are project configuration.
35
43
 
36
- ### Create a worktree
44
+ ## Configuration
37
45
 
38
- ```bash
39
- git fetch origin
40
- bin/worktree feature-branch origin/main # Explicit base required
46
+ `.worktree.yml` in the repository root, all keys optional:
47
+
48
+ ```yaml
49
+ level: full # default level for create and init
50
+ copy: # gitignored files to copy from the main checkout (globs)
51
+ - .env
52
+ - .env.*
53
+ - config/database.yml
54
+ - config/master.key
55
+ - config/credentials/*.key
56
+ - Procfile.dev
57
+ directories: [tmp/pids] # created in every provisioned checkout
58
+ touch: [tmp/caching-dev.txt] # touched in every provisioned checkout
59
+ node_modules: link # link (symlink to the main checkout), copy or skip
60
+ database:
61
+ development_variable: DATABASE_NAME # written to .env.development.local
62
+ test_variable: DATABASE_NAME # written to .env.test.local
63
+ template: true # clone the main development database (PostgreSQL TEMPLATE, SQLite copy)
64
+ template_max_gb: 5 # larger source databases load the schema instead
65
+ test_command: bin/agent-test-setup # --level test runs this instead of creating a test database
41
66
  ```
42
67
 
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.
68
+ ### Databases
46
69
 
47
- This creates a new worktree with:
48
- - Separate databases: `myapp_feature-branch_development` and `myapp_feature-branch_test`
49
- - Copied configuration files (`.env`, `database.yml`, `Procfile.dev`, credentials)
50
- - Copied `node_modules`
51
- - Migrated and seeded databases
70
+ Before it touches a database, `init` asks Rails how `config/database.yml` resolves in the worktree and
71
+ picks one of three paths:
52
72
 
53
- ### Run commands in the worktree
73
+ - **SQLite files inside the checkout** (the `rails new` default, `storage/development.sqlite3`): every
74
+ checkout already has its own files. `full` copies the main development database into the worktree
75
+ (subject to `template` and `template_max_gb`) and prepares the test database; `test` prepares only the
76
+ test database. No `.env.*.local` files are written and `close` simply removes them with the checkout.
77
+ - **A database name read from the configured variable, with dotenv loaded**: the names go into
78
+ `.env.development.local` and `.env.test.local` and the databases are created as described below.
79
+ - **Anything else**, such as a fresh `rails new -d postgresql` app with fixed names: `init` warns and
80
+ skips the database setup, so the checkout shares the main databases and nothing in them is changed.
54
81
 
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
- ```
82
+ To give PostgreSQL or MySQL worktrees their own databases, add `gem "dotenv-rails", groups: [:development, :test]`
83
+ and read the name from the configured variable in `config/database.yml`:
61
84
 
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.
85
+ ```yaml
86
+ development:
87
+ <<: *default
88
+ database: <%= ENV.fetch("DATABASE_NAME", "myapp_development") %>
89
+ test:
90
+ <<: *default
91
+ database: <%= ENV.fetch("DATABASE_NAME", "myapp_test") %>
92
+ ```
68
93
 
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.
94
+ Database names are `<prefix>_<worktree>_development` and `<prefix>_<worktree>_test`. The prefix comes from
95
+ `config/database.yml` in the main checkout, the worktree name from the directory, with other characters
96
+ replaced by `_`. The names go into `.env.development.local` and `.env.test.local`, which dotenv loads per
97
+ environment and which stay out of git. `database.yml` is copied unchanged. An application without
98
+ `db/schema.rb` or `db/structure.sql` yet gets `db:migrate` instead of `db:schema:load`.
74
99
 
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:
100
+ ## Usage
79
101
 
80
102
  ```bash
81
- bundle exec rake worktree:install FORCE=1
103
+ git fetch origin
104
+ bin/worktree create feature origin/main # full provisioning
105
+ bin/worktree create spike origin/main --level minimal # shares the main development database
106
+ bin/worktree create feature origin/main --seed # also run db:seed
107
+
108
+ # In a checkout another tool created (herdr, claude --worktree, Codex, git worktree add)
109
+ bin/worktree init # level from .worktree.yml
110
+ bin/worktree init --level test # plus the isolated test setup
111
+ bin/worktree init --base origin/main # also record the base and install the push guard
112
+ bin/worktree init --force # overwrite copied files with the main checkout's
113
+
114
+ bin/worktree close feature # from the main checkout, by directory or branch name
115
+ bin/worktree close # from inside the worktree
82
116
  ```
83
117
 
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
118
+ The base for `create` is required and the resolved commit is recorded; creation does not fetch or change
119
+ your base branch. If the template clone cannot run, because the main development database is in use or larger
120
+ than `template_max_gb`, `init` creates the database and loads the schema instead and says so.
89
121
 
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.
122
+ `close` drops only databases named in the worktree's `.env.development.local` and `.env.test.local`.
123
+ A `minimal` checkout shares the main databases and they stay untouched.
93
124
 
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
-
108
- ### Close a worktree
125
+ ## Verified execution
109
126
 
110
127
  ```bash
111
- bin/worktree --close feature-branch # From main repo
112
- bin/worktree --close # From within worktree
128
+ cd .worktrees/feature
129
+ bin/worktree exec -- bundle exec rspec
130
+ bin/worktree verify
113
131
  ```
114
132
 
115
- Drops both databases, removes the directory, deletes the branch, and cleans up git references.
116
-
117
- ## Database Configuration
133
+ `exec` runs from the worktree root with `BUNDLE_GEMFILE` pinned to this checkout, clears inherited Bundler
134
+ state and Git directory overrides, removes other repositories' `bin` directories from PATH and puts this
135
+ checkout's `bin` first. It rejects binstubs that resolve into another repository. It needs the metadata
136
+ that `create` or `init --base` records in the private `rails-worktree.json` under `git rev-parse --git-dir`;
137
+ checkouts without it skip the guard.
118
138
 
119
- The gem uses environment variables for database names:
139
+ `create` and `init --base` also install a pre-push wrapper at Git's configured hook location. An existing
140
+ hook is preserved as `pre-push.before-rails-worktree` and runs after verification. In managed worktrees the
141
+ hook checks the branch, base ancestry and bundle root, then prints the change summary against the recorded
142
+ base. Git's `--no-verify` still bypasses hooks.
120
143
 
121
- - `DATABASE_NAME_DEVELOPMENT` - Development database name
122
- - `DATABASE_NAME_TEST` - Test database name
144
+ If `core.hooksPath` points at a directory that Git tracks (for example `script/hooks`), the gem does not
145
+ write there and prints a warning instead. Call the check from your own pre-push hook:
123
146
 
124
- Your `config/database.yml` should look like:
125
-
126
- ```yaml
127
- development:
128
- <<: *default
129
- database: <%= ENV.fetch("DATABASE_NAME_DEVELOPMENT", "myapp_development") %>
130
- test:
131
- <<: *default
132
- database: <%= ENV.fetch("DATABASE_NAME_TEST", "myapp_test") %>
147
+ ```sh
148
+ bin/worktree verify --push || exit 1
133
149
  ```
134
150
 
135
- The gem automatically:
136
- 1. Sets these variables in the worktree's `.env` file
137
- 2. Updates the worktree's `database.yml` to use them
138
- 3. Creates both development and test databases with unique names
151
+ It reads the pushed refs from standard input and does nothing in checkouts without worktree metadata.
139
152
 
140
153
  ## Requirements
141
154
 
142
- - Ruby >= 2.6.0
143
- - Rails project with standard structure
144
- - PostgreSQL (or modify for your database)
155
+ - Ruby >= 3.0
156
+ - Rails project with `bin/rails`
157
+ - For own databases: SQLite files in the checkout, or dotenv and a `database.yml` that reads the configured
158
+ variable (see Databases)
159
+ - PostgreSQL for template cloning; other server databases fall back to `db:create` and `db:schema:load`
145
160
 
146
161
  ## License
147
162
 
@@ -3,6 +3,8 @@ module RailsWorktree
3
3
  end
4
4
 
5
5
  require_relative "worktree/version"
6
+ require_relative "worktree/config"
7
+ require_relative "worktree/managed_block"
6
8
  require_relative "worktree/context"
7
9
  require_relative "worktree/launcher"
8
10
  require_relative "worktree/installer"
data/lib/worktree/cli.rb CHANGED
@@ -1,5 +1,37 @@
1
1
  module RailsWorktree
2
2
  class CLI
3
+ USAGE = <<~USAGE.freeze
4
+ Usage: worktree create <name> <base> [--level LEVEL] [--seed]
5
+ worktree <name> <base> [--level LEVEL] [--seed]
6
+ worktree init [name] [--level LEVEL] [--seed] [--base REF] [--force]
7
+ worktree close [name]
8
+ worktree exec -- <command> [arguments]
9
+ worktree verify [--push]
10
+ worktree install [--force]
11
+
12
+ Commands:
13
+ create NAME BASE Create .worktrees/NAME from BASE and provision it
14
+ init [NAME] Provision the current checkout, whoever created it (safe to repeat)
15
+ close [NAME] Drop the worktree-local databases, remove the checkout and its branch
16
+ exec -- COMMAND Run a command with verified worktree context
17
+ verify Check the recorded base and bundle root
18
+ install Write bin/worktree, .worktree.yml, .worktreeinclude, the AGENTS.md block
19
+ and the herdr plugin into this repository
20
+
21
+ Options:
22
+ --level LEVEL minimal (config files), test (plus test setup) or full (plus own databases).
23
+ Default: the level in .worktree.yml, or full
24
+ --seed Seed the development database (full level)
25
+ --base REF Record REF as the base and install the push guard (init)
26
+ --force Overwrite copied files (init) or the launcher and plugin files (install)
27
+
28
+ Examples:
29
+ worktree create feature origin/main
30
+ worktree create spike origin/main --level minimal
31
+ worktree init --level minimal # inside a herdr, Claude Code or Codex checkout
32
+ worktree close feature
33
+ USAGE
34
+
3
35
  def self.run(args)
4
36
  new(args).run
5
37
  end
@@ -9,33 +41,33 @@ module RailsWorktree
9
41
  end
10
42
 
11
43
  def run
12
- if @args.empty?
13
- print_usage
14
- exit 1
15
- end
16
-
17
- case @args.first
44
+ command = @args.shift
45
+ case command
46
+ when nil, "--help", "-h", "help"
47
+ puts USAGE
48
+ exit 1 if command.nil?
18
49
  when "exec"
19
- return Commands::Exec.new(@args.drop(1)).run
50
+ Commands::Exec.new(@args).run
20
51
  when "verify"
21
- return Context.new.verify(push: @args.include?("--push"))
22
- end
23
-
24
- # Extract flags
25
- @skip_seeds = @args.delete("--skip-seeds")
26
-
27
- case @args[0]
28
- when "--close", "close"
29
- @args.shift
30
- Commands::Close.new(@args).run
31
- when "--init", "init"
32
- @args.shift
33
- Commands::Init.new(@args, skip_seeds: @skip_seeds).run
34
- when "--help", "-h", "help"
35
- print_usage
52
+ Context.new.verify(push: @args.include?("--push"))
53
+ when "install"
54
+ positional, options = parse_options
55
+ raise Error, "install takes no arguments" unless positional.empty?
56
+ Installer.install(Context.new.root, replace: options[:force], quiet: false)
57
+ puts "✓ rails-worktree is installed. Link the herdr plugin once with: herdr plugin link #{Installer::HERDR_DIR}"
58
+ when "create"
59
+ positional, options = parse_options
60
+ Commands::Create.new(positional, level: options[:level], seed: options[:seed]).run
61
+ when "init", "--init"
62
+ positional, options = parse_options
63
+ Commands::Init.new(positional, level: options[:level], seed: options[:seed], base: options[:base], force: options[:force]).run
64
+ when "close", "--close"
65
+ positional, _options = parse_options
66
+ Commands::Close.new(positional).run
36
67
  else
37
- # Default: create worktree
38
- Commands::Create.new(@args, skip_seeds: @skip_seeds).run
68
+ @args.unshift(command)
69
+ positional, options = parse_options
70
+ Commands::Create.new(positional, level: options[:level], seed: options[:seed]).run
39
71
  end
40
72
  rescue Error => error
41
73
  warn "Error: #{error.message}"
@@ -44,37 +76,26 @@ module RailsWorktree
44
76
 
45
77
  private
46
78
 
47
- def print_usage
48
- puts <<~USAGE
49
- Usage: worktree <name> <base-branch> [options]
50
- worktree --close [worktree-name]
51
- worktree exec -- <command> [arguments]
52
- worktree verify [--push]
53
- worktree --init <worktree-name> [options]
54
-
55
- Creates a new git worktree and initializes it with configuration
56
-
57
- Commands:
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
61
- --close [name] Close and remove a worktree
62
- --init <name> Initialize a worktree (usually called automatically)
63
- --help, -h Show this help message
64
-
65
- Options:
66
- --skip-seeds Skip database seeding during initialization
67
-
68
- Arguments:
69
- <name> Name of the worktree (required)
70
- <base-branch> Explicit branch or commit to create worktree from (required)
71
-
72
- Examples:
73
- worktree feature-branch origin/main # Create from an explicit base
74
- worktree feature-branch origin/main --skip-seeds # Create without seeding database
75
- worktree --close feature-branch # Close worktree from main repo
76
- worktree --close # Close worktree from within it
77
- USAGE
79
+ def parse_options
80
+ positional = []
81
+ options = {seed: false, force: false}
82
+ until @args.empty?
83
+ argument = @args.shift
84
+ case argument
85
+ when "--seed" then options[:seed] = true
86
+ when "--skip-seeds" then options[:seed] = false
87
+ when "--force" then options[:force] = true
88
+ when /\A--(level|base)(?:=(.*))?\z/
89
+ value = Regexp.last_match(2) || @args.shift
90
+ raise Error, "#{argument} needs a value" if value.nil? || value.start_with?("--")
91
+ options[Regexp.last_match(1).to_sym] = value
92
+ when /\A--/
93
+ raise Error, "Unknown option #{argument}"
94
+ else
95
+ positional << argument
96
+ end
97
+ end
98
+ [positional, options]
78
99
  end
79
100
  end
80
101
  end
@@ -0,0 +1,23 @@
1
+ # Runs inside `bin/rails runner` in the worktree, without the worktree database
2
+ # override, so the configured development database is the main checkout's.
3
+ # Usage: bin/rails runner clone_database.rb TARGET
4
+ # RAILS_WORKTREE_TEMPLATE_MAX_BYTES caps the source size; a larger database is not cloned.
5
+ target = ARGV.fetch(0)
6
+ max_bytes = ENV.fetch("RAILS_WORKTREE_TEMPLATE_MAX_BYTES", "0").to_i
7
+ config = ActiveRecord::Base.connection_db_config.configuration_hash
8
+ source = config[:database]
9
+ abort "Template cloning needs PostgreSQL (adapter #{config[:adapter]})." unless config[:adapter].to_s.include?("postg")
10
+ abort "Source and target databases are the same: #{source}" if source == target
11
+
12
+ ActiveRecord::Base.establish_connection(config.merge(database: "postgres"))
13
+ connection = ActiveRecord::Base.connection
14
+ if connection.select_value("SELECT 1 FROM pg_database WHERE datname = #{connection.quote(target)}")
15
+ puts "Database #{target} already exists"
16
+ else
17
+ size = connection.select_value("SELECT pg_database_size(#{connection.quote(source)})").to_i
18
+ if max_bytes.positive? && size > max_bytes
19
+ abort format("Source database %s is %.1f GB, above database.template_max_gb in .worktree.yml; not cloning.", source, size / 1024.0**3)
20
+ end
21
+ connection.execute("CREATE DATABASE #{connection.quote_table_name(target)} TEMPLATE #{connection.quote_table_name(source)}")
22
+ puts "Cloned #{source} into #{target}"
23
+ end