gemvault 0.2.5 → 0.2.6

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: 4447e59a767dbc08bd6c0964964fefaf00bb1f2768421f7ce1fc6247f9e8f2c4
4
- data.tar.gz: b017dc605efee9f1a576b1fc362b2cf51ebab25e48b424c2be7e50d9cb5f1e3f
3
+ metadata.gz: 6c8bf335258cabfbfbe016fe547e1302da87c37060d1523cae394e941bd493dd
4
+ data.tar.gz: f4a027c561cb8edf619f777989654d657bae70d741106ba8730b540b38de7f2f
5
5
  SHA512:
6
- metadata.gz: 5c993d8b4fd00d8c35ed6de9645653400882565860f44449e7adbbcd53730ad7ca161e4cae10e7e817f512c3b63e2101e2ceb58496ebebe97eadd32eceefc46a
7
- data.tar.gz: 39bfbb4959725d6ad92401dfd3d6841ea72c94021031bdf7680bdde9f2563bd721b51329db7cd76eaf0ef01a32dbfb0a9a788e7795befc50cdc6396f8c2db595
6
+ metadata.gz: 9f3691b960faaa2d6ab493d18363091691ce3ac106a23d8859a052e946fd6aaa78178282da8269823e07c21cf52cb5ba5e9e262c51767aea19d805b620ee6287
7
+ data.tar.gz: 99feeb31a5af138c4da80b61b3fe3853cd545044954b895e202d57a5f7c2817b3cb59701f752140efbce75c839120f4b6bcf0a5871a806a94cc80cc6d12670ed
data/CHANGELOG.md CHANGED
@@ -33,6 +33,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
33
33
  plugin.)` — Bundler reinstalls the gem correctly but validates the ghost's
34
34
  dead path — and nothing project-local can recover, because the wreck lives
35
35
  in the ambient gem home (issue #23).
36
+ - `gemvault doctor` now works in a project whose Gemfile is inline
37
+ (`bundler/inline`). Such a script keeps its plugin index in
38
+ `<project>/.bundle/plugin`, a root bundler only consults mid-script, so
39
+ doctor's uninstall silently repaired nothing and the `bundle install` that
40
+ followed dumped bundler's entire usage screen. Doctor now reaches the
41
+ project's index the way `bundler/inline` does and, with no Gemfile to
42
+ reinstall from, says so and exits 0 — the inline gemfile reinstalls the
43
+ plugin the next time the script runs (issue #14). Run outside any project,
44
+ doctor no longer claims to have cleared a project index it never touched.
45
+ - `gemvault doctor` no longer strands a machine without the plugin. Its
46
+ closing `bundle install` does more than reinstall the plugin, and when that
47
+ extra work failed (a Ruby version pin, an unresolvable gem) the already-run
48
+ uninstall had cleared the plugin index for nothing — strictly worse than
49
+ the wreck doctor was asked to repair. The index is now snapshotted before
50
+ the uninstall and restored when the reinstall never happened, `bundle
51
+ install` runs as a child rather than replacing the process, and every
52
+ failure is one line on stderr and exit 1 instead of a backtrace
53
+ (issues #27, #24).
54
+ - The shim no longer spams `already initialized constant` warnings when
55
+ bundler evaluates two installed copies of it in one process — an upgrade
56
+ resolving a newer bundler-source-vault while the plugin index still names
57
+ the old one. The first copy wins, matching the guard plugins.rb already
58
+ applies to the vault source class (issue #26).
36
59
  - `bundle plugin install bundler-source-vault` no longer dies with
37
60
  `LoadError: cannot load such file -- bundler/plugin/vault_source`. The shim's
38
61
  `plugins.rb` now derives the gem root from its own installed location (local
data/README.md CHANGED
@@ -98,7 +98,11 @@ To recover, update the Gemfile to point at the new path and run:
98
98
  gemvault doctor
99
99
  ```
100
100
 
101
- `doctor` removes any ghost installation records, clears the broken entry from bundler's plugin index (`bundle plugin uninstall bundler-source-vault`) and then re-runs `bundle install`, which reinstalls the plugin against whatever the current Gemfile declares. Run it from your project directory. If a ghost record sits in a root-owned gem home, doctor says so on one line — re-run it with permissions for that gem home (e.g. `sudo gemvault doctor`).
101
+ `doctor` removes any ghost installation records, clears the broken entry from bundler's plugin index (`bundle plugin uninstall bundler-source-vault`) and then, when a Gemfile exists, re-runs `bundle install`, which reinstalls the plugin against whatever the current Gemfile declares. Run it from your project directory. If a ghost record sits in a root-owned gem home, doctor says so on one line — re-run it with permissions for that gem home (e.g. `sudo gemvault doctor`).
102
+
103
+ A project whose Gemfile is inline (`require "bundler/inline"`) works too. Such a script keeps its plugin index in `<project>/.bundle/plugin`, a root bundler only consults while the script runs; doctor reaches it the same way the script did. With no Gemfile on disk there is nothing to reinstall from, so doctor clears the entry, says so, and exits 0 — the inline gemfile reinstalls the plugin the next time the script runs. Run outside any project, doctor works against bundler's global index and points you back at the project directory for the reinstall.
104
+
105
+ The repair is transactional. `bundle install` does more than reinstall the plugin, and that extra work can fail on its own — a Ruby version pin, an unresolvable gem. When it does, doctor says so in one line and exits 1; and if the failure struck before the plugin was reinstalled, doctor puts the plugin index back the way it found it. A failed run never leaves the machine with less than it started with — fix the install error and re-run.
102
106
 
103
107
  The published `bundler-source-vault` gem installed from rubygems.org is immune to this: it lives in a bundler-managed directory that does not move.
104
108
 
data/issues.rec CHANGED
@@ -750,7 +750,42 @@ Id: 14
750
750
  Updated: Wed, 29 Jul 2026 16:41:11 -0400
751
751
  Name: gemvault doctor fails when bundler/inline is used
752
752
  Description: Loudly fails with 2 errors, one of which is "can't find gemfile"
753
- Status: open
753
+ +
754
+ + RESOLUTION: both errors reproduced in podman, and doctor turned out to be
755
+ + broken twice over rather than once.
756
+ +
757
+ + (1) "Plugin bundler-source-vault is not installed". bundler/inline sets
758
+ + BUNDLE_GEMFILE to a bare "Gemfile" for the life of the script (bundler
759
+ + inline.rb:53), so Bundler.root is the script's working directory and the plugin
760
+ + index is written to <project>/.bundle/plugin. A later `gemvault doctor` sets no
761
+ + such variable and finds no Gemfile on disk, so Bundler::Plugin.root --
762
+ + local_root only when SharedHelpers.in_bundle?, global_root otherwise
763
+ + (plugin.rb:162) -- falls back to ~/.bundle/plugin. The uninstall therefore
764
+ + looked in the wrong root and repaired nothing, silently.
765
+ +
766
+ + (2) "Could not locate Gemfile". doctor then exec'd `bundle install`, which with
767
+ + no Gemfile prints its entire usage screen and exits 10 -- reading as gemvault
768
+ + itself failing, after the repair it was asked for had already not happened.
769
+ +
770
+ + doctor now runs the uninstall with BUNDLE_GEMFILE="Gemfile" when the project
771
+ + owns a plugin root Bundler would ignore, which is bundler/inline's own trick and
772
+ + needs no file to exist; verified against the real thing (plain uninstall says
773
+ + "not installed", the same command with BUNDLE_GEMFILE set says "Uninstalled
774
+ + plugin bundler-source-vault" and empties the index). When no Gemfile exists it
775
+ + skips the reinstall and says so, exiting 0, because clearing the entry is the
776
+ + repair and only the reinstall is unavailable -- an inline gemfile reinstalls the
777
+ + plugin the next time the script runs.
778
+ +
779
+ + Gemvault::BundlerGemfile mirrors SharedHelpers#find_gemfile and
780
+ + Gemvault::BundlerPluginRoot mirrors Plugin.root, both reimplemented rather than
781
+ + delegated because bundler is deliberately not a dependency and doctor runs
782
+ + outside any Bundler process. Replicated first by
783
+ + spec/integration/cli/commands/doctor_inline_spec.rb -- all 8 examples failed
784
+ + against the old code -- with unit cover in spec/gemvault/bundler_gemfile_spec.rb,
785
+ + spec/gemvault/bundler_plugin_root_spec.rb and the extended doctor_spec.rb. The
786
+ + inline support now runs its script from the project directory, as a real user
787
+ + does; that is what puts the plugin root where the report describes.
788
+ Status: closed
754
789
 
755
790
  Id: 15
756
791
  Updated: Wed, 29 Jul 2026 17:39:23 -0400
@@ -783,7 +818,11 @@ Id: 16
783
818
  Updated: Wed, 29 Jul 2026 18:25:48 -0400
784
819
  Name: Add jruby-ci workflow
785
820
  Description: It should be already set up on the jruby-support branch. It just needs to be picked from it. DO NOT use the other code in that - it is very outdated
786
- Status: open
821
+ +
822
+ + RESOLUTION: shipped in commit 635ba41 ("ci: add JRuby test job") -- the jruby
823
+ + job in .github/workflows/ci.yml runs the minitest suite on jruby-10.1.0.0 and
824
+ + is green on CI.
825
+ Status: closed
787
826
 
788
827
  Id: 17
789
828
  Updated: Thu, 30 Jul 2026 00:07:42 +0000
@@ -972,7 +1011,15 @@ Description: [dev@rstore]/workspace% bundle install
972
1011
  + from /usr/local/bin/bundle:25:in '<main>'
973
1012
  +
974
1013
  + Though I will add that gemvault doctor followed by bundle install fixed the issue
975
- Status: open
1014
+ +
1015
+ + RESOLUTION: the broken-plugin-path wreck, recovered exactly as reported --
1016
+ + gemvault doctor then bundle install. The plugin index entry pointed into a
1017
+ + reinstalled rbenv ruby's gem home; bundler does not revalidate stored plugin
1018
+ + paths, skips loading the plugin, and crashes in SourceList#add_plugin_source,
1019
+ + upstream behavior README documents under "A broken plugin path". doctor is
1020
+ + the repair. Hinting at doctor from the failure site was considered and
1021
+ + declined: the crash happens in bundler frames before any gemvault code loads.
1022
+ Status: closed
976
1023
 
977
1024
  Id: 21
978
1025
  Updated: Fri, 31 Jul 2026 13:54:09 -0400
@@ -1011,12 +1058,27 @@ Description: bundler-cache: true on the ruby/setup-ruby action will not work cor
1011
1058
  + run: | # confine gems to vendor/bundle so the cache never shadows vendor/vendored.gemv
1012
1059
  + bundle config set --local path vendor/bundle
1013
1060
  + bundle install --jobs 4 --retry 3
1061
+ Status: open
1062
+
1063
+ Id: 26
1014
1064
  Updated: Mon, 10 Aug 2026 10:14:51 -0400
1015
1065
  Name: Gemvault spams warnings on bundle install
1016
1066
  Description: /workspace/.bundle/plugin/gems/bundler-source-vault-0.2.2/gemvault_load_path.rb:31: warning: already initialized constant BundlerSourceVault::GemvaultLoadPath::GEM
1017
1067
  + /workspace/.bundle/plugin/gems/bundler-source-vault-0.2.2/gemvault_load_path.rb:32: warning: already initialized constant BundlerSourceVault::GemvaultLoadPath::SHIM
1018
1068
  + /workspace/.bundle/plugin/gems/bundler-source-vault-0.2.2/gemvault_load_path.rb:33: warning: already initialized constant BundlerSourceVault::GemvaultLoadPath::VAULT_SOURCE
1019
- Status: open
1069
+ +
1070
+ + RESOLUTION: bundler can evaluate two installed copies of the shim in one
1071
+ + process -- an upgrade resolving a newer bundler-source-vault while the index
1072
+ + still names the old one, the dual-version plugin root the failure report
1073
+ + also recorded as issue #29 -- and require_relative cannot deduplicate
1074
+ + across paths, so the second copy reopened GemvaultLoadPath and warned on
1075
+ + every constant. gemvault_load_path.rb now returns before redefining
1076
+ + anything when the module is already loaded, first copy winning as
1077
+ + plugins.rb already arranges for Bundler::Plugin::VaultSource. Replicated by
1078
+ + spec/shim_reload_spec.rb (two copies loaded in one process, red against the
1079
+ + old shim), with the plain-install path guarded in
1080
+ + spec/integration/bundle_install_spec.rb.
1081
+ Status: closed
1020
1082
 
1021
1083
  Id: 23
1022
1084
  Updated: Mon, 10 Aug 2026 17:40:28 -0400
@@ -1086,4 +1148,78 @@ Id: 24
1086
1148
  Updated: Mon, 10 Aug 2026 23:16:35 +0000
1087
1149
  Name: doctor prints a backtrace when the plugin uninstall step fails
1088
1150
  Description: gemvault doctor's `bundle plugin uninstall` step runs with exception: true; a nonzero exit raises RuntimeError and a missing bundle raises Errno::ENOENT, and both escape Command#run to command_kit's ExceptionHandler, which prints error.full_message -- a full backtrace -- violating the one-line-and-exit-1 convention issue #15 established. Adjacent to issue #14 (doctor failing under bundler/inline). The ghost-specification sweep added for issue #23 reports its own failures as one line; the uninstall step should match.
1151
+ +
1152
+ + RESOLUTION: fixed alongside issue #27's transactional rework. The uninstall
1153
+ + step no longer runs with exception: true; a failing or missing bundle is one
1154
+ + line on stderr ("doctor: bundle plugin uninstall bundler-source-vault
1155
+ + failed") and exit 1, matching the convention issues #15 and #23 established,
1156
+ + and the snapshotted index is left untouched.
1157
+ Status: closed
1158
+
1159
+ Id: 25
1160
+ Updated: Tue, 11 Aug 2026 20:05:27 -0400
1161
+ Name: Project with Gemfile + locked json version + gemvault throws error
1162
+ Description: Project with Gemfile + locked json version + gemvault throws "bundler: failed to load command: rails (/private/tmp/local/flipmine/flipmine/vendor/ruby/4.0.0/bin/rails)
1163
+ + /Users/davidgillis/.rbenv/versions/4.0.1/lib/ruby/gems/4.0.0/gems/bundler-4.0.18/lib/bundler/runtime.rb:328:in 'Bundler::Runtime#check_for_activated_spec!': You have already activated json 2.21.2, but your Gemfile requires json 2.19.7. Prepending `bundle exec` to your command may solve this. (Gem::LoadError)"
1164
+ +
1165
+ + I believe this error to be coming from gemvault's dependency on JSON. It is clear that dependencies are cursed in this project, whether its the added load time for reinstalling them on every `bundle install`, or errors like this one. Rather than rely on JSON, I think it would be better to just marshal our own manifest, or use some other serialization tool if any that rubygems already provides.
1166
+ Status: open
1167
+
1168
+ Id: 27
1169
+ Updated: Wed, 12 Aug 2026 16:05:55 +0000
1170
+ Name: gemvault doctor can leave the machine with no plugin at all
1171
+ Description: Reported from a container where bundle install could not succeed
1172
+ + (ruby version pin mismatch): doctor removed ghost records, uninstalled the
1173
+ + plugin, emptied .bundle/plugin/index, then exec'd bundle install -- which
1174
+ + failed, leaving no plugin installed. Strictly worse than the broken-path
1175
+ + state doctor was invoked to repair, and recoverable only by knowing to run
1176
+ + `bundle plugin install bundler-source-vault` by hand. doctor repairs by
1177
+ + mutation with no dry-run, no report of findings before acting, and no
1178
+ + rollback when the reinstall it hands off to fails. Suggested in the report:
1179
+ + report-then-fix (--dry-run, or report by default and repair under --fix);
1180
+ + reinstall the plugin directly with `bundle plugin install` and verify the
1181
+ + index rather than exec'ing into `bundle install`, so a failed dependency
1182
+ + install cannot take the plugin down with it; or restore the previous index
1183
+ + when the step after the uninstall fails.
1184
+ +
1185
+ + RESOLUTION: the uninstall-reinstall pair is now transactional. doctor
1186
+ + snapshots the plugin index bundler will consult (Gemvault::BundlerPluginIndex
1187
+ + over BundlerPluginRoot#consulted) before clearing it, runs `bundle install`
1188
+ + as a child instead of exec'ing into it, and on failure checks whether the
1189
+ + plugin came back: reinstalled means the repair stands and the one-line error
1190
+ + says only the bundle is unfinished; not reinstalled means the snapshot is
1191
+ + restored, so the machine is left as found and re-running doctor once the
1192
+ + install error is fixed recovers fully. Replicated first by
1193
+ + spec/integration/cli/commands/doctor_reinstall_failure_spec.rb -- doctor run
1194
+ + against a Gemfile still naming a renamed plugin path, an un-installable
1195
+ + bundle in the reporter's shape -- which fails against the old code and
1196
+ + proves the restored index heals on the next run. Unit cover in
1197
+ + spec/gemvault/bundler_plugin_index_spec.rb and the reworked doctor_spec.rb.
1198
+ Status: closed
1199
+
1200
+ Id: 28
1201
+ Updated: Wed, 12 Aug 2026 16:05:55 +0000
1202
+ Name: plugins.rb dies with a bare LoadError when gemvault is installed nowhere
1203
+ Description: A plugin root holding only the shim -- the normal state wherever
1204
+ + `gem install gemvault` has run, since Bundler skips installing a plugin
1205
+ + dependency already present -- stops loading when the tree lands on a machine
1206
+ + with no gemvault in any gem root: GemvaultLoadPath.entries returns [] and
1207
+ + `require "bundler/plugin/vault_source"` raises a bare LoadError with no hint.
1208
+ + Observed after a macOS working tree was copied into a Linux container (the
1209
+ + shim alone in .bundle/plugin, gemvault absent from every gem root on the
1210
+ + box); also reachable by `gem uninstall gemvault` on a single machine. The
1211
+ + ambient-root search cannot cover gemvault being truly absent. Suggested:
1212
+ + fail with an explicit "gemvault not found in any gem root" message naming
1213
+ + the remedy (gem install gemvault, or bundle plugin uninstall + install to
1214
+ + get a self-contained root), or vendor gemvault into the shim.
1215
+ Status: open
1216
+
1217
+ Id: 29
1218
+ Updated: Wed, 12 Aug 2026 16:05:55 +0000
1219
+ Name: Stale plugin versions are never reaped from the plugin root
1220
+ Description: bundler-source-vault-0.2.4 survived in the plugin root's gems/,
1221
+ + specifications/ and cache/ across the 0.2.5 upgrade and an explicit `bundle
1222
+ + plugin uninstall`; only 0.2.5 was ever referenced by the index. Possibly
1223
+ + Bundler's doing rather than the plugin's, but doctor is well placed to sweep
1224
+ + plugin-root versions the index no longer references.
1089
1225
  Status: open
@@ -0,0 +1,63 @@
1
+ require "pathname"
2
+
3
+ module Gemvault
4
+ ##
5
+ # The Gemfile Bundler would load from a directory.
6
+ #
7
+ # Mirrors Bundler::SharedHelpers#find_gemfile: <tt>BUNDLE_GEMFILE</tt> wins
8
+ # when set and non-empty, otherwise the search walks up looking for
9
+ # <tt>gems.rb</tt> then <tt>Gemfile</tt> in each directory. Reimplemented
10
+ # rather than delegated because bundler is deliberately not a gemvault
11
+ # dependency (see gemvault.gemspec) and +gemvault doctor+ runs as a plain
12
+ # CLI, outside any Bundler process.
13
+ #
14
+ # One difference is deliberate. Bundler returns <tt>BUNDLE_GEMFILE</tt>
15
+ # whatever it points at, existing or not -- bundler/inline relies on that,
16
+ # setting it to a bare "Gemfile" purely to make Bundler treat the working
17
+ # directory as the app root. The question here is whether +bundle install+
18
+ # could work, so a value that does not name a file is ignored and the
19
+ # walk-up search decides instead.
20
+ class BundlerGemfile
21
+ # Checked in this order within each directory, as Bundler checks them.
22
+ NAMES = ["gems.rb", "Gemfile"].freeze
23
+
24
+ def initialize(dir: Dir.pwd, env: ENV)
25
+ @dir = Pathname(dir)
26
+ @env = env
27
+ end
28
+
29
+ # :call-seq:
30
+ # path -> Pathname or nil
31
+ #
32
+ # The Gemfile Bundler would load, or +nil+ when there is none.
33
+ def path
34
+ return @path if defined?(@path)
35
+
36
+ @path = from_env || search_up
37
+ end
38
+
39
+ # Whether Bundler would find a Gemfile at all.
40
+ def exist?
41
+ !path.nil?
42
+ end
43
+
44
+ private
45
+
46
+ def from_env
47
+ given = @env["BUNDLE_GEMFILE"].to_s
48
+ return nil if given.empty?
49
+
50
+ candidate = Pathname(given).expand_path(@dir)
51
+ candidate.file? ? candidate : nil
52
+ end
53
+
54
+ def search_up
55
+ @dir.expand_path.ascend do |dir|
56
+ found = NAMES.map { |name| dir / name }.find(&:file?)
57
+ return found if found
58
+ end
59
+
60
+ nil
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,58 @@
1
+ require "pathname"
2
+
3
+ module Gemvault
4
+ ##
5
+ # Bundler's plugin index file inside a plugin root.
6
+ #
7
+ # The uninstall step of +gemvault doctor+ clears entries from this file, and
8
+ # the reinstall that follows can fail for reasons unrelated to the plugin.
9
+ # Snapshot and restore make that pair transactional: a repair that cannot
10
+ # finish puts the index back instead of leaving the machine with no plugin
11
+ # registered at all (issue #27).
12
+ #
13
+ # The plugin_paths section is scanned textually rather than parsed as YAML:
14
+ # loading a YAML library into doctor's process invites the same
15
+ # stdlib-activation conflicts as issue #25, and bundler's own emitter writes
16
+ # one two-space-indented "name: path" line per plugin.
17
+ class BundlerPluginIndex
18
+ def initialize(root)
19
+ @root = Pathname(root)
20
+ end
21
+
22
+ def file
23
+ @root / "index"
24
+ end
25
+
26
+ # Whether plugin_paths currently lists +plugin+.
27
+ def registered?(plugin)
28
+ plugin_paths.any? { |line| line.match?(/\A\s{2}#{Regexp.escape(plugin)}:/) }
29
+ end
30
+
31
+ # :call-seq:
32
+ # snapshot -> String or nil
33
+ #
34
+ # The index content as it stands, +nil+ when no index exists.
35
+ def snapshot
36
+ file.file? ? file.read : nil
37
+ end
38
+
39
+ # Puts the index back the way +snapshot+ recorded it; restoring +nil+
40
+ # removes an index that did not exist at snapshot time.
41
+ def restore(snapshot)
42
+ return file.write(snapshot) if snapshot
43
+
44
+ file.delete if file.exist?
45
+ end
46
+
47
+ private
48
+
49
+ def plugin_paths
50
+ return [] unless file.file?
51
+
52
+ file.readlines
53
+ .drop_while { |line| !line.start_with?("plugin_paths:") }
54
+ .drop(1)
55
+ .take_while { |line| line.start_with?(" ") }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,59 @@
1
+ require "pathname"
2
+ require_relative "bundler_gemfile"
3
+
4
+ module Gemvault
5
+ ##
6
+ # Where Bundler keeps plugin data for a directory.
7
+ #
8
+ # Bundler::Plugin.root answers the project-local <tt>.bundle/plugin</tt> when
9
+ # <tt>SharedHelpers.in_bundle?</tt> holds, and the global
10
+ # <tt>~/.bundle/plugin</tt> when it does not. Since +in_bundle?+ is just
11
+ # "was a Gemfile found", a project can own a plugin root that Bundler
12
+ # declines to look in.
13
+ class BundlerPluginRoot
14
+ LOCAL_DIR = ".bundle/plugin".freeze
15
+
16
+ def initialize(dir: Dir.pwd, gemfile: BundlerGemfile.new(dir: dir), env: ENV)
17
+ @dir = Pathname(dir)
18
+ @gemfile = gemfile
19
+ @env = env
20
+ end
21
+
22
+ # The project's own plugin root, whether or not Bundler would consult it.
23
+ def local
24
+ @dir.expand_path / LOCAL_DIR
25
+ end
26
+
27
+ # Where Bundler keeps plugins for a user outside any project, mirroring
28
+ # Bundler's user_bundle_path lookup for plugins: <tt>BUNDLE_USER_PLUGIN</tt>
29
+ # names the root directly, <tt>BUNDLE_USER_HOME</tt> relocates
30
+ # <tt>.bundle</tt>, and the home directory is the default.
31
+ def global
32
+ named = @env["BUNDLE_USER_PLUGIN"]
33
+ return Pathname(named) if named
34
+
35
+ Pathname(@env["BUNDLE_USER_HOME"] || File.join(Dir.home, ".bundle")) / "plugin"
36
+ end
37
+
38
+ # The plugin root Bundler will consult here: beside the Gemfile when one
39
+ # was found, the project's own root when doctor points Bundler at it (see
40
+ # #unreachable?), the global root otherwise.
41
+ def consulted
42
+ return @gemfile.path.dirname / LOCAL_DIR if @gemfile.exist?
43
+ return local if unreachable?
44
+
45
+ global
46
+ end
47
+
48
+ # Whether this project has a plugin root that Bundler currently ignores.
49
+ #
50
+ # bundler/inline installs plugins into <tt><cwd>/.bundle/plugin</tt>, having
51
+ # set <tt>BUNDLE_GEMFILE</tt> to a bare "Gemfile" for the duration of the
52
+ # script. A later command in that same directory sets no such variable and
53
+ # finds no Gemfile on disk, so Bundler falls back to the global root and
54
+ # reports the plugin as not installed -- while the entry sits here.
55
+ def unreachable?
56
+ !@gemfile.exist? && local.directory?
57
+ end
58
+ end
59
+ end
@@ -1,4 +1,7 @@
1
1
  require_relative "../command"
2
+ require_relative "../../bundler_gemfile"
3
+ require_relative "../../bundler_plugin_index"
4
+ require_relative "../../bundler_plugin_root"
2
5
  require_relative "../../ghost_specification"
3
6
 
4
7
  module Gemvault
@@ -25,9 +28,22 @@ module Gemvault
25
28
  # Re-running bundle install afterwards triggers Bundler to reinstall
26
29
  # the plugin against whatever the current Gemfile declares. Run this
27
30
  # from the project directory.
31
+ #
32
+ # A project whose Gemfile is inline has no Gemfile to reinstall from, and
33
+ # keeps its plugins in a root Bundler will not consult unless told to.
34
+ # Both are handled below.
35
+ #
36
+ # The repair is transactional. `bundle install` does more than reinstall
37
+ # the plugin, and that extra work can fail on its own; the index the
38
+ # uninstall clears is snapshotted first and put back when the reinstall
39
+ # never happened, so a failed run leaves the machine as found rather
40
+ # than with no plugin at all (issue #27). Every failure is one line on
41
+ # stderr and exit 1 (issue #24).
28
42
  class Doctor < Command
29
43
  description "Repair broken bundler-source-vault plugin state and reinstall the plugin"
30
44
 
45
+ PLUGIN = "bundler-source-vault".freeze
46
+
31
47
  OWNED_GEMS = %w[gemvault bundler-source-vault].freeze
32
48
 
33
49
  PERMISSION_HINT = "(re-run with permissions for that gem home, e.g. sudo gemvault doctor)".freeze
@@ -39,8 +55,9 @@ module Gemvault
39
55
  print_error("#{e.message} #{PERMISSION_HINT}")
40
56
  exit(1)
41
57
  end
42
- system("bundle", "plugin", "uninstall", "bundler-source-vault", exception: true)
43
- exec("bundle", "install")
58
+ saved = index.snapshot
59
+ uninstall_plugin
60
+ reinstall_or_explain(saved)
44
61
  end
45
62
 
46
63
  private
@@ -51,6 +68,77 @@ module Gemvault
51
68
  puts "Removed ghost specification #{ghost}"
52
69
  end
53
70
  end
71
+
72
+ def gemfile
73
+ @gemfile ||= BundlerGemfile.new
74
+ end
75
+
76
+ def plugin_root
77
+ @plugin_root ||= BundlerPluginRoot.new(gemfile: gemfile)
78
+ end
79
+
80
+ def index
81
+ @index ||= BundlerPluginIndex.new(plugin_root.consulted)
82
+ end
83
+
84
+ # bundler/inline's own trick: a non-empty BUNDLE_GEMFILE is the whole of
85
+ # what Bundler::Plugin.root consults to prefer a project's plugin
86
+ # directory over the global one, so setting it reaches the entry that
87
+ # needs clearing. The file it names never has to exist.
88
+ def uninstall_env
89
+ return {} unless plugin_root.unreachable?
90
+
91
+ { "BUNDLE_GEMFILE" => "Gemfile" }
92
+ end
93
+
94
+ def uninstall_plugin
95
+ return if system(uninstall_env, "bundle", "plugin", "uninstall", PLUGIN)
96
+
97
+ print_error("bundle plugin uninstall #{PLUGIN} failed")
98
+ exit(1)
99
+ end
100
+
101
+ # `bundle install` with no Gemfile prints its entire usage screen and
102
+ # exits 10, which reads as gemvault itself failing. By this point the
103
+ # repair has already happened; only the reinstall is unavailable.
104
+ def reinstall_or_explain(saved)
105
+ return reinstall(saved) if gemfile.exist?
106
+ return explain_inline_repair if plugin_root.unreachable?
107
+
108
+ puts "No Gemfile here to reinstall from -- re-run gemvault doctor from the"
109
+ puts "project directory to reinstall the plugin."
110
+ end
111
+
112
+ def reinstall(saved)
113
+ return if system("bundle", "install")
114
+ return exit_partially_repaired if index.registered?(PLUGIN)
115
+
116
+ exit_restoring(saved)
117
+ end
118
+
119
+ # The plugin came back before the install died, so the repair stands;
120
+ # only the rest of the bundle is unfinished.
121
+ def exit_partially_repaired
122
+ print_error("bundle install failed; the #{PLUGIN} plugin itself was reinstalled " \
123
+ "-- fix the install error and re-run bundle install")
124
+ exit(1)
125
+ end
126
+
127
+ def exit_restoring(saved)
128
+ index.restore(saved)
129
+ print_error("bundle install failed before reinstalling #{PLUGIN}; restored the " \
130
+ "previous plugin index -- fix the install error and re-run gemvault doctor")
131
+ exit(1)
132
+ end
133
+
134
+ # The local path is named only when it was the uninstall's target --
135
+ # otherwise bundler worked against its global root, and naming this
136
+ # project's path would misreport what happened.
137
+ def explain_inline_repair
138
+ puts "Cleared the #{PLUGIN} plugin index at #{plugin_root.local}."
139
+ puts "No Gemfile here to reinstall from -- an inline gemfile installs the plugin"
140
+ puts "again the next time the script runs."
141
+ end
54
142
  end
55
143
  end
56
144
  end
@@ -1,3 +1,3 @@
1
1
  module Gemvault
2
- VERSION = "0.2.5".freeze
2
+ VERSION = "0.2.6".freeze
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gemvault
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Gillis
@@ -56,6 +56,9 @@ files:
56
56
  - lib/bundler/plugin/vaulted_gem.rb
57
57
  - lib/gemvault.rb
58
58
  - lib/gemvault/archive_entry.rb
59
+ - lib/gemvault/bundler_gemfile.rb
60
+ - lib/gemvault/bundler_plugin_index.rb
61
+ - lib/gemvault/bundler_plugin_root.rb
59
62
  - lib/gemvault/cli.rb
60
63
  - lib/gemvault/cli/command.rb
61
64
  - lib/gemvault/cli/commands/add.rb