Skip to content
Browse files

added license and readme

  • Loading branch information...
1 parent 4441c0e commit 511140ee131008b78b290dd0e6daca7e2622eb78 @georgi committed
Showing with 431 additions and 101 deletions.
  1. +18 −0 LICENSE
  2. +66 −0 README.md
  3. +230 −0 lib/git_store.rb
  4. +0 −101 lib/store.rb
  5. +117 −0 spec/git_store_spec.rb
View
18 LICENSE
@@ -0,0 +1,18 @@
+Copyright (c) 2008 Matthias Georgi <http://www.matthias-georgi.de>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
View
66 README.md
@@ -0,0 +1,66 @@
+GitStore - using Git as versioned data store in Ruby
+====================================================
+
+GitStore is a small Ruby library, providing an easy interface to the
+version control system [Git][1]. It aims to use Git as a versioned
+data store much like the well known PStore. Basically GitStore checks
+out the repository into a in-memory representation, which can be
+modified and finally committed. In this way your data is stored in a
+folder structure and can be checked out and examined, but the
+application may access the data in a convenient hash-like way.
+
+This library is based on [Grit][2], the main technology behind
+[GitHub][3].
+
+## Usage Example
+
+First thing you should do, is to initialize a new git repository.
+
+ git init
+
+Now you can instantiate a GitStore instance and store some data. The
+data will be serialized depending on the file extension. So for YAML
+storage you can use the 'yml' extension:
+
+ class WikiPage < Struct.new(:author, :title, :body); end
+ class User < Struct.new(:name); end
+
+ store = GitStore.new('.')
+
+ store['users/matthias.yml'] = User.new('Matthias')
+ store['pages/home.yml'] = WikiPage.new('matthias', 'Home', 'This is the home page...')
+
+ store.commit 'Added user and page'
+
+Note that direcories will be created automatically by using the path
+syntax. Same for multi arguments hash syntax:
+
+ store[config', 'wiki.yml'] = { 'name' => 'My Personal Wiki' }
+
+In this case the directory config is created automatically and
+the file wiki.yml contains be the YAML representation of the given Hash.
+
+## Iteration
+
+Iterating over the stored datat is one of the common use cases, so
+this one is really easy and scales well at the same time, if you user
+a clever directory structure:
+
+ store['pages/home.yml'] = WikiPage.new('matthias', 'Home', 'This is the home page...')
+ store['pages/about.yml'] = WikiPage.new('matthias', About', 'About this site...')
+ store['pages/links.yml'] = WikiPage.new('matthias', 'Links', 'Some useful links...')
+ store['config/wiki.yml'] = { 'name' => 'My Personal Wiki' }
+
+ store.each { |obj| ... } # yields all pages and the config hash
+ store['pages'].each { |page| ... } # yields only the pages
+
+## References
+
+John Wiegley already has done [something similar for Python][4]. His
+implementation has its own git interface, GitStore uses the wonderful
+[Grit][2] library.
+
+[1] http://git.or.cz/
+[2] http://github.com/mojombo/grit/tree/master
+[3] http://github.com/
+[4] http://www.newartisans.com/blog_files/git.versioned.data.store.php
View
230 lib/git_store.rb
@@ -0,0 +1,230 @@
+require 'grit'
+require 'erb'
+
+class GitStore
+
+ class DefaultHandler
+ def read(id, name, data)
+ data
+ end
+
+ def write(data)
+ data
+ end
+ end
+
+ class YAMLHandler
+ def read(id, name, data)
+ YAML.load(data)
+ end
+
+ def write(data)
+ data.to_yaml
+ end
+ end
+
+ class RubyHandler
+ def read(id, name, data)
+ Object.module_eval(data)
+ end
+ end
+
+ class ERBHandler
+ def read(id, name, data)
+ ERB.new(data)
+ end
+ end
+
+ Handler = {
+ 'yml' => YAMLHandler.new,
+ 'rhtml' => ERBHandler.new,
+ 'rxml' => ERBHandler.new,
+ 'rb' => RubyHandler.new
+ }
+
+ Handler.default = DefaultHandler.new
+
+ class Blob
+
+ attr_reader :id
+ attr_accessor :name
+
+ def initialize(*args)
+ if args.first.is_a?(Grit::Blob)
+ @blob = args.first
+ @id = @blob.id
+ @name = @blob.name
+ else
+ @name = args[0]
+ self.data = args[1]
+ end
+ end
+
+ def extname
+ File.extname(name)[1..-1]
+ end
+
+ def load(data)
+ @data = handler.read(id, name, data)
+ end
+
+ def handler
+ Handler[extname]
+ end
+
+ def data
+ @data or (@blob and load(@blob.data))
+ end
+
+ def data=(data)
+ @data = data
+ end
+
+ def to_s
+ if handler.respond_to?(:write)
+ handler.write(data)
+ else
+ @blob.data
+ end
+ end
+
+ end
+
+ class Tree
+ include Enumerable
+
+ attr_reader :id, :data
+ attr_accessor :name
+
+ def initialize(name = nil)
+ @data = {}
+ @name = name
+ end
+
+ def load(tree)
+ @id = tree.id
+ @name = tree.name
+ @data = tree.contents.inject({}) do |hash, file|
+ if file.is_a?(Grit::Tree)
+ hash[file.name] = (@data[file.name] || Tree.new).load(file)
+ else
+ hash[file.name] = Blob.new(file)
+ end
+ hash
+ end
+ self
+ end
+
+ def inspect
+ "#<GitStore::Tree #{@data.inspect}>"
+ end
+
+ def fetch(name)
+ name = name.to_s
+ entry = @data[name]
+ case entry
+ when Blob then entry.data
+ when Tree then entry
+ end
+ end
+
+ def store(name, value)
+ name = name.to_s
+ if value.is_a?(Tree)
+ value.name = name
+ @data[name] = value
+ else
+ @data[name] = Blob.new(name, value)
+ end
+ end
+
+ def has_key?(name)
+ @data.has_key?(name)
+ end
+
+ def [](*args)
+ args = args.first.to_s.split('/') if args.size == 1
+ args.inject(self) { |tree, key| tree.fetch(key) or return nil }
+ end
+
+ def []=(*args)
+ value = args.pop
+ args = args.first.to_s.split('/') if args.size == 1
+ tree = args[0..-2].to_a.inject(self) do |tree, key|
+ tree.has_key?(key) ? tree.fetch(key) : tree.store(key, Tree.new(key))
+ end
+ tree.store(args.last, value)
+ end
+
+ def delete(name)
+ @data.delete(name)
+ end
+
+ def each(&block)
+ @data.values.each do |entry|
+ case entry
+ when Blob then yield entry.data
+ when Tree then entry.each(&block)
+ end
+ end
+ end
+
+ def each_with_path(path = [], &block)
+ @data.each do |name, entry|
+ child_path = path + [name]
+ case entry
+ when Blob then yield entry, child_path.join('/')
+ when Tree then entry.each_with_path(child_path, &block)
+ end
+ end
+ end
+
+ def to_hash
+ @data.inject({}) do |hash, (name, entry)|
+ hash[name] = entry.is_a?(Tree) ? entry.to_hash : entry.to_s
+ hash
+ end
+ end
+
+ end
+
+ attr_reader :repo, :index, :tree
+
+ def initialize(path, &block)
+ @repo = Grit::Repo.new(path)
+ @index = Grit::Index.new(@repo)
+ @tree = Tree.new
+ end
+
+ def commit(message="")
+ index.tree = tree.to_hash
+ head = repo.heads.first
+ index.commit(message, head ? head.commit.id : nil)
+ end
+
+ def [](*args)
+ tree[*args]
+ end
+
+ def []=(*args)
+ value = args.pop
+ tree[*args] = value
+ end
+
+ def delete(path)
+ tree.delete(path)
+ end
+
+ def load
+ tree.load(repo.tree)
+ end
+
+ def each(&block)
+ tree.each(&block)
+ end
+
+ def each_with_path(&block)
+ tree.each_with_path(&block)
+ end
+
+end
View
101 lib/store.rb
@@ -1,101 +0,0 @@
-module Kontrol
-
- class Store
-
- module FileCache
-
- def real_path(path)
- "#{self.path == '/' ? '.' : self.path}/#{path}"
- end
-
- def changed_on_disk?(path)
- real_path = real_path(path)
- return false if not File.exist?(real_path)
- time = self.time[path]
- time.nil? or time != File.mtime(real_path)
- end
-
- def load_file(path, blob = tree / path)
- real_path = real_path(path)
- time[path] = File.mtime(real_path)
- self.data[path] = @reader.call(path, File.read(real_path), tree / path)
- end
-
- end
-
- include Enumerable
-
- attr_reader :repo, :index, :path, :tree, :data, :time, :reader, :writer
-
- def initialize(repo, path = '/', file_cache = false, &block)
- @repo = repo.is_a?(String) ? Grit::Repo.new(repo) : repo
- @index = @repo.index
- @path = path
- @tree = @repo.tree / path
- @data = {}
- @time = {}
- @reader = block || lambda { |path, data, blob| data }
- @writer = lambda { |path, data| data }
-
- extend FileCache if file_cache
- find_files_in(tree)
- end
-
- def [](path)
- changed_on_disk?(path) ? load_file(path) : data[path]
- end
-
- def []=(path, data)
- @data[path] = data
- end
-
- def delete(path)
- @data.delete(path)
- end
-
- def commit(message="")
- @data.each do |path, data|
- index.add(path, @writer.call(path, data))
- end
-
- head = repo.heads.first
- index.commit(message, head ? head.commit.id : nil)
- end
-
- def load
- @tree = repo.tree / path
- data.clear
- time.clear
- find_files_in(tree)
- end
-
- def each(&block)
- data.values.each(&block)
- end
-
- private
-
- def changed_on_disk?(path)
- false
- end
-
- def load_file(path, blob = tree / path)
- self.data[path] = reader.call(path, blob.data, blob)
- end
-
- def find_files_in(tree, parent = [])
- for file in tree.contents
- path = parent + [file.name]
- if file.is_a?(Grit::Tree)
- find_files_in(file, path)
- else
- name = path.join('/')
- load_file(name) if !data[name] and changed_on_disk?(name)
- end
- end
- end
-
- end
-
-
-end
View
117 spec/git_store_spec.rb
@@ -0,0 +1,117 @@
+$:.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
+
+require 'git_store'
+require 'yaml'
+
+describe GitStore do
+
+ REPO = File.expand_path(File.dirname(__FILE__) + '/test_repo')
+
+ before do
+ FileUtils.rm_rf REPO
+ Dir.mkdir REPO
+ Dir.chdir REPO
+ `git init`
+ end
+
+ def store
+ @store or
+ begin
+ @store = GitStore.new(REPO)
+ @store.load
+ @store
+ end
+ end
+
+ def file(file, data)
+ FileUtils.mkpath(File.dirname(file))
+ open(file, 'w') { |io| io << data }
+ `git add #{file}`
+ `git commit -m 'added #{file}'`
+ File.unlink(file)
+ end
+
+ it 'should load a repo' do
+ file 'a', 'Hello'
+ file 'b', 'World'
+
+ store['a'].should == 'Hello'
+ store['b'].should == 'World'
+ end
+
+ it 'should load folders' do
+ file 'x/a', 'Hello'
+ file 'y/b', 'World'
+
+ store['x'].should be_kind_of(GitStore::Tree)
+ store['y'].should be_kind_of(GitStore::Tree)
+
+ store['x']['a'].should == 'Hello'
+ store['y']['b'].should == 'World'
+ end
+
+ it 'should commit added files' do
+ store['c'] = 'Hello'
+ store['d'] = 'World'
+ store.commit
+
+ `git checkout`
+
+ File.should be_exist('c')
+ File.should be_exist('d')
+
+ File.read('c').should == 'Hello'
+ File.read('d').should == 'World'
+ end
+
+ it 'should load yaml' do
+ file 'x/a.yml', '[1, 2, 3, 4]'
+
+ store['x']['a.yml'].should == [1,2,3,4]
+
+ store['x']['a.yml'] = [1,2,3,4,5]
+
+ store.commit
+ store.load
+
+ store['x']['a.yml'].should == [1,2,3,4,5]
+ end
+
+ it 'should resolv paths' do
+ file 'x/a', 'Hello'
+ file 'y/b', 'World'
+
+ store['x/a'].should == 'Hello'
+ store['y/b'].should == 'World'
+
+ store['y/b'] = 'Now this'
+
+ store['y']['b'].should == 'Now this'
+ store.commit
+ store.load
+
+ store['y/b'].should == 'Now this'
+ end
+
+ it 'should create new trees' do
+ store['new/tree'] = 'This tree'
+ store['this', 'tree'] = 'Another'
+ store.commit
+ store.load
+
+ store['new/tree'].should == 'This tree'
+ store['this/tree'].should == 'Another'
+ end
+
+ it 'should preserve loaded trees' do
+ tree = store['tree'] = GitStore::Tree.new
+ store['tree']['example'] = 'Example'
+ store.commit
+ store.load
+
+ store['tree'].should == tree
+ end
+
+end
+
+

0 comments on commit 511140e

Please sign in to comment.
Something went wrong with that request. Please try again.