workroom 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: 709e14f9e8836400b840ad9772320ad6ddc9f2fbb163c800a0ba932b5d26ebe1
4
+ data.tar.gz: 6334a7e2daa13faaf34e3d7931bfe1082ec752b456f88531b255d429515c99da
5
+ SHA512:
6
+ metadata.gz: 07ec6e20dfc896dec2a75cd11fe402b31c9dad2619ca323ac5b191ef23aa8cef681c742d41254356e62edef460aa5377c1e37d20c461d1e9f14caf15dec14658
7
+ data.tar.gz: 8915fa7afb229916379e8c0343550734b25300f87d6a64d3b495b4509afe1112e976e3138720920c1881dbac02366aee68dee7bed4e8f26d6c27ed95d6d49e40
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Joel Moss
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # Workroom
2
+
3
+ Create and manage local development workrooms using [JJ](https://martinvonz.github.io/jj/) workspaces or git worktrees.
4
+
5
+ A workroom is an isolated copy of your project created as a sibling directory, allowing you to work on multiple branches or features simultaneously without stashing or switching contexts.
6
+
7
+ ## Installation
8
+
9
+ Add to your Gemfile:
10
+
11
+ ```ruby
12
+ gem 'workroom'
13
+ ```
14
+
15
+ Then run `bundle install`.
16
+
17
+ Or install directly:
18
+
19
+ ```bash
20
+ gem install workroom
21
+ ```
22
+
23
+ ## Requirements
24
+
25
+ - Ruby >= 3.1
26
+ - [JJ (Jujutsu)](https://martinvonz.github.io/jj/) or Git
27
+
28
+ ## Usage
29
+
30
+ ### Create a workroom
31
+
32
+ ```bash
33
+ workroom create my-feature
34
+ ```
35
+
36
+ This creates a new workroom at `../my-feature` relative to your project root. Workroom automatically detects whether you're using JJ or Git and uses the appropriate mechanism (JJ workspace or git worktree).
37
+
38
+ ### Delete a workroom
39
+
40
+ ```bash
41
+ workroom delete my-feature
42
+ ```
43
+
44
+ Removes the workspace/worktree and cleans up the directory. You'll be prompted for confirmation before deletion.
45
+
46
+ ### Options
47
+
48
+ - `-v`, `--verbose` - Print detailed output
49
+ - `-p`, `--pretend` - Run through the command without making changes (dry run)
50
+
51
+ ### Naming rules
52
+
53
+ Workroom names must be alphanumeric (dashes and underscores allowed) and must not start or end with a dash or underscore.
54
+
55
+ ## Setup and teardown scripts
56
+
57
+ Workroom supports user-defined scripts that run automatically during create and delete operations.
58
+
59
+ ### Setup script
60
+
61
+ Place an executable script at `scripts/workroom_setup` in your project. It will run inside the new workroom directory after creation.
62
+
63
+ ### Teardown script
64
+
65
+ Place an executable script at `scripts/workroom_teardown` in your project. It will run after a workroom is deleted.
66
+
67
+ ## Rails integration
68
+
69
+ Workroom includes a Rails Engine for auto-discovery by host Rails apps. Simply add the gem to your Rails app's Gemfile and it will be loaded automatically.
70
+
71
+ ## License
72
+
73
+ [MIT](MIT-LICENSE)
data/bin/workroom ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler'
5
+ Bundler.require
6
+
7
+ Workroom::Commands.start
@@ -0,0 +1,294 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'thor'
5
+
6
+ module Workroom
7
+ class Commands < Thor
8
+ include Thor::Actions
9
+
10
+ IGNORED_JJ_WORKSPACE_NAMES = ['', 'default'].freeze
11
+
12
+ attr_reader :name
13
+
14
+ class_option :verbose, type: :boolean, aliases: '-v', group: :runtime,
15
+ desc: 'Print detailed and verbose output'
16
+ add_runtime_options!
17
+
18
+ def self.exit_on_failure?
19
+ true
20
+ end
21
+
22
+ desc 'create NAME', 'Create a new workroom'
23
+ long_desc <<-DESC, wrap: false
24
+ Create a new workroom with the given NAME at the same level as your main project directory,
25
+ using JJ workspaces if available, otherwise falling back to git worktrees.
26
+ DESC
27
+ def create(name)
28
+ @name = name
29
+ check_not_in_workroom!
30
+ validate_name!
31
+
32
+ if !options[:pretend]
33
+ if workroom_exists?
34
+ exception = jj? ? JJWorkspaceExistsError : GitWorktreeExistsError
35
+ raise_error exception, "#{vcs_label} '#{name}' already exists!"
36
+ end
37
+
38
+ if workroom_path.exist?
39
+ raise_error DirExistsError, "Workroom directory '#{workroom_path}' already exists!"
40
+ end
41
+ end
42
+
43
+ create_workroom
44
+ run_setup_script
45
+
46
+ say
47
+ say "Workroom '#{name}' created successfully at #{workroom_path}.", :green
48
+
49
+ return if !@setup_result
50
+
51
+ say 'Setup script output:', :blue
52
+ say @setup_result
53
+ end
54
+
55
+ desc 'delete NAME', 'Delete an existing workroom'
56
+ def delete(name)
57
+ @name = name
58
+ check_not_in_workroom!
59
+ validate_name!
60
+
61
+ if !options[:pretend]
62
+ if !workroom_exists?
63
+ exception = jj? ? JJWorkspaceExistsError : GitWorktreeExistsError
64
+ raise_error exception, "#{vcs_label} '#{name}' does not exist!"
65
+ end
66
+
67
+ if !yes?("Are you sure you want to delete workroom '#{name}'?")
68
+ say_error "Aborting. Workroom '#{name}' was not deleted.", :yellow
69
+ return
70
+ end
71
+ end
72
+
73
+ delete_workroom
74
+ cleanup_directory if jj?
75
+ run_teardown_script
76
+
77
+ say
78
+ say "Workroom '#{name}' deleted successfully.", :green
79
+
80
+ if !jj?
81
+ say
82
+ say "Note: Git branch '#{name}' was not deleted."
83
+ say " Delete manually with `git branch -D #{name}` if needed."
84
+ end
85
+
86
+ return if !@teardown_result
87
+
88
+ say
89
+ say 'Teardown script output:', :blue
90
+ say @teardown_result
91
+ end
92
+
93
+ private
94
+
95
+ def run_setup_script
96
+ return if !setup_script.exist?
97
+
98
+ inside workroom_path do
99
+ run_user_script :setup, setup_script_to_run.to_s
100
+ end
101
+ end
102
+
103
+ def setup_script
104
+ @setup_script ||= workroom_path.join('scripts', 'workroom_setup')
105
+ end
106
+
107
+ def setup_script_to_run
108
+ setup_script
109
+ end
110
+
111
+ def run_teardown_script
112
+ return if !teardown_script.exist?
113
+
114
+ run_user_script :teardown, teardown_script_to_run.to_s
115
+ end
116
+
117
+ def teardown_script
118
+ @teardown_script ||= Pathname.pwd.join('scripts', 'workroom_teardown')
119
+ end
120
+
121
+ def teardown_script_to_run
122
+ teardown_script
123
+ end
124
+
125
+ def run_user_script(type, command)
126
+ return if behavior != :invoke
127
+
128
+ destination = relative_to_original_destination_root(destination_root, false)
129
+
130
+ say_status type, "Running #{command} from #{destination.inspect}"
131
+
132
+ return if options[:pretend]
133
+
134
+ result, status = Open3.capture2e(command)
135
+
136
+ instance_variable_set :"@#{type}_result", result
137
+
138
+ return if status.success?
139
+
140
+ exception_class = Object.const_get("::Workroom::#{type.to_s.capitalize}Error")
141
+
142
+ raise_error exception_class, "#{command} returned a non-zero exit code.\n#{result}"
143
+ end
144
+
145
+ def raise_error(exception_class, message)
146
+ message = shell.set_color message, :red if !testing?
147
+ raise exception_class, message
148
+ end
149
+
150
+ def workroom_path
151
+ @workroom_path ||= Pathname.pwd.join("../#{name}")
152
+ end
153
+
154
+ def jj?
155
+ vcs == :jj
156
+ end
157
+
158
+ def vcs
159
+ @vcs ||= if Dir.exist?('.jj')
160
+ say_status :repo, 'Detected Jujutsu'
161
+ :jj
162
+ elsif Dir.exist?('.git')
163
+ say_status :repo, 'Detected Git'
164
+ :git
165
+ else
166
+ say_status :repo, 'No supported VCS detected', :red
167
+ raise_error UnsupportedVCSError, <<~_
168
+ No supported VCS detected. Workroom requires either Jujutsu or Git to manage workspaces.
169
+ _
170
+ end
171
+ end
172
+
173
+ def vcs_label
174
+ jj? ? 'JJ workspace' : 'Git worktree'
175
+ end
176
+
177
+ def workroom_exists?
178
+ jj? ? jj_workspace_exists? : git_worktree_exists?
179
+ end
180
+
181
+ def jj_workspace_exists?
182
+ jj_workspaces.include?(name)
183
+ end
184
+
185
+ def git_worktree_exists?
186
+ git_worktrees.any? { |path| File.basename(path) == name }
187
+ end
188
+
189
+ def jj_workspaces
190
+ @jj_workspaces ||= begin
191
+ out = raw_jj_workspace_list.lines
192
+ out.map { |line| line.split(':').first.strip }.reject do |name|
193
+ IGNORED_JJ_WORKSPACE_NAMES.include?(name)
194
+ end.compact
195
+ end
196
+ end
197
+
198
+ def raw_jj_workspace_list
199
+ run 'jj workspace list --color never', capture: true
200
+ end
201
+
202
+ def git_worktrees
203
+ @git_worktrees ||= begin
204
+ arr = []
205
+ directory = ''
206
+ raw_git_worktree_list.split("\n").each do |w|
207
+ s = w.split
208
+ directory = s[1] if s[0] == 'worktree'
209
+ arr << directory if s[0] == 'HEAD' && Dir.pwd != directory
210
+ end
211
+ arr
212
+ end
213
+ end
214
+
215
+ def raw_git_worktree_list
216
+ run 'git worktree list --porcelain', capture: true
217
+ end
218
+
219
+ def validate_name!
220
+ return if /\A[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?\z/.match?(name)
221
+
222
+ say_status :create, name, :red
223
+ raise_error InvalidNameError, <<~_
224
+ Workroom name must be alphanumeric (dashes and underscores allowed), and must not start or end with a dash or underscore.
225
+ _
226
+ end
227
+
228
+ # Ensure the command is not being run from within an existing workroom by checking for the
229
+ # presence of the a `.Workroom`.
230
+ def check_not_in_workroom!
231
+ return if !Pathname.pwd.join('.Workroom').exist?
232
+
233
+ say_status :create, name, :red
234
+ raise_error InWorkroomError, <<~_
235
+ Looks like you are already in a workroom. Run this command from the root of your main development directory, not from within an existing workroom.
236
+ _
237
+ end
238
+
239
+ def create_workroom
240
+ if testing?
241
+ FileUtils.copy('./', workroom_path)
242
+ return
243
+ end
244
+
245
+ if jj?
246
+ run "jj workspace add #{workroom_path}"
247
+ else
248
+ run "git worktree add -b #{name} #{workroom_path}"
249
+ end
250
+ end
251
+
252
+ def delete_workroom
253
+ if testing?
254
+ FileUtils.rm_rf(workroom_path)
255
+ return
256
+ end
257
+
258
+ if jj?
259
+ run "jj workspace forget #{name}"
260
+ else
261
+ run "git worktree remove #{workroom_path} --force"
262
+ end
263
+ end
264
+
265
+ def cleanup_directory
266
+ return if !workroom_path.exist?
267
+
268
+ remove_dir(workroom_path, verbose:)
269
+ end
270
+
271
+ def run(command, config = {})
272
+ if !config[:force] && testing?
273
+ raise TestError, "Command execution blocked during testing: `#{command}`"
274
+ end
275
+
276
+ config[:verbose] = verbose
277
+ config[:capture] = !verbose if !config.key?(:capture)
278
+
279
+ super
280
+ end
281
+
282
+ def say_status(...)
283
+ super if verbose
284
+ end
285
+
286
+ def verbose
287
+ options[:verbose]
288
+ end
289
+
290
+ def testing?
291
+ ENV['WORKROOM_TEST'] == '1'
292
+ end
293
+ end
294
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Workroom
4
+ class Engine < ::Rails::Engine
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Workroom
4
+ VERSION = '0.1.0'
5
+ end
data/lib/workroom.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zeitwerk'
4
+ loader = Zeitwerk::Loader.for_gem
5
+ loader.setup
6
+
7
+ require 'thor'
8
+
9
+ module Workroom
10
+ class Error < Thor::Error; end
11
+ class TestError < Thor::Error; end
12
+ class InvalidNameError < Error; end
13
+ class InWorkroomError < Error; end
14
+ class DirExistsError < Error; end
15
+ class UnsupportedVCSError < Error; end
16
+ class JJWorkspaceExistsError < Error; end
17
+ class GitWorktreeExistsError < Error; end
18
+ class SetupError < Error; end
19
+ class TeardownError < Error; end
20
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: workroom
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Joel Moss
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: thor
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.5'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.5'
26
+ - !ruby/object:Gem::Dependency
27
+ name: zeitwerk
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.7'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.7'
40
+ description: Create and manage local development workrooms using JJ workspaces or
41
+ git worktrees
42
+ email:
43
+ - joel@developwithstyle.com
44
+ executables:
45
+ - workroom
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - MIT-LICENSE
50
+ - README.md
51
+ - bin/workroom
52
+ - lib/workroom.rb
53
+ - lib/workroom/commands.rb
54
+ - lib/workroom/engine.rb
55
+ - lib/workroom/version.rb
56
+ homepage: https://github.com/joelmoss/workroom
57
+ licenses:
58
+ - MIT
59
+ metadata:
60
+ rubygems_mfa_required: 'true'
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 3.1.0
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 4.0.6
76
+ specification_version: 4
77
+ summary: Manage development workrooms
78
+ test_files: []