git_keyvalue 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in git_keyvalue.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Alexis Gallagher
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # git_keyvalue
2
+
3
+ This gem provides a GET/PUT-style wrapper for a remote git repo.
4
+
5
+ In effect, it presents the repo as a key/value store, where every key
6
+ is a path (always interpreted relative to the repo's root) and every
7
+ key's value is just the contents of the file at that path.
8
+
9
+ You can directly get/put the string contents of files. Or you can use
10
+ getfile/putfile to copy files from/into the repo, which will be more
11
+ efficient for large binary files like videos or images.
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ gem 'git_keyvalue'
18
+
19
+ And then execute:
20
+
21
+ $ bundle
22
+
23
+ Or install it yourself as:
24
+
25
+ $ gem install git_keyvalue
26
+
27
+ ## Usage
28
+
29
+ An example:
30
+
31
+ $ require 'rubygems'
32
+ $ require 'bundler/setup
33
+ $ require 'git_keyvalue'
34
+ $ # make sure you have network and permissions access to the repo URL
35
+ $ # which can be any valid git URL (i.e.: http, ssh, git, or file schemes)
36
+ $ g = GitKeyvalue::KeyValueRepo.new('http://githuben.com/path/to/repo/')
37
+ $ g.get('metadata/file1.txt')
38
+ $ => 'I am the contents of metadata/file1.txt!'
39
+ $ g.put('metadata/file1.txt','new contents of metadata/file1.txt')
40
+ $ => nil
41
+ $ # remote repo has now been updated
42
+ $ g.put('metadata/file2.txt','contents for metadata/file2.txt')
43
+ $ => nil
44
+ $ # you just created a new file file2.txt, in metadata/, in the remote repo
45
+
46
+ ## Contributing
47
+
48
+ 1. Fork it
49
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
50
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
51
+ 4. Push to the branch (`git push origin my-new-feature`)
52
+ 5. Create new Pull Request
53
+
54
+ ## Compiling the docs
55
+
56
+ Do ``gem install yard`` to ensure you have yard installed.
57
+
58
+ Compile the docs from source docstrings with ``rake yard``.
59
+
60
+ Do ``yard server --reload`` to start a webserver where you can browse
61
+ the generated HTML.
62
+
63
+
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require "yard"
4
+ require "redcarpet" # needed for yard's markdown processing (?)
5
+
6
+ YARD::Rake::YardocTask.new do |t|
7
+ t.files = ['lib/**/*.rb']
8
+ t.options = ['--output-dir', 'doc/lib/']
9
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'git_keyvalue/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "git_keyvalue"
8
+ spec.version = GitKeyvalue::VERSION
9
+ spec.authors = ["Alexis Gallagher"]
10
+ spec.email = ["alexis@alexisgallagher.com"]
11
+ spec.description = %q{Treat a remote git repo as a simple key/value store}
12
+ spec.summary = %q{Treat a remote git repo as a simple key/value store}
13
+ spec.homepage = "https://github.com/algal/git_keyvalue"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.has_rdoc = 'yard'
22
+ spec.extra_rdoc_files = ['README.md']
23
+ spec.required_ruby_version = '>= 1.9.3'
24
+ spec.requirements = 'git (known good with v1.7.9.6)'
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.3"
27
+ spec.add_development_dependency "rake"
28
+ spec.add_development_dependency "yard"
29
+ spec.add_development_dependency "redcarpet"
30
+ end
@@ -0,0 +1,261 @@
1
+ require "git_keyvalue/version"
2
+
3
+ module GitKeyvalue
4
+ # known good with ruby 1.9.3, with git 1.7.9.6
5
+
6
+ require 'pathname'
7
+ require 'tmpdir'
8
+ require 'fileutils'
9
+
10
+ class KeyValueGitError < StandardError
11
+ end
12
+
13
+ ##
14
+ # Provides a GET/PUT-style interface for a git repo. In effect, it
15
+ # presents the repo as a key/value store, where the keys are file
16
+ # paths (relative to the repo's root) and the values are the contents
17
+ # of those files.
18
+ #
19
+ # == Requirements
20
+ # Known good with ruby 1.9.3 and git 1.7.9.6.
21
+ #
22
+ # == Performance & resource usage
23
+ #
24
+ # Not performant. Must clone the repo before performing any
25
+ # operations. Needs whatever disk space is required for a repo clone.
26
+ # Clears this space when the object is destroyed.
27
+ #
28
+ # == Object lifetime
29
+ #
30
+ # Stores the local repo in the OS's temporary directory. Therefore,
31
+ # you should not expect this object to remain valid across automated
32
+ # housekeeping events that might destroy this directory.
33
+ #
34
+ # == Footnote on shallow cloning
35
+ #
36
+ # Okay, technically, this does not clone the entire repo. For better
37
+ # performance it does a "shallow clone" of the repo, which grabs only
38
+ # the files necessary to represent the HEAD commit. Such a shallow
39
+ # clone is officially enough to enable GET operations, which read only
40
+ # those files anyway. However, according to the git-clone docs, the
41
+ # shallow clone is _not_ officially enough to enable git-push to
42
+ # update those files on the remote repo. However, this seems like a
43
+ # bug in the git-clone docs since, in reality, a shallow clone is
44
+ # enough and should be enough for pushing new commits, since a new
45
+ # commit only needs to reference its parent commit(s).
46
+ #
47
+ # The bottom line: by using shallow cloning for better perf, this
48
+ # class is relying on undocumented behavior in git-push. This works
49
+ # fine as of git version 1.7.9.6. I see no reason to expect this to
50
+ # break in the future, since this undocumented behavior follows
51
+ # directly from git's data model, which is stable. However, if it does
52
+ # break, and you want to switch to using the documented git behavior,
53
+ # then set USE_SHALLOW_CLONING to false.
54
+ #
55
+ class KeyValueRepo
56
+ private
57
+ # whether to git-clone only the HEAD commit of the remote repo
58
+ USE_SHALLOW_CLONING = true
59
+
60
+ # @return [Proc] proc which removes the temporary local clone of the repo
61
+ def self.make_finalizer(tmp_dir)
62
+ proc do
63
+ puts 'KeyValueRepo: Remove local repo clone in ' + tmp_dir
64
+ FileUtils.remove_entry_secure(tmp_dir)
65
+ end
66
+ end
67
+
68
+ ##
69
+ # Updates the local clone of the repo
70
+ # @raise [KeyValueGitError] if cannot pull the repo.
71
+ def update_local_repo
72
+ Dir.chdir(@path_to_repo) do
73
+ success = system('git','pull')
74
+ if not success
75
+ raise KeyValueGitError, 'Failed to pull updated version of the repo, even though it was cloned successfully. Aborting.'
76
+ end
77
+ end
78
+ end
79
+
80
+ ##
81
+ # Checks if path_in_repo points to a file existing in the repo.
82
+ #
83
+ # @param [String] path_in_repo
84
+ # @return [Boolean] whether
85
+ #
86
+ # Even if path_in_repo starts with /, it will be interpreted as
87
+ # relative to the repo's root.
88
+ def isFileExistingWithinRepo(path_in_repo)
89
+ abspath = Pathname.new(File.join(@path_to_repo,path_in_repo))
90
+ # see if the file exists and is a file
91
+ if abspath.file?
92
+ # and if it's within the repo
93
+ abspath.realpath.to_s.start_with?(Pathname.new(@path_to_repo).realpath.to_s)
94
+ else
95
+ false
96
+ end
97
+ end
98
+
99
+ ##
100
+ # Strips any initial / chars from +maybe_abspath+
101
+ #
102
+ # @param [String] maybe_abspath
103
+ # @return [String]
104
+ #
105
+ def blindly_relativize_path(maybe_abspath)
106
+ (maybe_abspath.split('').drop_while {|ch| ch=='/'}).join
107
+ end
108
+
109
+ ##
110
+ # Ensure a file exists and execute a GET-like operation, passed as a block.
111
+ #
112
+ # @param path_in_repo [String] relative path of a repo file
113
+ # @yieldparam abspath [String] absolute filesystem path for the block to GET
114
+ # @yieldreturn [Object,nil] result of GETting the file, or nil if the block returned its value through side-effects
115
+ # @return [Object,nil] the result returned by the block, or nil if the file does not exist
116
+ # @raise [KeyValueGitError] if cannot pull from the repo
117
+ # @raise [Exception] if the block raises an Exception
118
+ #
119
+ # Updates the local repo. Verifies the file exists at
120
+ # path_in_repo. If it does not exist or is outside of the repo,
121
+ # returns nil. Otherwise, returns the result of calling the block.
122
+ #
123
+ # This method will raise whatever the block raises
124
+ def outer_get(path_in_repo)
125
+ update_local_repo
126
+ if not isFileExistingWithinRepo(path_in_repo)
127
+ nil
128
+ else
129
+ abspath = Pathname.new(File.join(@path_to_repo,path_in_repo)).realpath.to_s
130
+ yield abspath
131
+ end
132
+ end
133
+
134
+ ##
135
+ # Prepares and executes a PUT-like operation, passed as a block
136
+ #
137
+ # @param path_in_repo [String] relative path of repo file
138
+ # @yield
139
+ # @return the result returned by the block
140
+ #
141
+ # @raise [KeyValueGitError] if can't pull or push the repo
142
+ # @raise [Exception] if the block raises an Exception
143
+ #
144
+ # Update the local repo, changes to its root directory, then calls
145
+ # the block to execute the PUT operation on the repo's working
146
+ # tree. Then commits and push that change to the remote repo.
147
+ def outer_put(path_in_repo)
148
+ update_local_repo
149
+ Dir.chdir(@path_to_repo) do
150
+
151
+ yield
152
+
153
+ # add and commit to repo
154
+ system('git','add',path_in_repo)
155
+ system('git','commit','-m','\'git-keyvalue: updating ' + path_in_repo + '\'')
156
+ success = system('git','push')
157
+ if not success
158
+ # restore local repo to a good state
159
+ system('git','clean','--force','-d')
160
+ # report the failure
161
+ raise KeyValueGitError, 'Failed to push commit with updated file. This could be because someone else pushed to the repository in the middle of this operation. If this is the problem, you should be able simply to re-try this operation. If the problem is deeper, you might create a fresh object before re-trying.'
162
+ end
163
+ end
164
+ end
165
+
166
+ public
167
+
168
+ # @return [String] URL of the remote git repo
169
+ attr_reader :repo_url
170
+ # @return [String] absolute filesystem path of the local repo
171
+ attr_reader :path_to_repo
172
+
173
+ ##
174
+ # Clones the remote repo, failing if it is invalid or inaccessible.
175
+ #
176
+ # @param repo_url [String] URL of a valid, network-accessible, permissions-accessible git repo
177
+ #
178
+ # As it clones the entire repo, this may take a long time if you are
179
+ # manipulating a large remote repo. Keeps the repo in the OS's
180
+ # temporary directory, so you should not expect this object to
181
+ # remain valid across automated cleanups of that temporary directory
182
+ # (which happen, for instance, typically at restart).
183
+ #
184
+ # @raise [KeyValueGitError] if unable to clone the repo
185
+ def initialize(repo_url)
186
+ @repo_url = repo_url
187
+ @path_to_repo = Dir.mktmpdir('KeyValueGitTempDir')
188
+ if USE_SHALLOW_CLONING
189
+ # experimental variant. uses undocumented behaviour of
190
+ # git-clone. This is because setting --depth 1 produces a
191
+ # shallow clone, which according to the docs does not let you
192
+ # git-push aftewards, but in reality should and does let you
193
+ # git-push. This is a bug in the git documentation.
194
+ success = system('git','clone','--depth','1',@repo_url,@path_to_repo)
195
+ else
196
+ # stable variant. uses documented behavior of git-clone
197
+ success = system('git','clone',@repo_url,@path_to_repo)
198
+ end
199
+ if not success
200
+ raise KeyValueGitError, 'Failed to initialize, because could not clone the remote repo: ' + repo_url + '. Please verify this is a valid git URL, and that any required network connection or login credentials are available.'
201
+ end
202
+ ObjectSpace.define_finalizer(self, self.class.make_finalizer(@path_to_repo))
203
+ end
204
+
205
+ ##
206
+ # Get contents of a file, or nil if it does not exist
207
+ #
208
+ # @param path_in_repo [String] relative path of repo file to get
209
+ # @return [String,nil] string contents of file, or nil if non-existent
210
+ #
211
+ def get(path_in_repo)
212
+ outer_get(path_in_repo) { |abspath| File.read(abspath) }
213
+ end
214
+
215
+ ##
216
+ # Copies the repo file at +path_in_repo+ to +dest_path+
217
+ #
218
+ # @param path_in_repo [String] relative path of repo file to get
219
+ # @param dest_path [String] path to which to copy the gotten file
220
+ #
221
+ # Does no validation regarding dest_path. If dest_path points to a
222
+ # file, it will overwrite that file. If it points to a directory, it
223
+ # will copy into that directory.
224
+ def getfile(path_in_repo, dest_path)
225
+ outer_get(path_in_repo) { |abspath| FileUtils.cp(abspath, dest_path) }
226
+ end
227
+
228
+ ##
229
+ # Sets the contents of the file at +path_in_repo+, creating it if necessary
230
+ #
231
+ # @param path_in_repo [String] relative path of repo file to add or update
232
+ # @param string_value [String] the new contents for the file at this path
233
+ #
234
+ def put(path_in_repo, string_value)
235
+ path_in_repo = blindly_relativize_path(path_in_repo)
236
+ outer_put(path_in_repo) {
237
+ # create parent directories if needed
238
+ FileUtils.mkdir_p(File.dirname(path_in_repo))
239
+ # write new file contents
240
+ File.open(path_in_repo,'w') { |f| f.write(string_value) }
241
+ }
242
+ end
243
+
244
+ ##
245
+ # Sets the contents of the file at path, creating it if necessary
246
+ #
247
+ # @param path_in_repo [String] relative path of repo file to add or update
248
+ # @param src_file_path [String] file to use for replacing +path_in_repo+
249
+ #
250
+ def putfile(path_in_repo, src_file_path)
251
+ path_in_repo = blindly_relativize_path(path_in_repo)
252
+ outer_put(path_in_repo) {
253
+ # create parent directories if needed
254
+ FileUtils.mkdir_p(File.dirname(path_in_repo))
255
+ # copy file at src_file_path into the path_in_repo
256
+ abspath = Pathname.new(File.join(@path_to_repo,path_in_repo)).to_s
257
+ FileUtils.cp(src_file_path, abspath)
258
+ }
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,3 @@
1
+ module GitKeyvalue
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: git_keyvalue
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alexis Gallagher
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-04-23 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.3'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.3'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: yard
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: redcarpet
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ description: Treat a remote git repo as a simple key/value store
79
+ email:
80
+ - alexis@alexisgallagher.com
81
+ executables: []
82
+ extensions: []
83
+ extra_rdoc_files:
84
+ - README.md
85
+ files:
86
+ - .gitignore
87
+ - Gemfile
88
+ - LICENSE.txt
89
+ - README.md
90
+ - Rakefile
91
+ - git_keyvalue.gemspec
92
+ - lib/git_keyvalue.rb
93
+ - lib/git_keyvalue/version.rb
94
+ homepage: https://github.com/algal/git_keyvalue
95
+ licenses:
96
+ - MIT
97
+ post_install_message:
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ! '>='
105
+ - !ruby/object:Gem::Version
106
+ version: 1.9.3
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ none: false
109
+ requirements:
110
+ - - ! '>='
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ segments:
114
+ - 0
115
+ hash: -728910074357862692
116
+ requirements:
117
+ - git (known good with v1.7.9.6)
118
+ rubyforge_project:
119
+ rubygems_version: 1.8.25
120
+ signing_key:
121
+ specification_version: 3
122
+ summary: Treat a remote git repo as a simple key/value store
123
+ test_files: []
124
+ has_rdoc: yard