Skip to content

Instantly share code, notes, and snippets.

@mattsears
Created September 5, 2011 07:41
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mattsears/1194327 to your computer and use it in GitHub Desktop.
Save mattsears/1194327 to your computer and use it in GitHub Desktop.
Gittr: A Git key/value store

Gittr.rb

Git as a key-value store! Build with Grit, it supports SET, GET, KEYS, and DELETE operations. In addition, we can also get the change history of key/values.

And since it's Git, we can easily enhance it to include other awesome Git features such as branches, diffs, reverting, and more!

Example:

@store = Gittr.new(:repo => File.expand_path('..', __FILE__))
@store.clear  # Deletes all keys/values from the store

# SET
@store['lady'] = "gaga"

# GET
@store['lady'] #=> "gaga"

# KEYS
@store.keys  #=> ['lady']

# DELETE
@store.delete('lady') #=> 'gaga'

# LOG
@store.log('key')

# Produces:
[
 {"message"=>"all clear","committer"=>{"name"=>"Matt Sears", "email"=>"matt@mattsears.com"}, "committed_date"=>"..."},
 {"message"=>"set 'lady' ", "committer"=>{"name"=>"Matt Sears", "email"=>"matt@mattsears.com"}, "committed_date"=>"..."}
 {"message"=>"delete 'lady' ", "committer"=>{"name"=>"Matt Sears", "email"=>"matt@mattsears.com"}, "committed_date"=>"..."}
]
require 'yaml'
require 'grit'
class Gittr
def initialize(options = {})
@options = options
unless ::File.exists?(File.join(path,'.git'))
Grit::Repo.init(path)
end
end
# Add the value to the to the store
#
# Example
# @store.set('key', 'value')
#
# Returns nothing
def set(key, value)
save("set '#{key}'") do |index|
index.add(key_for(key), encode(value))
end
end
# Shortcut for #set
#
# Example:
# @store[key] = 'value'
#
def []=(key, value)
set(key, value)
end
# Retrieve the value for the given key with a default value
#
# Example:
# @store.get(key) #=> value
#
# Returns the object found in the repo matching the key
def get(key, value = nil, *)
if head && blob = head.commit.tree / key_for(key)
decode(blob.data)
end
end
# Shortcut for #get
#
# Example:
# @store['key'] #=> value
#
def [](key)
get(key)
end
# Returns an array of key names contained in store
#
# Example:
# @store.keys #=> ['key1', 'key2']
#
def keys
head.commit.tree.contents.map{|blob| deserialize(blob.name) }
end
# Deletes commits matching the given key
#
# Example:
# @store.delete('key')
#
# Returns nothing
def delete(key, *)
self[key].tap do
save("deleted #{key}") {|index| index.delete(key_for(key)) }
end
end
# Deletes all contents of the store
#
# Returns nothing
def clear
save("all clear") do |index|
if tree = index.current_tree
tree.contents.each do |entry|
index.delete(key_for(entry.name))
end
end
end
end
# The commit log for the given key
#
# Example:
# @store.log('key') #=> [{"message"=>"Updated key"...}]
#
# Returns Array of commit data
def log(key)
git.log(branch, key_for(key)).map{ |commit| commit.to_hash }
end
# Find the key if exists in the git repo
#
# Example:
# @store.key? 'key' #=> true
#
# Returns true if found; false if not found
def key?(key)
!(head && head.commit.tree / key_for(key)).nil?
end
private
# Format the given key so that it ensures it's git worthy
def key_for(key)
key.is_a?(String) ? key : serialize(key)
end
# Given the file path, return a new Grit::Repo if found
def git
@git ||= Grit::Repo.new(path)
end
# The git branch to use for this store
def branch
@options[:branch] || 'master'
end
# Checks out the branch on the repo
def head
git.get_head(branch)
end
# Commits the the value into the git repository with the given commit message
def save(message)
index = git.index
if head
commit = head.commit
index.current_tree = commit.tree
end
yield index
index.commit(message, :parents => Array(commit), :head => branch) if index.tree.any?
end
# Converts the value to yaml format
def encode(value)
value.to_yaml
end
# Loads value as a Yaml structure
def decode(value)
YAML.load(value)
end
# Convert value to byte stream. This allows keys to be objects too
def serialize(value)
Marshal.dump(value)
end
# Converts value back to an object.
def deserialize(value)
Marshal.restore(value) rescue value
end
# Given that repo path set in the options, return the expanded file path
def path(key = '')
@path ||= File.join(File.expand_path(@options[:repo]), key)
end
end
require 'minitest/autorun'
require File.join(File.dirname(__FILE__), 'gittr.rb')
describe Gittr do
@types = {
"String" => ["lady", "gaga"],
"Object" => [{:lady => :gaga}, {:gaga => :ohai}]
}
before do
@store = Gittr.new(:repo => File.expand_path('..', __FILE__))
@store.clear
end
@types.each do |type, (key, key2)|
it "writes String values to keys" do
@store[key] = "value"
@store[key].must_equal "value"
end
it "reads from keys" do
@store[key].must_be_nil
end
it "returns a list of keys" do
@store[key] = "value"
@store.keys.must_include(key)
end
it "guarantees that a different String value is retrieved" do
value = "value"
@store[key] = value
@store[key].wont_be_same_as(value)
end
it "writes Object values to keys" do
@store[key] = {:foo => :bar}
@store[key].must_equal({:foo => :bar})
end
it "guarantees that a different Object value is retrieved" do
value = {:foo => :bar}
@store[key] = value
@store[key].wont_be_same_as(:foo => :bar)
end
it "returns false from key? if a key is not available" do
@store.key?(key).must_equal false
end
it "returns true from key? if a key is available" do
@store[key] = "value"
@store.key?(key).must_equal true
end
it "removes and return an element with a key from the store via delete if it exists" do
@store[key] = "value"
@store.delete(key).must_equal "value"
@store.key?(key).must_equal false
end
it "returns nil from delete if an element for a key does not exist" do
@store.delete(key).must_be_nil
end
it "removes all keys from the store with clear" do
@store[key] = "value"
@store[key2] = "value2"
@store.clear
@store.key?(key).wont_equal true
@store.key?(key2).wont_equal true
end
it "does not run the block if the #{type} key is available" do
@store[key] = "value"
unaltered = "unaltered"
@store.get(key) { unaltered = "altered" }
unaltered.must_equal "unaltered"
end
it "stores #{key} values with #set" do
@store.set(key, "value")
@store[key].must_equal "value"
end
it "returns a list of commit history for the key" do
@store.log(key).wont_be_empty
end
end
end
@martinciu
Copy link

Nice one! A was thinking about creating same thing!

@jeffkreeftmeijer
Copy link

This is awesome, you should turn it into a gem after the contest is over! :D

@csirac2
Copy link

csirac2 commented Sep 19, 2011

Nifty :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment