git_worktree_manager 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/LICENSE +21 -0
- data/README.md +106 -0
- data/bin/worktree +22 -0
- data/lib/generators/git_worktree_manager/install_generator.rb +34 -0
- data/lib/git_worktree_manager/cli.rb +53 -0
- data/lib/git_worktree_manager/config.rb +139 -0
- data/lib/git_worktree_manager/database.rb +81 -0
- data/lib/git_worktree_manager/environment.rb +74 -0
- data/lib/git_worktree_manager/git_operations.rb +63 -0
- data/lib/git_worktree_manager/logger.rb +29 -0
- data/lib/git_worktree_manager/ports.rb +13 -0
- data/lib/git_worktree_manager/shell.rb +30 -0
- data/lib/git_worktree_manager/version.rb +3 -0
- data/lib/git_worktree_manager/worktree.rb +345 -0
- data/lib/git_worktree_manager.rb +15 -0
- metadata +156 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b65284335c00fd4d1c52d5ec1502638ffa09124b1287d8530dcb6b5771bbc77c
|
4
|
+
data.tar.gz: 9601148263bd10cd38311c94de945be980fa0f8fc8f3848317ee5e307aa87d9c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 91881dfa53a2277f76e2994132ab28ba083d3bb433ddd607fbed542a14a803010a483c6b9f79903cfc6a4b1de96a0f039f7b2c7b3c63389af4003de05d771d47
|
7
|
+
data.tar.gz: 9751f6b855a7ccc289c506ea4d9c4fe41929a27e9849de47673f894120e2ef5aa3185427bc0065b924d7148d404423f16fe92718a1f94998a9994dc768900871
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Franck
|
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,106 @@
|
|
1
|
+
# Git Worktree Manager
|
2
|
+
|
3
|
+
A shell-agnostic Ruby gem for managing git worktrees with isolated databases, unique ports for Rails and Vite servers, and separate environments.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- ✅ **Shell Agnostic** - Works with bash, zsh, fish, or any POSIX shell
|
8
|
+
- ✅ **Isolated Databases** - Each worktree gets its own PostgreSQL database
|
9
|
+
- ✅ **Automatic Port Assignment** - Rails (3001+) and Vite (5174+) ports
|
10
|
+
- ✅ **Environment Management** - Copies and configures .env files
|
11
|
+
- ✅ **Dependency Installation** - Auto-runs bundle/yarn install
|
12
|
+
- ✅ **Easy Cleanup** - Remove worktrees and databases with one command
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add to your `Gemfile`:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
gem 'git_worktree_manager'
|
20
|
+
```
|
21
|
+
|
22
|
+
Then run:
|
23
|
+
|
24
|
+
```bash
|
25
|
+
bundle install
|
26
|
+
rails generate git_worktree_manager:install
|
27
|
+
```
|
28
|
+
|
29
|
+
This will install the `worktree` command in your project's `bin/` directory.
|
30
|
+
|
31
|
+
### Shell Function (for cd functionality)
|
32
|
+
|
33
|
+
Add this to your shell config file:
|
34
|
+
|
35
|
+
**For Bash/Zsh (`~/.bashrc` or `~/.zshrc`):**
|
36
|
+
|
37
|
+
```bash
|
38
|
+
worktree() {
|
39
|
+
./bin/worktree "$@"
|
40
|
+
if [ -f /tmp/worktree_cd_$$ ]; then
|
41
|
+
cd "$(cat /tmp/worktree_cd_$$)"
|
42
|
+
rm /tmp/worktree_cd_$$
|
43
|
+
fi
|
44
|
+
}
|
45
|
+
```
|
46
|
+
|
47
|
+
**For Fish (`~/.config/fish/config.fish`):**
|
48
|
+
|
49
|
+
```fish
|
50
|
+
function worktree
|
51
|
+
./bin/worktree $argv
|
52
|
+
if test -f /tmp/worktree_cd_(echo %self)
|
53
|
+
cd (cat /tmp/worktree_cd_(echo %self))
|
54
|
+
rm /tmp/worktree_cd_(echo %self)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
## Configuration
|
60
|
+
|
61
|
+
On first use, the gem will auto-detect your main database name from `config/database.yml` or `.env` files. You can view or change the configuration:
|
62
|
+
|
63
|
+
```bash
|
64
|
+
worktree config
|
65
|
+
worktree config --main-database=myapp_development
|
66
|
+
worktree config --database-prefix=myapp
|
67
|
+
```
|
68
|
+
|
69
|
+
The configuration is stored in `.worktree_config.yml` in your project root.
|
70
|
+
|
71
|
+
## Usage
|
72
|
+
|
73
|
+
```bash
|
74
|
+
worktree create feature-branch-name
|
75
|
+
worktree list
|
76
|
+
worktree status feature-branch-name
|
77
|
+
worktree start feature-branch-name
|
78
|
+
worktree remove feature-branch-name
|
79
|
+
worktree cleanup
|
80
|
+
worktree config
|
81
|
+
```
|
82
|
+
|
83
|
+
### Options
|
84
|
+
|
85
|
+
**Create:**
|
86
|
+
- `--copy-data` - Copy data from main database to worktree database
|
87
|
+
- `--no-install` - Skip bundle/yarn install
|
88
|
+
|
89
|
+
## Requirements
|
90
|
+
|
91
|
+
- Ruby >= 3.0.0
|
92
|
+
- Git
|
93
|
+
- PostgreSQL (optional, only if using database features)
|
94
|
+
- Rails project
|
95
|
+
|
96
|
+
## Recommendations
|
97
|
+
|
98
|
+
Add `.worktree_config.yml` to your `.gitignore` if you don't want to share worktree configuration with your team:
|
99
|
+
|
100
|
+
```bash
|
101
|
+
echo ".worktree_config.yml" >> .gitignore
|
102
|
+
```
|
103
|
+
|
104
|
+
## License
|
105
|
+
|
106
|
+
MIT License
|
data/bin/worktree
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'git_worktree_manager'
|
6
|
+
rescue LoadError => e
|
7
|
+
$stderr.puts "Error loading git_worktree_manager: #{e.message}"
|
8
|
+
$stderr.puts "Make sure the gem is installed: bundle install"
|
9
|
+
exit 1
|
10
|
+
end
|
11
|
+
|
12
|
+
begin
|
13
|
+
GitWorktreeManager::CLI.start(ARGV)
|
14
|
+
rescue Interrupt
|
15
|
+
puts "\n"
|
16
|
+
puts "Interrupted"
|
17
|
+
exit 130
|
18
|
+
rescue => e
|
19
|
+
puts "Error: #{e.message}"
|
20
|
+
puts e.backtrace if ENV['DEBUG']
|
21
|
+
exit 1
|
22
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
module GitWorktreeManager
|
4
|
+
module Generators
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
6
|
+
source_root File.expand_path('../../../..', __dir__)
|
7
|
+
|
8
|
+
def copy_executable
|
9
|
+
template 'bin/worktree', 'bin/worktree'
|
10
|
+
chmod 'bin/worktree', 0o755
|
11
|
+
end
|
12
|
+
|
13
|
+
def show_readme
|
14
|
+
readme 'INSTALL_README' if behavior == :invoke
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def readme(_filename)
|
20
|
+
say "\n" + '=' * 80
|
21
|
+
say 'Git Worktree Manager installed successfully!'
|
22
|
+
say '=' * 80
|
23
|
+
say "\nThe 'worktree' command has been added to your project's bin/ directory."
|
24
|
+
say "\nYou can now use it with:"
|
25
|
+
say ' ./bin/worktree', :green
|
26
|
+
say "\nOr add ./bin to your PATH to use it directly:"
|
27
|
+
say ' export PATH="./bin:$PATH"', :cyan
|
28
|
+
say ' worktree', :green
|
29
|
+
say "\nRun './bin/worktree help' to see available commands."
|
30
|
+
say '=' * 80 + "\n"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'thor'
|
2
|
+
|
3
|
+
module GitWorktreeManager
|
4
|
+
class CLI < Thor
|
5
|
+
desc 'create BRANCH', 'Create a new worktree for a branch'
|
6
|
+
option :copy_data, type: :boolean, default: false, desc: 'Copy database from main worktree'
|
7
|
+
option :no_install, type: :boolean, default: false, desc: 'Skip bundle/yarn install'
|
8
|
+
def create(branch)
|
9
|
+
Worktree.new.create(
|
10
|
+
branch,
|
11
|
+
copy_data: options[:copy_data],
|
12
|
+
no_install: options[:no_install]
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'list', 'List all worktrees with their info'
|
17
|
+
def list
|
18
|
+
Worktree.new.list
|
19
|
+
end
|
20
|
+
|
21
|
+
desc 'status BRANCH', 'Show status of a specific worktree'
|
22
|
+
def status(branch)
|
23
|
+
Worktree.new.status(branch)
|
24
|
+
end
|
25
|
+
|
26
|
+
desc 'start BRANCH', 'Start tmux session with servers for a worktree'
|
27
|
+
def start(branch)
|
28
|
+
Worktree.new.start(branch)
|
29
|
+
end
|
30
|
+
|
31
|
+
desc 'remove BRANCH', 'Remove a worktree and its database'
|
32
|
+
option :force, type: :boolean, default: false, aliases: '-f', desc: 'Force removal without confirmation'
|
33
|
+
def remove(branch)
|
34
|
+
Worktree.new.remove(branch, force: options[:force])
|
35
|
+
end
|
36
|
+
|
37
|
+
desc 'cleanup', 'Clean up stale worktree references'
|
38
|
+
def cleanup
|
39
|
+
Worktree.new.cleanup
|
40
|
+
end
|
41
|
+
|
42
|
+
desc 'config', 'Show or set configuration'
|
43
|
+
option :main_database, type: :string, desc: 'Set the main database name'
|
44
|
+
option :database_prefix, type: :string, desc: 'Set the database prefix for worktrees'
|
45
|
+
def config
|
46
|
+
Worktree.new.config(options)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.exit_on_failure?
|
50
|
+
true
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
module GitWorktreeManager
|
6
|
+
class Config
|
7
|
+
attr_reader :config_file, :root_path
|
8
|
+
|
9
|
+
def initialize(root_path)
|
10
|
+
@root_path = root_path
|
11
|
+
@config_file = File.join(root_path, '.worktree_config.yml')
|
12
|
+
@config = load_config
|
13
|
+
ensure_settings
|
14
|
+
end
|
15
|
+
|
16
|
+
def main_database
|
17
|
+
@config['settings']&.dig('main_database') || detect_main_database
|
18
|
+
end
|
19
|
+
|
20
|
+
def database_prefix
|
21
|
+
@config['settings']&.dig('database_prefix') || File.basename(@root_path)
|
22
|
+
end
|
23
|
+
|
24
|
+
def save_settings(main_database: nil, database_prefix: nil)
|
25
|
+
@config['settings'] ||= {}
|
26
|
+
@config['settings']['main_database'] = main_database if main_database
|
27
|
+
@config['settings']['database_prefix'] = database_prefix if database_prefix
|
28
|
+
save_config
|
29
|
+
end
|
30
|
+
|
31
|
+
def save_worktree(branch, path:, rails_port:, vite_port:, database:)
|
32
|
+
@config[normalize_branch_name(branch)] = {
|
33
|
+
'path' => path,
|
34
|
+
'rails_port' => rails_port,
|
35
|
+
'vite_port' => vite_port,
|
36
|
+
'database' => database,
|
37
|
+
'created_at' => Time.now.iso8601
|
38
|
+
}
|
39
|
+
save_config
|
40
|
+
end
|
41
|
+
|
42
|
+
def get_worktree(branch)
|
43
|
+
@config[normalize_branch_name(branch)]
|
44
|
+
end
|
45
|
+
|
46
|
+
def remove_worktree(branch)
|
47
|
+
@config.delete(normalize_branch_name(branch))
|
48
|
+
save_config
|
49
|
+
end
|
50
|
+
|
51
|
+
def all_worktrees
|
52
|
+
@config.select { |_k, v| v.is_a?(Hash) && v['path'] }
|
53
|
+
end
|
54
|
+
|
55
|
+
def next_ports
|
56
|
+
return [3001, 5174] if @config.empty?
|
57
|
+
|
58
|
+
rails_ports = @config.values.map { |v| v['rails_port'] }.compact
|
59
|
+
vite_ports = @config.values.map { |v| v['vite_port'] }.compact
|
60
|
+
|
61
|
+
max_rails = rails_ports.max || 3000
|
62
|
+
max_vite = vite_ports.max || 5173
|
63
|
+
|
64
|
+
rails_port = max_rails >= 3001 ? max_rails + 1 : 3001
|
65
|
+
vite_port = max_vite >= 5174 ? max_vite + 1 : 5174
|
66
|
+
|
67
|
+
[rails_port, vite_port]
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def normalize_branch_name(branch)
|
73
|
+
branch.to_s.gsub(/[^a-zA-Z0-9_]/, '_')
|
74
|
+
end
|
75
|
+
|
76
|
+
def load_config
|
77
|
+
return {} unless File.exist?(@config_file)
|
78
|
+
|
79
|
+
YAML.load_file(@config_file) || {}
|
80
|
+
rescue StandardError => e
|
81
|
+
warn "Warning: Could not load config file: #{e.message}"
|
82
|
+
{}
|
83
|
+
end
|
84
|
+
|
85
|
+
def save_config
|
86
|
+
FileUtils.mkdir_p(File.dirname(@config_file))
|
87
|
+
File.write(@config_file, YAML.dump(@config))
|
88
|
+
end
|
89
|
+
|
90
|
+
def ensure_settings
|
91
|
+
return if @config['settings'] && @config['settings']['main_database'] && @config['settings']['database_prefix']
|
92
|
+
|
93
|
+
@config['settings'] ||= {}
|
94
|
+
@config['settings']['main_database'] ||= detect_main_database
|
95
|
+
@config['settings']['database_prefix'] ||= File.basename(@root_path)
|
96
|
+
save_config
|
97
|
+
end
|
98
|
+
|
99
|
+
def detect_main_database
|
100
|
+
db_config_path = File.join(@root_path, 'config', 'database.yml')
|
101
|
+
|
102
|
+
if File.exist?(db_config_path)
|
103
|
+
require 'yaml'
|
104
|
+
db_config = YAML.load_file(db_config_path)
|
105
|
+
return db_config.dig('development', 'database') if db_config.dig('development', 'database')
|
106
|
+
end
|
107
|
+
|
108
|
+
env_local = File.join(@root_path, '.env.local')
|
109
|
+
env_file = File.join(@root_path, '.env')
|
110
|
+
|
111
|
+
[env_local, env_file].each do |env_path|
|
112
|
+
next unless File.exist?(env_path)
|
113
|
+
|
114
|
+
File.readlines(env_path).each do |line|
|
115
|
+
line = line.strip
|
116
|
+
next if line.empty? || line.start_with?('#')
|
117
|
+
|
118
|
+
return ::Regexp.last_match(1).strip.gsub(/^["']|["']$/, '') if line =~ /^DB_NAME=(.+)$/
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
"#{File.basename(@root_path)}_development"
|
123
|
+
end
|
124
|
+
|
125
|
+
def load_config
|
126
|
+
return {} unless File.exist?(@config_file)
|
127
|
+
|
128
|
+
YAML.load_file(@config_file) || {}
|
129
|
+
rescue StandardError => e
|
130
|
+
warn "Warning: Could not load config file: #{e.message}"
|
131
|
+
{}
|
132
|
+
end
|
133
|
+
|
134
|
+
def save_config
|
135
|
+
FileUtils.mkdir_p(File.dirname(@config_file))
|
136
|
+
File.write(@config_file, YAML.dump(@config))
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
|
3
|
+
module GitWorktreeManager
|
4
|
+
class Database
|
5
|
+
def initialize(credentials, logger)
|
6
|
+
@credentials = credentials
|
7
|
+
@logger = logger
|
8
|
+
end
|
9
|
+
|
10
|
+
def create(database_name)
|
11
|
+
run_rails_command("db:create", database_name)
|
12
|
+
end
|
13
|
+
|
14
|
+
def migrate(database_name)
|
15
|
+
run_rails_command("db:migrate", database_name)
|
16
|
+
end
|
17
|
+
|
18
|
+
def drop(database_name)
|
19
|
+
cmd = build_dropdb_command(database_name)
|
20
|
+
env = { 'PGPASSWORD' => @credentials[:password].to_s }
|
21
|
+
|
22
|
+
success = system(env, cmd, out: File::NULL, err: File::NULL)
|
23
|
+
|
24
|
+
if success
|
25
|
+
@logger.success("Database dropped")
|
26
|
+
else
|
27
|
+
@logger.warning("Failed to drop database (may not exist)")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def copy(source_db, target_db)
|
32
|
+
dump_cmd = build_pg_dump_command(source_db)
|
33
|
+
psql_cmd = build_psql_command(target_db)
|
34
|
+
|
35
|
+
env = { 'PGPASSWORD' => @credentials[:password].to_s }
|
36
|
+
full_cmd = "#{dump_cmd} | #{psql_cmd}"
|
37
|
+
|
38
|
+
success = system(env, full_cmd)
|
39
|
+
|
40
|
+
if success
|
41
|
+
@logger.success("Database copied successfully")
|
42
|
+
else
|
43
|
+
@logger.warning("Failed to copy database data (this is optional)")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def run_rails_command(task, database_name)
|
50
|
+
env = {
|
51
|
+
'RAILS_ENV' => 'development',
|
52
|
+
'DB_NAME' => database_name
|
53
|
+
}
|
54
|
+
system(env, "bundle exec rails #{task}")
|
55
|
+
end
|
56
|
+
|
57
|
+
def build_dropdb_command(database_name)
|
58
|
+
cmd = "dropdb"
|
59
|
+
cmd += " -h #{Shellwords.escape(@credentials[:host])}" if @credentials[:host]
|
60
|
+
cmd += " -U #{Shellwords.escape(@credentials[:user])}" if @credentials[:user]
|
61
|
+
cmd += " #{Shellwords.escape(database_name)}"
|
62
|
+
cmd
|
63
|
+
end
|
64
|
+
|
65
|
+
def build_pg_dump_command(database_name)
|
66
|
+
cmd = "pg_dump"
|
67
|
+
cmd += " -h #{Shellwords.escape(@credentials[:host])}" if @credentials[:host]
|
68
|
+
cmd += " -U #{Shellwords.escape(@credentials[:user])}" if @credentials[:user]
|
69
|
+
cmd += " #{Shellwords.escape(database_name)}"
|
70
|
+
cmd
|
71
|
+
end
|
72
|
+
|
73
|
+
def build_psql_command(database_name)
|
74
|
+
cmd = "psql"
|
75
|
+
cmd += " -h #{Shellwords.escape(@credentials[:host])}" if @credentials[:host]
|
76
|
+
cmd += " -U #{Shellwords.escape(@credentials[:user])}" if @credentials[:user]
|
77
|
+
cmd += " #{Shellwords.escape(database_name)}"
|
78
|
+
cmd
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module GitWorktreeManager
|
4
|
+
class Environment
|
5
|
+
def initialize(project_root)
|
6
|
+
@project_root = project_root
|
7
|
+
end
|
8
|
+
|
9
|
+
def setup_env_files(worktree_path, rails_port:, vite_port:, database:, db_credentials:)
|
10
|
+
copy_env_files(worktree_path)
|
11
|
+
append_worktree_config(worktree_path, rails_port, vite_port, database, db_credentials)
|
12
|
+
end
|
13
|
+
|
14
|
+
def read_db_credentials
|
15
|
+
credentials = {}
|
16
|
+
|
17
|
+
['.env.local', '.env'].each do |env_file|
|
18
|
+
env_path = File.join(@project_root, env_file)
|
19
|
+
next unless File.exist?(env_path)
|
20
|
+
|
21
|
+
File.readlines(env_path).each do |line|
|
22
|
+
line = line.strip
|
23
|
+
next if line.empty? || line.start_with?('#')
|
24
|
+
|
25
|
+
if line =~ /^DB_USER=(.+)$/
|
26
|
+
credentials[:user] ||= unquote($1)
|
27
|
+
elsif line =~ /^DB_PASSWORD=(.+)$/
|
28
|
+
credentials[:password] ||= unquote($1)
|
29
|
+
elsif line =~ /^DB_HOST=(.+)$/
|
30
|
+
credentials[:host] ||= unquote($1)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
credentials[:user] ||= 'postgres'
|
36
|
+
credentials[:password] ||= 'postgres'
|
37
|
+
credentials[:host] ||= 'localhost'
|
38
|
+
|
39
|
+
credentials
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def copy_env_files(worktree_path)
|
45
|
+
['.env', '.env.local'].each do |env_file|
|
46
|
+
source = File.join(@project_root, env_file)
|
47
|
+
target = File.join(worktree_path, env_file)
|
48
|
+
|
49
|
+
FileUtils.cp(source, target) if File.exist?(source)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def append_worktree_config(worktree_path, rails_port, vite_port, database, db_credentials)
|
54
|
+
env_local = File.join(worktree_path, '.env.local')
|
55
|
+
|
56
|
+
config = <<~CONFIG
|
57
|
+
|
58
|
+
# Worktree-specific configuration
|
59
|
+
DB_NAME=#{database}
|
60
|
+
DB_USER=#{db_credentials[:user]}
|
61
|
+
DB_PASSWORD=#{db_credentials[:password]}
|
62
|
+
DB_HOST=#{db_credentials[:host]}
|
63
|
+
RAILS_PORT=#{rails_port}
|
64
|
+
VITE_PORT=#{vite_port}
|
65
|
+
CONFIG
|
66
|
+
|
67
|
+
File.open(env_local, 'a') { |f| f.write(config) }
|
68
|
+
end
|
69
|
+
|
70
|
+
def unquote(value)
|
71
|
+
value.strip.gsub(/^["']|["']$/, '')
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'open3'
|
2
|
+
require 'shellwords'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module GitWorktreeManager
|
6
|
+
class GitOperations
|
7
|
+
def initialize(project_root, logger)
|
8
|
+
@project_root = project_root
|
9
|
+
@logger = logger
|
10
|
+
end
|
11
|
+
|
12
|
+
def worktree_exists?(path)
|
13
|
+
Dir.exist?(path)
|
14
|
+
end
|
15
|
+
|
16
|
+
def branch_exists?(branch)
|
17
|
+
cmd = "git rev-parse --verify #{Shellwords.escape(branch)}"
|
18
|
+
system(cmd, out: File::NULL, err: File::NULL)
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_worktree(path, branch)
|
22
|
+
Dir.chdir(@project_root) do
|
23
|
+
if branch_exists?(branch)
|
24
|
+
@logger.info("Checking out existing branch: #{branch}")
|
25
|
+
system("git worktree add #{Shellwords.escape(path)} #{Shellwords.escape(branch)}")
|
26
|
+
else
|
27
|
+
@logger.info("Creating new branch: #{branch}")
|
28
|
+
system("git worktree add -b #{Shellwords.escape(branch)} #{Shellwords.escape(path)}")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def remove_worktree(path)
|
34
|
+
Dir.chdir(@project_root) do
|
35
|
+
system("git worktree remove #{Shellwords.escape(path)} --force 2>/dev/null")
|
36
|
+
|
37
|
+
if Dir.exist?(path)
|
38
|
+
FileUtils.rm_rf(path)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def list_worktrees
|
44
|
+
Dir.chdir(@project_root) do
|
45
|
+
output, _status = Open3.capture2("git worktree list")
|
46
|
+
output
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def prune_worktrees
|
51
|
+
Dir.chdir(@project_root) do
|
52
|
+
system("git worktree prune")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def git_status(worktree_path)
|
57
|
+
Dir.chdir(worktree_path) do
|
58
|
+
output, _status = Open3.capture2("git status -sb")
|
59
|
+
output
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'pastel'
|
2
|
+
|
3
|
+
module GitWorktreeManager
|
4
|
+
class Logger
|
5
|
+
def initialize
|
6
|
+
@pastel = Pastel.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def info(message)
|
10
|
+
puts "#{@pastel.blue('ℹ')} #{message}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def success(message)
|
14
|
+
puts "#{@pastel.green('✓')} #{message}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def warning(message)
|
18
|
+
puts "#{@pastel.yellow('⚠')} #{message}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def error(message)
|
22
|
+
puts "#{@pastel.red('✗')} #{message}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def plain(message)
|
26
|
+
puts message
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module GitWorktreeManager
|
2
|
+
class Ports
|
3
|
+
def self.check_port_available(port)
|
4
|
+
cmd = "lsof -i :#{port}"
|
5
|
+
!system(cmd, out: File::NULL, err: File::NULL)
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.find_process_on_port(port)
|
9
|
+
output = `lsof -i :#{port} 2>/dev/null | grep LISTEN`
|
10
|
+
output.strip.empty? ? nil : output
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
|
3
|
+
module GitWorktreeManager
|
4
|
+
class Shell
|
5
|
+
def self.change_directory(path)
|
6
|
+
marker_file = "/tmp/worktree_cd_#{Process.pid}"
|
7
|
+
File.write(marker_file, path)
|
8
|
+
|
9
|
+
Dir.chdir(path) if Dir.exist?(path)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.spawn_shell_in_directory(path)
|
13
|
+
Dir.chdir(path) if Dir.exist?(path)
|
14
|
+
|
15
|
+
shell = ENV['SHELL'] || '/bin/bash'
|
16
|
+
|
17
|
+
change_directory(path)
|
18
|
+
|
19
|
+
exec(shell)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.current_shell
|
23
|
+
ENV['SHELL'] || '/bin/bash'
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.in_directory?(current_path, target_path)
|
27
|
+
current_path.start_with?(target_path)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,345 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'tty-prompt'
|
3
|
+
require 'tty-table'
|
4
|
+
|
5
|
+
module GitWorktreeManager
|
6
|
+
class Worktree
|
7
|
+
def initialize(project_root: nil)
|
8
|
+
@project_root = project_root || detect_project_root
|
9
|
+
@project_name = File.basename(@project_root)
|
10
|
+
@parent_dir = File.dirname(@project_root)
|
11
|
+
|
12
|
+
@logger = Logger.new
|
13
|
+
@config = Config.new(@project_root)
|
14
|
+
@env = Environment.new(@project_root)
|
15
|
+
@git = GitOperations.new(@project_root, @logger)
|
16
|
+
end
|
17
|
+
|
18
|
+
def create(branch, copy_data: false, no_install: false)
|
19
|
+
worktree_name = "#{@project_name}-#{branch}"
|
20
|
+
worktree_path = File.join(@parent_dir, worktree_name)
|
21
|
+
|
22
|
+
if @git.worktree_exists?(worktree_path)
|
23
|
+
@logger.error("Worktree already exists at #{worktree_path}")
|
24
|
+
exit 1
|
25
|
+
end
|
26
|
+
|
27
|
+
@logger.info("Creating worktree for branch: #{branch}")
|
28
|
+
|
29
|
+
unless @git.create_worktree(worktree_path, branch)
|
30
|
+
@logger.error('Failed to create worktree')
|
31
|
+
exit 1
|
32
|
+
end
|
33
|
+
|
34
|
+
@logger.success("Worktree created at #{worktree_path}")
|
35
|
+
|
36
|
+
rails_port, vite_port = @config.next_ports
|
37
|
+
db_prefix = @config.database_prefix
|
38
|
+
db_name = "#{db_prefix}_development_#{branch.gsub(/[^a-zA-Z0-9]/, '_')}"
|
39
|
+
|
40
|
+
@logger.info('Setting up environment (.env and .env.local)')
|
41
|
+
|
42
|
+
db_credentials = @env.read_db_credentials
|
43
|
+
@env.setup_env_files(
|
44
|
+
worktree_path,
|
45
|
+
rails_port: rails_port,
|
46
|
+
vite_port: vite_port,
|
47
|
+
database: db_name,
|
48
|
+
db_credentials: db_credentials
|
49
|
+
)
|
50
|
+
|
51
|
+
@logger.success("Environment configured (Rails: #{rails_port}, Vite: #{vite_port}, DB: #{db_name})")
|
52
|
+
|
53
|
+
@config.save_worktree(
|
54
|
+
branch,
|
55
|
+
path: worktree_path,
|
56
|
+
rails_port: rails_port,
|
57
|
+
vite_port: vite_port,
|
58
|
+
database: db_name
|
59
|
+
)
|
60
|
+
|
61
|
+
Dir.chdir(worktree_path)
|
62
|
+
|
63
|
+
install_dependencies unless no_install
|
64
|
+
|
65
|
+
setup_database(db_name, db_credentials, copy_data)
|
66
|
+
|
67
|
+
@logger.success('Worktree setup complete!')
|
68
|
+
@logger.plain('')
|
69
|
+
@logger.info('Environment configured:')
|
70
|
+
@logger.plain(" Rails port: #{rails_port}")
|
71
|
+
@logger.plain(" Vite port: #{vite_port}")
|
72
|
+
@logger.plain(" Database: #{db_name}")
|
73
|
+
@logger.plain('')
|
74
|
+
@logger.info('To start servers, run: ./.tmux')
|
75
|
+
|
76
|
+
Shell.spawn_shell_in_directory(worktree_path)
|
77
|
+
end
|
78
|
+
|
79
|
+
def list
|
80
|
+
@logger.info('Git Worktrees:')
|
81
|
+
@logger.plain('')
|
82
|
+
@logger.plain(@git.list_worktrees)
|
83
|
+
|
84
|
+
@logger.plain('')
|
85
|
+
@logger.info('Configured Worktrees:')
|
86
|
+
@logger.plain('')
|
87
|
+
|
88
|
+
worktrees = @config.all_worktrees
|
89
|
+
|
90
|
+
if worktrees.empty?
|
91
|
+
@logger.warning('No worktree configuration found')
|
92
|
+
return
|
93
|
+
end
|
94
|
+
|
95
|
+
rows = worktrees.map do |branch, info|
|
96
|
+
next unless Dir.exist?(info['path'])
|
97
|
+
|
98
|
+
[
|
99
|
+
branch,
|
100
|
+
"#{info['rails_port']}:#{info['vite_port']}",
|
101
|
+
info['database'],
|
102
|
+
info['path']
|
103
|
+
]
|
104
|
+
end.compact
|
105
|
+
|
106
|
+
table = TTY::Table.new(
|
107
|
+
header: ['BRANCH', 'RAILS:VITE', 'DATABASE', 'PATH'],
|
108
|
+
rows: rows
|
109
|
+
)
|
110
|
+
|
111
|
+
puts table.render(:basic)
|
112
|
+
end
|
113
|
+
|
114
|
+
def status(branch)
|
115
|
+
info = @config.get_worktree(branch)
|
116
|
+
|
117
|
+
unless info
|
118
|
+
@logger.error("Worktree not found for branch: #{branch}")
|
119
|
+
exit 1
|
120
|
+
end
|
121
|
+
|
122
|
+
@logger.plain("Branch: #{branch}")
|
123
|
+
@logger.plain("Path: #{info['path']}")
|
124
|
+
@logger.plain("Rails Port: #{info['rails_port']}")
|
125
|
+
@logger.plain("Vite Port: #{info['vite_port']}")
|
126
|
+
@logger.plain("Database: #{info['database']}")
|
127
|
+
@logger.plain('')
|
128
|
+
|
129
|
+
if Dir.exist?(info['path'])
|
130
|
+
@logger.success('Worktree directory exists')
|
131
|
+
else
|
132
|
+
@logger.error('Worktree directory not found')
|
133
|
+
end
|
134
|
+
|
135
|
+
@logger.plain('')
|
136
|
+
@logger.info('Git status:')
|
137
|
+
@logger.plain(@git.git_status(info['path']))
|
138
|
+
|
139
|
+
@logger.plain('')
|
140
|
+
@logger.info('Server status:')
|
141
|
+
|
142
|
+
check_port_status(info['rails_port'], 'Rails')
|
143
|
+
check_port_status(info['vite_port'], 'Vite')
|
144
|
+
end
|
145
|
+
|
146
|
+
def start(branch)
|
147
|
+
info = @config.get_worktree(branch)
|
148
|
+
|
149
|
+
unless info
|
150
|
+
@logger.error("Worktree not found for branch: #{branch}")
|
151
|
+
exit 1
|
152
|
+
end
|
153
|
+
|
154
|
+
worktree_path = info['path']
|
155
|
+
|
156
|
+
unless Dir.exist?(worktree_path)
|
157
|
+
@logger.error("Worktree directory not found: #{worktree_path}")
|
158
|
+
exit 1
|
159
|
+
end
|
160
|
+
|
161
|
+
@logger.info("Starting tmux session for branch: #{branch}")
|
162
|
+
@logger.info("Path: #{worktree_path}")
|
163
|
+
@logger.info("Rails port: #{info['rails_port']}, Vite port: #{info['vite_port']}")
|
164
|
+
@logger.plain('')
|
165
|
+
|
166
|
+
Dir.chdir(worktree_path)
|
167
|
+
|
168
|
+
tmux_script = File.join(worktree_path, '.tmux')
|
169
|
+
|
170
|
+
if File.exist?(tmux_script)
|
171
|
+
exec(tmux_script)
|
172
|
+
else
|
173
|
+
@logger.error('.tmux script not found')
|
174
|
+
@logger.info('Start servers manually:')
|
175
|
+
@logger.plain(" rails s -p #{info['rails_port']}")
|
176
|
+
@logger.plain(" VITE_RUBY_PORT=#{info['vite_port']} bin/vite dev")
|
177
|
+
exit 1
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def remove(branch, force: false)
|
182
|
+
info = @config.get_worktree(branch)
|
183
|
+
|
184
|
+
unless info
|
185
|
+
worktree_name = "#{@project_name}-#{branch}"
|
186
|
+
worktree_path = File.join(@parent_dir, worktree_name)
|
187
|
+
|
188
|
+
if Dir.exist?(worktree_path) && @git.worktree_exists?(worktree_path)
|
189
|
+
@logger.warning("Worktree exists but not configured. Attempting to remove: #{worktree_path}")
|
190
|
+
|
191
|
+
unless force
|
192
|
+
prompt = TTY::Prompt.new
|
193
|
+
return unless prompt.yes?('Remove unconfigured worktree?')
|
194
|
+
end
|
195
|
+
|
196
|
+
current_dir = Dir.pwd
|
197
|
+
Shell.in_directory?(current_dir, worktree_path)
|
198
|
+
|
199
|
+
@git.remove_worktree(worktree_path)
|
200
|
+
@logger.success('Worktree removed')
|
201
|
+
|
202
|
+
@logger.info('Navigating to main worktree...')
|
203
|
+
Shell.spawn_shell_in_directory(@project_root)
|
204
|
+
return
|
205
|
+
end
|
206
|
+
|
207
|
+
@logger.error("Worktree not found for branch: #{branch}")
|
208
|
+
exit 1
|
209
|
+
end
|
210
|
+
|
211
|
+
worktree_path = info['path']
|
212
|
+
db_name = info['database']
|
213
|
+
|
214
|
+
unless force
|
215
|
+
@logger.warning('This will remove:')
|
216
|
+
@logger.plain(" - Worktree at: #{worktree_path}")
|
217
|
+
@logger.plain(" - Database: #{db_name}")
|
218
|
+
@logger.plain('')
|
219
|
+
|
220
|
+
prompt = TTY::Prompt.new
|
221
|
+
return unless prompt.yes?('Are you sure?')
|
222
|
+
end
|
223
|
+
|
224
|
+
@logger.info('Removing worktree...')
|
225
|
+
|
226
|
+
current_dir = Dir.pwd
|
227
|
+
Shell.in_directory?(current_dir, worktree_path)
|
228
|
+
|
229
|
+
@git.remove_worktree(worktree_path)
|
230
|
+
@logger.success('Worktree removed')
|
231
|
+
|
232
|
+
if db_name
|
233
|
+
@logger.info("Dropping database: #{db_name}")
|
234
|
+
db_credentials = @env.read_db_credentials
|
235
|
+
db = Database.new(db_credentials, @logger)
|
236
|
+
db.drop(db_name)
|
237
|
+
end
|
238
|
+
|
239
|
+
@config.remove_worktree(branch)
|
240
|
+
@logger.success('Cleanup complete')
|
241
|
+
|
242
|
+
@logger.info('Navigating to main worktree...')
|
243
|
+
Shell.spawn_shell_in_directory(@project_root)
|
244
|
+
end
|
245
|
+
|
246
|
+
def cleanup
|
247
|
+
@logger.info('Cleaning up stale worktree references...')
|
248
|
+
|
249
|
+
@git.prune_worktrees
|
250
|
+
|
251
|
+
worktrees = @config.all_worktrees
|
252
|
+
cleaned = 0
|
253
|
+
|
254
|
+
worktrees.each do |branch, info|
|
255
|
+
unless Dir.exist?(info['path'])
|
256
|
+
@logger.info("Removing stale config for: #{branch}")
|
257
|
+
@config.remove_worktree(branch)
|
258
|
+
cleaned += 1
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
@logger.success("Cleaned up #{cleaned} stale configuration(s)")
|
263
|
+
end
|
264
|
+
|
265
|
+
def config(options)
|
266
|
+
if options[:main_database] || options[:database_prefix]
|
267
|
+
@config.save_settings(
|
268
|
+
main_database: options[:main_database],
|
269
|
+
database_prefix: options[:database_prefix]
|
270
|
+
)
|
271
|
+
@logger.success('Configuration updated')
|
272
|
+
end
|
273
|
+
|
274
|
+
@logger.plain('Current configuration:')
|
275
|
+
@logger.plain('')
|
276
|
+
@logger.plain(" Main database: #{@config.main_database}")
|
277
|
+
@logger.plain(" Database prefix: #{@config.database_prefix}")
|
278
|
+
@logger.plain('')
|
279
|
+
@logger.info('To change settings, use:')
|
280
|
+
@logger.plain(' worktree config --main-database=mydb_development')
|
281
|
+
@logger.plain(' worktree config --database-prefix=myproject')
|
282
|
+
end
|
283
|
+
|
284
|
+
@logger.success("Cleaned up #{cleaned} stale configuration(s)")
|
285
|
+
end
|
286
|
+
|
287
|
+
private
|
288
|
+
|
289
|
+
def detect_project_root
|
290
|
+
current_dir = Dir.pwd
|
291
|
+
|
292
|
+
current_dir = File.dirname(current_dir) until current_dir == '/' || File.exist?(File.join(current_dir, '.git'))
|
293
|
+
|
294
|
+
if current_dir == '/'
|
295
|
+
puts 'Error: Not in a git repository'
|
296
|
+
exit 1
|
297
|
+
end
|
298
|
+
|
299
|
+
current_dir
|
300
|
+
end
|
301
|
+
|
302
|
+
def install_dependencies
|
303
|
+
@logger.info('Installing dependencies...')
|
304
|
+
|
305
|
+
if File.exist?('Gemfile')
|
306
|
+
@logger.info('Running bundle install...')
|
307
|
+
system('bundle install')
|
308
|
+
end
|
309
|
+
|
310
|
+
if File.exist?('package.json')
|
311
|
+
@logger.info('Running yarn install...')
|
312
|
+
system('yarn install')
|
313
|
+
end
|
314
|
+
|
315
|
+
@logger.success('Dependencies installed')
|
316
|
+
end
|
317
|
+
|
318
|
+
def setup_database(db_name, db_credentials, copy_data)
|
319
|
+
db = Database.new(db_credentials, @logger)
|
320
|
+
|
321
|
+
@logger.info("Creating database: #{db_name}")
|
322
|
+
db.create(db_name)
|
323
|
+
|
324
|
+
@logger.info('Running migrations...')
|
325
|
+
db.migrate(db_name)
|
326
|
+
|
327
|
+
return unless copy_data
|
328
|
+
|
329
|
+
main_db = @config.main_database
|
330
|
+
@logger.info("Copying data from #{main_db} to #{db_name}...")
|
331
|
+
db.copy(main_db, db_name)
|
332
|
+
end
|
333
|
+
|
334
|
+
def check_port_status(port, server_name)
|
335
|
+
process_info = Ports.find_process_on_port(port)
|
336
|
+
|
337
|
+
if process_info
|
338
|
+
@logger.success("#{server_name} server is running on port #{port}")
|
339
|
+
@logger.plain(process_info)
|
340
|
+
else
|
341
|
+
@logger.info("#{server_name} server is not running on port #{port}")
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative 'git_worktree_manager/version'
|
2
|
+
require_relative 'git_worktree_manager/logger'
|
3
|
+
require_relative 'git_worktree_manager/config'
|
4
|
+
require_relative 'git_worktree_manager/environment'
|
5
|
+
require_relative 'git_worktree_manager/database'
|
6
|
+
require_relative 'git_worktree_manager/shell'
|
7
|
+
require_relative 'git_worktree_manager/git_operations'
|
8
|
+
require_relative 'git_worktree_manager/ports'
|
9
|
+
require_relative 'git_worktree_manager/worktree'
|
10
|
+
require_relative 'git_worktree_manager/cli'
|
11
|
+
|
12
|
+
require_relative 'generators/git_worktree_manager/install_generator' if defined?(Rails::Railtie)
|
13
|
+
|
14
|
+
module GitWorktreeManager
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: git_worktree_manager
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Franck
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: dotenv
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '2.8'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '2.8'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: pastel
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0.8'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0.8'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: thor
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.3'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.3'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: tty-prompt
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0.23'
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0.23'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: tty-table
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0.12'
|
75
|
+
type: :runtime
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0.12'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: rspec
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '3.12'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '3.12'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: rubocop
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '1.50'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '1.50'
|
110
|
+
description: Shell-agnostic tool for managing Rails git worktrees with separate databases,
|
111
|
+
unique ports for Rails and Vite, and isolated environments
|
112
|
+
email:
|
113
|
+
- franck.dagostini@gmail.com
|
114
|
+
executables:
|
115
|
+
- worktree
|
116
|
+
extensions: []
|
117
|
+
extra_rdoc_files: []
|
118
|
+
files:
|
119
|
+
- LICENSE
|
120
|
+
- README.md
|
121
|
+
- bin/worktree
|
122
|
+
- lib/generators/git_worktree_manager/install_generator.rb
|
123
|
+
- lib/git_worktree_manager.rb
|
124
|
+
- lib/git_worktree_manager/cli.rb
|
125
|
+
- lib/git_worktree_manager/config.rb
|
126
|
+
- lib/git_worktree_manager/database.rb
|
127
|
+
- lib/git_worktree_manager/environment.rb
|
128
|
+
- lib/git_worktree_manager/git_operations.rb
|
129
|
+
- lib/git_worktree_manager/logger.rb
|
130
|
+
- lib/git_worktree_manager/ports.rb
|
131
|
+
- lib/git_worktree_manager/shell.rb
|
132
|
+
- lib/git_worktree_manager/version.rb
|
133
|
+
- lib/git_worktree_manager/worktree.rb
|
134
|
+
homepage: https://github.com/franck/git_worktree_manager
|
135
|
+
licenses:
|
136
|
+
- MIT
|
137
|
+
metadata:
|
138
|
+
rubygems_mfa_required: 'true'
|
139
|
+
rdoc_options: []
|
140
|
+
require_paths:
|
141
|
+
- lib
|
142
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - ">="
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: 3.0.0
|
147
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
requirements: []
|
153
|
+
rubygems_version: 3.6.9
|
154
|
+
specification_version: 4
|
155
|
+
summary: Manage git worktrees with isolated databases and ports
|
156
|
+
test_files: []
|