git_cache 0.0.0 → 0.1.1
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/.yardopts +11 -0
- data/CHANGELOG.md +9 -0
- data/LICENSE.md +21 -0
- data/README.md +66 -7
- data/lib/git_cache/error.rb +28 -0
- data/lib/git_cache/repo_info.rb +256 -0
- data/lib/git_cache/repo_lock.rb +200 -0
- data/lib/git_cache/version.rb +9 -0
- data/lib/git_cache.rb +433 -7
- metadata +54 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d3b7101169665dc9115fad2454928336b6ef83cca3b1db38a8ae6acef81062b1
|
|
4
|
+
data.tar.gz: f58df6572f0e3511ef30e4e7f1b0843e54ee59b157edb77d84f268b8a4bc0135
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0be87372ec841eb2288330508ce7bd5aaa61cb7f1332b78ff4866f63c071534204309dfb2bc4417bb47f093901dc22063cf1ba5c84d9f0f0391addf660fa0b91
|
|
7
|
+
data.tar.gz: 45fa8853c29cc760cd59f3054a39d43bf13d72b5fc9f2afcb4cdf5460a3b30b96a212754d6c878d08d418986cf2858a1108c1c68e5b191b3a437ed76e0d775b7
|
data/.yardopts
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# License
|
|
2
|
+
|
|
3
|
+
Copyright 2026 Daniel Azuma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
20
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
21
|
+
IN THE SOFTWARE.
|
data/README.md
CHANGED
|
@@ -1,9 +1,68 @@
|
|
|
1
|
-
#
|
|
1
|
+
# GitCache
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
The `GitCache` class provides cached access to remote git data. Given a remote
|
|
4
|
+
repository, a path, and a commit, it makes the files from that repository
|
|
5
|
+
available in the local file system. Access is cached, so repeated requests for
|
|
6
|
+
the same commit and path in the same repo do not make additional network calls.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
`
|
|
8
|
+
## Getting started
|
|
9
|
+
|
|
10
|
+
Install `GitCache` via the
|
|
11
|
+
[git_cache gem](https://rubygems.org/gems/git_cache).
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
% gem install git_cache
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
or add it to your Gemfile:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem "git_cache"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
To use the service, instantiate `GitCache`, and call the `get` method to access
|
|
24
|
+
files from a repository, pulling them from the remote if necessary:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
require "git_cache"
|
|
28
|
+
git_cache = GitCache.new
|
|
29
|
+
readme_path = git_cache.get("https://github.com/dazuma/git_cache.git",
|
|
30
|
+
path: "README.md")
|
|
31
|
+
puts File.read(readme_path)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Contributing
|
|
35
|
+
|
|
36
|
+
Development is done in GitHub at https://github.com/dazuma/git_cache.
|
|
37
|
+
|
|
38
|
+
* To file issues: https://github.com/dazuma/git_cache/issues.
|
|
39
|
+
* For questions and discussion, please do not file an issue. Instead, use the
|
|
40
|
+
discussions feature: https://github.com/dazuma/git_cache/discussions.
|
|
41
|
+
* Pull requests are welcome, but in general please open an issue first before
|
|
42
|
+
contributing significant changes.
|
|
43
|
+
|
|
44
|
+
The library uses [toys](https://dazuma.github.io/toys) for testing and CI. To
|
|
45
|
+
run the test suite, `gem install toys` and then run `toys ci`. You can also run
|
|
46
|
+
unit tests, rubocop, and build tests independently.
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
Copyright 2026 Daniel Azuma
|
|
51
|
+
|
|
52
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
53
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
54
|
+
in the Software without restriction, including without limitation the rights
|
|
55
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
56
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
57
|
+
furnished to do so, subject to the following conditions:
|
|
58
|
+
|
|
59
|
+
The above copyright notice and this permission notice shall be included in
|
|
60
|
+
all copies or substantial portions of the Software.
|
|
61
|
+
|
|
62
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
63
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
64
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
65
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
66
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
67
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
68
|
+
IN THE SOFTWARE.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class GitCache
|
|
4
|
+
##
|
|
5
|
+
# GitCache encountered a failure
|
|
6
|
+
#
|
|
7
|
+
class Error < ::StandardError
|
|
8
|
+
##
|
|
9
|
+
# Create a GitCache::Error.
|
|
10
|
+
#
|
|
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.
|
|
15
|
+
#
|
|
16
|
+
def initialize(message, result)
|
|
17
|
+
super(message)
|
|
18
|
+
@exec_result = result
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# @return [::ExecService::Result] The result of a git command
|
|
23
|
+
# execution, or `nil` if this error was not due to a git command
|
|
24
|
+
# error.
|
|
25
|
+
#
|
|
26
|
+
attr_reader :exec_result
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class GitCache
|
|
4
|
+
##
|
|
5
|
+
# Information about a remote git repository in the cache.
|
|
6
|
+
#
|
|
7
|
+
# This object is returned from {GitCache#repo_info}.
|
|
8
|
+
#
|
|
9
|
+
class RepoInfo
|
|
10
|
+
include ::Comparable
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
# The base directory of this git repository's cache entry. This
|
|
14
|
+
# directory contains all cached data related to this repo. Deleting it
|
|
15
|
+
# effectively removes the repo from the cache.
|
|
16
|
+
#
|
|
17
|
+
# @return [String]
|
|
18
|
+
#
|
|
19
|
+
attr_reader :base_dir
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# The git remote, usually a file system path or URL.
|
|
23
|
+
#
|
|
24
|
+
# @return [String]
|
|
25
|
+
#
|
|
26
|
+
attr_reader :remote
|
|
27
|
+
|
|
28
|
+
##
|
|
29
|
+
# The last time any cached data from this repo was accessed, or `nil`
|
|
30
|
+
# if the information is unavailable.
|
|
31
|
+
#
|
|
32
|
+
# @return [Time,nil]
|
|
33
|
+
#
|
|
34
|
+
attr_reader :last_accessed
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
# A list of git refs (branches, tags, shas) that have been accessed
|
|
38
|
+
# from this repo.
|
|
39
|
+
#
|
|
40
|
+
# @param ref [String,nil] If provided, return only entries matching
|
|
41
|
+
# this ref name. If omitted, return all entries.
|
|
42
|
+
# @return [Array<RefInfo>]
|
|
43
|
+
#
|
|
44
|
+
def refs(ref: nil)
|
|
45
|
+
return @refs.dup if ref.nil?
|
|
46
|
+
@refs.find_all { |elem| elem.ref == ref }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
##
|
|
50
|
+
# A list of shared source files and directories accessed for this repo.
|
|
51
|
+
#
|
|
52
|
+
# @param sha [String,nil] If provided, return only entries matching
|
|
53
|
+
# this SHA. If omitted, entries for all SHAs are included.
|
|
54
|
+
# @param git_path [String,nil] If provided, return only entries
|
|
55
|
+
# matching this git path. If omitted, entries for all paths are
|
|
56
|
+
# included.
|
|
57
|
+
# @return [Array<SourceInfo>]
|
|
58
|
+
#
|
|
59
|
+
def sources(sha: nil, git_path: nil)
|
|
60
|
+
return @sources.dup if sha.nil? && git_path.nil?
|
|
61
|
+
@sources.find_all do |elem|
|
|
62
|
+
(sha.nil? || elem.sha == sha) &&
|
|
63
|
+
(git_path.nil? || elem.git_path == git_path)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
##
|
|
68
|
+
# Convert this RepoInfo to a hash suitable for JSON output
|
|
69
|
+
#
|
|
70
|
+
# @return [Hash]
|
|
71
|
+
#
|
|
72
|
+
def to_h
|
|
73
|
+
result = {
|
|
74
|
+
"remote" => remote,
|
|
75
|
+
"base_dir" => base_dir,
|
|
76
|
+
}
|
|
77
|
+
result["last_accessed"] = last_accessed.to_i if last_accessed
|
|
78
|
+
result["refs"] = refs.map(&:to_h)
|
|
79
|
+
result["sources"] = sources.map(&:to_h)
|
|
80
|
+
result
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# Comparison function
|
|
85
|
+
#
|
|
86
|
+
# @param other [RepoInfo]
|
|
87
|
+
# @return [Integer]
|
|
88
|
+
#
|
|
89
|
+
def <=>(other)
|
|
90
|
+
remote <=> other.remote
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
##
|
|
94
|
+
# @private
|
|
95
|
+
#
|
|
96
|
+
def initialize(base_dir, data)
|
|
97
|
+
@base_dir = base_dir
|
|
98
|
+
@remote = data["remote"]
|
|
99
|
+
accessed = data["accessed"]
|
|
100
|
+
@last_accessed = accessed ? ::Time.at(accessed).utc : nil
|
|
101
|
+
@refs = (data["refs"] || {}).map { |ref, ref_data| RefInfo.new(ref, ref_data) }
|
|
102
|
+
@sources = (data["sources"] || {}).flat_map do |sha, sha_data|
|
|
103
|
+
sha_data.map do |path, path_data|
|
|
104
|
+
SourceInfo.new(base_dir, sha, path, path_data)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
@refs.sort!
|
|
108
|
+
@sources.sort!
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
##
|
|
113
|
+
# Information about a git ref used in a cache.
|
|
114
|
+
#
|
|
115
|
+
class RefInfo
|
|
116
|
+
include ::Comparable
|
|
117
|
+
|
|
118
|
+
##
|
|
119
|
+
# The git ref
|
|
120
|
+
#
|
|
121
|
+
# @return [String]
|
|
122
|
+
#
|
|
123
|
+
attr_reader :ref
|
|
124
|
+
|
|
125
|
+
##
|
|
126
|
+
# The git sha last associated with the ref
|
|
127
|
+
#
|
|
128
|
+
# @return [String]
|
|
129
|
+
#
|
|
130
|
+
attr_reader :sha
|
|
131
|
+
|
|
132
|
+
##
|
|
133
|
+
# The timestamp when this ref was last accessed
|
|
134
|
+
#
|
|
135
|
+
# @return [Time,nil]
|
|
136
|
+
#
|
|
137
|
+
attr_reader :last_accessed
|
|
138
|
+
|
|
139
|
+
##
|
|
140
|
+
# The timestamp when this ref was last updated
|
|
141
|
+
#
|
|
142
|
+
# @return [Time,nil]
|
|
143
|
+
#
|
|
144
|
+
attr_reader :last_updated
|
|
145
|
+
|
|
146
|
+
##
|
|
147
|
+
# Convert this RefInfo to a hash suitable for JSON output
|
|
148
|
+
#
|
|
149
|
+
# @return [Hash]
|
|
150
|
+
#
|
|
151
|
+
def to_h
|
|
152
|
+
result = {
|
|
153
|
+
"ref" => ref,
|
|
154
|
+
"sha" => sha,
|
|
155
|
+
}
|
|
156
|
+
result["last_accessed"] = last_accessed.to_i if last_accessed
|
|
157
|
+
result["last_updated"] = last_updated.to_i if last_updated
|
|
158
|
+
result
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
##
|
|
162
|
+
# Comparison function
|
|
163
|
+
#
|
|
164
|
+
# @param other [RefInfo]
|
|
165
|
+
# @return [Integer]
|
|
166
|
+
#
|
|
167
|
+
def <=>(other)
|
|
168
|
+
ref <=> other.ref
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
##
|
|
172
|
+
# @private
|
|
173
|
+
#
|
|
174
|
+
def initialize(ref, ref_data)
|
|
175
|
+
@ref = ref
|
|
176
|
+
@sha = ref_data["sha"]
|
|
177
|
+
@last_accessed = ref_data["accessed"]
|
|
178
|
+
@last_accessed = ::Time.at(@last_accessed).utc if @last_accessed
|
|
179
|
+
@last_updated = ref_data["updated"]
|
|
180
|
+
@last_updated = ::Time.at(@last_updated).utc if @last_updated
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
##
|
|
185
|
+
# Information about shared source files provided from the cache.
|
|
186
|
+
#
|
|
187
|
+
class SourceInfo
|
|
188
|
+
include ::Comparable
|
|
189
|
+
|
|
190
|
+
##
|
|
191
|
+
# The git sha the source comes from
|
|
192
|
+
#
|
|
193
|
+
# @return [String]
|
|
194
|
+
#
|
|
195
|
+
attr_reader :sha
|
|
196
|
+
|
|
197
|
+
##
|
|
198
|
+
# The path within the git repo
|
|
199
|
+
#
|
|
200
|
+
# @return [String]
|
|
201
|
+
#
|
|
202
|
+
attr_reader :git_path
|
|
203
|
+
|
|
204
|
+
##
|
|
205
|
+
# The path to the source file or directory
|
|
206
|
+
#
|
|
207
|
+
# @return [String]
|
|
208
|
+
#
|
|
209
|
+
attr_reader :source
|
|
210
|
+
|
|
211
|
+
##
|
|
212
|
+
# The timestamp when this ref was last accessed
|
|
213
|
+
#
|
|
214
|
+
# @return [Time,nil]
|
|
215
|
+
#
|
|
216
|
+
attr_reader :last_accessed
|
|
217
|
+
|
|
218
|
+
##
|
|
219
|
+
# Convert this SourceInfo to a hash suitable for JSON output
|
|
220
|
+
#
|
|
221
|
+
# @return [Hash]
|
|
222
|
+
#
|
|
223
|
+
def to_h
|
|
224
|
+
result = {
|
|
225
|
+
"sha" => sha,
|
|
226
|
+
"git_path" => git_path,
|
|
227
|
+
"source" => source,
|
|
228
|
+
}
|
|
229
|
+
result["last_accessed"] = last_accessed.to_i if last_accessed
|
|
230
|
+
result
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
##
|
|
234
|
+
# Comparison function
|
|
235
|
+
#
|
|
236
|
+
# @param other [SourceInfo]
|
|
237
|
+
# @return [Integer]
|
|
238
|
+
#
|
|
239
|
+
def <=>(other)
|
|
240
|
+
result = sha <=> other.sha
|
|
241
|
+
result.zero? ? git_path <=> other.git_path : result
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
##
|
|
245
|
+
# @private
|
|
246
|
+
#
|
|
247
|
+
def initialize(base_dir, sha, git_path, path_data)
|
|
248
|
+
@sha = sha
|
|
249
|
+
@git_path = git_path
|
|
250
|
+
root_dir = ::File.join(base_dir, sha)
|
|
251
|
+
@source = ::GitCache.safe_join(root_dir, git_path)
|
|
252
|
+
@last_accessed = path_data["accessed"]
|
|
253
|
+
@last_accessed = @last_accessed ? ::Time.at(@last_accessed).utc : nil
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class GitCache
|
|
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.
|
|
8
|
+
#
|
|
9
|
+
# This object represents the state of the repo, and is made available to
|
|
10
|
+
# the block passed to lock_repo. It has the following schema:
|
|
11
|
+
#
|
|
12
|
+
# remote: (String) # the remote url
|
|
13
|
+
# accessed: (Integer) # last accessed timestamp
|
|
14
|
+
# refs:
|
|
15
|
+
# (String): # git ref
|
|
16
|
+
# sha: (String) # resolved sha
|
|
17
|
+
# updated: (Integer) # last updated timestamp
|
|
18
|
+
# accessed: (Integer) # last accessed timestamp
|
|
19
|
+
# sources:
|
|
20
|
+
# (String): # sha of the shared source
|
|
21
|
+
# (String): # path populated
|
|
22
|
+
# accessed: (Integer) # last accessed timestamp
|
|
23
|
+
#
|
|
24
|
+
# @private
|
|
25
|
+
#
|
|
26
|
+
class RepoLock
|
|
27
|
+
##
|
|
28
|
+
# @private
|
|
29
|
+
#
|
|
30
|
+
def initialize(io, remote, timestamp)
|
|
31
|
+
@data = ::JSON.parse(io.read) rescue {} # rubocop:disable Style/RescueModifier
|
|
32
|
+
@data["remote"] ||= remote
|
|
33
|
+
@data["refs"] ||= {}
|
|
34
|
+
@data["sources"] ||= {}
|
|
35
|
+
@modified = false
|
|
36
|
+
@timestamp = timestamp || ::Time.now.to_i
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
##
|
|
40
|
+
# @private
|
|
41
|
+
#
|
|
42
|
+
attr_reader :data
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
# @private
|
|
46
|
+
#
|
|
47
|
+
def modified?
|
|
48
|
+
@modified
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
##
|
|
52
|
+
# @private
|
|
53
|
+
#
|
|
54
|
+
def dump(io)
|
|
55
|
+
::JSON.dump(@data, io)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
##
|
|
59
|
+
# @private
|
|
60
|
+
#
|
|
61
|
+
def remote
|
|
62
|
+
@data["remote"]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
##
|
|
66
|
+
# @private
|
|
67
|
+
#
|
|
68
|
+
def refs
|
|
69
|
+
@data["refs"].keys
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
##
|
|
73
|
+
# @private
|
|
74
|
+
#
|
|
75
|
+
def lookup_ref(ref)
|
|
76
|
+
return ref if ::GitCache.valid_sha?(ref)
|
|
77
|
+
@data["refs"][ref]&.fetch("sha", nil)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
##
|
|
81
|
+
# @private
|
|
82
|
+
#
|
|
83
|
+
def ref_data(ref)
|
|
84
|
+
@data["refs"][ref]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
##
|
|
88
|
+
# @private
|
|
89
|
+
#
|
|
90
|
+
def ref_stale?(ref, age)
|
|
91
|
+
ref_info = @data["refs"][ref]
|
|
92
|
+
last_updated = ref_info ? ref_info.fetch("updated", 0) : 0
|
|
93
|
+
return true if last_updated.zero?
|
|
94
|
+
return age unless age.is_a?(::Numeric)
|
|
95
|
+
@timestamp >= last_updated + age
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
##
|
|
99
|
+
# @private
|
|
100
|
+
#
|
|
101
|
+
def source_exists?(sha, path = nil)
|
|
102
|
+
sha_info = @data["sources"][sha]
|
|
103
|
+
return false if sha_info.nil?
|
|
104
|
+
return true if path.nil?
|
|
105
|
+
sha_info.key?(path) || sha_info.key?(".") ||
|
|
106
|
+
sha_info.keys.any? { |existing_path| path.start_with?("#{existing_path}/") }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
##
|
|
110
|
+
# @private
|
|
111
|
+
#
|
|
112
|
+
def source_data(sha, path)
|
|
113
|
+
@data["sources"][sha]&.fetch(path, nil)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
##
|
|
117
|
+
# @private
|
|
118
|
+
#
|
|
119
|
+
def find_sources(paths: nil, shas: nil)
|
|
120
|
+
results = []
|
|
121
|
+
@data["sources"].each do |sha, sha_data|
|
|
122
|
+
next unless shas.nil? || shas.include?(sha)
|
|
123
|
+
sha_data.each_key do |path|
|
|
124
|
+
next unless paths.nil? || paths.include?(path)
|
|
125
|
+
results << [sha, path]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
results
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
##
|
|
132
|
+
# @private
|
|
133
|
+
#
|
|
134
|
+
def access_repo!
|
|
135
|
+
is_first = !@data.key?("accessed")
|
|
136
|
+
@data["accessed"] = @timestamp
|
|
137
|
+
@modified = true
|
|
138
|
+
is_first
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
##
|
|
142
|
+
# @private
|
|
143
|
+
#
|
|
144
|
+
def access_ref!(ref, sha)
|
|
145
|
+
ref_info = @data["refs"][ref] ||= {}
|
|
146
|
+
ref_info["sha"] = sha
|
|
147
|
+
is_first = !ref_info.key?("accessed")
|
|
148
|
+
ref_info["accessed"] = @timestamp
|
|
149
|
+
@modified = true
|
|
150
|
+
is_first
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
##
|
|
154
|
+
# @private
|
|
155
|
+
#
|
|
156
|
+
def update_ref!(ref)
|
|
157
|
+
ref_info = @data["refs"][ref] ||= {}
|
|
158
|
+
is_first = !ref_info.key?("updated")
|
|
159
|
+
ref_info["updated"] = @timestamp
|
|
160
|
+
@modified = true
|
|
161
|
+
is_first
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
##
|
|
165
|
+
# @private
|
|
166
|
+
#
|
|
167
|
+
def delete_ref!(ref)
|
|
168
|
+
ref_data = @data["refs"].delete(ref)
|
|
169
|
+
@modified = true if ref_data
|
|
170
|
+
ref_data
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
##
|
|
174
|
+
# @private
|
|
175
|
+
#
|
|
176
|
+
def access_source!(sha, path)
|
|
177
|
+
@data["accessed"] = @timestamp
|
|
178
|
+
source_info = @data["sources"][sha] ||= {}
|
|
179
|
+
path_info = source_info[path] ||= {}
|
|
180
|
+
is_first = !path_info.key?("accessed")
|
|
181
|
+
path_info["accessed"] = @timestamp
|
|
182
|
+
@modified = true
|
|
183
|
+
is_first
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
##
|
|
187
|
+
# @private
|
|
188
|
+
#
|
|
189
|
+
def delete_source!(sha, path)
|
|
190
|
+
sha_data = @data["sources"][sha]
|
|
191
|
+
return nil if sha_data.nil?
|
|
192
|
+
source_data = sha_data.delete(path)
|
|
193
|
+
if source_data
|
|
194
|
+
@modified = true
|
|
195
|
+
@data["sources"].delete(sha) if sha_data.empty?
|
|
196
|
+
end
|
|
197
|
+
source_data
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
data/lib/git_cache.rb
CHANGED
|
@@ -1,8 +1,434 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "git_cache/error"
|
|
4
|
+
require "git_cache/repo_info"
|
|
5
|
+
require "git_cache/repo_lock"
|
|
6
|
+
|
|
7
|
+
##
|
|
8
|
+
# This object provides cached access to remote git data. Given a remote
|
|
9
|
+
# repository, a path, and a commit, it makes the files available in the
|
|
10
|
+
# local filesystem. Access is cached, so repeated requests for the same
|
|
11
|
+
# commit and path in the same repo do not hit the remote repository again.
|
|
1
12
|
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
13
|
+
class GitCache
|
|
14
|
+
##
|
|
15
|
+
# Access a git cache.
|
|
16
|
+
#
|
|
17
|
+
# @param cache_dir [String] The path to the cache directory. Defaults to
|
|
18
|
+
# a specific directory in the user's XDG cache.
|
|
19
|
+
#
|
|
20
|
+
def initialize(cache_dir: nil)
|
|
21
|
+
require "digest"
|
|
22
|
+
require "fileutils"
|
|
23
|
+
require "json"
|
|
24
|
+
require "exec_service"
|
|
25
|
+
@cache_dir = ::File.expand_path(cache_dir || default_cache_dir)
|
|
26
|
+
@exec = ::ExecService.new(out: :capture, err: :capture)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
##
|
|
30
|
+
# The cache directory.
|
|
31
|
+
#
|
|
32
|
+
# @return [String]
|
|
33
|
+
#
|
|
34
|
+
attr_reader :cache_dir
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
# Get the given git-based files from the git cache, loading from the
|
|
38
|
+
# remote repo if necessary.
|
|
39
|
+
#
|
|
40
|
+
# The resulting files are either copied into a directory you provide in
|
|
41
|
+
# the `:into` parameter, or populated into a _shared_ source directory if
|
|
42
|
+
# you omit the `:into` parameter. In the latter case, it is important
|
|
43
|
+
# that you do not modify the returned files or directories, nor add or
|
|
44
|
+
# remove any files from the directories returned, to avoid confusing
|
|
45
|
+
# callers that could be given the same directory. If you need to make any
|
|
46
|
+
# modifications to the returned files, use `:into` to provide your own
|
|
47
|
+
# private directory.
|
|
48
|
+
#
|
|
49
|
+
# @param remote [String] The URL of the git repo. Required.
|
|
50
|
+
# @param path [String] The path to the file or directory within the repo.
|
|
51
|
+
# Optional. Defaults to the entire repo.
|
|
52
|
+
# @param commit [String] The commit reference, which may be a SHA or any
|
|
53
|
+
# git ref such as a branch or tag. Optional. Defaults to `HEAD`.
|
|
54
|
+
# @param into [String] If provided, copies the specified files into the
|
|
55
|
+
# given directory path. If omitted or `nil`, populates and returns a
|
|
56
|
+
# shared source file or directory.
|
|
57
|
+
# @param update [boolean,Integer] Whether to update non-SHA commit
|
|
58
|
+
# references if they were previously loaded. This is useful, for
|
|
59
|
+
# example, if the commit is `HEAD` or a branch name. Pass `true` or
|
|
60
|
+
# `false` to specify whether to update, or an integer to update if
|
|
61
|
+
# last update was done at least that many seconds ago. Default is
|
|
62
|
+
# `false`.
|
|
63
|
+
# @param timestamp [Integer,nil] The timestamp for recording the access
|
|
64
|
+
# time and determining whether a resource is stale. Normally, you
|
|
65
|
+
# should leave this out and it will default to the current time.
|
|
66
|
+
#
|
|
67
|
+
# @return [String] The full path to the cached files. The returned path
|
|
68
|
+
# will correspond to the path given. For example, if you provide the
|
|
69
|
+
# path `Gemfile` representing a single file in the repository, the
|
|
70
|
+
# returned path will point directly to the cached copy of that file.
|
|
71
|
+
#
|
|
72
|
+
def get(remote, path: nil, commit: nil, into: nil, update: false, timestamp: nil)
|
|
73
|
+
path = ::GitCache.normalize_path(path)
|
|
74
|
+
commit ||= "HEAD"
|
|
75
|
+
timestamp ||= ::Time.now.to_i
|
|
76
|
+
dir = ensure_repo_base_dir(remote)
|
|
77
|
+
lock_repo(dir, remote, timestamp) do |repo_lock|
|
|
78
|
+
ensure_repo(dir, remote)
|
|
79
|
+
sha = ensure_commit(dir, commit, repo_lock, update)
|
|
80
|
+
if into
|
|
81
|
+
copy_files(dir, sha, path, repo_lock, into)
|
|
82
|
+
else
|
|
83
|
+
ensure_source(dir, sha, path, repo_lock)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
alias find get
|
|
88
|
+
|
|
89
|
+
##
|
|
90
|
+
# Returns an array of the known remote names.
|
|
91
|
+
#
|
|
92
|
+
# @return [Array<String>]
|
|
93
|
+
#
|
|
94
|
+
def remotes
|
|
95
|
+
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
|
|
104
|
+
end
|
|
105
|
+
result.sort
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
##
|
|
109
|
+
# Returns a {RepoInfo} describing the cache for the given remote, or
|
|
110
|
+
# `nil` if the given remote has never been cached.
|
|
111
|
+
#
|
|
112
|
+
# @param remote [String] Remote name for a repo
|
|
113
|
+
# @return [RepoInfo,nil]
|
|
114
|
+
#
|
|
115
|
+
def repo_info(remote)
|
|
116
|
+
dir = repo_base_dir_for(remote)
|
|
117
|
+
return nil unless ::File.directory?(dir)
|
|
118
|
+
lock_repo(dir, remote) do |repo_lock|
|
|
119
|
+
RepoInfo.new(dir, repo_lock.data)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
##
|
|
124
|
+
# Removes caches for the given repos, or all repos if specified.
|
|
125
|
+
#
|
|
126
|
+
# Removes all cache information for the specified repositories, including
|
|
127
|
+
# local clones and shared source directories. The next time these
|
|
128
|
+
# repositories are requested, they will be reloaded from the remote
|
|
129
|
+
# repository from scratch.
|
|
130
|
+
#
|
|
131
|
+
# Be careful not to remove repos that are currently in use by other
|
|
132
|
+
# GitCache clients.
|
|
133
|
+
#
|
|
134
|
+
# @param remotes [Array<String>,:all,nil] The remotes to remove. If set
|
|
135
|
+
# to :all or nil, removes all repos.
|
|
136
|
+
# @return [Array<String>] The remotes actually removed.
|
|
137
|
+
#
|
|
138
|
+
def remove_repos(remotes)
|
|
139
|
+
remotes = self.remotes if remotes.nil? || remotes == :all
|
|
140
|
+
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)
|
|
145
|
+
remote
|
|
146
|
+
end
|
|
147
|
+
end.compact.sort
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
##
|
|
151
|
+
# Remove records of the given refs (i.e. branches, tags, or `HEAD`) from
|
|
152
|
+
# the given repository's cache. The next time those refs are requested,
|
|
153
|
+
# they will be pulled from the remote repo.
|
|
154
|
+
#
|
|
155
|
+
# If you provide the `refs:` argument, only those refs are removed.
|
|
156
|
+
# Otherwise, all refs are removed.
|
|
157
|
+
#
|
|
158
|
+
# @param remote [String] The repository
|
|
159
|
+
# @param refs [Array<String>] The refs to remove. Optional.
|
|
160
|
+
# @return [Array<RefInfo>,nil] The refs actually forgotten, or `nil` if
|
|
161
|
+
# the given repo is not in the cache.
|
|
162
|
+
#
|
|
163
|
+
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
|
|
169
|
+
Array(refs).each do |ref|
|
|
170
|
+
ref_data = repo_lock.delete_ref!(ref)
|
|
171
|
+
results << RefInfo.new(ref, ref_data) if ref_data
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
results.sort
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
##
|
|
178
|
+
# Removes shared sources for the given cache. The next time a client
|
|
179
|
+
# requests them, the removed sources will be recopied from the repo.
|
|
180
|
+
#
|
|
181
|
+
# If you provide the `commits:` argument, only sources associated with
|
|
182
|
+
# those commits are removed. Otherwise, all sources are removed.
|
|
183
|
+
#
|
|
184
|
+
# Be careful not to remove sources that are currently in use by other
|
|
185
|
+
# GitCache clients.
|
|
186
|
+
#
|
|
187
|
+
# @param remote [String] The repository
|
|
188
|
+
# @param commits [Array<String>] Remove only the sources for the given
|
|
189
|
+
# commits. Optional.
|
|
190
|
+
# @return [Array<SourceInfo>,nil] The sources actually removed, or `nil`
|
|
191
|
+
# if the given repo is not in the cache.
|
|
192
|
+
#
|
|
193
|
+
def remove_sources(remote, commits: nil)
|
|
194
|
+
dir = repo_base_dir_for(remote)
|
|
195
|
+
return nil unless ::File.directory?(dir)
|
|
196
|
+
results = []
|
|
197
|
+
lock_repo(dir, remote) do |repo_lock|
|
|
198
|
+
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)
|
|
202
|
+
results << SourceInfo.new(dir, sha, path, data)
|
|
203
|
+
end
|
|
204
|
+
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)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
results.sort
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
private
|
|
216
|
+
|
|
217
|
+
FORMAT_VERSION = "v1"
|
|
218
|
+
REPO_DIR_NAME = "repo"
|
|
219
|
+
LOCK_FILE_NAME = "repo.lock"
|
|
220
|
+
private_constant :REPO_DIR_NAME, :LOCK_FILE_NAME, :FORMAT_VERSION
|
|
221
|
+
|
|
222
|
+
def repo_base_dir_for(remote)
|
|
223
|
+
::File.join(@cache_dir, ::GitCache.remote_dir_name(remote))
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def default_cache_dir
|
|
227
|
+
require "simple_xdg"
|
|
228
|
+
::File.join(::SimpleXDG.new.cache_home, "git-cache", FORMAT_VERSION)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def git(dir, cmd, error_message: nil)
|
|
232
|
+
result = @exec.exec(["git"] + cmd, chdir: dir)
|
|
233
|
+
if !result.success? && error_message
|
|
234
|
+
raise ::GitCache::Error.new(error_message, result)
|
|
235
|
+
end
|
|
236
|
+
result
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def ensure_repo_base_dir(remote)
|
|
240
|
+
dir = repo_base_dir_for(remote)
|
|
241
|
+
::FileUtils.mkdir_p(dir)
|
|
242
|
+
dir
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def lock_repo(dir, remote = nil, timestamp = nil)
|
|
246
|
+
lock_path = ::File.join(dir, LOCK_FILE_NAME)
|
|
247
|
+
::File.open(lock_path, ::File::RDWR | ::File::CREAT) do |file|
|
|
248
|
+
file.flock(::File::LOCK_EX)
|
|
249
|
+
file.rewind
|
|
250
|
+
repo_lock = RepoLock.new(file, remote, timestamp)
|
|
251
|
+
begin
|
|
252
|
+
yield repo_lock
|
|
253
|
+
ensure
|
|
254
|
+
if repo_lock.modified?
|
|
255
|
+
file.rewind
|
|
256
|
+
file.truncate(0)
|
|
257
|
+
repo_lock.dump(file)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def ensure_repo(dir, remote)
|
|
264
|
+
repo_dir = ::File.join(dir, REPO_DIR_NAME)
|
|
265
|
+
::FileUtils.mkdir_p(repo_dir)
|
|
266
|
+
result = git(repo_dir, ["remote", "get-url", "origin"])
|
|
267
|
+
unless result.success? && result.captured_out.strip == remote
|
|
268
|
+
::FileUtils.chmod_R("u+w", repo_dir, force: true)
|
|
269
|
+
::FileUtils.rm_rf(repo_dir)
|
|
270
|
+
::FileUtils.mkdir_p(repo_dir)
|
|
271
|
+
git(repo_dir, ["init"],
|
|
272
|
+
error_message: "Unable to initialize git repository")
|
|
273
|
+
git(repo_dir, ["remote", "add", "origin", remote],
|
|
274
|
+
error_message: "Unable to add git remote: #{remote}")
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def ensure_commit(dir, commit, repo_lock, update = false)
|
|
279
|
+
local_commit = "git-cache/#{commit}"
|
|
280
|
+
repo_dir = ::File.join(dir, REPO_DIR_NAME)
|
|
281
|
+
is_sha = ::GitCache.valid_sha?(commit)
|
|
282
|
+
update = repo_lock.ref_stale?(commit, update) unless is_sha
|
|
283
|
+
if (update && !is_sha) || !commit_exists?(repo_dir, local_commit)
|
|
284
|
+
git(repo_dir, ["fetch", "--depth=1", "--force", "origin", "#{commit}:#{local_commit}"],
|
|
285
|
+
error_message: "Unable to fetch commit: #{commit}")
|
|
286
|
+
repo_lock.update_ref!(commit)
|
|
287
|
+
end
|
|
288
|
+
result = git(repo_dir, ["rev-parse", local_commit],
|
|
289
|
+
error_message: "Unable to retrieve commit: #{local_commit}")
|
|
290
|
+
sha = result.captured_out.strip
|
|
291
|
+
repo_lock.access_ref!(commit, sha)
|
|
292
|
+
sha
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def commit_exists?(repo_dir, commit)
|
|
296
|
+
result = git(repo_dir, ["cat-file", "-t", commit])
|
|
297
|
+
result.success? && result.captured_out.strip == "commit"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def ensure_source(dir, sha, path, repo_lock)
|
|
301
|
+
repo_path = ::File.join(dir, REPO_DIR_NAME)
|
|
302
|
+
source_path = ::File.join(dir, sha)
|
|
303
|
+
result =
|
|
304
|
+
if repo_lock.source_exists?(sha, path)
|
|
305
|
+
::GitCache.safe_join(source_path, path)
|
|
306
|
+
else
|
|
307
|
+
::FileUtils.chmod_R("u+w", source_path, force: true)
|
|
308
|
+
begin
|
|
309
|
+
copy_from_repo(repo_path, source_path, sha, path)
|
|
310
|
+
ensure
|
|
311
|
+
::FileUtils.chmod_R("a-w", source_path, force: true) unless ::GitCache.sources_writable?
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
repo_lock.access_source!(sha, path)
|
|
315
|
+
result
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def copy_files(dir, sha, path, repo_lock, into)
|
|
319
|
+
repo_path = ::File.join(dir, REPO_DIR_NAME)
|
|
320
|
+
result = copy_from_repo(repo_path, into, sha, path)
|
|
321
|
+
repo_lock.access_repo!
|
|
322
|
+
result
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def copy_from_repo(repo_dir, into, sha, path)
|
|
326
|
+
git(repo_dir, ["switch", "--detach", sha],
|
|
327
|
+
error_message: "Unable to switch to SHA #{sha}")
|
|
328
|
+
repo_path = ::GitCache.safe_join(repo_dir, path)
|
|
329
|
+
unless ::File.exist?(repo_path)
|
|
330
|
+
raise Error, "Path #{path.inspect} does not exist at SHA #{sha}"
|
|
331
|
+
end
|
|
332
|
+
into_path = ::GitCache.safe_join(into, path)
|
|
333
|
+
if path == "."
|
|
334
|
+
::FileUtils.mkdir_p(into)
|
|
335
|
+
else
|
|
336
|
+
::FileUtils.mkdir_p(::File.dirname(into_path))
|
|
337
|
+
end
|
|
338
|
+
copy_recursive(repo_path, into_path, is_root: path == ".")
|
|
339
|
+
into_path
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def copy_recursive(from_path, to_path, is_root: false)
|
|
343
|
+
from_stat = safe_stat(from_path)
|
|
344
|
+
to_stat = safe_stat(to_path)
|
|
345
|
+
if to_stat && from_stat
|
|
346
|
+
if from_stat.directory? && to_stat.directory?
|
|
347
|
+
::Dir.children(from_path).each do |child|
|
|
348
|
+
next if child == ".git" && is_root
|
|
349
|
+
copy_recursive(::File.join(from_path, child), ::File.join(to_path, child))
|
|
350
|
+
end
|
|
351
|
+
else
|
|
352
|
+
::FileUtils.rm_rf(to_path)
|
|
353
|
+
::FileUtils.copy_entry(from_path, to_path)
|
|
354
|
+
end
|
|
355
|
+
elsif to_stat
|
|
356
|
+
::FileUtils.rm_rf(to_path)
|
|
357
|
+
elsif from_stat
|
|
358
|
+
::FileUtils.copy_entry(from_path, to_path)
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def safe_stat(path)
|
|
363
|
+
::File.lstat(path)
|
|
364
|
+
rescue ::SystemCallError
|
|
365
|
+
nil
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
class << self
|
|
369
|
+
##
|
|
370
|
+
# @private
|
|
371
|
+
# Returns whether shared source files are writable by default.
|
|
372
|
+
# Normally, shared sources are made read-only to protect them from being
|
|
373
|
+
# modified accidentally since multiple clients may be accessing them.
|
|
374
|
+
# However, you can disable this feature by setting the environment
|
|
375
|
+
# variable `GIT_CACHE_WRITABLE` to any non-empty value. This can be
|
|
376
|
+
# useful in environments that want to clean up temporary directories and
|
|
377
|
+
# are being hindered by read-only files.
|
|
378
|
+
#
|
|
379
|
+
# @return [boolean]
|
|
380
|
+
#
|
|
381
|
+
def sources_writable?
|
|
382
|
+
!::ENV["GIT_CACHE_WRITABLE"].to_s.empty?
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
##
|
|
386
|
+
# @private
|
|
387
|
+
# Whether a given ref is a valid SHA-1 or SHA-256
|
|
388
|
+
#
|
|
389
|
+
# @param ref [String]
|
|
390
|
+
# @return [boolean]
|
|
391
|
+
#
|
|
392
|
+
def valid_sha?(ref)
|
|
393
|
+
/^[0-9a-f]+$/.match?(ref) && [40, 64].include?(ref.size)
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
##
|
|
397
|
+
# @private
|
|
398
|
+
# Adds a path element to an existing path, handling the case where the
|
|
399
|
+
# new path element is ".".
|
|
400
|
+
#
|
|
401
|
+
# @param dir [String]
|
|
402
|
+
# @param path [String]
|
|
403
|
+
# @return [String]
|
|
404
|
+
#
|
|
405
|
+
def safe_join(dir, path)
|
|
406
|
+
path == "." ? dir : ::File.join(dir, path)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
##
|
|
410
|
+
# @private
|
|
411
|
+
#
|
|
412
|
+
def remote_dir_name(remote)
|
|
413
|
+
::Digest::MD5.hexdigest(remote)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
##
|
|
417
|
+
# @private
|
|
418
|
+
#
|
|
419
|
+
def normalize_path(orig_path)
|
|
420
|
+
segs = []
|
|
421
|
+
orig_segs = orig_path.to_s.sub(%r{^/+}, "").split(%r{/+})
|
|
422
|
+
orig_segs.each do |seg|
|
|
423
|
+
if seg == ".."
|
|
424
|
+
raise ::ArgumentError, "Path #{orig_path.inspect} references its parent" if segs.empty?
|
|
425
|
+
segs.pop
|
|
426
|
+
elsif seg != "."
|
|
427
|
+
segs.push(seg)
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
raise ::ArgumentError, "Path #{orig_path.inspect} reads .git directory" if segs.first == ".git"
|
|
431
|
+
segs.empty? ? "." : segs.join("/")
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
metadata
CHANGED
|
@@ -1,26 +1,69 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: git_cache
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- Daniel Azuma
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: exec_service
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: simple_xdg
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.1'
|
|
40
|
+
description: The GitCache class provides cached access to remote git data. Given a
|
|
41
|
+
remote repository, a path, and a commit, it makes the files from that repository
|
|
42
|
+
available in the local file system. Access is cached, so repeated requests for the
|
|
43
|
+
same commit and path in the same repo do not make additional network calls.
|
|
44
|
+
email:
|
|
45
|
+
- dazuma@gmail.com
|
|
16
46
|
executables: []
|
|
17
47
|
extensions: []
|
|
18
48
|
extra_rdoc_files: []
|
|
19
49
|
files:
|
|
50
|
+
- ".yardopts"
|
|
51
|
+
- CHANGELOG.md
|
|
52
|
+
- LICENSE.md
|
|
20
53
|
- README.md
|
|
21
54
|
- lib/git_cache.rb
|
|
22
|
-
|
|
23
|
-
|
|
55
|
+
- lib/git_cache/error.rb
|
|
56
|
+
- lib/git_cache/repo_info.rb
|
|
57
|
+
- lib/git_cache/repo_lock.rb
|
|
58
|
+
- lib/git_cache/version.rb
|
|
59
|
+
homepage: https://github.com/dazuma/git_cache
|
|
60
|
+
licenses:
|
|
61
|
+
- MIT
|
|
62
|
+
metadata:
|
|
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
|
|
66
|
+
homepage_uri: https://github.com/dazuma/git_cache
|
|
24
67
|
rdoc_options: []
|
|
25
68
|
require_paths:
|
|
26
69
|
- lib
|
|
@@ -28,7 +71,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
28
71
|
requirements:
|
|
29
72
|
- - ">="
|
|
30
73
|
- !ruby/object:Gem::Version
|
|
31
|
-
version: '
|
|
74
|
+
version: '2.7'
|
|
32
75
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
33
76
|
requirements:
|
|
34
77
|
- - ">="
|
|
@@ -37,5 +80,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
37
80
|
requirements: []
|
|
38
81
|
rubygems_version: 4.0.6
|
|
39
82
|
specification_version: 4
|
|
40
|
-
summary:
|
|
83
|
+
summary: A local file system cache of data from git repositories.
|
|
41
84
|
test_files: []
|