git_cache 0.1.1 → 0.2.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: d3b7101169665dc9115fad2454928336b6ef83cca3b1db38a8ae6acef81062b1
4
- data.tar.gz: f58df6572f0e3511ef30e4e7f1b0843e54ee59b157edb77d84f268b8a4bc0135
3
+ metadata.gz: bf5b975bb07d2180af37c7d22c0823d9266adb7993485bbb4f5708ab3b769421
4
+ data.tar.gz: d2ff0b8e4b2e41ad1f19e36b81bab3ba6a9b08483a0495a1d270065f6e15473f
5
5
  SHA512:
6
- metadata.gz: 0be87372ec841eb2288330508ce7bd5aaa61cb7f1332b78ff4866f63c071534204309dfb2bc4417bb47f093901dc22063cf1ba5c84d9f0f0391addf660fa0b91
7
- data.tar.gz: 45fa8853c29cc760cd59f3054a39d43bf13d72b5fc9f2afcb4cdf5460a3b30b96a212754d6c878d08d418986cf2858a1108c1c68e5b191b3a437ed76e0d775b7
6
+ metadata.gz: 90401416dea171255778bb4372c3af86f0a7235195db3de596f5fdf60542e23538685a27dcaa6c82b3583f187b7f0608ada0b3e63978aefdc3dccfd4bbf03132
7
+ data.tar.gz: 2d58c482b5d55949e9eab5a06ef789aaa184a760c80185b78680562448e7eaf913b8a4e317374b0c0b1278777965a5e0aa106a4c65ae18b6d8cad81975353e6d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Release History
2
2
 
3
+ ### v0.2.0 / 2026-10-06
4
+
5
+ Note: This release changes the internal cache format from v1 to v2. This release will start with a new empty cache, but will not interfere with older versions and older caches.
6
+
7
+ * FIXED: Raise GitCache::Error when the cache directory cannot be created
8
+ * FIXED: Move repo locks out of the directories they protect
9
+ * FIXED: Store cache data in a format version subdirectory of custom cache dirs
10
+ * FIXED: Write repo state atomically
11
+ * FIXED: Record the remote even if the first get fails
12
+ * FIXED: Do not mask a failed operation's error with a state write error
13
+
14
+ ### v0.1.2 / 2026-08-20
15
+
16
+ * FIXED: Prevent git auto maintenance from racing with cache removals
17
+
3
18
  ### v0.1.1 / 2026-05-05
4
19
 
5
20
  * BREAKING CHANGE: Make GitCache.sources_writable? private for now
@@ -9,11 +9,11 @@ class GitCache
9
9
  # Create a GitCache::Error.
10
10
  #
11
11
  # @param message [String] The error message
12
- # @param result [::ExecService::Result] The result of a git
13
- # command execution, or `nil` if this error was not due to a git
14
- # command error.
12
+ # @param result [::ExecService::Result,nil] The result of a git
13
+ # command execution. Omit or pass `nil` if this error was not due to
14
+ # a git command error.
15
15
  #
16
- def initialize(message, result)
16
+ def initialize(message, result = nil)
17
17
  super(message)
18
18
  @exec_result = result
19
19
  end
@@ -2,9 +2,10 @@
2
2
 
3
3
  class GitCache
4
4
  ##
5
- # Associated with each repo (remote) is a lock file that saves the status
6
- # of the cache, and also serves as a file system lock for updates to the
7
- # repo. This is handled by the lock_repo method.
5
+ # Associated with each repo (remote) is a state file, `state.json` in the
6
+ # repo's base dir, that saves the status of the cache. It is read and
7
+ # written only while holding the repo's lock, which is a separate file
8
+ # outside the base dir. This is handled by the lock_repo method.
8
9
  #
9
10
  # This object represents the state of the repo, and is made available to
10
11
  # the block passed to lock_repo. It has the following schema:
@@ -23,16 +24,18 @@ class GitCache
23
24
  #
24
25
  # @private
25
26
  #
26
- class RepoLock
27
+ class RepoState
27
28
  ##
28
29
  # @private
29
30
  #
30
- def initialize(io, remote, timestamp)
31
- @data = ::JSON.parse(io.read) rescue {} # rubocop:disable Style/RescueModifier
31
+ def initialize(json, remote, timestamp)
32
+ @data = ::JSON.parse(json) rescue {} # rubocop:disable Style/RescueModifier
33
+ # Record the remote if the state lacks it (e.g. a new repo), so the
34
+ # repo is listed by GitCache#remotes even if nothing else is recorded.
35
+ @modified = @data["remote"].nil? && !remote.nil?
32
36
  @data["remote"] ||= remote
33
37
  @data["refs"] ||= {}
34
38
  @data["sources"] ||= {}
35
- @modified = false
36
39
  @timestamp = timestamp || ::Time.now.to_i
37
40
  end
38
41
 
@@ -51,8 +54,8 @@ class GitCache
51
54
  ##
52
55
  # @private
53
56
  #
54
- def dump(io)
55
- ::JSON.dump(@data, io)
57
+ def dump
58
+ ::JSON.dump(@data)
56
59
  end
57
60
 
58
61
  ##
@@ -5,5 +5,5 @@ class GitCache
5
5
  # Version of the git_cache gem
6
6
  # @return [String]
7
7
  #
8
- VERSION = "0.1.1"
8
+ VERSION = "0.2.0"
9
9
  end
data/lib/git_cache.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "git_cache/error"
4
4
  require "git_cache/repo_info"
5
- require "git_cache/repo_lock"
5
+ require "git_cache/repo_state"
6
6
 
7
7
  ##
8
8
  # This object provides cached access to remote git data. Given a remote
@@ -15,14 +15,19 @@ class GitCache
15
15
  # Access a git cache.
16
16
  #
17
17
  # @param cache_dir [String] The path to the cache directory. Defaults to
18
- # a specific directory in the user's XDG cache.
18
+ # a specific directory in the user's XDG cache. Cache data is stored
19
+ # in a subdirectory named for the cache format version, so that clients
20
+ # using incompatible formats can share a cache directory safely.
19
21
  #
20
22
  def initialize(cache_dir: nil)
21
23
  require "digest"
22
24
  require "fileutils"
23
25
  require "json"
26
+ require "securerandom"
24
27
  require "exec_service"
28
+ @using_default_cache_dir = cache_dir.nil?
25
29
  @cache_dir = ::File.expand_path(cache_dir || default_cache_dir)
30
+ @data_dir = ::File.join(@cache_dir, FORMAT_VERSION)
26
31
  @exec = ::ExecService.new(out: :capture, err: :capture)
27
32
  end
28
33
 
@@ -73,14 +78,15 @@ class GitCache
73
78
  path = ::GitCache.normalize_path(path)
74
79
  commit ||= "HEAD"
75
80
  timestamp ||= ::Time.now.to_i
76
- dir = ensure_repo_base_dir(remote)
77
- lock_repo(dir, remote, timestamp) do |repo_lock|
81
+ name = ::GitCache.remote_dir_name(remote)
82
+ dir = repo_base_dir_for(name)
83
+ lock_repo(name, remote, timestamp, create: true) do |repo_state|
78
84
  ensure_repo(dir, remote)
79
- sha = ensure_commit(dir, commit, repo_lock, update)
85
+ sha = ensure_commit(dir, commit, repo_state, update)
80
86
  if into
81
- copy_files(dir, sha, path, repo_lock, into)
87
+ copy_files(dir, sha, path, repo_state, into)
82
88
  else
83
- ensure_source(dir, sha, path, repo_lock)
89
+ ensure_source(dir, sha, path, repo_state)
84
90
  end
85
91
  end
86
92
  end
@@ -93,14 +99,13 @@ class GitCache
93
99
  #
94
100
  def remotes
95
101
  result = []
96
- return result unless ::File.directory?(cache_dir)
97
- ::Dir.entries(cache_dir).each do |child|
98
- next if child.start_with?(".")
99
- dir = ::File.join(cache_dir, child)
100
- if ::File.file?(::File.join(dir, LOCK_FILE_NAME))
101
- remote = lock_repo(dir, &:remote)
102
- result << remote if remote
103
- end
102
+ repos_dir = ::File.join(@data_dir, REPOS_DIR_NAME)
103
+ return result unless ::File.directory?(repos_dir)
104
+ ::Dir.children(repos_dir).each do |name|
105
+ next if name.start_with?(".")
106
+ next unless ::File.file?(::File.join(repos_dir, name, STATE_FILE_NAME))
107
+ remote = lock_repo(name, &:remote)
108
+ result << remote if remote
104
109
  end
105
110
  result.sort
106
111
  end
@@ -113,10 +118,11 @@ class GitCache
113
118
  # @return [RepoInfo,nil]
114
119
  #
115
120
  def repo_info(remote)
116
- dir = repo_base_dir_for(remote)
121
+ name = ::GitCache.remote_dir_name(remote)
122
+ dir = repo_base_dir_for(name)
117
123
  return nil unless ::File.directory?(dir)
118
- lock_repo(dir, remote) do |repo_lock|
119
- RepoInfo.new(dir, repo_lock.data)
124
+ lock_repo(name, remote) do |repo_state|
125
+ RepoInfo.new(dir, repo_state.data)
120
126
  end
121
127
  end
122
128
 
@@ -128,8 +134,9 @@ class GitCache
128
134
  # repositories are requested, they will be reloaded from the remote
129
135
  # repository from scratch.
130
136
  #
131
- # Be careful not to remove repos that are currently in use by other
132
- # GitCache clients.
137
+ # This waits for any in-progress {#get} calls for these repos to finish.
138
+ # However, be careful not to remove repos whose shared sources are
139
+ # currently in use by other GitCache clients.
133
140
  #
134
141
  # @param remotes [Array<String>,:all,nil] The remotes to remove. If set
135
142
  # to :all or nil, removes all repos.
@@ -138,10 +145,15 @@ class GitCache
138
145
  def remove_repos(remotes)
139
146
  remotes = self.remotes if remotes.nil? || remotes == :all
140
147
  Array(remotes).map do |remote|
141
- dir = repo_base_dir_for(remote)
142
- if ::File.directory?(dir)
143
- ::FileUtils.chmod_R("u+w", dir, force: true)
144
- ::FileUtils.rm_rf(dir)
148
+ name = ::GitCache.remote_dir_name(remote)
149
+ dir = repo_base_dir_for(name)
150
+ next unless ::File.directory?(dir)
151
+ # Take the lock so we wait for any in-flight operation on this repo.
152
+ # The lock file lives outside the directory being removed, so it keeps
153
+ # excluding later clients after the removal.
154
+ flock_repo(name) do
155
+ next unless ::File.directory?(dir)
156
+ remove_dir(dir)
145
157
  remote
146
158
  end
147
159
  end.compact.sort
@@ -161,17 +173,17 @@ class GitCache
161
173
  # the given repo is not in the cache.
162
174
  #
163
175
  def remove_refs(remote, refs: nil)
164
- dir = repo_base_dir_for(remote)
165
- return nil unless ::File.directory?(dir)
166
- results = []
167
- lock_repo(dir, remote) do |repo_lock|
168
- refs = repo_lock.refs if refs.nil? || refs == :all
176
+ name = ::GitCache.remote_dir_name(remote)
177
+ return nil unless ::File.directory?(repo_base_dir_for(name))
178
+ lock_repo(name, remote) do |repo_state|
179
+ results = []
180
+ refs = repo_state.refs if refs.nil? || refs == :all
169
181
  Array(refs).each do |ref|
170
- ref_data = repo_lock.delete_ref!(ref)
182
+ ref_data = repo_state.delete_ref!(ref)
171
183
  results << RefInfo.new(ref, ref_data) if ref_data
172
184
  end
185
+ results.sort
173
186
  end
174
- results.sort
175
187
  end
176
188
 
177
189
  ##
@@ -191,82 +203,228 @@ class GitCache
191
203
  # if the given repo is not in the cache.
192
204
  #
193
205
  def remove_sources(remote, commits: nil)
194
- dir = repo_base_dir_for(remote)
206
+ name = ::GitCache.remote_dir_name(remote)
207
+ dir = repo_base_dir_for(name)
195
208
  return nil unless ::File.directory?(dir)
196
- results = []
197
- lock_repo(dir, remote) do |repo_lock|
209
+ lock_repo(name, remote) do |repo_state|
210
+ results = []
198
211
  commits = nil if commits == :all
199
- shas = Array(commits).map { |ref| repo_lock.lookup_ref(ref) }.compact.uniq if commits
200
- repo_lock.find_sources(shas: shas).each do |(sha, path)|
201
- data = repo_lock.delete_source!(sha, path)
212
+ shas = Array(commits).map { |ref| repo_state.lookup_ref(ref) }.compact.uniq if commits
213
+ repo_state.find_sources(shas: shas).each do |(sha, path)|
214
+ data = repo_state.delete_source!(sha, path)
202
215
  results << SourceInfo.new(dir, sha, path, data)
203
216
  end
204
217
  results.map(&:sha).uniq.each do |sha|
205
- unless repo_lock.source_exists?(sha)
206
- sha_dir = ::File.join(dir, sha)
207
- ::FileUtils.chmod_R("u+w", sha_dir, force: true)
208
- ::FileUtils.rm_rf(sha_dir)
218
+ unless repo_state.source_exists?(sha)
219
+ remove_dir(::File.join(dir, sha))
209
220
  end
210
221
  end
222
+ results.sort
211
223
  end
212
- results.sort
213
224
  end
214
225
 
215
226
  private
216
227
 
217
- FORMAT_VERSION = "v1"
228
+ # Cache layout, relative to the cache directory:
229
+ #
230
+ # <FORMAT_VERSION>/ Data dir. Bumping FORMAT_VERSION on
231
+ # incompatible layout changes isolates clients
232
+ # using different formats.
233
+ # locks/<name>.lock Lock file for the repo. Empty; used only as a
234
+ # flock target. Never deleted (see flock_repo).
235
+ # repos/<name>/ Base dir for the repo. Removing it (via a
236
+ # rename) removes the repo from the cache.
237
+ # state.json Repo state (see RepoState).
238
+ # repo/ Working clone of the remote.
239
+ # <sha>/ Shared sources for a commit.
240
+ #
241
+ # where <name> is the remote_dir_name of the remote.
242
+ #
243
+ FORMAT_VERSION = "v2"
244
+ LOCKS_DIR_NAME = "locks"
245
+ REPOS_DIR_NAME = "repos"
246
+ LOCK_FILE_SUFFIX = ".lock"
247
+ STATE_FILE_NAME = "state.json"
218
248
  REPO_DIR_NAME = "repo"
219
- LOCK_FILE_NAME = "repo.lock"
220
- private_constant :REPO_DIR_NAME, :LOCK_FILE_NAME, :FORMAT_VERSION
249
+ TRASH_DIR_PREFIX = ".trash-"
221
250
 
222
- def repo_base_dir_for(remote)
223
- ::File.join(@cache_dir, ::GitCache.remote_dir_name(remote))
251
+ # Config applied to every git invocation. Auto maintenance would otherwise
252
+ # spawn a detached `git maintenance run --auto` process after each fetch,
253
+ # which keeps writing into the cache repo after the fetch has returned, and
254
+ # thus races with our own traversals and removals of that directory. These
255
+ # are managed cache repos that we recreate at will, so background repacking
256
+ # buys them nothing. Requires git 2.30 or later; older gits ignore config
257
+ # keys they do not recognize.
258
+ GIT_CONFIG_ARGS = ["-c", "maintenance.auto=false"].freeze
259
+
260
+ private_constant :FORMAT_VERSION, :LOCKS_DIR_NAME, :REPOS_DIR_NAME,
261
+ :LOCK_FILE_SUFFIX, :STATE_FILE_NAME, :REPO_DIR_NAME,
262
+ :TRASH_DIR_PREFIX, :GIT_CONFIG_ARGS
263
+
264
+ # Takes the remote_dir_name of a remote
265
+ def repo_base_dir_for(name)
266
+ ::File.join(@data_dir, REPOS_DIR_NAME, name)
267
+ end
268
+
269
+ # Takes the remote_dir_name of a remote
270
+ def repo_lock_path_for(name)
271
+ ::File.join(@data_dir, LOCKS_DIR_NAME, "#{name}#{LOCK_FILE_SUFFIX}")
224
272
  end
225
273
 
226
274
  def default_cache_dir
227
275
  require "simple_xdg"
228
- ::File.join(::SimpleXDG.new.cache_home, "git-cache", FORMAT_VERSION)
276
+ ::File.join(::SimpleXDG.new.cache_home, "git-cache")
229
277
  end
230
278
 
231
279
  def git(dir, cmd, error_message: nil)
232
- result = @exec.exec(["git"] + cmd, chdir: dir)
280
+ result = @exec.exec(["git"] + GIT_CONFIG_ARGS + cmd, chdir: dir)
233
281
  if !result.success? && error_message
234
282
  raise ::GitCache::Error.new(error_message, result)
235
283
  end
236
284
  result
237
285
  end
238
286
 
239
- def ensure_repo_base_dir(remote)
240
- dir = repo_base_dir_for(remote)
287
+ # Removes a directory from the cache.
288
+ #
289
+ # The directory is first renamed out of the way, which is atomic and thus
290
+ # unaffected by any concurrent writes happening inside it, so the cache
291
+ # entry is gone as far as any client is concerned as soon as this returns.
292
+ # Only then is the renamed copy deleted, best effort. Anything left behind
293
+ # is invisible to clients and is swept up by later removals.
294
+ #
295
+ # Raises {GitCache::Error} if the directory could not be removed at all.
296
+ #
297
+ def remove_dir(dir)
298
+ return unless ::File.exist?(dir)
299
+ parent = ::File.dirname(dir)
300
+ begin
301
+ ::File.rename(dir, ::File.join(parent, "#{TRASH_DIR_PREFIX}#{::SecureRandom.hex(8)}"))
302
+ rescue ::SystemCallError
303
+ # Some filesystems (notably on Windows) refuse to rename a directory
304
+ # that has open handles under it. Fall back to deleting in place.
305
+ unless remove_dir_in_place(dir)
306
+ raise ::GitCache::Error, "Unable to remove directory: #{dir}"
307
+ end
308
+ end
309
+ sweep_trash(parent)
310
+ end
311
+
312
+ # Deletes a directory in place, retrying because a concurrent writer can
313
+ # defeat a single pass. Returns whether the directory is gone.
314
+ #
315
+ def remove_dir_in_place(dir, attempts: 3)
316
+ attempts.times do
317
+ chmod_recursive("u+w", dir)
318
+ ::FileUtils.rm_rf(dir)
319
+ return true unless ::File.exist?(dir)
320
+ end
321
+ false
322
+ end
323
+
324
+ # Recursive chmod that tolerates entries disappearing while it runs. The
325
+ # force: option of FileUtils.chmod_R covers only the chmod of each entry,
326
+ # not the traversal that finds them, so a concurrent writer removing a
327
+ # directory mid-walk raises out of chmod_R despite it.
328
+ #
329
+ def chmod_recursive(mode, dir)
330
+ ::FileUtils.chmod_R(mode, dir, force: true)
331
+ rescue ::SystemCallError
332
+ nil
333
+ end
334
+
335
+ # Deletes any leftover trash directories in the given directory. Failures
336
+ # are ignored; the leftovers are harmless and we can try again next time.
337
+ #
338
+ def sweep_trash(dir)
339
+ ::Dir.children(dir).each do |child|
340
+ remove_dir_in_place(::File.join(dir, child)) if child.start_with?(TRASH_DIR_PREFIX)
341
+ end
342
+ rescue ::SystemCallError
343
+ nil
344
+ end
345
+
346
+ def ensure_cache_subdir(dir)
241
347
  ::FileUtils.mkdir_p(dir)
242
- dir
348
+ rescue ::SystemCallError => e
349
+ message = "Unable to create git cache directory #{dir}: #{e.message}"
350
+ message += ". Set XDG_CACHE_HOME to a writable directory." if @using_default_cache_dir
351
+ raise Error, message
243
352
  end
244
353
 
245
- def lock_repo(dir, remote = nil, timestamp = nil)
246
- lock_path = ::File.join(dir, LOCK_FILE_NAME)
354
+ # Takes an exclusive lock on the given repo for the duration of the block,
355
+ # and returns the value of the block. Takes the remote_dir_name of a remote.
356
+ #
357
+ # The lock file lives outside the repo's base dir, so that removing the
358
+ # base dir does not also remove the lock. Lock files must never be deleted:
359
+ # a flock belongs to an inode, so if the file were deleted, a newcomer
360
+ # would create a new one and "acquire" it while an older client still
361
+ # holds the lock on the old one.
362
+ #
363
+ def flock_repo(name)
364
+ lock_path = repo_lock_path_for(name)
365
+ ensure_cache_subdir(::File.dirname(lock_path))
247
366
  ::File.open(lock_path, ::File::RDWR | ::File::CREAT) do |file|
248
367
  file.flock(::File::LOCK_EX)
249
- file.rewind
250
- repo_lock = RepoLock.new(file, remote, timestamp)
368
+ yield
369
+ end
370
+ end
371
+
372
+ # Takes an exclusive lock on the given repo, and yields its state as a
373
+ # {RepoState}, writing the state back afterward if it was modified. Returns
374
+ # the value of the block. Takes the remote_dir_name of a remote.
375
+ #
376
+ # If create is true, creates the repo's base dir if it does not exist.
377
+ # Otherwise, if the base dir does not exist (e.g. because it was removed
378
+ # while we were waiting for the lock), returns nil without yielding.
379
+ #
380
+ def lock_repo(name, remote = nil, timestamp = nil, create: false)
381
+ flock_repo(name) do
382
+ dir = repo_base_dir_for(name)
383
+ if create
384
+ ensure_cache_subdir(dir)
385
+ elsif !::File.directory?(dir)
386
+ next nil
387
+ end
388
+ state_path = ::File.join(dir, STATE_FILE_NAME)
389
+ content = ::File.file?(state_path) ? ::File.read(state_path) : ""
390
+ repo_state = RepoState.new(content, remote, timestamp)
391
+ completed = false
251
392
  begin
252
- yield repo_lock
393
+ result = yield repo_state
394
+ completed = true
395
+ result
253
396
  ensure
254
- if repo_lock.modified?
255
- file.rewind
256
- file.truncate(0)
257
- repo_lock.dump(file)
397
+ if repo_state.modified?
398
+ begin
399
+ write_state(state_path, repo_state)
400
+ rescue ::StandardError
401
+ # If the block failed, let its error propagate rather than this
402
+ # one. The atomic write leaves the previous state intact.
403
+ raise if completed
404
+ end
258
405
  end
259
406
  end
260
407
  end
261
408
  end
262
409
 
410
+ # Writes the repo state to a temp file and renames it into place, so a
411
+ # failure partway through the write leaves the previous state intact. Must
412
+ # be called while holding the repo's lock.
413
+ #
414
+ def write_state(state_path, repo_state)
415
+ temp_path = "#{state_path}.tmp-#{::SecureRandom.hex(8)}"
416
+ ::File.write(temp_path, repo_state.dump)
417
+ ::File.rename(temp_path, state_path)
418
+ ensure
419
+ ::FileUtils.rm_f(temp_path)
420
+ end
421
+
263
422
  def ensure_repo(dir, remote)
264
423
  repo_dir = ::File.join(dir, REPO_DIR_NAME)
265
424
  ::FileUtils.mkdir_p(repo_dir)
266
425
  result = git(repo_dir, ["remote", "get-url", "origin"])
267
426
  unless result.success? && result.captured_out.strip == remote
268
- ::FileUtils.chmod_R("u+w", repo_dir, force: true)
269
- ::FileUtils.rm_rf(repo_dir)
427
+ remove_dir(repo_dir)
270
428
  ::FileUtils.mkdir_p(repo_dir)
271
429
  git(repo_dir, ["init"],
272
430
  error_message: "Unable to initialize git repository")
@@ -275,20 +433,20 @@ class GitCache
275
433
  end
276
434
  end
277
435
 
278
- def ensure_commit(dir, commit, repo_lock, update = false)
436
+ def ensure_commit(dir, commit, repo_state, update = false)
279
437
  local_commit = "git-cache/#{commit}"
280
438
  repo_dir = ::File.join(dir, REPO_DIR_NAME)
281
439
  is_sha = ::GitCache.valid_sha?(commit)
282
- update = repo_lock.ref_stale?(commit, update) unless is_sha
440
+ update = repo_state.ref_stale?(commit, update) unless is_sha
283
441
  if (update && !is_sha) || !commit_exists?(repo_dir, local_commit)
284
442
  git(repo_dir, ["fetch", "--depth=1", "--force", "origin", "#{commit}:#{local_commit}"],
285
443
  error_message: "Unable to fetch commit: #{commit}")
286
- repo_lock.update_ref!(commit)
444
+ repo_state.update_ref!(commit)
287
445
  end
288
446
  result = git(repo_dir, ["rev-parse", local_commit],
289
447
  error_message: "Unable to retrieve commit: #{local_commit}")
290
448
  sha = result.captured_out.strip
291
- repo_lock.access_ref!(commit, sha)
449
+ repo_state.access_ref!(commit, sha)
292
450
  sha
293
451
  end
294
452
 
@@ -297,28 +455,28 @@ class GitCache
297
455
  result.success? && result.captured_out.strip == "commit"
298
456
  end
299
457
 
300
- def ensure_source(dir, sha, path, repo_lock)
458
+ def ensure_source(dir, sha, path, repo_state)
301
459
  repo_path = ::File.join(dir, REPO_DIR_NAME)
302
460
  source_path = ::File.join(dir, sha)
303
461
  result =
304
- if repo_lock.source_exists?(sha, path)
462
+ if repo_state.source_exists?(sha, path)
305
463
  ::GitCache.safe_join(source_path, path)
306
464
  else
307
- ::FileUtils.chmod_R("u+w", source_path, force: true)
465
+ chmod_recursive("u+w", source_path)
308
466
  begin
309
467
  copy_from_repo(repo_path, source_path, sha, path)
310
468
  ensure
311
- ::FileUtils.chmod_R("a-w", source_path, force: true) unless ::GitCache.sources_writable?
469
+ chmod_recursive("a-w", source_path) unless ::GitCache.sources_writable?
312
470
  end
313
471
  end
314
- repo_lock.access_source!(sha, path)
472
+ repo_state.access_source!(sha, path)
315
473
  result
316
474
  end
317
475
 
318
- def copy_files(dir, sha, path, repo_lock, into)
476
+ def copy_files(dir, sha, path, repo_state, into)
319
477
  repo_path = ::File.join(dir, REPO_DIR_NAME)
320
478
  result = copy_from_repo(repo_path, into, sha, path)
321
- repo_lock.access_repo!
479
+ repo_state.access_repo!
322
480
  result
323
481
  end
324
482
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: git_cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Azuma
@@ -54,15 +54,15 @@ files:
54
54
  - lib/git_cache.rb
55
55
  - lib/git_cache/error.rb
56
56
  - lib/git_cache/repo_info.rb
57
- - lib/git_cache/repo_lock.rb
57
+ - lib/git_cache/repo_state.rb
58
58
  - lib/git_cache/version.rb
59
59
  homepage: https://github.com/dazuma/git_cache
60
60
  licenses:
61
61
  - MIT
62
62
  metadata:
63
63
  bug_tracker_uri: https://github.com/dazuma/git_cache/issues
64
- changelog_uri: https://rubydoc.info/gems/git_cache/0.1.1/file/CHANGELOG.md
65
- documentation_uri: https://rubydoc.info/gems/git_cache/0.1.1
64
+ changelog_uri: https://rubydoc.info/gems/git_cache/0.2.0/file/CHANGELOG.md
65
+ documentation_uri: https://rubydoc.info/gems/git_cache/0.2.0
66
66
  homepage_uri: https://github.com/dazuma/git_cache
67
67
  rdoc_options: []
68
68
  require_paths:
@@ -78,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
78
78
  - !ruby/object:Gem::Version
79
79
  version: '0'
80
80
  requirements: []
81
- rubygems_version: 4.0.6
81
+ rubygems_version: 4.0.20
82
82
  specification_version: 4
83
83
  summary: A local file system cache of data from git repositories.
84
84
  test_files: []