branch_db 0.1.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 +7 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +435 -0
- data/Rakefile +10 -0
- data/lib/branch_db/cleaner.rb +121 -0
- data/lib/branch_db/cloner.rb +79 -0
- data/lib/branch_db/configuration.rb +12 -0
- data/lib/branch_db/git_utils.rb +48 -0
- data/lib/branch_db/logging.rb +17 -0
- data/lib/branch_db/naming.rb +24 -0
- data/lib/branch_db/pg_utils.rb +35 -0
- data/lib/branch_db/preparer.rb +58 -0
- data/lib/branch_db/railtie.rb +9 -0
- data/lib/branch_db/tasks/branch_db.rake +34 -0
- data/lib/branch_db/version.rb +3 -0
- data/lib/branch_db.rb +32 -0
- data/lib/generators/branch_db/install_generator.rb +43 -0
- data/lib/generators/branch_db/templates/initializer.rb +15 -0
- data/sig/branch_db.rbs +4 -0
- metadata +79 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c68b9335eb0602387a79af5a1e9c715249e2e30070faaa916510ff8e97f21203
|
|
4
|
+
data.tar.gz: b7f409fd18171d5845e812f1b5a1b2ec052b02b46d809c5b64a611bb5ee3a966
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6ef2f12ea030b88a18549ab83d9b8aac9db5afae73ab607165d5667e789d86af0292b0a4c7b64cad56c828120c10da5a7d538529e8afd4c609748a096a70e903
|
|
7
|
+
data.tar.gz: 2314b2ab78c68af51f995ffc9d2600dbc13ed3a78a2c504661396e243fed6e6bd24326841ea6de6ada234adc01b72fbb9b67523fe99322bc51a4e9ea21eef94f
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-01-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Automatic per-branch PostgreSQL database management for Rails
|
|
8
|
+
- Seamless integration with Rails `db:prepare` task
|
|
9
|
+
- Automatic cloning from parent branch database (with main as fallback) when creating new branch databases
|
|
10
|
+
- Smart parent branch detection via git reflog analysis
|
|
11
|
+
- `BRANCH_DB_PARENT` environment variable to override parent branch detection
|
|
12
|
+
- Development-only cloning (test databases use standard Rails schema load)
|
|
13
|
+
- `BranchDb.database_name` helper for dynamic database naming in `database.yml`
|
|
14
|
+
- `rails db:branch:list` task to list all branch databases
|
|
15
|
+
- `rails db:branch:purge` task to remove all branch databases (keeps main and current)
|
|
16
|
+
- `rails db:branch:prune` task to remove databases for deleted git branches
|
|
17
|
+
- Support for Rails multiple database configurations
|
|
18
|
+
- Configurable main branch name (default: `main`)
|
|
19
|
+
- Configurable branch name length limit (default: 33 characters)
|
|
20
|
+
- Configurable database suffixes (`development_suffix`, `test_suffix`)
|
|
21
|
+
- Active connection detection to prevent dropping databases in use
|
|
22
|
+
- Rails generator for easy installation (`rails generate branch_db:install`)
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MilkStraw AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
# BranchDb
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/branch_db)
|
|
4
|
+
[](https://github.com/milkstrawai/branch_db/actions)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
**Automatic per-branch PostgreSQL databases for Rails development.**
|
|
8
|
+
|
|
9
|
+
BranchDb eliminates database migration conflicts by giving each git branch its own isolated database. Switch branches freely without worrying about schema mismatches or losing development data.
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
- [The Problem](#the-problem)
|
|
14
|
+
- [The Solution](#the-solution)
|
|
15
|
+
- [Installation](#installation)
|
|
16
|
+
- [Configuration](#configuration)
|
|
17
|
+
- [Usage](#usage)
|
|
18
|
+
- [How It Works](#how-it-works)
|
|
19
|
+
- [Requirements](#requirements)
|
|
20
|
+
- [Important Notes](#important-notes)
|
|
21
|
+
- [Troubleshooting](#troubleshooting)
|
|
22
|
+
- [Development](#development)
|
|
23
|
+
- [Roadmap](#roadmap)
|
|
24
|
+
- [Contributing](#contributing)
|
|
25
|
+
- [License](#license)
|
|
26
|
+
|
|
27
|
+
## The Problem
|
|
28
|
+
|
|
29
|
+
Working on multiple feature branches with different migrations causes pain:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
# On feature-a branch: Add a 'status' column
|
|
33
|
+
rails generate migration AddStatusToUsers status:string
|
|
34
|
+
rails db:migrate
|
|
35
|
+
|
|
36
|
+
# Switch to feature-b branch
|
|
37
|
+
git checkout feature-b
|
|
38
|
+
git status
|
|
39
|
+
# => modified: db/schema.rb <- Contains 'status' column from feature-a!
|
|
40
|
+
|
|
41
|
+
# Now your schema.rb has changes that don't belong to this branch
|
|
42
|
+
# Accidentally commit it? You've just mixed schema changes across branches
|
|
43
|
+
# Run db:migrate? Schema.rb still shows the foreign column
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## The Solution
|
|
47
|
+
|
|
48
|
+
BranchDb automatically manages separate databases for each branch:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
main branch → myapp_development_main
|
|
52
|
+
feature-auth → myapp_development_feature_auth
|
|
53
|
+
feature-payments → myapp_development_feature_payments
|
|
54
|
+
bugfix-login → myapp_development_bugfix_login
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Each branch has its own isolated database with its own schema and data. Switch branches, restart your server, and you're working with the right database automatically.
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
Add BranchDb to your Gemfile:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
group :development, :test do
|
|
65
|
+
gem 'branch_db'
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Install and run the generator:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
bundle install
|
|
73
|
+
rails generate branch_db:install
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Update your `config/database.yml`:
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
development:
|
|
80
|
+
<<: *default
|
|
81
|
+
database: <%= BranchDb.database_name('myapp_development') %>
|
|
82
|
+
|
|
83
|
+
test:
|
|
84
|
+
<<: *default
|
|
85
|
+
database: <%= BranchDb.database_name('myapp_test') %>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
> **Note:** Replace `myapp` with your application name. Keep `_development` and `_test` suffixes for the cleanup feature to work correctly.
|
|
89
|
+
|
|
90
|
+
Initialize your first branch database:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
rails db:prepare
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Configuration
|
|
97
|
+
|
|
98
|
+
The generator creates `config/initializers/branch_db.rb`:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
BranchDb.configure do |config|
|
|
102
|
+
# The name of your main/stable branch (default: 'main')
|
|
103
|
+
config.main_branch = 'main'
|
|
104
|
+
|
|
105
|
+
# Maximum length for branch name suffix (default: 33)
|
|
106
|
+
# PostgreSQL has a 63 character limit for database names
|
|
107
|
+
# Formula: base_name_length + 1 (underscore) + max_branch_length <= 63
|
|
108
|
+
config.max_branch_length = 33
|
|
109
|
+
|
|
110
|
+
# Database name suffixes for cleanup feature (default: '_development', '_test')
|
|
111
|
+
# Customize if your database names use different conventions
|
|
112
|
+
# config.development_suffix = '_development'
|
|
113
|
+
# config.test_suffix = '_test'
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Configuration Options
|
|
118
|
+
|
|
119
|
+
| Option | Default | Description |
|
|
120
|
+
|--------|---------|-------------|
|
|
121
|
+
| `main_branch` | `'main'` | Your primary branch name (used as fallback clone source) |
|
|
122
|
+
| `max_branch_length` | `33` | Max characters for branch suffix (prevents exceeding PostgreSQL's 63-char limit) |
|
|
123
|
+
| `development_suffix` | `'_development'` | Suffix pattern for development databases |
|
|
124
|
+
| `test_suffix` | `'_test'` | Suffix pattern for test databases |
|
|
125
|
+
|
|
126
|
+
## Usage
|
|
127
|
+
|
|
128
|
+
### Daily Workflow
|
|
129
|
+
|
|
130
|
+
BranchDb enhances Rails' built-in `db:prepare` command:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Just use Rails' standard command - now branch-aware!
|
|
134
|
+
rails db:prepare
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**What happens (development only):**
|
|
138
|
+
1. Checks if your branch's database exists and has schema
|
|
139
|
+
2. If missing/empty and parent/main exists: **clones from parent branch** (or main as fallback)
|
|
140
|
+
3. If on main branch or no source exists: defers to standard Rails behavior
|
|
141
|
+
4. Rails then runs pending migrations and seeds as usual
|
|
142
|
+
|
|
143
|
+
**Test databases** use standard Rails behavior (schema load, no cloning):
|
|
144
|
+
```bash
|
|
145
|
+
RAILS_ENV=test rails db:prepare
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
> **Note:** Cloning only runs in development environment. All commands support Rails' multiple database feature.
|
|
149
|
+
|
|
150
|
+
### Available Commands
|
|
151
|
+
|
|
152
|
+
| Command | Description |
|
|
153
|
+
|---------|-------------|
|
|
154
|
+
| `rails db:prepare` | Rails' standard command, enhanced with cloning from parent/main |
|
|
155
|
+
| `rails db:branch:list` | List all branch databases |
|
|
156
|
+
| `rails db:branch:purge` | Remove all branch databases except current and main |
|
|
157
|
+
| `rails db:branch:prune` | Remove databases for branches that no longer exist in git |
|
|
158
|
+
|
|
159
|
+
### Examples
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# Starting work on a new feature branch
|
|
163
|
+
git checkout -b feature-new-thing
|
|
164
|
+
rails db:prepare # Clones from parent branch (or main as fallback)
|
|
165
|
+
rails server
|
|
166
|
+
|
|
167
|
+
# Switching to another branch
|
|
168
|
+
git checkout feature-other-thing
|
|
169
|
+
# Restart your Rails server to connect to the other database
|
|
170
|
+
rails server
|
|
171
|
+
|
|
172
|
+
# Purging all branch databases (keeps current and main only)
|
|
173
|
+
rails db:branch:purge
|
|
174
|
+
# => Found 5 database(s) to remove:
|
|
175
|
+
# => - myapp_development_feature_old
|
|
176
|
+
# => - myapp_test_feature_old
|
|
177
|
+
# => ...
|
|
178
|
+
# => Proceed with deletion? [y/N]
|
|
179
|
+
|
|
180
|
+
# Pruning databases for deleted git branches only
|
|
181
|
+
rails db:branch:prune
|
|
182
|
+
# => Found 2 database(s) to remove:
|
|
183
|
+
# => - myapp_development_merged_feature
|
|
184
|
+
# => - myapp_test_merged_feature
|
|
185
|
+
# => Proceed with deletion? [y/N]
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## How It Works
|
|
189
|
+
|
|
190
|
+
### Rails Integration
|
|
191
|
+
|
|
192
|
+
BranchDb enhances Rails' `db:prepare` task by adding a prerequisite that clones from the parent branch when needed. This means:
|
|
193
|
+
|
|
194
|
+
- **Zero learning curve** - use `rails db:prepare` as usual
|
|
195
|
+
- **Automatic cloning** - new branch databases are cloned from their parent (or main as fallback)
|
|
196
|
+
- **Rails handles the rest** - migrations, seeds, and schema dumps work normally
|
|
197
|
+
|
|
198
|
+
### Database Naming
|
|
199
|
+
|
|
200
|
+
BranchDb generates database names by combining your base name with a sanitized branch name:
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
Base name: myapp_development
|
|
204
|
+
Branch: feature/user-auth
|
|
205
|
+
Sanitized: feature_user_auth
|
|
206
|
+
Result: myapp_development_feature_user_auth
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Branch names are sanitized: non-alphanumeric characters become underscores, and names are truncated to `max_branch_length`.
|
|
210
|
+
|
|
211
|
+
### Cloning Process
|
|
212
|
+
|
|
213
|
+
When `db:prepare` detects a missing or empty database:
|
|
214
|
+
|
|
215
|
+
1. **Detects** the parent branch to clone from (see below)
|
|
216
|
+
2. **Checks** if the parent database exists; if not, falls back to main
|
|
217
|
+
3. **If source exists:** Creates the target database and uses `pg_dump | psql` for efficient cloning
|
|
218
|
+
4. **If no source:** Defers to Rails' standard `db:prepare` (loads schema, runs migrations, seeds)
|
|
219
|
+
5. **On main branch:** Defers to Rails' standard `db:prepare`
|
|
220
|
+
|
|
221
|
+
### Parent Branch Detection
|
|
222
|
+
|
|
223
|
+
BranchDb intelligently detects which branch you branched from and clones its database. This enables nested feature branch workflows:
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
main → feature-a → feature-a-child
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
When you create `feature-a-child` from `feature-a`, BranchDb will clone from `feature-a`'s database (if it exists), not main.
|
|
230
|
+
|
|
231
|
+
**Detection priority:**
|
|
232
|
+
|
|
233
|
+
1. `BRANCH_DB_PARENT` environment variable (explicit override)
|
|
234
|
+
2. Git reflog analysis (finds the last "checkout: moving from X to current-branch")
|
|
235
|
+
3. Configured `main_branch` (fallback)
|
|
236
|
+
|
|
237
|
+
**Fallback behavior:** If the detected parent's database doesn't exist, BranchDb automatically falls back to the main branch database.
|
|
238
|
+
|
|
239
|
+
**Override with environment variable:**
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
# Force cloning from main, even if on a nested feature branch
|
|
243
|
+
BRANCH_DB_PARENT=main rails db:prepare
|
|
244
|
+
|
|
245
|
+
# Clone from a specific branch
|
|
246
|
+
BRANCH_DB_PARENT=feature-other rails db:prepare
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Purge Safety
|
|
250
|
+
|
|
251
|
+
The purge command protects important databases:
|
|
252
|
+
- Current branch's development and test databases
|
|
253
|
+
- Main branch's development and test databases
|
|
254
|
+
- Databases with active connections (skipped with warning)
|
|
255
|
+
|
|
256
|
+
## Requirements
|
|
257
|
+
|
|
258
|
+
- **Ruby** >= 3.2
|
|
259
|
+
- **Rails** >= 7.0
|
|
260
|
+
- **PostgreSQL** (any supported version)
|
|
261
|
+
- **PostgreSQL client tools** in PATH:
|
|
262
|
+
- `psql` - for database operations
|
|
263
|
+
- `pg_dump` - for cloning databases
|
|
264
|
+
- `dropdb` - for purge/prune operations
|
|
265
|
+
|
|
266
|
+
### Verifying PostgreSQL Tools
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
which psql pg_dump dropdb
|
|
270
|
+
# Should output paths for all three tools
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
If missing, install PostgreSQL client tools:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
# macOS
|
|
277
|
+
brew install postgresql
|
|
278
|
+
|
|
279
|
+
# Ubuntu/Debian
|
|
280
|
+
sudo apt-get install postgresql-client
|
|
281
|
+
|
|
282
|
+
# Docker (add to your Dockerfile)
|
|
283
|
+
RUN apt-get update && apt-get install -y postgresql-client
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Important Notes
|
|
287
|
+
|
|
288
|
+
### Server Restart Required
|
|
289
|
+
|
|
290
|
+
Database selection happens at Rails boot time (ERB in `database.yml` is evaluated once). After switching branches, **restart your Rails server** to connect to the correct database.
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
git checkout other-branch
|
|
294
|
+
# Must restart Rails to use other-branch's database
|
|
295
|
+
rails server # Now connected to myapp_development_other_branch
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Detached HEAD State
|
|
299
|
+
|
|
300
|
+
In detached HEAD state (e.g., `git checkout abc123`), BranchDb cannot determine a branch name. It falls back to using the base database name without a suffix. All detached HEAD checkouts share this database.
|
|
301
|
+
|
|
302
|
+
For CI environments, ensure you checkout an actual branch:
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
# CI script
|
|
306
|
+
git checkout $BRANCH_NAME # Not just the commit SHA
|
|
307
|
+
rails db:prepare
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Database Name Length
|
|
311
|
+
|
|
312
|
+
PostgreSQL limits database names to 63 characters. With default settings:
|
|
313
|
+
- Base name: up to 29 characters
|
|
314
|
+
- Underscore: 1 character
|
|
315
|
+
- Branch suffix: up to 33 characters
|
|
316
|
+
|
|
317
|
+
If your base name is longer, reduce `max_branch_length` accordingly.
|
|
318
|
+
|
|
319
|
+
## Troubleshooting
|
|
320
|
+
|
|
321
|
+
### "PostgreSQL tool 'X' not found in PATH"
|
|
322
|
+
|
|
323
|
+
Install PostgreSQL client tools (see [Requirements](#requirements)).
|
|
324
|
+
|
|
325
|
+
### "Could not connect to Postgres on port X"
|
|
326
|
+
|
|
327
|
+
Ensure PostgreSQL is running and accessible:
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
# Check if PostgreSQL is running
|
|
331
|
+
pg_isready -h localhost -p 5432
|
|
332
|
+
|
|
333
|
+
# For Docker users
|
|
334
|
+
docker ps | grep postgres
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Database not switching when I change branches
|
|
338
|
+
|
|
339
|
+
Remember to restart your Rails server after switching branches. The database name is determined at boot time.
|
|
340
|
+
|
|
341
|
+
### Clone is slow for large databases
|
|
342
|
+
|
|
343
|
+
`pg_dump | psql` is already efficient, but for very large databases consider:
|
|
344
|
+
- Keeping your main branch database lean
|
|
345
|
+
- Using database-level compression
|
|
346
|
+
- Running cleanup regularly to remove old branch databases
|
|
347
|
+
|
|
348
|
+
### Branch name too long
|
|
349
|
+
|
|
350
|
+
Long branch names are automatically truncated to `max_branch_length` (default: 33). Two branches with the same prefix might collide:
|
|
351
|
+
|
|
352
|
+
```
|
|
353
|
+
feature/very-long-descriptive-name-for-auth → _feature_very_long_descriptive_na
|
|
354
|
+
feature/very-long-descriptive-name-for-payments → _feature_very_long_descriptive_na # Same!
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Use shorter branch names or increase `max_branch_length` (if your base name is short enough).
|
|
358
|
+
|
|
359
|
+
## Development
|
|
360
|
+
|
|
361
|
+
### Setup
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
git clone https://github.com/milkstrawai/branch_db.git
|
|
365
|
+
cd branch_db
|
|
366
|
+
bin/setup
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Running Tests
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
# Run test suite
|
|
373
|
+
bundle exec rspec
|
|
374
|
+
|
|
375
|
+
# Run with coverage report
|
|
376
|
+
bundle exec rspec && open coverage/index.html
|
|
377
|
+
|
|
378
|
+
# Run linter
|
|
379
|
+
bundle exec rubocop
|
|
380
|
+
|
|
381
|
+
# Run both
|
|
382
|
+
bundle exec rake
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Test Coverage
|
|
386
|
+
|
|
387
|
+
The project maintains high test coverage standards:
|
|
388
|
+
- Line coverage: 100%
|
|
389
|
+
- Branch coverage: 90%
|
|
390
|
+
|
|
391
|
+
## Roadmap
|
|
392
|
+
|
|
393
|
+
Features we're considering for future releases:
|
|
394
|
+
|
|
395
|
+
- [ ] **SQLite and MySQL support** - Database adapter pattern for non-PostgreSQL databases
|
|
396
|
+
- [ ] **Standalone clone task** - `rails db:branch:clone FROM=branch-name` for manual cloning
|
|
397
|
+
- [ ] **Post-checkout git hook** - Auto-restart Rails server on branch switch (Doable?)
|
|
398
|
+
- [ ] **Database info task** - `rails db:branch:info` showing current branch, DB name, size, and parent
|
|
399
|
+
- [ ] **Clone progress indicator** - Visual feedback for large database clones
|
|
400
|
+
- [ ] **Disk usage report** - `rails db:branch:list --size` to show storage per branch
|
|
401
|
+
|
|
402
|
+
Have a feature request? [Open an issue](https://github.com/milkstrawai/branch_db/issues) to discuss it!
|
|
403
|
+
|
|
404
|
+
## Contributing
|
|
405
|
+
|
|
406
|
+
Contributions are welcome! Here's how you can help:
|
|
407
|
+
|
|
408
|
+
1. **Fork** the repository
|
|
409
|
+
2. **Create** a feature branch (`git checkout -b feature/amazing-feature`)
|
|
410
|
+
3. **Commit** your changes (`git commit -m 'Add amazing feature'`)
|
|
411
|
+
4. **Push** to the branch (`git push origin feature/amazing-feature`)
|
|
412
|
+
5. **Open** a Pull Request
|
|
413
|
+
|
|
414
|
+
### Guidelines
|
|
415
|
+
|
|
416
|
+
- Write tests for new features
|
|
417
|
+
- Follow existing code style (RuboCop will help)
|
|
418
|
+
- Update documentation as needed
|
|
419
|
+
- Keep commits focused and atomic
|
|
420
|
+
|
|
421
|
+
### Reporting Issues
|
|
422
|
+
|
|
423
|
+
Found a bug? Please open an issue with:
|
|
424
|
+
- Ruby and Rails versions
|
|
425
|
+
- PostgreSQL version
|
|
426
|
+
- Steps to reproduce
|
|
427
|
+
- Expected vs actual behavior
|
|
428
|
+
|
|
429
|
+
## License
|
|
430
|
+
|
|
431
|
+
BranchDb is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
432
|
+
|
|
433
|
+
## Acknowledgments
|
|
434
|
+
|
|
435
|
+
Inspired by the pain of database migration conflicts and the joy of isolated development environments.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
|
|
3
|
+
module BranchDb
|
|
4
|
+
class Cleaner
|
|
5
|
+
include PgUtils
|
|
6
|
+
include Logging
|
|
7
|
+
|
|
8
|
+
attr_reader :config, :output, :input, :name
|
|
9
|
+
|
|
10
|
+
def initialize(config, output: $stdout, input: $stdin, prefix: true, name: nil)
|
|
11
|
+
@config = config.is_a?(Hash) ? config : config.configuration_hash
|
|
12
|
+
@output = output
|
|
13
|
+
@input = input
|
|
14
|
+
@prefix = prefix
|
|
15
|
+
@name = name
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def list_branch_databases
|
|
19
|
+
dev_dbs = find_databases(dev_prefix)
|
|
20
|
+
test_dbs = find_databases(test_prefix)
|
|
21
|
+
|
|
22
|
+
if dev_dbs.empty? && test_dbs.empty?
|
|
23
|
+
log "No branch databases found#{db_label}."
|
|
24
|
+
return []
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
all_dbs = dev_dbs + test_dbs
|
|
28
|
+
log "Found #{all_dbs.size} branch database(s)#{db_label}:"
|
|
29
|
+
all_dbs.each { |db| log " - #{db}" }
|
|
30
|
+
all_dbs
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def purge(confirm: true)
|
|
34
|
+
delete_databases(deletable_databases, empty_msg: "No old branch databases to purge#{db_label}.",
|
|
35
|
+
done_msg: "Purge complete#{db_label}!", confirm:)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def prune(confirm: true)
|
|
39
|
+
delete_databases(prunable_databases, empty_msg: "No stale branch databases to prune#{db_label}.",
|
|
40
|
+
done_msg: "Prune complete#{db_label}!", confirm:)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def protected_databases
|
|
44
|
+
current_dev = config[:database]
|
|
45
|
+
current_test = current_dev.sub(dev_prefix, test_prefix)
|
|
46
|
+
|
|
47
|
+
[
|
|
48
|
+
current_dev,
|
|
49
|
+
current_test,
|
|
50
|
+
"#{dev_prefix}#{BranchDb.configuration.main_branch}",
|
|
51
|
+
"#{test_prefix}#{BranchDb.configuration.main_branch}"
|
|
52
|
+
]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def deletable_databases
|
|
58
|
+
all_branch_databases.reject { |db| protected_databases.include?(db) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def prunable_databases
|
|
62
|
+
existing = BranchDb::Naming.git_branches.map { BranchDb::Naming.sanitize_branch(_1) }
|
|
63
|
+
all_branch_databases.reject { |db| protected_databases.include?(db) || branch_exists?(db, existing) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def all_branch_databases = find_databases(dev_prefix) + find_databases(test_prefix)
|
|
67
|
+
|
|
68
|
+
def branch_exists?(db, existing_branches)
|
|
69
|
+
prefix = db.start_with?(test_prefix) ? test_prefix : dev_prefix
|
|
70
|
+
existing_branches.include?(db.sub(prefix, ""))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def delete_databases(to_delete, empty_msg:, done_msg:, confirm:)
|
|
74
|
+
return log(empty_msg) if to_delete.empty?
|
|
75
|
+
|
|
76
|
+
display_databases(to_delete)
|
|
77
|
+
return log("Aborted.") if confirm && !user_confirmed?
|
|
78
|
+
|
|
79
|
+
to_delete.each { |db| drop_database(db) }
|
|
80
|
+
log done_msg
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def display_databases(databases)
|
|
84
|
+
log "Found #{databases.size} database(s) to remove:"
|
|
85
|
+
databases.each { log " - #{_1}" }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def user_confirmed? = (output.print "\nProceed with deletion? [y/N] ") || input.gets&.chomp&.downcase == "y"
|
|
89
|
+
|
|
90
|
+
def find_databases(prefix)
|
|
91
|
+
check_pg_tools!(:psql, :dropdb)
|
|
92
|
+
stdout, = Open3.capture2(pg_env, "bash", "-c", "#{list_databases_cmd} | grep ^#{prefix.shellescape}")
|
|
93
|
+
stdout.split("\n").map(&:strip).reject(&:empty?)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def drop_database(db)
|
|
97
|
+
active = active_connections(db)
|
|
98
|
+
return log "⚠️ Skipping #{db} (#{active} active connection#{"s" if active > 1})" if active.positive?
|
|
99
|
+
|
|
100
|
+
dropped = system(pg_env, "bash", "-c", "dropdb #{psql_flags} #{db.shellescape}")
|
|
101
|
+
log dropped ? "✅ Dropped #{db}" : "❌ Failed to drop #{db}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def active_connections(db)
|
|
105
|
+
query = "SELECT count(*) FROM pg_stat_activity WHERE datname = '#{db.gsub("'", "''")}'"
|
|
106
|
+
stdout, = Open3.capture2(pg_env, "bash", "-c", "psql #{psql_flags} -d postgres -tAc \"#{query}\"")
|
|
107
|
+
stdout.strip.to_i
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def db_label = name && name != "primary" ? " for #{name}" : ""
|
|
111
|
+
|
|
112
|
+
def dev_prefix = "#{base_name}_"
|
|
113
|
+
|
|
114
|
+
def test_prefix = "#{base_name.sub(BranchDb.configuration.development_suffix, BranchDb.configuration.test_suffix)}_"
|
|
115
|
+
|
|
116
|
+
def base_name
|
|
117
|
+
suffix = BranchDb::Naming.branch_suffix
|
|
118
|
+
suffix.empty? ? config[:database] : config[:database].sub(/#{Regexp.escape(suffix)}\z/, "")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module BranchDb
|
|
2
|
+
class Cloner
|
|
3
|
+
include PgUtils
|
|
4
|
+
include Logging
|
|
5
|
+
|
|
6
|
+
attr_reader :config, :output
|
|
7
|
+
|
|
8
|
+
def initialize(config, output: $stdout)
|
|
9
|
+
@config = config.is_a?(Hash) ? config : config.configuration_hash
|
|
10
|
+
@output = output
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def clone
|
|
14
|
+
log "📦 Cloning #{source_db} → #{target_db}..."
|
|
15
|
+
create_or_recreate_database
|
|
16
|
+
transfer_data
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def source_exists?
|
|
20
|
+
check_pg_tools!(:psql, :pg_dump)
|
|
21
|
+
database_exists?(source_db)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def source_db
|
|
25
|
+
@source_db ||= determine_source_db
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def target_db
|
|
29
|
+
config[:database]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def base_name
|
|
33
|
+
suffix = BranchDb::Naming.branch_suffix
|
|
34
|
+
return target_db if suffix.empty?
|
|
35
|
+
|
|
36
|
+
target_db.sub(/#{Regexp.escape(suffix)}\z/, "")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def determine_source_db
|
|
42
|
+
parent_db = BranchDb::Naming.parent_database_name(base_name)
|
|
43
|
+
main_db = "#{base_name}_#{BranchDb.configuration.main_branch}"
|
|
44
|
+
|
|
45
|
+
return main_db if parent_db == main_db
|
|
46
|
+
|
|
47
|
+
check_pg_tools!(:psql, :pg_dump)
|
|
48
|
+
database_exists?(parent_db) ? parent_db : main_db
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def database_exists?(db_name)
|
|
52
|
+
check_cmd = "#{list_databases_cmd} | grep -qx #{db_name.shellescape}"
|
|
53
|
+
system(pg_env, "bash", "-c", check_cmd)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def create_or_recreate_database
|
|
57
|
+
ActiveRecord::Tasks::DatabaseTasks.create(config)
|
|
58
|
+
log_indented "Created database '#{target_db}'"
|
|
59
|
+
rescue ActiveRecord::DatabaseAlreadyExists
|
|
60
|
+
log_indented "Database '#{target_db}' already exists. Recreating..."
|
|
61
|
+
ActiveRecord::Tasks::DatabaseTasks.drop(config)
|
|
62
|
+
ActiveRecord::Tasks::DatabaseTasks.create(config)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def transfer_data
|
|
66
|
+
dump_cmd = "pg_dump #{psql_flags} --no-owner --no-acl #{source_db.shellescape}"
|
|
67
|
+
restore_cmd = "psql #{psql_flags} #{target_db.shellescape}"
|
|
68
|
+
full_command = "set -o pipefail; #{dump_cmd} | #{restore_cmd}"
|
|
69
|
+
|
|
70
|
+
log_indented "Transferring data..."
|
|
71
|
+
|
|
72
|
+
unless system(pg_env, "bash", "-c", full_command, %i[out err] => File::NULL)
|
|
73
|
+
raise Error, "Clone failed! Check PostgreSQL connection."
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
log "✅ Database cloned successfully!"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module BranchDb
|
|
2
|
+
class Configuration
|
|
3
|
+
attr_accessor :main_branch, :max_branch_length, :development_suffix, :test_suffix
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@main_branch = "main"
|
|
7
|
+
@max_branch_length = 33
|
|
8
|
+
@development_suffix = "_development"
|
|
9
|
+
@test_suffix = "_test"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module BranchDb
|
|
2
|
+
module GitUtils
|
|
3
|
+
def current_branch
|
|
4
|
+
`git symbolic-ref HEAD 2>/dev/null`.chomp.sub("refs/heads/", "")
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def git_branches
|
|
8
|
+
output = `git branch --format='%(refname:short)' 2>/dev/null`
|
|
9
|
+
output.split("\n").map(&:strip).reject(&:empty?)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def parent_branch
|
|
13
|
+
@parent_branch ||= detect_parent_branch
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def reset_parent_cache!
|
|
17
|
+
@parent_branch = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def detect_parent_branch
|
|
23
|
+
return ENV["BRANCH_DB_PARENT"] if ENV["BRANCH_DB_PARENT"]
|
|
24
|
+
|
|
25
|
+
detect_parent_from_reflog || BranchDb.configuration.main_branch
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def detect_parent_from_reflog
|
|
29
|
+
current = current_branch
|
|
30
|
+
return nil if current.empty?
|
|
31
|
+
|
|
32
|
+
`git reflog show --format='%gs' -n 100 2>/dev/null`.each_line do |line|
|
|
33
|
+
parent = extract_parent_from_reflog_line(line.chomp, current)
|
|
34
|
+
return parent if parent
|
|
35
|
+
end
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def extract_parent_from_reflog_line(line, current)
|
|
40
|
+
return nil unless line =~ /\Acheckout: moving from (.+) to #{Regexp.escape(current)}\z/
|
|
41
|
+
|
|
42
|
+
parent = ::Regexp.last_match(1).strip
|
|
43
|
+
return nil if parent == current || parent =~ /\A[a-f0-9]{40}\z/
|
|
44
|
+
|
|
45
|
+
parent
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module BranchDb
|
|
2
|
+
module Logging
|
|
3
|
+
PREFIX = "[branch_db]".freeze
|
|
4
|
+
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def log(message)
|
|
8
|
+
output.puts prefix? ? "#{PREFIX} #{message}" : message
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def log_indented(message)
|
|
12
|
+
output.puts prefix? ? "#{PREFIX} #{message}" : " #{message}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def prefix? = @prefix != false
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module BranchDb
|
|
2
|
+
module Naming
|
|
3
|
+
extend GitUtils
|
|
4
|
+
|
|
5
|
+
class << self
|
|
6
|
+
def sanitize_branch(branch)
|
|
7
|
+
branch.gsub(/[^a-zA-Z0-9_]/, "_")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def branch_suffix
|
|
11
|
+
branch = sanitize_branch(current_branch)
|
|
12
|
+
max_length = BranchDb.configuration.max_branch_length
|
|
13
|
+
truncated = branch[0, max_length]
|
|
14
|
+
truncated.empty? ? "" : "_#{truncated}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def database_name(base_name) = "#{base_name}#{branch_suffix}"
|
|
18
|
+
|
|
19
|
+
def main_database_name(base_name) = "#{base_name}_#{BranchDb.configuration.main_branch}"
|
|
20
|
+
|
|
21
|
+
def parent_database_name(base_name) = "#{base_name}_#{sanitize_branch(parent_branch)}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require "shellwords"
|
|
2
|
+
|
|
3
|
+
module BranchDb
|
|
4
|
+
module PgUtils
|
|
5
|
+
PG_TOOLS = %w[psql pg_dump dropdb].freeze
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def psql_flags
|
|
10
|
+
host = config[:host].to_s.shellescape
|
|
11
|
+
port = config[:port].to_s.shellescape
|
|
12
|
+
username = config[:username].to_s.shellescape
|
|
13
|
+
|
|
14
|
+
"-h #{host} -p #{port} -U #{username}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def pg_env
|
|
18
|
+
{ "PGPASSWORD" => config[:password].to_s }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def list_databases_cmd
|
|
22
|
+
"psql #{psql_flags} -lqt | cut -d \\| -f 1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def check_pg_tools!(*tools)
|
|
26
|
+
tools = PG_TOOLS if tools.empty?
|
|
27
|
+
|
|
28
|
+
tools.each do |tool|
|
|
29
|
+
unless system("which #{tool} > /dev/null 2>&1")
|
|
30
|
+
raise Error, "PostgreSQL tool '#{tool}' not found in PATH. Please install PostgreSQL client tools."
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module BranchDb
|
|
2
|
+
# Checks if database needs initialization and triggers cloning if needed.
|
|
3
|
+
# Used by the db:prepare rake task enhancement.
|
|
4
|
+
class Preparer
|
|
5
|
+
include Logging
|
|
6
|
+
|
|
7
|
+
attr_reader :db_config, :output
|
|
8
|
+
|
|
9
|
+
def initialize(db_config, output: $stdout)
|
|
10
|
+
@db_config = db_config
|
|
11
|
+
@output = output
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def prepare_if_needed
|
|
15
|
+
log "📦 Checking database#{db_label}..."
|
|
16
|
+
|
|
17
|
+
unless needs_cloning?
|
|
18
|
+
log "✅ Database '#{config[:database]}' ready."
|
|
19
|
+
return
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attempt_clone
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def config
|
|
28
|
+
db_config.configuration_hash
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def db_label
|
|
32
|
+
db_config.name == "primary" ? "" : " (#{db_config.name})"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def needs_cloning?
|
|
36
|
+
establish_connection
|
|
37
|
+
!ActiveRecord::Base.connection.table_exists?("schema_migrations")
|
|
38
|
+
rescue ActiveRecord::NoDatabaseError
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def establish_connection
|
|
43
|
+
ActiveRecord::Base.establish_connection(db_config)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def attempt_clone
|
|
47
|
+
cloner = Cloner.new(config, output:)
|
|
48
|
+
|
|
49
|
+
if cloner.target_db == cloner.source_db
|
|
50
|
+
log_indented "On main branch. Deferring to db:prepare..."
|
|
51
|
+
elsif cloner.source_exists?
|
|
52
|
+
cloner.clone
|
|
53
|
+
else
|
|
54
|
+
log_indented "Source database not found. Deferring to db:prepare..."
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
def db_configs = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env)
|
|
2
|
+
|
|
3
|
+
def cleaner_for(db_config) = BranchDb::Cleaner.new(db_config.configuration_hash, prefix: false, name: db_config.name)
|
|
4
|
+
|
|
5
|
+
namespace :db do
|
|
6
|
+
namespace :branch do
|
|
7
|
+
desc "List all branch databases"
|
|
8
|
+
task list: :environment do
|
|
9
|
+
db_configs.each { cleaner_for(_1).list_branch_databases }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
desc "Remove all branch databases (keeps main and current branch)"
|
|
13
|
+
task purge: :environment do
|
|
14
|
+
db_configs.each { cleaner_for(_1).purge }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
desc "Remove databases for branches that no longer exist in git"
|
|
18
|
+
task prune: :environment do
|
|
19
|
+
db_configs.each { cleaner_for(_1).prune }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc "Ensure branch database exists (used by db:prepare enhancement)"
|
|
23
|
+
task ensure_cloned: :environment do
|
|
24
|
+
next unless Rails.env.development?
|
|
25
|
+
|
|
26
|
+
db_configs.each { BranchDb::Preparer.new(_1).prepare_if_needed }
|
|
27
|
+
rescue ActiveRecord::ConnectionNotEstablished, PG::ConnectionBad => e
|
|
28
|
+
abort "❌ Could not connect to Postgres: #{e.message}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Enhance Rails' db:prepare to clone from parent branch when needed
|
|
34
|
+
Rake::Task["db:prepare"].enhance(["db:branch:ensure_cloned"])
|
data/lib/branch_db.rb
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require_relative "branch_db/version"
|
|
2
|
+
require_relative "branch_db/configuration"
|
|
3
|
+
require_relative "branch_db/git_utils"
|
|
4
|
+
require_relative "branch_db/naming"
|
|
5
|
+
require_relative "branch_db/pg_utils"
|
|
6
|
+
require_relative "branch_db/logging"
|
|
7
|
+
require_relative "branch_db/cloner"
|
|
8
|
+
require_relative "branch_db/cleaner"
|
|
9
|
+
require_relative "branch_db/preparer"
|
|
10
|
+
require_relative "branch_db/railtie" if defined?(Rails::Railtie)
|
|
11
|
+
|
|
12
|
+
module BranchDb
|
|
13
|
+
class Error < StandardError; end
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def configuration
|
|
17
|
+
@configuration ||= Configuration.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def configure
|
|
21
|
+
yield(configuration)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def database_name(base_name)
|
|
25
|
+
Naming.database_name(base_name)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def main_database_name(base_name)
|
|
29
|
+
Naming.main_database_name(base_name)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require "rails/generators/base"
|
|
2
|
+
|
|
3
|
+
module BranchDb
|
|
4
|
+
class InstallGenerator < Rails::Generators::Base
|
|
5
|
+
source_root File.expand_path("templates", __dir__)
|
|
6
|
+
|
|
7
|
+
desc "Creates a BranchDb initializer and shows setup instructions"
|
|
8
|
+
|
|
9
|
+
def create_initializer
|
|
10
|
+
template "initializer.rb", "config/initializers/branch_db.rb"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def show_instructions # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
14
|
+
say ""
|
|
15
|
+
say "=== BranchDb Installation Complete ===", :green
|
|
16
|
+
say ""
|
|
17
|
+
say "Next steps:", :yellow
|
|
18
|
+
say ""
|
|
19
|
+
say "1. Update your config/database.yml to use dynamic database names:"
|
|
20
|
+
say ""
|
|
21
|
+
say " development:"
|
|
22
|
+
say " database: <%= BranchDb.database_name('#{app_name}_development') %>"
|
|
23
|
+
say ""
|
|
24
|
+
say " test:"
|
|
25
|
+
say " database: <%= BranchDb.database_name('#{app_name}_test') %>"
|
|
26
|
+
say ""
|
|
27
|
+
say "2. Initialize your database:"
|
|
28
|
+
say " rails db:prepare # Creates and clones from main"
|
|
29
|
+
say ""
|
|
30
|
+
say "3. Other available tasks:"
|
|
31
|
+
say " rails db:branch:list # List all branch databases"
|
|
32
|
+
say " rails db:branch:purge # Remove all branch databases (keeps main/current)"
|
|
33
|
+
say " rails db:branch:prune # Remove databases for deleted git branches"
|
|
34
|
+
say ""
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def app_name
|
|
40
|
+
Rails.application.class.module_parent_name.underscore
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
BranchDb.configure do |config|
|
|
2
|
+
# The name of your main/stable branch (default: 'main')
|
|
3
|
+
# config.main_branch = 'main'
|
|
4
|
+
|
|
5
|
+
# Maximum length for branch name suffix in database names (default: 33)
|
|
6
|
+
# PostgreSQL has a 63 character limit for database names
|
|
7
|
+
# Ensure: base_name_length + 1 + max_branch_length <= 63
|
|
8
|
+
# config.max_branch_length = 33
|
|
9
|
+
|
|
10
|
+
# Database name suffixes for dev/test environment matching (default: '_development', '_test')
|
|
11
|
+
# Used by cleanup to find corresponding test databases for each dev database
|
|
12
|
+
# Customize if your database names use different suffixes (e.g., '_dev', '_test')
|
|
13
|
+
# config.development_suffix = '_development'
|
|
14
|
+
# config.test_suffix = '_test'
|
|
15
|
+
end
|
data/sig/branch_db.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: branch_db
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ali Hamdi Ali Fadel
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: railties
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.0'
|
|
26
|
+
description: Creates isolated database copies for each git branch, enabling parallel
|
|
27
|
+
feature development without schema conflicts.
|
|
28
|
+
email:
|
|
29
|
+
- aliosm1997@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- CHANGELOG.md
|
|
35
|
+
- LICENSE.txt
|
|
36
|
+
- README.md
|
|
37
|
+
- Rakefile
|
|
38
|
+
- lib/branch_db.rb
|
|
39
|
+
- lib/branch_db/cleaner.rb
|
|
40
|
+
- lib/branch_db/cloner.rb
|
|
41
|
+
- lib/branch_db/configuration.rb
|
|
42
|
+
- lib/branch_db/git_utils.rb
|
|
43
|
+
- lib/branch_db/logging.rb
|
|
44
|
+
- lib/branch_db/naming.rb
|
|
45
|
+
- lib/branch_db/pg_utils.rb
|
|
46
|
+
- lib/branch_db/preparer.rb
|
|
47
|
+
- lib/branch_db/railtie.rb
|
|
48
|
+
- lib/branch_db/tasks/branch_db.rake
|
|
49
|
+
- lib/branch_db/version.rb
|
|
50
|
+
- lib/generators/branch_db/install_generator.rb
|
|
51
|
+
- lib/generators/branch_db/templates/initializer.rb
|
|
52
|
+
- sig/branch_db.rbs
|
|
53
|
+
homepage: https://github.com/milkstrawai/branch_db
|
|
54
|
+
licenses:
|
|
55
|
+
- MIT
|
|
56
|
+
metadata:
|
|
57
|
+
allowed_push_host: https://rubygems.org
|
|
58
|
+
homepage_uri: https://github.com/milkstrawai/branch_db
|
|
59
|
+
source_code_uri: https://github.com/milkstrawai/branch_db
|
|
60
|
+
changelog_uri: https://github.com/milkstrawai/branch_db/blob/main/CHANGELOG.md
|
|
61
|
+
rubygems_mfa_required: 'true'
|
|
62
|
+
rdoc_options: []
|
|
63
|
+
require_paths:
|
|
64
|
+
- lib
|
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: 3.2.0
|
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
requirements: []
|
|
76
|
+
rubygems_version: 3.6.9
|
|
77
|
+
specification_version: 4
|
|
78
|
+
summary: Automatic per-branch PostgreSQL databases for Rails development
|
|
79
|
+
test_files: []
|