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 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
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
data/exe/workbush ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "workbush"
5
+
6
+ Workbush::CLI.start(ARGV)
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Workbush
4
+ VERSION = "0.1.0"
5
+ 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: []