rails-worktrees 0.3.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46ec0bdd9361eabe4925e37760bcd5d050c8f71072ec93cfdb427e244186ea8e
4
- data.tar.gz: 965a2ea5c6d454739f833cf0a534a4b195903531d159e2fb25f5eff9bb71083c
3
+ metadata.gz: 9f11c54fe45c6061774b7181c80c7a25ba1c824d04fe06668ca0d6fe0bc3c453
4
+ data.tar.gz: 9470d8a967f77f0188679e8f9b50fd5a78c4aebe6f10e268cba00ffaff1ebea0
5
5
  SHA512:
6
- metadata.gz: fa02fa0f6786fb3865326f8cef1e4beb804a25411cc7b5d15676236424b23a5cd4e5b55bd21550f1b1e3f422384f1c06662ff05b9d40e291e931b1bd09775af2
7
- data.tar.gz: 43269ef67990a1a4986f407a96696b9b01faabf8a64930e676c3c505a214d38b228ba3dae5e9ffa76fc74dc59d96bbdad3eafceb42c99236c64a23954fbefced
6
+ metadata.gz: c3775b72de293d0cf888f9b94cbbeae5b6031f001c641a1365f8f2662caa38de8b04bd4235a4e5c3f7b81279f6a23ce4637f4b2cfa6d6e1ba9d0fd0619daede2
7
+ data.tar.gz: 0c07249ae8bad61d6ae0cdad68f58752579145175e0d43005772e0d91f2f04b8d672f99a1952ce168e881d04e0b88ab4bbb8603183d4a9a2ace12f5c1767584e
@@ -1 +1 @@
1
- {".":"0.3.0"}
1
+ {".":"0.5.0"}
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.0](https://github.com/asjer/rails-worktrees/compare/v0.4.0...v0.5.0) (2026-04-02)
4
+
5
+
6
+ ### Features
7
+
8
+ * **maintenance:** add doctor and update maintenance commands ([fce44f7](https://github.com/asjer/rails-worktrees/commit/fce44f757ce9835eb663010f30a192c23f82080c))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **install:** support development-only gem installs ([bf9e742](https://github.com/asjer/rails-worktrees/commit/bf9e74260a88ba37674a5318b48ad5c93f1c0399))
14
+
15
+ ## [0.4.0](https://github.com/asjer/rails-worktrees/compare/v0.3.0...v0.4.0) (2026-03-30)
16
+
17
+
18
+ ### Features
19
+
20
+ * **delete:** add `bin/wt remove` command to delete worktrees and local branches ([5905723](https://github.com/asjer/rails-worktrees/commit/5905723712e3ca11fd6614633e68855e33bb50e3))
21
+
3
22
  ## [0.3.0](https://github.com/asjer/rails-worktrees/compare/v0.2.2...v0.3.0) (2026-03-30)
4
23
 
5
24
 
@@ -45,12 +64,3 @@
45
64
  ### Features
46
65
 
47
66
  * add wt command and rails installer ([5d798d5](https://github.com/asjer/rails-worktrees/commit/5d798d5129585331780f0259b39061194feb66e3))
48
-
49
- ## [Unreleased]
50
-
51
- - Add a gem-managed `wt` CLI for creating Rails worktrees.
52
- - Add an optional gem-managed `ob` CLI plus generated `bin/ob` wrapper for opening `localhost:$DEV_PORT` routes.
53
- - Add a Rails installer generator that creates `bin/wt` and `config/initializers/rails_worktrees.rb`.
54
- - Add conservative `config/database.yml` patching for common development/test database names.
55
- - Add a manual-dispatch GitHub Actions workflow for the disposable Rails smoke test.
56
- - Add smoke-test workflow debug controls for retained artifacts and verbose output.
data/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  ## Installation
12
12
 
13
13
  ```bash
14
- bundle add rails-worktrees
14
+ bundle add rails-worktrees --group development
15
15
  bin/rails generate worktrees:install
16
16
  # or, to also generate bin/ob without the yolo follow-ups:
17
17
  bin/rails generate worktrees:install --browser
@@ -19,6 +19,8 @@ bin/rails generate worktrees:install --browser
19
19
  bin/rails generate worktrees:install --yolo
20
20
  ```
21
21
 
22
+ The generated initializer checks that `rails-worktrees` is actually loaded before it calls `Rails::Worktrees.configure`, so the app can still boot in environments like `test` when the gem is only bundled for `:development`.
23
+
22
24
  The installer adds:
23
25
 
24
26
  - `bin/wt` — a thin wrapper that executes the gem-owned CLI
@@ -42,6 +44,13 @@ bin/wt # auto-pick a name from bundled *.txt lists
42
44
  bin/wt my-feature # use an explicit worktree name
43
45
  bin/wt --dry-run my-feature # preview the full setup without changing anything
44
46
  bin/wt --print-env my-feature # preview DEV_PORT and WORKTREE_DATABASE_SUFFIX
47
+ bin/wt doctor # audit install/config drift and basic worktree health
48
+ bin/wt update --dry-run # preview safe maintenance fixes
49
+ bin/wt update # apply safe maintenance fixes for managed files
50
+ bin/wt remove my-feature # remove a worktree and delete its local branch
51
+ bin/wt delete my-feature # alias for `bin/wt remove`
52
+ bin/wt remove --force my-feature # also delete an unmerged local branch
53
+ bin/wt prune # remove merged worktrees created by wt
45
54
 
46
55
  bin/ob # open http://localhost:$DEV_PORT/
47
56
  bin/ob contact # open http://localhost:$DEV_PORT/contact
@@ -55,7 +64,8 @@ bin/ob --print-url '?from=nav' # print the resolved URL without opening a brows
55
64
  |------|-------------|
56
65
  | `-h`, `--help` | Show the help message |
57
66
  | `-v`, `--version` | Show the script version |
58
- | `--dry-run [name]` | Preview the full worktree setup without changing anything |
67
+ | `--dry-run [name]` | Preview worktree creation or cleanup without changing anything |
68
+ | `--force` | Force branch deletion for `bin/wt remove` / `bin/wt delete` |
59
69
  | `--env`, `--print-env <name>` | Preview `DEV_PORT` and `WORKTREE_DATABASE_SUFFIX` |
60
70
 
61
71
  ### Default behavior
@@ -80,6 +90,45 @@ workspace/
80
90
 
81
91
  `WT_WORKSPACES_ROOT` or `config.workspace_root` overrides the destination root and uses the layout `<root>/<project>/<name>`.
82
92
 
93
+ ### Cleanup commands
94
+
95
+ `bin/wt` also supports cleanup commands for worktrees it manages:
96
+
97
+ - `bin/wt remove <name>` — remove the named worktree and delete its local branch
98
+ - `bin/wt delete <name>` — alias for `bin/wt remove <name>`
99
+ - `bin/wt prune` — remove merged worktrees created by `bin/wt` in bulk
100
+
101
+ `bin/wt remove` and `bin/wt prune` can be run from the main checkout or any sibling worktree in the same repository family.
102
+
103
+ `bin/wt remove` refuses to remove the worktree you're currently in, and by default only deletes a local branch after confirming it is already merged into `origin`'s default branch. Use `bin/wt remove --force <name>` when you intentionally want to delete an unmerged local branch too.
104
+
105
+ `bin/wt prune` only targets linked worktrees created by `bin/wt` whose local branches are already merged into `origin`'s default branch. It skips the main checkout, skips the checkout you're currently in, and asks for confirmation before deleting the batch.
106
+
107
+ If you want to preview a single removal first, use `--dry-run` with `bin/wt remove`:
108
+
109
+ ```bash
110
+ bin/wt remove --dry-run feature-auth
111
+ ```
112
+
113
+ If you want to see what `bin/wt prune` would clean up before saying yes, use `--dry-run`:
114
+
115
+ ```bash
116
+ bin/wt prune --dry-run
117
+ ```
118
+
119
+ ### Maintenance commands
120
+
121
+ `bin/wt` also includes a small maintenance surface for installer drift and checkout health:
122
+
123
+ - `bin/wt doctor` — audit managed files plus basic Git/worktree health without changing anything
124
+ - `bin/wt update --dry-run` — preview safe file-based fixes
125
+ - `bin/wt update` — apply safe file-based fixes for supported managed files
126
+
127
+ `bin/wt doctor` checks the generated initializer, `bin/wt`, optional `bin/ob`, supported config files such as `config/database.yml`, `Procfile.dev`, `config/puma.rb`, and `mise.toml`/`.mise.toml`, plus basic repository/worktree conditions like resolving `origin`'s default branch and spotting stale registered worktree paths.
128
+
129
+ `bin/wt update` is intentionally conservative: it only applies safe file-based fixes for managed templates and supported config shapes. It does **not** delete branches, remove worktrees, or rewrite ambiguous custom files automatically.
130
+
131
+
83
132
  ### Interactive prompts
84
133
 
85
134
  `bin/wt` handles several edge cases interactively:
@@ -88,6 +137,7 @@ workspace/
88
137
  - **Branch already exists on origin** — asks whether to create a local tracking worktree
89
138
  - **Target directory already exists with matching branch** — asks whether to reuse it
90
139
  - **Target directory already exists with a different branch** — asks whether to remove and recreate it
140
+ - **Prune found merged worktrees created by `wt`** — asks whether to delete the batch
91
141
  - **Retired bundled name used explicitly** — rejects it and suggests running `wt` with no argument
92
142
 
93
143
  ### Name validation
@@ -98,6 +148,8 @@ Worktree names must not contain `/` or whitespace, must not be `.` or `..`, and
98
148
 
99
149
  The installer generates `config/initializers/rails_worktrees.rb` where you can override:
100
150
 
151
+ The initializer becomes a no-op whenever `rails-worktrees` is not loaded in the current bundle groups.
152
+
101
153
  | Option | Default | Description |
102
154
  |--------|---------|-------------|
103
155
  | `bootstrap_env` | `true` | Write `.env` when creating a worktree |
@@ -197,7 +249,7 @@ This smoke test:
197
249
  - installs `rails-worktrees` from the current checkout path
198
250
  - runs `bin/rails generate worktrees:install --yolo`
199
251
  - verifies `bin/wt`, `bin/ob`, the generated initializer, that `--yolo` skips the Procfile example, yolo updates to `Procfile.dev`, `config/puma.rb`, and `mise.toml`, `config/database.yml` patching, and worktree `.env` bootstrapping
200
- - creates a temporary bare `origin` and confirms `bin/wt smoke-branch` creates a real worktree
252
+ - creates a temporary bare `origin`, confirms `bin/wt smoke-branch` creates a real worktree, and confirms `bin/wt remove smoke-branch` can remove that merged worktree from a sibling worktree checkout
201
253
 
202
254
  By default, the script cleans up all temp directories after the run. Set `KEEP_SMOKE_TEST_ARTIFACTS=1` to keep them around for debugging, or set `RAILS_WORKTREES_SMOKE_RAILS_VERSION` to try a different compatible Rails version.
203
255
 
@@ -1,19 +1,23 @@
1
- Rails::Worktrees.configure do |config|
1
+ if Gem.loaded_specs.key?('rails-worktrees') &&
2
+ defined?(Rails::Worktrees) &&
3
+ Rails::Worktrees.respond_to?(:configure)
4
+ Rails::Worktrees.configure do |config|
2
5
  <% if options['conductor'] -%>
3
- config.workspace_root = <%= conductor_workspace_root %>
6
+ config.workspace_root = <%= conductor_workspace_root %>
4
7
  <% else -%>
5
- # By default, worktrees go in a sibling "<project>.worktrees" directory.
6
- # Uncomment to override with a custom parent directory that uses <root>/<project>/<name>.
7
- # config.workspace_root = File.expand_path('~/worktrees')
8
+ # By default, worktrees go in a sibling "<project>.worktrees" directory.
9
+ # Uncomment to override with a custom parent directory that uses <root>/<project>/<name>.
10
+ # config.workspace_root = File.expand_path('~/worktrees')
8
11
  <% end -%>
9
- # config.bootstrap_env = false
10
- # config.dev_port_range = 3000..3999
11
- # config.worktree_database_suffix_max_length = 18
12
- # config.branch_prefix = '🚂'
13
- # config.name_sources_path = Rails.root.join('config/worktree_names').to_s
14
- # config.used_names_file = File.join(
15
- # ENV.fetch('XDG_STATE_HOME', File.expand_path('~/.local/state')),
16
- # 'rails-worktrees',
17
- # 'used-names.tsv'
18
- # )
12
+ # config.bootstrap_env = false
13
+ # config.dev_port_range = 3000..3999
14
+ # config.worktree_database_suffix_max_length = 18
15
+ # config.branch_prefix = '🚂'
16
+ # config.name_sources_path = Rails.root.join('config/worktree_names').to_s
17
+ # config.used_names_file = File.join(
18
+ # ENV.fetch('XDG_STATE_HOME', File.expand_path('~/.local/state')),
19
+ # 'rails-worktrees',
20
+ # 'used-names.tsv'
21
+ # )
22
+ end
19
23
  end
@@ -1,9 +1,11 @@
1
1
  require 'open3'
2
+ require 'fileutils'
2
3
 
3
4
  module Rails
4
5
  module Worktrees
5
6
  class Command
6
7
  # Shell-level git helpers, branch/worktree queries, and worktree creation.
8
+ # rubocop:disable Metrics/ModuleLength
7
9
  module GitOperations
8
10
  private
9
11
 
@@ -40,14 +42,18 @@ module Rails
40
42
  worktree_branch_checked_out?(branch_name, worktree_list_output)
41
43
  end
42
44
 
45
+ def current_checkout_branch
46
+ git_capture('branch', '--show-current', allow_failure: true).strip
47
+ end
48
+
43
49
  def registered_worktree_path?(target_dir)
44
50
  normalized_target = canonical_path(target_dir)
45
51
 
46
- worktree_list_output.each_line.any? do |line|
47
- next unless line.start_with?('worktree ')
52
+ worktree_entries.any? { |entry| entry[:path] == normalized_target }
53
+ end
48
54
 
49
- canonical_path(line.delete_prefix('worktree ').strip) == normalized_target
50
- end
55
+ def worktree_entries_for_branch(branch_name)
56
+ worktree_entries.select { |entry| entry[:branch_name] == branch_name }
51
57
  end
52
58
 
53
59
  def worktree_branch_checked_out?(branch_name, output)
@@ -59,11 +65,38 @@ module Rails
59
65
 
60
66
  def worktree_list_output = git_capture('worktree', 'list', '--porcelain')
61
67
 
68
+ def worktree_entries
69
+ worktree_list_output.split("\n\n").filter_map do |block|
70
+ entry = parse_worktree_entry(block)
71
+ entry[:path] ? entry : nil
72
+ end
73
+ end
74
+
75
+ def parse_worktree_entry(block)
76
+ entry = { branch_name: nil }
77
+
78
+ block.each_line(chomp: true) do |line|
79
+ case line
80
+ when /\Aworktree /
81
+ entry[:path] = canonical_path(line.delete_prefix('worktree '))
82
+ when %r{\Abranch refs/heads/}
83
+ entry[:branch_name] = line.delete_prefix('branch refs/heads/')
84
+ end
85
+ end
86
+
87
+ entry
88
+ end
89
+
62
90
  def canonical_path(path)
63
91
  File.realpath(path)
64
92
  rescue Errno::ENOENT then File.expand_path(path)
65
93
  end
66
94
 
95
+ def branch_merged_into_default?(branch_name)
96
+ default_branch = resolve_default_branch
97
+ git_success?('merge-base', '--is-ancestor', branch_name, "origin/#{default_branch}")
98
+ end
99
+
67
100
  def create_new_branch_worktree(branch_name, target_dir)
68
101
  default_branch = resolve_default_branch
69
102
  if dry_run?
@@ -101,6 +134,33 @@ module Rails
101
134
  git!('worktree', 'remove', '--force', target_dir)
102
135
  end
103
136
 
137
+ def remove_target_path(target_dir)
138
+ if registered_worktree_path?(target_dir)
139
+ remove_registered_worktree(target_dir)
140
+ else
141
+ info("Removing existing target path '#{target_dir}'")
142
+ FileUtils.rm_rf(target_dir)
143
+ end
144
+ end
145
+
146
+ def delete_local_branch(branch_name, force: false)
147
+ ensure_local_branch_removable!(branch_name, force: force)
148
+
149
+ info("Deleting local branch '#{branch_name}'")
150
+ git!('branch', force ? '-D' : '-d', branch_name)
151
+ end
152
+
153
+ def ensure_local_branch_removable!(branch_name, force: false)
154
+ return if force
155
+
156
+ default_branch = resolve_default_branch
157
+ return if branch_merged_into_default?(branch_name)
158
+
159
+ raise Error,
160
+ "Local branch '#{branch_name}' is not merged into origin/#{default_branch}. " \
161
+ 'Re-run with --force to delete it.'
162
+ end
163
+
104
164
  def git!(*)
105
165
  stdout_str, stderr_str, status = Open3.capture3(@env.to_h, 'git', *, chdir: @cwd)
106
166
  return stdout_str if status.success?
@@ -137,6 +197,7 @@ module Rails
137
197
  git!('worktree', 'add', '--force', target_dir, branch_name)
138
198
  end
139
199
  end
200
+ # rubocop:enable Metrics/ModuleLength
140
201
  end
141
202
  end
142
203
  end
@@ -2,6 +2,7 @@ module Rails
2
2
  module Worktrees
3
3
  class Command
4
4
  # User-facing output, prompts, and help text.
5
+ # rubocop:disable Metrics/ModuleLength
5
6
  module Output
6
7
  private
7
8
 
@@ -26,16 +27,22 @@ module Rails
26
27
  def usage
27
28
  <<~USAGE
28
29
  wt #{::Rails::Worktrees::VERSION}
29
- Create Git worktrees for the current repository.
30
+ Create and clean up Git worktrees for the current repository.
30
31
 
31
32
  Usage: wt [worktree-name]
32
33
  wt --dry-run [worktree-name]
33
34
  wt --print-env <worktree-name>
35
+ wt doctor
36
+ wt update [--dry-run]
37
+ wt remove [--dry-run] [--force] <worktree-name>
38
+ wt delete [--dry-run] [--force] <worktree-name>
39
+ wt prune [--dry-run]
34
40
 
35
41
  Options:
36
42
  -h, --help Show this help message
37
43
  -v, --version Show the script version
38
- --dry-run [name] Preview the full worktree setup without changing anything
44
+ --dry-run [name] Preview worktree creation or cleanup without changing anything
45
+ --force Delete an unmerged local branch with wt remove/delete
39
46
  --env, --print-env <name> Preview DEV_PORT and WORKTREE_DATABASE_SUFFIX
40
47
 
41
48
  Quick start:
@@ -43,12 +50,21 @@ module Rails
43
50
  wt my-feature Use an explicit worktree name
44
51
  wt --dry-run my-feature
45
52
  wt --print-env my-feature
53
+ wt doctor
54
+ wt update --dry-run
55
+ wt remove my-feature
56
+ wt remove --force my-feature
57
+ wt prune
46
58
 
47
59
  How it works:
48
60
  - by default creates worktrees beside the repo in ../<project>.worktrees/<name>
49
61
  - when workspace_root or WT_WORKSPACES_ROOT is set, creates worktrees in <root>/<project>/<name>
50
62
  - always uses the branch name #{@configuration.branch_prefix}/<name>
51
63
  - bases new branches on the repository's origin default branch
64
+ - wt doctor audits install/config drift plus basic worktree health without changing files
65
+ - wt update applies safe file-based fixes for managed installer artifacts and config hints
66
+ - wt remove/delete can run from the main checkout or any sibling worktree, but never remove the worktree you're currently in
67
+ - wt prune removes merged worktrees created by wt while skipping the main checkout and the checkout you're in
52
68
  - auto-discovers bundled *.txt files from #{@configuration.name_sources_path}
53
69
  - retires bundled names in #{@configuration.used_names_file}
54
70
  USAGE
@@ -75,7 +91,7 @@ module Rails
75
91
  @stdout.puts("→ #{message}")
76
92
  end
77
93
 
78
- def announce_dry_run = info('Dry run: previewing worktree setup without making changes')
94
+ def announce_dry_run = info('Dry run: previewing worktree changes without applying them')
79
95
 
80
96
  def complete_dry_run(context, env_values:)
81
97
  success('Dry run complete')
@@ -90,6 +106,76 @@ module Rails
90
106
  complete_dry_run(context, env_values: preview_result&.values)
91
107
  end
92
108
 
109
+ def complete_remove_dry_run(context, worktree_exists:, branch_exists:)
110
+ info(remove_dry_run_target_message(context, worktree_exists: worktree_exists))
111
+ info(remove_dry_run_branch_message(context, branch_exists: branch_exists))
112
+
113
+ success('Dry run complete')
114
+ print_context_summary(context, env_values: nil)
115
+ info('No changes were made.')
116
+ 0
117
+ end
118
+
119
+ def remove_dry_run_target_message(context, worktree_exists:)
120
+ return "Would skip worktree removal because '#{context[:target_dir]}' does not exist" unless worktree_exists
121
+
122
+ action = if registered_worktree_path?(context[:target_dir])
123
+ 'remove registered worktree at'
124
+ else
125
+ 'remove existing target path'
126
+ end
127
+ "Would #{action} '#{context[:target_dir]}'"
128
+ end
129
+
130
+ def remove_dry_run_branch_message(context, branch_exists:)
131
+ unless branch_exists
132
+ return "Would skip local branch deletion because '#{context[:branch_name]}' does not exist"
133
+ end
134
+
135
+ "Would delete local branch '#{context[:branch_name]}'"
136
+ end
137
+
138
+ def print_prune_candidates(candidates)
139
+ candidates.each do |context|
140
+ info("#{context[:worktree_name]} => #{context[:target_dir]} (#{context[:branch_name]})")
141
+ end
142
+ end
143
+
144
+ def complete_prune_noop
145
+ info('No merged worktrees created by wt are ready to prune.')
146
+ 0
147
+ end
148
+
149
+ def complete_prune_dry_run(candidates)
150
+ info("Would prune #{candidates.length} merged worktree#{'s' unless candidates.length == 1}")
151
+ success('Dry run complete')
152
+ info('No changes were made.')
153
+ 0
154
+ end
155
+
156
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
157
+ def print_doctor_report(checks)
158
+ checks.each do |check|
159
+ printer = check.ok? ? :info : :warning
160
+ send(printer, "#{check.category}: #{check.headline}")
161
+ Array(check.messages).each { |message| info(message) }
162
+ end
163
+
164
+ fixable_count = checks.count(&:fixable?)
165
+ warning_count = checks.count(&:warning?)
166
+
167
+ if fixable_count.zero? && warning_count.zero?
168
+ success('Doctor found no issues.')
169
+ else
170
+ warning(
171
+ "Doctor found #{fixable_count} fixable issue#{'s' unless fixable_count == 1} and " \
172
+ "#{warning_count} warning#{'s' unless warning_count == 1}."
173
+ )
174
+ info('Run `wt update --dry-run` to preview safe fixes.') if fixable_count.positive?
175
+ end
176
+ end
177
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
178
+
93
179
  def warning(message)
94
180
  @stderr.puts("⚠️ #{message}")
95
181
  end
@@ -124,6 +210,7 @@ module Rails
124
210
  @stdout.puts("Suffix: #{suffix}")
125
211
  end
126
212
  end
213
+ # rubocop:enable Metrics/ModuleLength
127
214
  end
128
215
  end
129
216
  end
@@ -5,6 +5,28 @@ module Rails
5
5
  module WorkspacePaths
6
6
  private
7
7
 
8
+ def resolve_repository_context
9
+ current_root = canonical_path(git_capture('rev-parse', '--show-toplevel').strip)
10
+ common_dir = expand_git_path(git_capture('rev-parse', '--git-common-dir').strip)
11
+ primary_root = primary_checkout_root_for(current_root, common_dir)
12
+
13
+ repository_context_for(current_root, primary_root)
14
+ end
15
+
16
+ def repository_context_for(current_root, primary_root)
17
+ project_name = File.basename(primary_root)
18
+ workspaces = resolve_workspaces(primary_root, project_name)
19
+
20
+ {
21
+ current_root: current_root,
22
+ primary_root: primary_root,
23
+ project_name: project_name,
24
+ workspaces: workspaces,
25
+ workspaces_root: workspaces[:root],
26
+ uses_default_workspace_root: workspaces[:uses_default_root]
27
+ }
28
+ end
29
+
8
30
  def resolve_workspaces(repo_root, project_name)
9
31
  explicit_root = configured_workspaces_root
10
32
  return { root: explicit_root, uses_default_root: false } if explicit_root
@@ -38,6 +60,18 @@ module Rails
38
60
  FileUtils.mkdir_p(parent_dir)
39
61
  end
40
62
 
63
+ def primary_checkout_root_for(current_root, common_dir)
64
+ return current_root unless File.basename(common_dir) == '.git'
65
+
66
+ canonical_path(File.dirname(common_dir))
67
+ end
68
+
69
+ def expand_git_path(path)
70
+ return path if path.start_with?('/')
71
+
72
+ File.expand_path(path, @cwd)
73
+ end
74
+
41
75
  def present_path?(path)
42
76
  !path.nil? && !path.empty?
43
77
  end