ruby_workspace_manager 0.6.4 → 0.6.5

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: 38623d16221111f5899c657ecd424a14397cb4b33223ee7d8f95ebd766c61777
4
- data.tar.gz: 99992c68cb96d2ce4a7414e2ec5ee0fe5aed244c5fbcf9e3d70887090c1c155b
3
+ metadata.gz: 115ec9332203fbf44622ec26f179450eb6dcabce1bb9ad7a523bd6cd470b7cdd
4
+ data.tar.gz: 4d0c0fc8898f786091a86634408b018a236bcfcb772589b30e917bc5d6ece7c0
5
5
  SHA512:
6
- metadata.gz: 5f1651f55f29fe8cff0bbe731941e52baf5d173139cd62cd1c75ad80d99ae8008a2caf4ef3bea261a27fb8642d74006599eaa30919d1d16349678e4b1a08a927
7
- data.tar.gz: f663aebaa0ddaabb9032a6809105eab4df406170eda4f1ed2b540340cebdb16150e9fed5f4df745ca711330b0cc431b156623d9d61e0bb9cb1a4b7ddd34150e4
6
+ metadata.gz: b0a671c6a7b4e2a66f6120f7a2c635e52e2c9715312fe2b576a298a89878be4ee66f38758e3cbeb1fd865e7c5083ffd10e936d1041fabc10125e807e218b7947
7
+ data.tar.gz: a787011658b8f986c0b2353fffc97938978113cfd9e549f6d6a8287034a5ae575456661507dde36a4a01e7fc05957bcedb8db381bdc4ef5acb2f925782d874a0
@@ -62,7 +62,7 @@ module Rwm
62
62
  _, _, status = Open3.capture3("git", "-C", workspace.root, "rev-parse", "--verify", "#{@base_branch}^{commit}")
63
63
  return if status.success?
64
64
 
65
- raise Rwm::Error, "Base ref '#{@base_branch}' does not exist. Check the branch name or pass a valid --base ref."
65
+ raise Rwm::InvalidBaseRefError, @base_branch
66
66
  end
67
67
 
68
68
  def detect_base_branch
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "open3"
4
+
3
5
  module Rwm
4
6
  module Commands
5
7
  class Bootstrap
@@ -140,8 +142,8 @@ module Rwm
140
142
  return unless File.exist?(File.join(dir, "Gemfile"))
141
143
 
142
144
  puts " bundle install..."
143
- success = system("bundle", "install", chdir: dir)
144
- unless success
145
+ _, _, status = Open3.capture3(Rwm.bundle_env(dir), "bundle", "install", chdir: dir)
146
+ unless status.success?
145
147
  raise BootstrapError, "bundle install failed in #{dir}"
146
148
  end
147
149
  end
@@ -150,13 +152,13 @@ module Rwm
150
152
  return unless File.exist?(File.join(dir, "Rakefile"))
151
153
 
152
154
  # Check if bootstrap task exists before running it
153
- has_task = system("bundle", "exec", "rake", "-s", "-T", "bootstrap",
154
- chdir: dir, out: File::NULL, err: File::NULL)
155
- return unless has_task
155
+ _, _, status = Open3.capture3(Rwm.bundle_env(dir), "bundle", "exec", "rake", "-s", "-T", "bootstrap",
156
+ chdir: dir)
157
+ return unless status.success?
156
158
 
157
159
  puts " rake bootstrap..."
158
- success = system("bundle", "exec", "rake", "bootstrap", chdir: dir)
159
- unless success
160
+ _, _, status = Open3.capture3(Rwm.bundle_env(dir), "bundle", "exec", "rake", "bootstrap", chdir: dir)
161
+ unless status.success?
160
162
  raise BootstrapError, "rake bootstrap failed in #{dir}"
161
163
  end
162
164
  end
@@ -187,22 +187,21 @@ module Rwm
187
187
  end
188
188
 
189
189
  # Serialize to JSON for .rwm/graph.json
190
- def to_json_data
190
+ def to_json_data(workspace_root: "")
191
191
  {
192
192
  "version" => 1,
193
193
  "generated_at" => Time.now.iso8601,
194
194
  "packages" => @packages.transform_values do |pkg|
195
- { "name" => pkg.name, "type" => pkg.type.to_s, "path" => pkg.relative_path(@workspace_root || "") }
195
+ { "name" => pkg.name, "type" => pkg.type.to_s, "path" => pkg.relative_path(workspace_root) }
196
196
  end,
197
197
  "edges" => @edges.transform_values(&:sort)
198
198
  }
199
199
  end
200
200
 
201
201
  def save(path, workspace_root)
202
- @workspace_root = workspace_root
203
202
  dir = File.dirname(path)
204
203
  FileUtils.mkdir_p(dir)
205
- write_locked(path, JSON.pretty_generate(to_json_data) + "\n")
204
+ write_locked(path, JSON.pretty_generate(to_json_data(workspace_root: workspace_root)) + "\n")
206
205
  end
207
206
 
208
207
  def to_dot
data/lib/rwm/errors.rb CHANGED
@@ -40,4 +40,18 @@ module Rwm
40
40
  end
41
41
 
42
42
  class BootstrapError < Error; end
43
+
44
+ class InvalidBaseRefError < Error
45
+ def initialize(ref)
46
+ super("Base ref '#{ref}' does not exist. Check the branch name or pass a valid --base ref.")
47
+ end
48
+ end
49
+
50
+ class GemfileParseError < Error
51
+ def initialize(path, cause_message)
52
+ super("Failed to parse Gemfile at #{path}: #{cause_message}")
53
+ end
54
+ end
55
+
56
+ class CacheError < Error; end
43
57
  end
@@ -17,7 +17,11 @@ module Rwm
17
17
 
18
18
  def parse
19
19
  dsl = Bundler::Dsl.new
20
- dsl.eval_gemfile(@gemfile_path)
20
+ begin
21
+ dsl.eval_gemfile(@gemfile_path)
22
+ rescue Bundler::GemfileError, SyntaxError => e
23
+ raise Rwm::GemfileParseError.new(@gemfile_path, e.message)
24
+ end
21
25
  deps = dsl.dependencies
22
26
 
23
27
  gemfile_dir = File.expand_path(File.dirname(@gemfile_path))
@@ -8,6 +8,11 @@ require "open3"
8
8
 
9
9
  module Rwm
10
10
  class TaskCache
11
+ # Salt: bump when the hashing scheme changes (file ordering, what gets
12
+ # included, normalisation rules) so existing caches become misses rather
13
+ # than wrong hits.
14
+ CACHE_HASH_VERSION = "v1"
15
+
11
16
  def self.clean(workspace, package_name: nil)
12
17
  cache_dir = File.join(workspace.root, ".rwm", "cache")
13
18
  return unless Dir.exist?(cache_dir)
@@ -77,33 +82,27 @@ module Rwm
77
82
  !matches.empty?
78
83
  end
79
84
 
80
- # Compute a content hash for a package: SHA256 of all source files + dependency hashes
85
+ # Compute a content hash for a package: SHA256 of all source files + dependency hashes.
86
+ # Walks `package` and its transitive deps in topological order (deps before dependents)
87
+ # so each dep's hash is memoised before any package that depends on it is hashed —
88
+ # avoids the unbounded recursion of the natural recursive formulation on deep chains.
81
89
  def content_hash(package)
82
- @content_hash_mutex.synchronize do
83
- return @content_hashes[package.name] if @content_hashes.key?(package.name)
84
- end
90
+ cached = read_memoised_hash(package.name)
91
+ return cached if cached
85
92
 
86
- digest = Digest::SHA256.new
93
+ needed = @graph.transitive_dependencies(package.name).to_set
94
+ needed << package.name
87
95
 
88
- # Hash all source files in the package (sorted for determinism)
89
- source_files(package).each do |file|
90
- rel_path = file.delete_prefix("#{package.path}/")
91
- digest.update(rel_path)
92
- digest.update(File.read(file))
93
- end
96
+ @graph.topological_order.each do |name|
97
+ next unless needed.include?(name)
98
+ next if read_memoised_hash(name)
94
99
 
95
- # Include dependency content hashes (transitive invalidation).
96
- # If a dependency is missing, let it raise — a stale graph should
97
- # not silently produce incorrect cache hits.
98
- @graph.dependencies(package.name).sort.each do |dep_name|
99
- dep_pkg = @workspace.find_package(dep_name)
100
- digest.update(content_hash(dep_pkg))
100
+ pkg = name == package.name ? package : @workspace.find_package(name)
101
+ computed = compute_single_package_hash(pkg)
102
+ @content_hash_mutex.synchronize { @content_hashes[pkg.name] ||= computed }
101
103
  end
102
104
 
103
- computed = digest.hexdigest
104
- @content_hash_mutex.synchronize do
105
- @content_hashes[package.name] = computed
106
- end
105
+ read_memoised_hash(package.name)
107
106
  end
108
107
 
109
108
  # Preload cache declarations for multiple packages in parallel.
@@ -150,6 +149,39 @@ module Rwm
150
149
 
151
150
  private
152
151
 
152
+ def read_memoised_hash(name)
153
+ @content_hash_mutex.synchronize { @content_hashes[name] }
154
+ end
155
+
156
+ def compute_single_package_hash(package)
157
+ digest = Digest::SHA256.new
158
+ digest.update(CACHE_HASH_VERSION)
159
+
160
+ # Hash all source files in the package (sorted for determinism).
161
+ # Stream in 64 KiB chunks so multi-MB files don't load whole into memory.
162
+ source_files(package).each do |file|
163
+ rel_path = file.delete_prefix("#{package.path}/")
164
+ digest.update(rel_path)
165
+ File.open(file, "rb") do |f|
166
+ while (chunk = f.read(64 * 1024))
167
+ digest.update(chunk)
168
+ end
169
+ end
170
+ end
171
+
172
+ # Dep hashes are guaranteed memoised by the topological iteration in #content_hash.
173
+ # A nil here means the graph and the in-memory state disagree — surface it
174
+ # as a typed cache error rather than a generic TypeError from digest.update(nil).
175
+ @graph.dependencies(package.name).sort.each do |dep_name|
176
+ dep_hash = read_memoised_hash(dep_name)
177
+ raise Rwm::CacheError, "Missing memoised hash for dep '#{dep_name}' of '#{package.name}' (stale dep graph?)" if dep_hash.nil?
178
+
179
+ digest.update(dep_hash)
180
+ end
181
+
182
+ digest.hexdigest
183
+ end
184
+
153
185
  def source_files(package)
154
186
  # Tracked files + untracked-but-not-ignored files (null-delimited for safe filenames)
155
187
  output, _, status = Open3.capture3(
data/lib/rwm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rwm
4
- VERSION = "0.6.4"
4
+ VERSION = "0.6.5"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_workspace_manager
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.4
4
+ version: 0.6.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Siddharth Bhatt