workbush 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/.workbush.yml.example +39 -0
- data/LICENSE.txt +21 -0
- data/README.md +248 -0
- data/Rakefile +3 -0
- data/exe/workbush +6 -0
- data/lib/workbush/cli.rb +231 -0
- data/lib/workbush/command_runner.rb +142 -0
- data/lib/workbush/config.rb +103 -0
- data/lib/workbush/errors.rb +21 -0
- data/lib/workbush/file_copier.rb +150 -0
- data/lib/workbush/version.rb +5 -0
- data/lib/workbush/worktree.rb +144 -0
- data/lib/workbush.rb +12 -0
- metadata +72 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 132934e2d49cc1ddb117f480bf1a25ec9a4a91fcabc24df1b43ff0b57d046aad
|
|
4
|
+
data.tar.gz: 03ac659ce226d284e40e02a3b343181eb07b3dd43f02c48175c8421559b196ae
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1ba2a27a705a2f695a076fe9f28fb6875945706c62131a8dea8fad2f7f51cdad05b7be4faad549dd34e3feee3bc4ec0226d32a444e713d692eec57a94067b5f1
|
|
7
|
+
data.tar.gz: 627bea0fc9628d829d184eb23c4d6530fe969badebb76a54904c6bc42df885be55ac5b46f1289664c0528038091a602577765120e617eafcdd0baab6baa90588
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Workbush Configuration Example
|
|
2
|
+
# Copy this file to .workbush.yml in your project root and customize as needed
|
|
3
|
+
|
|
4
|
+
# Files to copy (exact paths relative to worktree root)
|
|
5
|
+
copy_files:
|
|
6
|
+
- .env
|
|
7
|
+
- .env.local
|
|
8
|
+
- .env.development
|
|
9
|
+
- config/database.yml
|
|
10
|
+
- config/credentials.yml.enc
|
|
11
|
+
|
|
12
|
+
# Glob patterns for copying multiple files
|
|
13
|
+
# Use standard Ruby glob syntax
|
|
14
|
+
copy_patterns:
|
|
15
|
+
- ".env.*" # All .env.* files
|
|
16
|
+
- "*.lock" # All lock files (Gemfile.lock, package-lock.json, etc.)
|
|
17
|
+
- "config/*.local" # All .local config files
|
|
18
|
+
|
|
19
|
+
# Directories to copy
|
|
20
|
+
# Useful for dependencies that are expensive to reinstall
|
|
21
|
+
copy_directories:
|
|
22
|
+
- node_modules # Node.js dependencies
|
|
23
|
+
- vendor/bundle # Ruby bundled gems
|
|
24
|
+
- .bundle # Bundler configuration
|
|
25
|
+
- tmp/cache # Application cache
|
|
26
|
+
|
|
27
|
+
# Commands to run after worktree creation
|
|
28
|
+
# These run in the new worktree directory
|
|
29
|
+
post_create_commands:
|
|
30
|
+
- bundle install # Install Ruby gems
|
|
31
|
+
- yarn install # Install Node packages
|
|
32
|
+
- rails db:migrate # Run database migrations
|
|
33
|
+
|
|
34
|
+
# You can also use ERB templates for dynamic configuration:
|
|
35
|
+
# copy_files:
|
|
36
|
+
# - <%= ENV.fetch("CONFIG_FILE", ".env") %>
|
|
37
|
+
#
|
|
38
|
+
# post_create_commands:
|
|
39
|
+
# - <%= ENV.fetch("SETUP_COMMAND", "bundle install") %>
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Oğulcan Girginç
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# Workbush 🌳
|
|
2
|
+
|
|
3
|
+
Manage git worktrees with automatic file copying and setup commands. Workbush simplifies the process of creating git worktrees by automatically copying dependencies, configuration files, and running setup commands based on a simple YAML configuration.
|
|
4
|
+
|
|
5
|
+
## Why Workbush?
|
|
6
|
+
|
|
7
|
+
Git worktrees are powerful for working on multiple branches simultaneously, but setting them up can be tedious:
|
|
8
|
+
- Manually copying `.env` files
|
|
9
|
+
- Re-running `bundle install` or `npm install`
|
|
10
|
+
- Copying `node_modules` or `vendor/bundle` to avoid reinstalling
|
|
11
|
+
- Running database migrations
|
|
12
|
+
- Copying other configuration files
|
|
13
|
+
|
|
14
|
+
Workbush automates all of this with a single command.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **Automatic file copying**: Copy dependencies, configs, and build artifacts
|
|
19
|
+
- **Post-creation commands**: Run setup commands automatically
|
|
20
|
+
- **Glob pattern support**: Copy files matching patterns
|
|
21
|
+
- **YAML configuration**: Simple, version-controlled setup
|
|
22
|
+
- **ERB templates**: Dynamic configuration with environment variables
|
|
23
|
+
- **Thor-based CLI**: Beautiful, colorized output
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Install the gem:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
gem install workbush
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or add to your Gemfile:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
gem 'workbush'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
1. **Initialize configuration** in your project:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
workbush init
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
2. **Edit `.workbush.yml`** to match your needs:
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
copy_files:
|
|
51
|
+
- .env
|
|
52
|
+
- .env.local
|
|
53
|
+
- mise.local.toml
|
|
54
|
+
|
|
55
|
+
copy_directories:
|
|
56
|
+
- node_modules
|
|
57
|
+
- vendor/bundle
|
|
58
|
+
|
|
59
|
+
post_create_commands:
|
|
60
|
+
- mise trust
|
|
61
|
+
- bundle install
|
|
62
|
+
- rails db:migrate
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
3. **Create a worktree** with automatic setup:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
workbush add ../feature-123 feature-branch
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
That's it! Your new worktree is ready with all files copied and commands executed.
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
### Create a worktree
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Create worktree for existing branch
|
|
79
|
+
workbush add ../feature-123 feature-branch
|
|
80
|
+
|
|
81
|
+
# Create worktree with new branch
|
|
82
|
+
workbush add ../bug-fix new-branch-name
|
|
83
|
+
|
|
84
|
+
# Create without copying files
|
|
85
|
+
workbush add --no-copy ../quick-test main
|
|
86
|
+
|
|
87
|
+
# Create without running commands
|
|
88
|
+
workbush add --no-run-commands ../feature-456 feature-branch
|
|
89
|
+
|
|
90
|
+
# Create with verbose output
|
|
91
|
+
workbush add --verbose ../feature-789 feature-branch
|
|
92
|
+
|
|
93
|
+
# Force creation (overwrite existing path)
|
|
94
|
+
workbush add --force ../existing-path feature-branch
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### List worktrees
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
workbush list
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Remove a worktree
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# With confirmation prompt
|
|
107
|
+
workbush remove ../feature-123
|
|
108
|
+
|
|
109
|
+
# Force removal without confirmation
|
|
110
|
+
workbush remove --force ../feature-123
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Initialize configuration
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Create .workbush.yml in current directory
|
|
117
|
+
workbush init
|
|
118
|
+
|
|
119
|
+
# Overwrite existing config
|
|
120
|
+
workbush init --force
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Show version
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
workbush version
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Configuration
|
|
130
|
+
|
|
131
|
+
Create a `.workbush.yml` file in your project root:
|
|
132
|
+
|
|
133
|
+
```yaml
|
|
134
|
+
# Files to copy (exact paths)
|
|
135
|
+
copy_files:
|
|
136
|
+
- .env
|
|
137
|
+
- .env.local
|
|
138
|
+
- .env.development
|
|
139
|
+
- config/database.yml
|
|
140
|
+
- config/credentials.yml.enc
|
|
141
|
+
|
|
142
|
+
# Glob patterns for multiple files
|
|
143
|
+
copy_patterns:
|
|
144
|
+
- ".env.*"
|
|
145
|
+
- "*.lock"
|
|
146
|
+
- "config/*.local"
|
|
147
|
+
|
|
148
|
+
# Directories to copy
|
|
149
|
+
copy_directories:
|
|
150
|
+
- node_modules
|
|
151
|
+
- vendor/bundle
|
|
152
|
+
- .bundle
|
|
153
|
+
- tmp/cache
|
|
154
|
+
|
|
155
|
+
# Commands to run after creation
|
|
156
|
+
post_create_commands:
|
|
157
|
+
- bundle install
|
|
158
|
+
- yarn install
|
|
159
|
+
- rails db:migrate
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### ERB Templates
|
|
163
|
+
|
|
164
|
+
Use ERB for dynamic configuration:
|
|
165
|
+
|
|
166
|
+
```yaml
|
|
167
|
+
copy_files:
|
|
168
|
+
- <%= ENV.fetch("CONFIG_FILE", ".env") %>
|
|
169
|
+
|
|
170
|
+
post_create_commands:
|
|
171
|
+
- <%= ENV.fetch("SETUP_COMMAND", "bundle install") %>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Use Cases
|
|
175
|
+
|
|
176
|
+
### Rails Development
|
|
177
|
+
|
|
178
|
+
```yaml
|
|
179
|
+
copy_files:
|
|
180
|
+
- .env
|
|
181
|
+
- mise.local.toml
|
|
182
|
+
- config/database.yml
|
|
183
|
+
- config/master.key
|
|
184
|
+
|
|
185
|
+
copy_directories:
|
|
186
|
+
- vendor/bundle
|
|
187
|
+
- node_modules
|
|
188
|
+
|
|
189
|
+
post_create_commands:
|
|
190
|
+
- mise trust
|
|
191
|
+
- bundle install
|
|
192
|
+
- yarn install
|
|
193
|
+
- rails db:migrate
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Node.js Projects
|
|
197
|
+
|
|
198
|
+
```yaml
|
|
199
|
+
copy_files:
|
|
200
|
+
- .env
|
|
201
|
+
- .env.local
|
|
202
|
+
|
|
203
|
+
copy_directories:
|
|
204
|
+
- node_modules
|
|
205
|
+
|
|
206
|
+
post_create_commands:
|
|
207
|
+
- npm install
|
|
208
|
+
- npm run build
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Ruby Gems
|
|
212
|
+
|
|
213
|
+
```yaml
|
|
214
|
+
copy_directories:
|
|
215
|
+
- vendor/bundle
|
|
216
|
+
|
|
217
|
+
post_create_commands:
|
|
218
|
+
- bundle install
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Global Options
|
|
222
|
+
|
|
223
|
+
All commands support these options:
|
|
224
|
+
|
|
225
|
+
- `--verbose` or `-v`: Enable detailed output
|
|
226
|
+
- `--config PATH` or `-c PATH`: Use custom config file
|
|
227
|
+
|
|
228
|
+
## Tips
|
|
229
|
+
|
|
230
|
+
1. **Commit your `.workbush.yml`**: Share configuration with your team
|
|
231
|
+
2. **Copy `node_modules`**: Much faster than re-running `npm install`
|
|
232
|
+
3. **Use glob patterns**: Copy all `.env.*` files at once
|
|
233
|
+
4. **Skip commands when needed**: Use `--no-run-commands` for quick testing
|
|
234
|
+
5. **Check the list**: Use `workbush list` to see all your worktrees
|
|
235
|
+
|
|
236
|
+
## Development
|
|
237
|
+
|
|
238
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
239
|
+
|
|
240
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
241
|
+
|
|
242
|
+
## Contributing
|
|
243
|
+
|
|
244
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/workbush.
|
|
245
|
+
|
|
246
|
+
## License
|
|
247
|
+
|
|
248
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/exe/workbush
ADDED
data/lib/workbush/cli.rb
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require_relative "config"
|
|
5
|
+
require_relative "worktree"
|
|
6
|
+
require_relative "file_copier"
|
|
7
|
+
require_relative "command_runner"
|
|
8
|
+
require_relative "errors"
|
|
9
|
+
|
|
10
|
+
module Workbush
|
|
11
|
+
# Command-line interface for Workbush
|
|
12
|
+
class CLI < Thor
|
|
13
|
+
class_option :verbose, type: :boolean, aliases: "-v", desc: "Enable verbose output"
|
|
14
|
+
class_option :config, type: :string, aliases: "-c", desc: "Path to config file"
|
|
15
|
+
|
|
16
|
+
desc "add PATH [BRANCH]", "Create a new worktree with automatic setup"
|
|
17
|
+
long_desc <<~LONGDESC
|
|
18
|
+
Creates a git worktree at the specified PATH, optionally checking out BRANCH.
|
|
19
|
+
|
|
20
|
+
Automatically copies files and runs setup commands based on .workbush.yml configuration.
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
# Create worktree for existing branch
|
|
24
|
+
$ workbush add ../feature-123 feature-branch
|
|
25
|
+
|
|
26
|
+
# Create worktree with new branch from current HEAD
|
|
27
|
+
$ workbush add ../bug-fix new-branch-name
|
|
28
|
+
|
|
29
|
+
# Create without copying files
|
|
30
|
+
$ workbush add --no-copy ../quick-test main
|
|
31
|
+
|
|
32
|
+
# Create with verbose output
|
|
33
|
+
$ workbush add --verbose ../feature-456 feature-branch
|
|
34
|
+
LONGDESC
|
|
35
|
+
method_option :copy, type: :boolean, default: true, desc: "Copy files from parent worktree"
|
|
36
|
+
method_option :run_commands, type: :boolean, default: true, desc: "Run post-creation commands"
|
|
37
|
+
method_option :force, type: :boolean, aliases: "-f", desc: "Force creation even if path exists"
|
|
38
|
+
def add(path, branch = nil)
|
|
39
|
+
config = load_config
|
|
40
|
+
|
|
41
|
+
# Create worktree
|
|
42
|
+
worktree = Worktree.new(path: path, branch: branch)
|
|
43
|
+
say "Creating worktree at #{path}...", :cyan
|
|
44
|
+
worktree.create(force: options[:force])
|
|
45
|
+
say "✓ Worktree created successfully", :green
|
|
46
|
+
|
|
47
|
+
# Copy files if enabled
|
|
48
|
+
if options[:copy] && has_copy_config?(config)
|
|
49
|
+
copy_files(worktree, config)
|
|
50
|
+
elsif options[:copy]
|
|
51
|
+
say "No copy configuration found, skipping file copy", :yellow if options[:verbose]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Run commands if enabled
|
|
55
|
+
if options[:run_commands] && has_commands?(config)
|
|
56
|
+
run_commands(worktree, config)
|
|
57
|
+
elsif options[:run_commands]
|
|
58
|
+
say "No post-creation commands configured", :yellow if options[:verbose]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
say
|
|
62
|
+
say "Worktree ready at: #{worktree.path}", :green
|
|
63
|
+
say "To switch to it: cd #{path}", :cyan
|
|
64
|
+
rescue GitError => e
|
|
65
|
+
say "Git error: #{e.message}", :red
|
|
66
|
+
exit 1
|
|
67
|
+
rescue WorktreeError => e
|
|
68
|
+
say "Worktree error: #{e.message}", :red
|
|
69
|
+
exit 1
|
|
70
|
+
rescue StandardError => e
|
|
71
|
+
say "Error: #{e.message}", :red
|
|
72
|
+
say e.backtrace.join("\n"), :red if options[:verbose]
|
|
73
|
+
exit 1
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
desc "init", "Create a .workbush.yml configuration file"
|
|
77
|
+
long_desc <<~LONGDESC
|
|
78
|
+
Creates a .workbush.yml configuration file in the current directory with example settings.
|
|
79
|
+
|
|
80
|
+
The configuration file defines:
|
|
81
|
+
- Files to copy from parent worktree
|
|
82
|
+
- Glob patterns for copying multiple files
|
|
83
|
+
- Directories to copy
|
|
84
|
+
- Commands to run after worktree creation
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
$ workbush init
|
|
88
|
+
$ vim .workbush.yml # Edit to your needs
|
|
89
|
+
LONGDESC
|
|
90
|
+
method_option :force, type: :boolean, aliases: "-f", desc: "Overwrite existing config file"
|
|
91
|
+
def init
|
|
92
|
+
config_path = File.join(Dir.pwd, Config::DEFAULT_CONFIG_NAME)
|
|
93
|
+
|
|
94
|
+
if File.exist?(config_path) && !options[:force]
|
|
95
|
+
say "Configuration file already exists: #{config_path}", :yellow
|
|
96
|
+
return unless yes?("Overwrite?")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
File.write(config_path, Config.default_template)
|
|
100
|
+
say "Created configuration file: #{config_path}", :green
|
|
101
|
+
say
|
|
102
|
+
say "Edit this file to customize your worktree setup:", :cyan
|
|
103
|
+
say " #{config_path}"
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
say "Error creating config: #{e.message}", :red
|
|
106
|
+
exit 1
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
desc "list", "List all worktrees in the repository"
|
|
110
|
+
long_desc <<~LONGDESC
|
|
111
|
+
Lists all worktrees in the current git repository.
|
|
112
|
+
|
|
113
|
+
Shows the path, branch, and status for each worktree.
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
$ workbush list
|
|
117
|
+
LONGDESC
|
|
118
|
+
def list
|
|
119
|
+
worktrees = Worktree.list
|
|
120
|
+
|
|
121
|
+
if worktrees.empty?
|
|
122
|
+
say "No worktrees found", :yellow
|
|
123
|
+
return
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
say "Worktrees:", :cyan
|
|
127
|
+
worktrees.each do |wt|
|
|
128
|
+
branch_info = wt[:branch] || wt[:head]
|
|
129
|
+
status = []
|
|
130
|
+
status << "bare" if wt[:bare]
|
|
131
|
+
status << "detached" if wt[:detached]
|
|
132
|
+
status << "locked" if wt[:locked]
|
|
133
|
+
|
|
134
|
+
status_str = status.empty? ? "" : " (#{status.join(", ")})"
|
|
135
|
+
say " #{wt[:path]} [#{branch_info}]#{status_str}"
|
|
136
|
+
end
|
|
137
|
+
rescue GitError => e
|
|
138
|
+
say "Git error: #{e.message}", :red
|
|
139
|
+
exit 1
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
desc "remove PATH", "Remove a worktree"
|
|
143
|
+
long_desc <<~LONGDESC
|
|
144
|
+
Removes a worktree at the specified PATH.
|
|
145
|
+
|
|
146
|
+
By default, asks for confirmation before removing.
|
|
147
|
+
Use --force to skip confirmation and remove even if worktree is dirty.
|
|
148
|
+
|
|
149
|
+
Examples:
|
|
150
|
+
$ workbush remove ../feature-123
|
|
151
|
+
$ workbush remove --force ../old-branch
|
|
152
|
+
LONGDESC
|
|
153
|
+
method_option :force, type: :boolean, aliases: "-f", desc: "Force removal without confirmation"
|
|
154
|
+
def remove(path)
|
|
155
|
+
expanded_path = File.expand_path(path)
|
|
156
|
+
|
|
157
|
+
unless options[:force]
|
|
158
|
+
return unless yes?("Remove worktree at #{expanded_path}?")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
Worktree.remove(expanded_path, force: options[:force])
|
|
162
|
+
say "✓ Worktree removed: #{expanded_path}", :green
|
|
163
|
+
rescue WorktreeError => e
|
|
164
|
+
say "Error: #{e.message}", :red
|
|
165
|
+
exit 1
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
desc "version", "Show Workbush version"
|
|
169
|
+
def version
|
|
170
|
+
say "Workbush version #{Workbush::VERSION}"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def load_config
|
|
176
|
+
Config.new(config_path: options[:config])
|
|
177
|
+
rescue ConfigError => e
|
|
178
|
+
say "Config error: #{e.message}", :red
|
|
179
|
+
exit 1
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def has_copy_config?(config)
|
|
183
|
+
!config.copy_files.empty? ||
|
|
184
|
+
!config.copy_patterns.empty? ||
|
|
185
|
+
!config.copy_directories.empty?
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def has_commands?(config)
|
|
189
|
+
!config.post_create_commands.empty?
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def copy_files(worktree, config)
|
|
193
|
+
say "Copying files...", :cyan if options[:verbose]
|
|
194
|
+
|
|
195
|
+
copier = FileCopier.new(
|
|
196
|
+
source_path: worktree.source_path,
|
|
197
|
+
dest_path: worktree.path,
|
|
198
|
+
verbose: options[:verbose]
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
stats = copier.copy_from_config(config)
|
|
202
|
+
|
|
203
|
+
if stats[:copied] > 0
|
|
204
|
+
say "✓ Copied #{stats[:copied]} items", :green
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
if stats[:failed] > 0
|
|
208
|
+
say "✗ Failed to copy #{stats[:failed]} items", :yellow
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def run_commands(worktree, config)
|
|
213
|
+
say "Running setup commands...", :cyan if options[:verbose]
|
|
214
|
+
|
|
215
|
+
runner = CommandRunner.new(
|
|
216
|
+
worktree_path: worktree.path,
|
|
217
|
+
verbose: options[:verbose]
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
stats = runner.run_from_config(config)
|
|
221
|
+
|
|
222
|
+
if stats[:success] > 0
|
|
223
|
+
say "✓ Executed #{stats[:success]} commands", :green
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
if stats[:failed] > 0
|
|
227
|
+
say "✗ #{stats[:failed]} commands failed", :yellow
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
|
|
6
|
+
module Workbush
|
|
7
|
+
# Handles executing post-creation commands in the new worktree
|
|
8
|
+
class CommandRunner
|
|
9
|
+
attr_reader :worktree_path, :verbose
|
|
10
|
+
|
|
11
|
+
# Initialize a new CommandRunner instance
|
|
12
|
+
#
|
|
13
|
+
# @param worktree_path [String] Path to the worktree where commands will run
|
|
14
|
+
# @param verbose [Boolean] Enable verbose output
|
|
15
|
+
def initialize(worktree_path:, verbose: false)
|
|
16
|
+
@worktree_path = File.expand_path(worktree_path)
|
|
17
|
+
@verbose = verbose
|
|
18
|
+
@success_count = 0
|
|
19
|
+
@failed_count = 0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Run commands from configuration
|
|
23
|
+
#
|
|
24
|
+
# @param config [Config] Configuration object with commands
|
|
25
|
+
# @return [Hash] Statistics about command execution
|
|
26
|
+
def run_from_config(config)
|
|
27
|
+
commands = config.post_create_commands
|
|
28
|
+
return { success: 0, failed: 0 } if commands.empty?
|
|
29
|
+
|
|
30
|
+
log "Running post-creation commands..."
|
|
31
|
+
|
|
32
|
+
commands.each do |command|
|
|
33
|
+
run_command(command)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
log_summary
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
success: @success_count,
|
|
40
|
+
failed: @failed_count
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Run a single command
|
|
45
|
+
#
|
|
46
|
+
# @param command [String] Command to execute
|
|
47
|
+
# @return [Boolean] True if successful
|
|
48
|
+
def run_command(command)
|
|
49
|
+
log "Executing: #{command}", :info
|
|
50
|
+
|
|
51
|
+
stdout, stderr, status = Open3.capture3(
|
|
52
|
+
command,
|
|
53
|
+
chdir: @worktree_path,
|
|
54
|
+
unsetenv_others: false
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if status.success?
|
|
58
|
+
log " ✓ Success", :success
|
|
59
|
+
log_output(stdout) if @verbose && !stdout.strip.empty?
|
|
60
|
+
@success_count += 1
|
|
61
|
+
true
|
|
62
|
+
else
|
|
63
|
+
log " ✗ Failed (exit #{status.exitstatus})", :error
|
|
64
|
+
log_output(stderr) unless stderr.strip.empty?
|
|
65
|
+
@failed_count += 1
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
log " ✗ Failed: #{e.message}", :error
|
|
70
|
+
@failed_count += 1
|
|
71
|
+
false
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Run commands with streaming output (for interactive commands)
|
|
75
|
+
#
|
|
76
|
+
# @param command [String] Command to execute
|
|
77
|
+
# @return [Boolean] True if successful
|
|
78
|
+
def run_command_streaming(command)
|
|
79
|
+
log "Executing: #{command}", :info
|
|
80
|
+
|
|
81
|
+
Open3.popen3(command, chdir: @worktree_path) do |_stdin, stdout, stderr, wait_thr|
|
|
82
|
+
# Stream stdout
|
|
83
|
+
Thread.new do
|
|
84
|
+
stdout.each_line { |line| puts line }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Stream stderr
|
|
88
|
+
Thread.new do
|
|
89
|
+
stderr.each_line { |line| warn line }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
status = wait_thr.value
|
|
93
|
+
|
|
94
|
+
if status.success?
|
|
95
|
+
log " ✓ Success", :success
|
|
96
|
+
@success_count += 1
|
|
97
|
+
true
|
|
98
|
+
else
|
|
99
|
+
log " ✗ Failed (exit #{status.exitstatus})", :error
|
|
100
|
+
@failed_count += 1
|
|
101
|
+
false
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
log " ✗ Failed: #{e.message}", :error
|
|
106
|
+
@failed_count += 1
|
|
107
|
+
false
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def log(message, type = :info)
|
|
113
|
+
return unless @verbose
|
|
114
|
+
|
|
115
|
+
case type
|
|
116
|
+
when :success
|
|
117
|
+
puts message
|
|
118
|
+
when :error
|
|
119
|
+
warn message
|
|
120
|
+
else
|
|
121
|
+
puts message
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def log_output(output)
|
|
126
|
+
return if output.strip.empty?
|
|
127
|
+
|
|
128
|
+
output.each_line do |line|
|
|
129
|
+
puts " #{line}"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def log_summary
|
|
134
|
+
return unless @verbose
|
|
135
|
+
|
|
136
|
+
puts
|
|
137
|
+
puts "Command summary:"
|
|
138
|
+
puts " #{@success_count} succeeded"
|
|
139
|
+
puts " #{@failed_count} failed" if @failed_count > 0
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "erb"
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
|
|
7
|
+
module Workbush
|
|
8
|
+
# Handles loading and parsing .workbush.yml configuration files
|
|
9
|
+
class Config
|
|
10
|
+
DEFAULT_CONFIG_NAME = ".workbush.yml"
|
|
11
|
+
|
|
12
|
+
attr_reader :copy_files, :copy_patterns, :copy_directories, :post_create_commands
|
|
13
|
+
|
|
14
|
+
# Initialize a new Config instance
|
|
15
|
+
#
|
|
16
|
+
# @param config_path [String, nil] Optional path to config file
|
|
17
|
+
def initialize(config_path: nil)
|
|
18
|
+
@config_path = config_path || find_config_file
|
|
19
|
+
load_config
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Check if a configuration file was found
|
|
23
|
+
#
|
|
24
|
+
# @return [Boolean]
|
|
25
|
+
def config_exists?
|
|
26
|
+
!@config_path.nil? && File.exist?(@config_path)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Generate a default configuration template
|
|
30
|
+
#
|
|
31
|
+
# @return [String] YAML template content
|
|
32
|
+
def self.default_template
|
|
33
|
+
<<~YAML
|
|
34
|
+
# Files to copy (exact paths relative to worktree root)
|
|
35
|
+
copy_files:
|
|
36
|
+
- .env
|
|
37
|
+
- .env.local
|
|
38
|
+
- mise.local.toml
|
|
39
|
+
- config/database.yml
|
|
40
|
+
|
|
41
|
+
# Glob patterns for copying multiple files
|
|
42
|
+
copy_patterns:
|
|
43
|
+
- ".env.*"
|
|
44
|
+
- "*.lock"
|
|
45
|
+
|
|
46
|
+
# Directories to copy
|
|
47
|
+
copy_directories:
|
|
48
|
+
- node_modules
|
|
49
|
+
- vendor/bundle
|
|
50
|
+
|
|
51
|
+
# Commands to run after worktree creation
|
|
52
|
+
post_create_commands:
|
|
53
|
+
- mise trust
|
|
54
|
+
- bundle install
|
|
55
|
+
- yarn install
|
|
56
|
+
YAML
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def load_config
|
|
62
|
+
if config_exists?
|
|
63
|
+
load_from_file
|
|
64
|
+
else
|
|
65
|
+
load_defaults
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def load_from_file
|
|
70
|
+
content = File.read(@config_path)
|
|
71
|
+
erb_content = ERB.new(content).result
|
|
72
|
+
data = YAML.safe_load(erb_content, permitted_classes: [Symbol], aliases: true) || {}
|
|
73
|
+
|
|
74
|
+
@copy_files = Array(data["copy_files"])
|
|
75
|
+
@copy_patterns = Array(data["copy_patterns"])
|
|
76
|
+
@copy_directories = Array(data["copy_directories"])
|
|
77
|
+
@post_create_commands = Array(data["post_create_commands"])
|
|
78
|
+
rescue StandardError => e
|
|
79
|
+
raise ConfigError, "Failed to load config from #{@config_path}: #{e.message}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def load_defaults
|
|
83
|
+
@copy_files = []
|
|
84
|
+
@copy_patterns = []
|
|
85
|
+
@copy_directories = []
|
|
86
|
+
@post_create_commands = []
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def find_config_file
|
|
90
|
+
current_dir = Dir.pwd
|
|
91
|
+
|
|
92
|
+
# Search from current directory up to root (like git does)
|
|
93
|
+
until current_dir == "/"
|
|
94
|
+
config_file = File.join(current_dir, DEFAULT_CONFIG_NAME)
|
|
95
|
+
return config_file if File.exist?(config_file)
|
|
96
|
+
|
|
97
|
+
current_dir = File.dirname(current_dir)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Workbush
|
|
4
|
+
# Base error class for all Workbush errors
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when git repository is not found or invalid
|
|
8
|
+
class GitError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when worktree creation fails
|
|
11
|
+
class WorktreeError < Error; end
|
|
12
|
+
|
|
13
|
+
# Raised when configuration file is invalid
|
|
14
|
+
class ConfigError < Error; end
|
|
15
|
+
|
|
16
|
+
# Raised when file operations fail
|
|
17
|
+
class FileCopyError < Error; end
|
|
18
|
+
|
|
19
|
+
# Raised when command execution fails
|
|
20
|
+
class CommandError < Error; end
|
|
21
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
|
|
6
|
+
module Workbush
|
|
7
|
+
# Handles copying files from source worktree to destination worktree
|
|
8
|
+
class FileCopier
|
|
9
|
+
attr_reader :source_path, :dest_path, :verbose
|
|
10
|
+
|
|
11
|
+
# Initialize a new FileCopier instance
|
|
12
|
+
#
|
|
13
|
+
# @param source_path [String] Source worktree path
|
|
14
|
+
# @param dest_path [String] Destination worktree path
|
|
15
|
+
# @param verbose [Boolean] Enable verbose output
|
|
16
|
+
def initialize(source_path:, dest_path:, verbose: false)
|
|
17
|
+
@source_path = File.expand_path(source_path)
|
|
18
|
+
@dest_path = File.expand_path(dest_path)
|
|
19
|
+
@verbose = verbose
|
|
20
|
+
@copied_count = 0
|
|
21
|
+
@skipped_count = 0
|
|
22
|
+
@failed_count = 0
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Copy files based on configuration
|
|
26
|
+
#
|
|
27
|
+
# @param config [Config] Configuration object with copy settings
|
|
28
|
+
# @return [Hash] Statistics about the copy operation
|
|
29
|
+
def copy_from_config(config)
|
|
30
|
+
log "Starting file copy operations..."
|
|
31
|
+
|
|
32
|
+
copy_exact_files(config.copy_files)
|
|
33
|
+
copy_matching_patterns(config.copy_patterns)
|
|
34
|
+
copy_directories(config.copy_directories)
|
|
35
|
+
|
|
36
|
+
log_summary
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
copied: @copied_count,
|
|
40
|
+
skipped: @skipped_count,
|
|
41
|
+
failed: @failed_count
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Copy specific files by exact path
|
|
46
|
+
#
|
|
47
|
+
# @param files [Array<String>] List of file paths relative to source
|
|
48
|
+
def copy_exact_files(files)
|
|
49
|
+
return if files.empty?
|
|
50
|
+
|
|
51
|
+
log "Copying exact files..."
|
|
52
|
+
files.each do |file|
|
|
53
|
+
source = File.join(@source_path, file)
|
|
54
|
+
dest = File.join(@dest_path, file)
|
|
55
|
+
|
|
56
|
+
copy_single_item(source, dest, file)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Copy files matching glob patterns
|
|
61
|
+
#
|
|
62
|
+
# @param patterns [Array<String>] List of glob patterns
|
|
63
|
+
def copy_matching_patterns(patterns)
|
|
64
|
+
return if patterns.empty?
|
|
65
|
+
|
|
66
|
+
log "Copying files matching patterns..."
|
|
67
|
+
patterns.each do |pattern|
|
|
68
|
+
matches = Dir.glob(File.join(@source_path, pattern))
|
|
69
|
+
|
|
70
|
+
if matches.empty?
|
|
71
|
+
log " No files matched pattern: #{pattern}", :skip
|
|
72
|
+
@skipped_count += 1
|
|
73
|
+
next
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
matches.each do |source|
|
|
77
|
+
relative_path = source.sub("#{@source_path}/", "")
|
|
78
|
+
dest = File.join(@dest_path, relative_path)
|
|
79
|
+
|
|
80
|
+
copy_single_item(source, dest, relative_path)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Copy entire directories
|
|
86
|
+
#
|
|
87
|
+
# @param directories [Array<String>] List of directory paths
|
|
88
|
+
def copy_directories(directories)
|
|
89
|
+
return if directories.empty?
|
|
90
|
+
|
|
91
|
+
log "Copying directories..."
|
|
92
|
+
directories.each do |dir|
|
|
93
|
+
source = File.join(@source_path, dir)
|
|
94
|
+
dest = File.join(@dest_path, dir)
|
|
95
|
+
|
|
96
|
+
copy_single_item(source, dest, dir)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def copy_single_item(source, dest, label)
|
|
103
|
+
unless File.exist?(source)
|
|
104
|
+
log " Skipped (not found): #{label}", :skip
|
|
105
|
+
@skipped_count += 1
|
|
106
|
+
return false
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Ensure destination directory exists
|
|
110
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
111
|
+
|
|
112
|
+
if File.directory?(source)
|
|
113
|
+
FileUtils.cp_r(source, dest, preserve: true)
|
|
114
|
+
else
|
|
115
|
+
FileUtils.cp(source, dest, preserve: true)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
log " Copied: #{label}", :success
|
|
119
|
+
@copied_count += 1
|
|
120
|
+
true
|
|
121
|
+
rescue StandardError => e
|
|
122
|
+
log " Failed: #{label} (#{e.message})", :error
|
|
123
|
+
@failed_count += 1
|
|
124
|
+
false
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def log(message, type = :info)
|
|
128
|
+
return unless @verbose
|
|
129
|
+
|
|
130
|
+
prefix = case type
|
|
131
|
+
when :success then "✓"
|
|
132
|
+
when :skip then "○"
|
|
133
|
+
when :error then "✗"
|
|
134
|
+
else "•"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
puts "#{prefix} #{message}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def log_summary
|
|
141
|
+
return unless @verbose
|
|
142
|
+
|
|
143
|
+
puts
|
|
144
|
+
puts "Copy summary:"
|
|
145
|
+
puts " #{@copied_count} files/directories copied"
|
|
146
|
+
puts " #{@skipped_count} skipped" if @skipped_count > 0
|
|
147
|
+
puts " #{@failed_count} failed" if @failed_count > 0
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
|
|
7
|
+
module Workbush
|
|
8
|
+
# Handles git worktree operations
|
|
9
|
+
class Worktree
|
|
10
|
+
attr_reader :path, :branch
|
|
11
|
+
|
|
12
|
+
# Initialize a new Worktree instance
|
|
13
|
+
#
|
|
14
|
+
# @param path [String] Path where the worktree will be created
|
|
15
|
+
# @param branch [String, nil] Branch name to checkout
|
|
16
|
+
# @param source_path [String] Path to the source/parent worktree
|
|
17
|
+
def initialize(path:, branch: nil, source_path: Dir.pwd)
|
|
18
|
+
@path = File.expand_path(path)
|
|
19
|
+
@branch = branch
|
|
20
|
+
@source_path = source_path
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Create the worktree
|
|
24
|
+
#
|
|
25
|
+
# @param force [Boolean] Force creation even if path exists
|
|
26
|
+
# @return [String] Git command output
|
|
27
|
+
# @raise [GitError] If not in a git repository
|
|
28
|
+
# @raise [WorktreeError] If worktree creation fails
|
|
29
|
+
def create(force: false)
|
|
30
|
+
validate_git_repository!
|
|
31
|
+
validate_path!(force)
|
|
32
|
+
|
|
33
|
+
cmd = build_git_command(force)
|
|
34
|
+
execute_git_command(cmd)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Get the parent/source worktree path
|
|
38
|
+
#
|
|
39
|
+
# @return [String]
|
|
40
|
+
def source_path
|
|
41
|
+
@source_path
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# List all worktrees in the repository
|
|
45
|
+
#
|
|
46
|
+
# @return [Array<Hash>] Array of worktree information
|
|
47
|
+
def self.list
|
|
48
|
+
stdout, stderr, status = Open3.capture3("git", "worktree", "list", "--porcelain")
|
|
49
|
+
|
|
50
|
+
raise GitError, "Failed to list worktrees: #{stderr}" unless status.success?
|
|
51
|
+
|
|
52
|
+
parse_worktree_list(stdout)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Remove a worktree
|
|
56
|
+
#
|
|
57
|
+
# @param path [String] Path to the worktree to remove
|
|
58
|
+
# @param force [Boolean] Force removal even if worktree is dirty
|
|
59
|
+
# @return [Boolean] True if successful
|
|
60
|
+
# @raise [WorktreeError] If removal fails
|
|
61
|
+
def self.remove(path, force: false)
|
|
62
|
+
cmd = ["git", "worktree", "remove", path]
|
|
63
|
+
cmd << "--force" if force
|
|
64
|
+
|
|
65
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
|
66
|
+
|
|
67
|
+
unless status.success?
|
|
68
|
+
raise WorktreeError, "Failed to remove worktree: #{stderr}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def validate_git_repository!
|
|
77
|
+
stdout, stderr, status = Open3.capture3("git", "rev-parse", "--git-dir", chdir: @source_path)
|
|
78
|
+
|
|
79
|
+
return if status.success?
|
|
80
|
+
|
|
81
|
+
raise GitError, "Not a git repository: #{@source_path}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def validate_path!(force)
|
|
85
|
+
return unless File.exist?(@path) && !force
|
|
86
|
+
|
|
87
|
+
raise WorktreeError, "Path already exists: #{@path}. Use --force to override."
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def build_git_command(force)
|
|
91
|
+
cmd = ["git", "worktree", "add"]
|
|
92
|
+
cmd << "--force" if force
|
|
93
|
+
|
|
94
|
+
if @branch
|
|
95
|
+
cmd += [@path, @branch]
|
|
96
|
+
else
|
|
97
|
+
cmd << @path
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
cmd
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def execute_git_command(cmd)
|
|
104
|
+
stdout, stderr, status = Open3.capture3(*cmd, chdir: @source_path)
|
|
105
|
+
|
|
106
|
+
unless status.success?
|
|
107
|
+
raise WorktreeError, "Failed to create worktree: #{stderr}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
stdout
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self.parse_worktree_list(output)
|
|
114
|
+
worktrees = []
|
|
115
|
+
current = {}
|
|
116
|
+
|
|
117
|
+
output.each_line do |line|
|
|
118
|
+
line = line.strip
|
|
119
|
+
next if line.empty?
|
|
120
|
+
|
|
121
|
+
key, value = line.split(" ", 2)
|
|
122
|
+
|
|
123
|
+
case key
|
|
124
|
+
when "worktree"
|
|
125
|
+
worktrees << current unless current.empty?
|
|
126
|
+
current = { path: value }
|
|
127
|
+
when "HEAD"
|
|
128
|
+
current[:head] = value
|
|
129
|
+
when "branch"
|
|
130
|
+
current[:branch] = value
|
|
131
|
+
when "bare"
|
|
132
|
+
current[:bare] = true
|
|
133
|
+
when "detached"
|
|
134
|
+
current[:detached] = true
|
|
135
|
+
when "locked"
|
|
136
|
+
current[:locked] = value || true
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
worktrees << current unless current.empty?
|
|
141
|
+
worktrees
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
data/lib/workbush.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "workbush/version"
|
|
4
|
+
require_relative "workbush/errors"
|
|
5
|
+
require_relative "workbush/config"
|
|
6
|
+
require_relative "workbush/worktree"
|
|
7
|
+
require_relative "workbush/file_copier"
|
|
8
|
+
require_relative "workbush/command_runner"
|
|
9
|
+
require_relative "workbush/cli"
|
|
10
|
+
|
|
11
|
+
module Workbush
|
|
12
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: workbush
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Oğulcan Girginç
|
|
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: thor
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.3'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.3'
|
|
26
|
+
description: Workbush simplifies git worktree creation by automatically copying dependencies,
|
|
27
|
+
configuration files, and running setup commands based on a simple YAML configuration.
|
|
28
|
+
email:
|
|
29
|
+
- ogulcan@girginc.com
|
|
30
|
+
executables:
|
|
31
|
+
- workbush
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- ".workbush.yml.example"
|
|
36
|
+
- LICENSE.txt
|
|
37
|
+
- README.md
|
|
38
|
+
- Rakefile
|
|
39
|
+
- exe/workbush
|
|
40
|
+
- lib/workbush.rb
|
|
41
|
+
- lib/workbush/cli.rb
|
|
42
|
+
- lib/workbush/command_runner.rb
|
|
43
|
+
- lib/workbush/config.rb
|
|
44
|
+
- lib/workbush/errors.rb
|
|
45
|
+
- lib/workbush/file_copier.rb
|
|
46
|
+
- lib/workbush/version.rb
|
|
47
|
+
- lib/workbush/worktree.rb
|
|
48
|
+
homepage: https://github.com/ogirginc/workbush
|
|
49
|
+
licenses:
|
|
50
|
+
- MIT
|
|
51
|
+
metadata:
|
|
52
|
+
allowed_push_host: https://rubygems.org
|
|
53
|
+
homepage_uri: https://github.com/ogirginc/workbush
|
|
54
|
+
source_code_uri: https://github.com/ogirginc/workbush
|
|
55
|
+
rdoc_options: []
|
|
56
|
+
require_paths:
|
|
57
|
+
- lib
|
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: 3.2.0
|
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
requirements: []
|
|
69
|
+
rubygems_version: 3.7.2
|
|
70
|
+
specification_version: 4
|
|
71
|
+
summary: Manage git worktrees with automatic file copying and setup commands
|
|
72
|
+
test_files: []
|