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 +4 -4
- data/CHANGELOG.md +15 -0
- data/lib/git_cache/error.rb +4 -4
- data/lib/git_cache/{repo_lock.rb → repo_state.rb} +12 -9
- data/lib/git_cache/version.rb +1 -1
- data/lib/git_cache.rb +232 -74
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bf5b975bb07d2180af37c7d22c0823d9266adb7993485bbb4f5708ab3b769421
|
|
4
|
+
data.tar.gz: d2ff0b8e4b2e41ad1f19e36b81bab3ba6a9b08483a0495a1d270065f6e15473f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/git_cache/error.rb
CHANGED
|
@@ -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
|
|
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
|
|
6
|
-
#
|
|
7
|
-
# repo
|
|
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
|
|
27
|
+
class RepoState
|
|
27
28
|
##
|
|
28
29
|
# @private
|
|
29
30
|
#
|
|
30
|
-
def initialize(
|
|
31
|
-
@data = ::JSON.parse(
|
|
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
|
|
55
|
-
::JSON.dump(@data
|
|
57
|
+
def dump
|
|
58
|
+
::JSON.dump(@data)
|
|
56
59
|
end
|
|
57
60
|
|
|
58
61
|
##
|
data/lib/git_cache/version.rb
CHANGED
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/
|
|
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
|
-
|
|
77
|
-
|
|
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,
|
|
85
|
+
sha = ensure_commit(dir, commit, repo_state, update)
|
|
80
86
|
if into
|
|
81
|
-
copy_files(dir, sha, path,
|
|
87
|
+
copy_files(dir, sha, path, repo_state, into)
|
|
82
88
|
else
|
|
83
|
-
ensure_source(dir, sha, path,
|
|
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
|
-
|
|
97
|
-
::
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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(
|
|
119
|
-
RepoInfo.new(dir,
|
|
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
|
-
#
|
|
132
|
-
#
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
165
|
-
return nil unless ::File.directory?(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
refs =
|
|
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 =
|
|
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
|
-
|
|
206
|
+
name = ::GitCache.remote_dir_name(remote)
|
|
207
|
+
dir = repo_base_dir_for(name)
|
|
195
208
|
return nil unless ::File.directory?(dir)
|
|
196
|
-
|
|
197
|
-
|
|
209
|
+
lock_repo(name, remote) do |repo_state|
|
|
210
|
+
results = []
|
|
198
211
|
commits = nil if commits == :all
|
|
199
|
-
shas = Array(commits).map { |ref|
|
|
200
|
-
|
|
201
|
-
data =
|
|
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
|
|
206
|
-
|
|
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
|
-
|
|
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
|
-
|
|
220
|
-
private_constant :REPO_DIR_NAME, :LOCK_FILE_NAME, :FORMAT_VERSION
|
|
249
|
+
TRASH_DIR_PREFIX = ".trash-"
|
|
221
250
|
|
|
222
|
-
|
|
223
|
-
|
|
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"
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|
-
|
|
246
|
-
|
|
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
|
-
|
|
250
|
-
|
|
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
|
|
393
|
+
result = yield repo_state
|
|
394
|
+
completed = true
|
|
395
|
+
result
|
|
253
396
|
ensure
|
|
254
|
-
if
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
462
|
+
if repo_state.source_exists?(sha, path)
|
|
305
463
|
::GitCache.safe_join(source_path, path)
|
|
306
464
|
else
|
|
307
|
-
|
|
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
|
-
|
|
469
|
+
chmod_recursive("a-w", source_path) unless ::GitCache.sources_writable?
|
|
312
470
|
end
|
|
313
471
|
end
|
|
314
|
-
|
|
472
|
+
repo_state.access_source!(sha, path)
|
|
315
473
|
result
|
|
316
474
|
end
|
|
317
475
|
|
|
318
|
-
def copy_files(dir, sha, path,
|
|
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
|
-
|
|
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.
|
|
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/
|
|
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.
|
|
65
|
-
documentation_uri: https://rubydoc.info/gems/git_cache/0.
|
|
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.
|
|
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: []
|