Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: f8028010ac
Fetching contributors…

Cannot retrieve contributors at this time

393 lines (320 sloc) 8.996 kb
require 'rubygems'
require 'zlib'
require 'digest/sha1'
require 'yaml'
require 'fileutils'
require 'git_store/blob'
require 'git_store/diff'
require 'git_store/tree'
require 'git_store/tag'
require 'git_store/user'
require 'git_store/pack'
require 'git_store/commit'
require 'git_store/handlers'
# GitStore implements a versioned data store based on the revision
# management system git. You can store object hierarchies as nested
# hashes, which will be mapped on the directory structure of a git
# repository.
#
# GitStore supports transactions, so that updates to the store either
# fail or succeed completely.
#
# GitStore manages concurrent access by a file locking scheme. So only
# one process can start a transaction at one time. This is implemented
# by locking the `refs/head/<branch>.lock` file, which is also respected
# by the git binary.
#
# A regular commit should be atomic by the nature of git, as the only
# critical part is writing the 40 bytes SHA1 hash of the commit object
# to the file `refs/head/<branch>`, which is done atomically by the
# operating system.
#
# So reading a repository should be always consistent in a git
# repository. The head of a branch points to a commit object, which in
# turn points to a tree object, which itself is a snapshot of the
# GitStore at commit time. All involved objects are keyed by their
# SHA1 value, so there is no chance for another process to write to
# the same files.
#
class GitStore
include Enumerable
TYPE_CLASS = {
'tree' => Tree,
'blob' => Blob,
'commit' => Commit,
'tag' => Tag
}
CLASS_TYPE = {
Tree => 'tree',
Blob => 'blob',
Commit => 'commit',
Tag => 'tag'
}
attr_reader :path, :index, :root, :branch, :lock_file, :head, :packs, :handler, :bare, :objects
# Initialize a store.
def initialize(path, branch = 'master', bare = false)
if bare && !File.exists?("#{path}") or
!bare && !File.exists?("#{path}/.git")
raise ArgumentError, "first argument must be a valid Git repository: `#{path}'"
end
@bare = bare
@path = path.chomp('/')
@branch = branch
@root = Tree.new(self)
@packs = {}
@objects = {}
@handler = {
'yml' => YAMLHandler.new
}
@handler.default = DefaultHandler.new
load_packs("#{git_path}/objects/pack")
load
end
# Returns the path to the current head file.
def head_path
"#{git_path}/refs/heads/#{branch}"
end
# Returns the path to the object file for given id.
def object_path(id)
"#{git_path}/objects/#{ id[0...2] }/#{ id[2..39] }"
end
# Returns the path to the git data directory.
def git_path
if bare
"#{path}"
else
"#{path}/.git"
end
end
# Read the id of the head commit.
#
# Returns the object id of the last commit.
def read_head_id
File.read(head_path).strip if File.exists?(head_path)
end
# Return a handler for a given path.
def handler_for(path)
handler[ path.split('.').last ]
end
# Read an object for the specified path.
def [](path)
root[path]
end
# Write an object to the specified path.
def []=(path, data)
root[path] = data
end
# Iterate over all key-values pairs found in this store.
def each(&block)
root.each(&block)
end
# Returns all paths found in this store.
def paths
root.paths
end
# Returns all values found in this store.
def values
root.values
end
# Remove given path from store.
def delete(path)
root.delete(path)
end
# Find or create a tree object with given path.
def tree(path)
root.tree(path)
end
# Returns the store as a hash tree.
def to_hash
root.to_hash
end
# Inspect the store.
def inspect
"#<GitStore #{path} #{branch}>"
end
# Has our store been changed on disk?
def changed?
head.nil? or head.id != read_head_id
end
# Load the current head version from repository.
def load(from_disk = false)
if id = read_head_id
@head = get(id)
@root = @head.tree
end
load_from_disk if from_disk
end
def load_from_disk
root.each_blob do |path, blob|
file = "#{self.path}/#{path}"
if File.file?(file)
blob.data = File.read(file)
end
end
end
# Reload the store, if it has been changed on disk.
def refresh!
load if changed?
end
# Is there any transaction going on?
def in_transaction?
Thread.current['git_store_lock']
end
# All changes made inside a transaction are atomic. If some
# exception occurs the transaction will be rolled back.
#
# Example:
# store.transaction { store['a'] = 'b' }
#
def transaction(message = "", author = User.from_config, committer = author)
start_transaction
result = yield
commit message, author, committer
result
rescue
rollback
raise
ensure
finish_transaction
end
# Start a transaction.
#
# Tries to get lock on lock file, reload the this store if
# has changed in the repository.
def start_transaction
file = open("#{head_path}.lock", "w")
file.flock(File::LOCK_EX)
Thread.current['git_store_lock'] = file
load if changed?
end
# Restore the state of the store.
#
# Any changes made to the store are discarded.
def rollback
objects.clear
load
finish_transaction
end
# Finish the transaction.
#
# Release the lock file.
def finish_transaction
Thread.current['git_store_lock'].close rescue nil
Thread.current['git_store_lock'] = nil
File.unlink("#{head_path}.lock") rescue nil
end
# Write a commit object to disk and set the head of the current branch.
#
# Returns the commit object
def commit(message = '', author = User.from_config, committer = author)
root.write
commit = Commit.new(self)
commit.tree = root
commit.parent << head.id if head
commit.author = author
commit.committer = committer
commit.message = message
commit.write
open(head_path, "wb") do |file|
file.write(commit.id)
end
@head = commit
end
# Returns a list of commits starting from head commit.
def commits(limit = 10, start = head)
entries = []
current = start
while current and entries.size < limit
entries << current
current = get(current.parent.first)
end
entries
end
# Get an object by its id.
#
# Returns a tree, blob, commit or tag object.
def get(id)
return nil if id.nil?
return objects[id] if objects.has_key?(id)
type, content = get_object(id)
klass = TYPE_CLASS[type] or raise NotImplementedError, "type not supported: #{type}"
objects[id] = klass.new(self, id, content)
end
# Save a git object to the store.
#
# Returns the object id.
def put(object)
type = CLASS_TYPE[object.class] or raise NotImplementedError, "class not supported: #{object.class}"
id = put_object(type, object.dump)
objects[id] = object
id
end
# Returns the hash value of an object string.
def sha(str)
Digest::SHA1.hexdigest(str)[0, 40]
end
# Calculate the id for a given type and raw data string.
def id_for(type, content)
sha "#{type} #{content.length}\0#{content}"
end
# Read the raw object with the given id from the repository.
#
# Returns a pair of content and type of the object
def get_object(id)
path = object_path(id)
if File.exists?(path)
buf = open(path, "rb") { |f| f.read }
raise "not a loose object: #{id}" if not legacy_loose_object?(buf)
header, content = Zlib::Inflate.inflate(buf).split(/\0/, 2)
type, size = header.split(/ /, 2)
raise "bad object: #{id}" if content.length != size.to_i
else
content, type = get_object_from_pack(id)
end
return type, content
end
# Write a raw object to the repository.
#
# Returns the object id.
def put_object(type, content)
data = "#{type} #{content.length}\0#{content}"
id = sha(data)
path = object_path(id)
unless File.exists?(path)
FileUtils.mkpath(File.dirname(path))
open(path, 'wb') do |f|
f.write Zlib::Deflate.deflate(data)
end
end
id
end
protected
if 'String'[0].respond_to?(:ord)
def legacy_loose_object?(buf)
buf[0] == ?x && (((buf[0].ord << 8) + buf[1].ord) % 31 == 0)
end
else
def legacy_loose_object?(buf)
word = (buf[0] << 8) + buf[1]
buf[0] == 0x78 && word % 31 == 0
end
end
def get_object_from_pack(id)
pack, offset = @packs[id]
pack.parse_object(offset) if pack
end
def load_packs(path)
if File.directory?(path)
Dir.open(path) do |dir|
entries = dir.select { |entry| entry =~ /\.pack$/i }
entries.each do |entry|
pack = PackStorage.new(File.join(path, entry))
pack.each_entry do |id, offset|
id = id.unpack("H*").first
@packs[id] = [pack, offset]
end
end
end
end
end
end
Jump to Line
Something went wrong with that request. Please try again.