geist 0.1.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.
- data/.gitignore +3 -0
- data/.yardopts +4 -0
- data/Changelog.md +18 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +20 -0
- data/LICENSE +25 -0
- data/README.md +61 -0
- data/Rakefile +38 -0
- data/geist.gemspec +27 -0
- data/lib/geist.rb +153 -0
- data/lib/geist/version.rb +11 -0
- metadata +123 -0
data/.gitignore
ADDED
data/.yardopts
ADDED
data/Changelog.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## Version 0.1.0
|
4
|
+
|
5
|
+
**September 22<sup>nd</sup>, 2011**
|
6
|
+
|
7
|
+
* Converted the Codebrawl contest entry into a gem
|
8
|
+
|
9
|
+
See the [Git history](https://github.com/koraktor/metior/commits/0.1.0) for
|
10
|
+
version 0.1.0
|
11
|
+
|
12
|
+
## Codebrawl contest entry
|
13
|
+
|
14
|
+
**September 1<sup>st</sup>, 2011**
|
15
|
+
|
16
|
+
See the
|
17
|
+
["Key/value stores" contest](http://codebrawl.com/contests/key-value-stores)
|
18
|
+
on Codebrawl to see the initial code
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
geist (0.1.0)
|
5
|
+
open4 (~> 1.1.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: http://rubygems.org/
|
9
|
+
specs:
|
10
|
+
open4 (1.1.0)
|
11
|
+
rake (0.9.2)
|
12
|
+
yard (0.7.2)
|
13
|
+
|
14
|
+
PLATFORMS
|
15
|
+
ruby
|
16
|
+
|
17
|
+
DEPENDENCIES
|
18
|
+
geist!
|
19
|
+
rake (~> 0.9.2)
|
20
|
+
yard (~> 0.7.2)
|
data/LICENSE
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
Copyright (c) 2011, Sebastian Staudt
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without modification,
|
5
|
+
are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
* Redistributions of source code must retain the above copyright notice,
|
8
|
+
this list of conditions and the following disclaimer.
|
9
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
11
|
+
and/or other materials provided with the distribution.
|
12
|
+
* Neither the name of the author nor the names of its contributors
|
13
|
+
may be used to endorse or promote products derived from this software
|
14
|
+
without specific prior written permission.
|
15
|
+
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
17
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
18
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
19
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
20
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
21
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
22
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
23
|
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
24
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
25
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
Geist
|
2
|
+
=====
|
3
|
+
|
4
|
+
Geist is a Git-backed key-value store written in Ruby.
|
5
|
+
|
6
|
+
## Usage
|
7
|
+
|
8
|
+
require 'geist'
|
9
|
+
|
10
|
+
g = Geist.new '~/some/storage/path'
|
11
|
+
g.set :foo, 'bar'
|
12
|
+
g.set :baz, 123
|
13
|
+
g.set :name => 'geist', :platform => 'ruby'
|
14
|
+
|
15
|
+
g.keys #=> ["foo", "name", "platform"]
|
16
|
+
g.get :foo #=> "bar"
|
17
|
+
g.get :baz #=> 123
|
18
|
+
g.get :name, :platform #=> ["geist", "ruby"]
|
19
|
+
|
20
|
+
g.delete :baz
|
21
|
+
g.get :baz #=> nil
|
22
|
+
|
23
|
+
## Internals
|
24
|
+
|
25
|
+
To be honest, the introduction was an outright fabrication. Geist is just a
|
26
|
+
Ruby API to misuse Git as a simple key-value store. Git itself is a pretty good
|
27
|
+
key-value store used to preserve blob (file), tree (directory), commit and tag
|
28
|
+
objects. A key-value store used to store versioned file histories in general.
|
29
|
+
|
30
|
+
Geist instead uses some low-level Git commands to store arbitrary Ruby objects
|
31
|
+
in a Git repository. The Ruby objects to store as values will be marshalled
|
32
|
+
into Git blob objects. These objects are referenced with lightweight Git tags
|
33
|
+
named by the given key.
|
34
|
+
|
35
|
+
Git will not double store duplicate values. Instead, the different key tags
|
36
|
+
will refer the same Git object.
|
37
|
+
|
38
|
+
## Caveats
|
39
|
+
|
40
|
+
As Geist uses Git tags as keys, only objects with a working `#to_s` method can
|
41
|
+
be used as keys. Additionally, based on Git's [ref naming rules][1], Geist
|
42
|
+
rejects keys that can't be used as Git tag names, e.g. containing non-printable
|
43
|
+
characters or backslashes.
|
44
|
+
|
45
|
+
## History
|
46
|
+
|
47
|
+
Geist was an idea for the Codebrawl contest ["Key/value stores"][2] and made it
|
48
|
+
to a honorable 6th place out of 18 contestants with a final score of 3.6.
|
49
|
+
|
50
|
+
## License
|
51
|
+
|
52
|
+
This code is free software; you can redistribute it and/or modify it under the
|
53
|
+
terms of the new BSD License. A copy of this license can be found in the
|
54
|
+
LICENSE file.
|
55
|
+
|
56
|
+
## Credits
|
57
|
+
|
58
|
+
* Sebastian Staudt – koraktor(at)gmail.com
|
59
|
+
|
60
|
+
[1]: http://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
|
61
|
+
[2]: http://codebrawl.com/contests/key-value-stores
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
# This code is free software; you can redistribute it and/or modify it under
|
4
|
+
# the terms of the new BSD License.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2011, Sebastian Staudt
|
7
|
+
|
8
|
+
require 'rubygems/package_task'
|
9
|
+
|
10
|
+
# Rake tasks for building the gem
|
11
|
+
spec = Gem::Specification.load('geist.gemspec')
|
12
|
+
Gem::PackageTask.new(spec) do |pkg|
|
13
|
+
end
|
14
|
+
|
15
|
+
# Check if YARD is installed
|
16
|
+
begin
|
17
|
+
require 'yard'
|
18
|
+
|
19
|
+
# Create a rake task `:doc to build the documentation using YARD
|
20
|
+
YARD::Rake::YardocTask.new do |yardoc|
|
21
|
+
yardoc.name = 'doc'
|
22
|
+
yardoc.files = [ 'lib/**/*.rb', 'Changelog.md', 'LICENSE', 'README.md' ]
|
23
|
+
yardoc.options = [ '--markup', 'markdown', '--private', '--title', 'Geist — API Documentation' ]
|
24
|
+
end
|
25
|
+
rescue LoadError
|
26
|
+
# Create a rake task `:doc` to show that YARD is not installed
|
27
|
+
desc 'Generate YARD Documentation (not available)'
|
28
|
+
task :doc do
|
29
|
+
$stderr.puts 'You need YARD to build the documentation. Install it using `gem install yard`.'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Task for cleaning documentation and package directories
|
34
|
+
desc 'Clean documentation and package directories'
|
35
|
+
task :clean do
|
36
|
+
FileUtils.rm_rf 'doc'
|
37
|
+
FileUtils.rm_rf 'pkg'
|
38
|
+
end
|
data/geist.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# This code is free software; you can redistribute it and/or modify it under
|
2
|
+
# the terms of the new BSD License.
|
3
|
+
#
|
4
|
+
# Copyright (c) 2011, Sebastian Staudt
|
5
|
+
|
6
|
+
require File.expand_path(File.dirname(__FILE__) + '/lib/geist/version')
|
7
|
+
|
8
|
+
Gem::Specification.new do |s|
|
9
|
+
s.name = 'geist'
|
10
|
+
s.version = Geist::VERSION
|
11
|
+
s.platform = Gem::Platform::RUBY
|
12
|
+
s.authors = [ 'Sebastian Staudt' ]
|
13
|
+
s.email = [ 'koraktor@gmail.com' ]
|
14
|
+
s.homepage = 'https://github.com/koraktor/geist'
|
15
|
+
s.summary = 'A Git-backed key-value store'
|
16
|
+
s.description = 'Geist is a Git-backed key-value store that stores Ruby objects into a Git repository.'
|
17
|
+
|
18
|
+
s.add_dependency 'open4', '~> 1.1.0'
|
19
|
+
|
20
|
+
s.add_development_dependency 'rake', '~> 0.9.2'
|
21
|
+
s.add_development_dependency 'yard', '~> 0.7.2'
|
22
|
+
|
23
|
+
s.requirements = [ 'git >= 1.6' ]
|
24
|
+
|
25
|
+
s.files = `git ls-files`.split("\n")
|
26
|
+
s.require_paths = [ 'lib' ]
|
27
|
+
end
|
data/lib/geist.rb
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
# This code is free software; you can redistribute it and/or modify it under
|
2
|
+
# the terms of the new BSD License.
|
3
|
+
#
|
4
|
+
# Copyright (c) 2011, Sebastian Staudt
|
5
|
+
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
require 'open4'
|
9
|
+
|
10
|
+
# Geist is a Git-backed key-value store written in Ruby
|
11
|
+
#
|
12
|
+
# @author Sebastian Staudt
|
13
|
+
class Geist
|
14
|
+
|
15
|
+
# Create a new Geist instance storing into the given Git repository
|
16
|
+
#
|
17
|
+
# @param [String] path The path of the Git repository to store objects into
|
18
|
+
# @raise [RuntimeError] if the directory is not empty and not a Git
|
19
|
+
# repository
|
20
|
+
def initialize(path)
|
21
|
+
@path = File.expand_path path
|
22
|
+
|
23
|
+
if !File.exist? @path
|
24
|
+
FileUtils.mkdir_p @path
|
25
|
+
cmd 'init'
|
26
|
+
elsif !cmd('ls-files').success?
|
27
|
+
raise "#{@path} is not a Git repository."
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Delete the objects with the given keys from the storage
|
32
|
+
#
|
33
|
+
# @param [#to_s, ...] keys One or more keys where the corresponding values
|
34
|
+
# should be removed from the storage
|
35
|
+
# @return [Boolean] `true` if the objects have been removed from the
|
36
|
+
# repository
|
37
|
+
# @todo This does not really remove the objects from the repository but only
|
38
|
+
# the tags. This should also trigger Git's garbage collection to remove
|
39
|
+
# the objects completely.
|
40
|
+
def delete(*keys)
|
41
|
+
success = true
|
42
|
+
|
43
|
+
keys.each do |key|
|
44
|
+
if id_for(key).nil?
|
45
|
+
success = false
|
46
|
+
else
|
47
|
+
cmd "tag -d '#{key}'"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
success
|
52
|
+
end
|
53
|
+
|
54
|
+
# Retrieve the objects with the given keys from the storage
|
55
|
+
#
|
56
|
+
# @param [#to_s, ...] keys One or more keys where the corresponding values
|
57
|
+
# should be loaded from the storage
|
58
|
+
# @return [Object, Array<Object>] One or more objects that belong to the
|
59
|
+
# given keys
|
60
|
+
def get(*keys)
|
61
|
+
return nil if keys.empty?
|
62
|
+
|
63
|
+
values = []
|
64
|
+
keys.each do |key|
|
65
|
+
value = nil
|
66
|
+
status = cmd "show '#{key}'" do |stdin, stdout|
|
67
|
+
if select [stdout]
|
68
|
+
blob = stdout.read.strip
|
69
|
+
value = Marshal.load blob unless blob.empty?
|
70
|
+
end
|
71
|
+
end
|
72
|
+
values << (status.success? ? value : nil)
|
73
|
+
end
|
74
|
+
|
75
|
+
keys.size == 1 ? values.first : values
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns all keys which have objects available in the storage
|
79
|
+
#
|
80
|
+
# @return [Array<String>] The keys available in the storage
|
81
|
+
def keys
|
82
|
+
keys = nil
|
83
|
+
cmd 'tag -l' do |stdin, stdout|
|
84
|
+
keys = stdout.lines.to_a.map(&:strip) if select [stdout]
|
85
|
+
end
|
86
|
+
keys
|
87
|
+
end
|
88
|
+
|
89
|
+
# Saves one ore more values into the storage
|
90
|
+
#
|
91
|
+
# @param [#to_s, Hash<#to_s, Object>] keys The key to use to store the value
|
92
|
+
# or a hash of multiple keys and values to store
|
93
|
+
# @param [Object] value The object to store as a value (only if a single key
|
94
|
+
# is given)
|
95
|
+
def set(keys, value = nil)
|
96
|
+
keys = { keys => value } unless keys.is_a? Hash
|
97
|
+
|
98
|
+
keys.each do |key, val|
|
99
|
+
if key.to_s.match(/(?:[\s^~:?*\[\\]|\.\.|@\{|(?:\/|\.|\.lock)$|^$)/)
|
100
|
+
warn "Warning: Invalid key '#{key}'"
|
101
|
+
return
|
102
|
+
end
|
103
|
+
|
104
|
+
delete key unless id_for(key).nil?
|
105
|
+
|
106
|
+
id = nil
|
107
|
+
cmd 'hash-object --stdin -w' do |stdin, stdout|
|
108
|
+
stdin.write Marshal.dump(val)
|
109
|
+
stdin.close
|
110
|
+
id = stdout.read.strip if select [stdout]
|
111
|
+
end
|
112
|
+
|
113
|
+
cmd "tag -f '#{key}' #{id}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
# Execute a Git command in the path of the Git repository of the current
|
120
|
+
# Geist instance
|
121
|
+
#
|
122
|
+
# @param [String] git_cmd The Git command to execute
|
123
|
+
# @param [Proc] block A block that can be used to read and write to and from
|
124
|
+
# the Git process' STDOUT and STDIN.
|
125
|
+
# @return [Process::Status] The exit status of the Git process
|
126
|
+
def cmd(git_cmd, &block)
|
127
|
+
cmd = "git --git-dir #{@path} #{git_cmd}"
|
128
|
+
status = Open4::popen4 cmd do |pid, stdin, stdout, stderr|
|
129
|
+
block.call stdin, stdout if block_given?
|
130
|
+
|
131
|
+
stdin.close unless stdin.closed?
|
132
|
+
stdout.close
|
133
|
+
stderr.close
|
134
|
+
end
|
135
|
+
status
|
136
|
+
end
|
137
|
+
|
138
|
+
# Returns the Git SHA ID of the blob where the value for the given key is
|
139
|
+
# stored
|
140
|
+
#
|
141
|
+
# @param [String, #to_s] key The key for which the Git SHA ID should be
|
142
|
+
# returned
|
143
|
+
# @return [String, nil] The SHA ID of the value's Git blob or `nil` if no
|
144
|
+
# such object exists
|
145
|
+
def id_for(key)
|
146
|
+
id = nil
|
147
|
+
status = cmd "rev-parse '#{key}'" do |stdin, stdout|
|
148
|
+
id = stdout.read.strip if select [stdout]
|
149
|
+
end
|
150
|
+
status.success? ? id : nil
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# This code is free software; you can redistribute it and/or modify it under
|
2
|
+
# the terms of the new BSD License.
|
3
|
+
#
|
4
|
+
# Copyright (c) 2011, Sebastian Staudt
|
5
|
+
|
6
|
+
class Geist
|
7
|
+
|
8
|
+
# This is the current version of the Geist gem
|
9
|
+
VERSION = '0.1.0' unless const_defined? :VERSION
|
10
|
+
|
11
|
+
end
|
metadata
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: geist
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Sebastian Staudt
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-09-22 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: open4
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 19
|
29
|
+
segments:
|
30
|
+
- 1
|
31
|
+
- 1
|
32
|
+
- 0
|
33
|
+
version: 1.1.0
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rake
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ~>
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 63
|
45
|
+
segments:
|
46
|
+
- 0
|
47
|
+
- 9
|
48
|
+
- 2
|
49
|
+
version: 0.9.2
|
50
|
+
type: :development
|
51
|
+
version_requirements: *id002
|
52
|
+
- !ruby/object:Gem::Dependency
|
53
|
+
name: yard
|
54
|
+
prerelease: false
|
55
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ~>
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
hash: 7
|
61
|
+
segments:
|
62
|
+
- 0
|
63
|
+
- 7
|
64
|
+
- 2
|
65
|
+
version: 0.7.2
|
66
|
+
type: :development
|
67
|
+
version_requirements: *id003
|
68
|
+
description: Geist is a Git-backed key-value store that stores Ruby objects into a Git repository.
|
69
|
+
email:
|
70
|
+
- koraktor@gmail.com
|
71
|
+
executables: []
|
72
|
+
|
73
|
+
extensions: []
|
74
|
+
|
75
|
+
extra_rdoc_files: []
|
76
|
+
|
77
|
+
files:
|
78
|
+
- .gitignore
|
79
|
+
- .yardopts
|
80
|
+
- Changelog.md
|
81
|
+
- Gemfile
|
82
|
+
- Gemfile.lock
|
83
|
+
- LICENSE
|
84
|
+
- README.md
|
85
|
+
- Rakefile
|
86
|
+
- geist.gemspec
|
87
|
+
- lib/geist.rb
|
88
|
+
- lib/geist/version.rb
|
89
|
+
homepage: https://github.com/koraktor/geist
|
90
|
+
licenses: []
|
91
|
+
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
hash: 3
|
103
|
+
segments:
|
104
|
+
- 0
|
105
|
+
version: "0"
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
|
+
none: false
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
hash: 3
|
112
|
+
segments:
|
113
|
+
- 0
|
114
|
+
version: "0"
|
115
|
+
requirements:
|
116
|
+
- git >= 1.6
|
117
|
+
rubyforge_project:
|
118
|
+
rubygems_version: 1.8.10
|
119
|
+
signing_key:
|
120
|
+
specification_version: 3
|
121
|
+
summary: A Git-backed key-value store
|
122
|
+
test_files: []
|
123
|
+
|