Class: GitStore

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/git_store.rb,
lib/git_store/tag.rb,
lib/git_store/blob.rb,
lib/git_store/diff.rb,
lib/git_store/pack.rb,
lib/git_store/tree.rb,
lib/git_store/user.rb,
lib/git_store/commit.rb,
lib/git_store/handlers.rb

Overview

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.

Defined Under Namespace

Classes: Blob, Commit, DefaultHandler, Diff, Mmap, PackFormatError, PackStorage, Tag, Tree, User, YAMLHandler

Constant Summary collapse

TYPE_CLASS =
{
  'tree' => Tree,
  'blob' => Blob,
  'commit' => Commit,
  'tag' => Tag
}
CLASS_TYPE =
{
  Tree => 'tree',
  Blob => 'blob',
  Commit => 'commit',
  Tag => 'tag'
}
PACK_SIGNATURE =
"PACK"
PACK_IDX_SIGNATURE =
"\377tOc"
OBJ_NONE =
0
OBJ_COMMIT =
1
OBJ_TREE =
2
OBJ_BLOB =
3
OBJ_TAG =
4
OBJ_TYPES =
[nil, 'commit', 'tree', 'blob', 'tag'].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path, branch = 'master', bare = false) ⇒ GitStore

Initialize a store.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/git_store.rb', line 61

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

Instance Attribute Details

#bareObject (readonly)

Returns the value of attribute bare.



58
59
60
# File 'lib/git_store.rb', line 58

def bare
  @bare
end

#branchObject (readonly)

Returns the value of attribute branch.



58
59
60
# File 'lib/git_store.rb', line 58

def branch
  @branch
end

#handlerObject (readonly)

Returns the value of attribute handler.



58
59
60
# File 'lib/git_store.rb', line 58

def handler
  @handler
end

#headObject (readonly)

Returns the value of attribute head.



58
59
60
# File 'lib/git_store.rb', line 58

def head
  @head
end

#indexObject (readonly)

Returns the value of attribute index.



58
59
60
# File 'lib/git_store.rb', line 58

def index
  @index
end

#lock_fileObject (readonly)

Returns the value of attribute lock_file.



58
59
60
# File 'lib/git_store.rb', line 58

def lock_file
  @lock_file
end

#objectsObject (readonly)

Returns the value of attribute objects.



58
59
60
# File 'lib/git_store.rb', line 58

def objects
  @objects
end

#packsObject (readonly)

Returns the value of attribute packs.



58
59
60
# File 'lib/git_store.rb', line 58

def packs
  @packs
end

#pathObject (readonly)

Returns the value of attribute path.



58
59
60
# File 'lib/git_store.rb', line 58

def path
  @path
end

#rootObject (readonly)

Returns the value of attribute root.



58
59
60
# File 'lib/git_store.rb', line 58

def root
  @root
end

Instance Method Details

#[](path) ⇒ Object

Read an object for the specified path.



117
118
119
# File 'lib/git_store.rb', line 117

def [](path)
  root[path]
end

#[]=(path, data) ⇒ Object

Write an object to the specified path.



122
123
124
# File 'lib/git_store.rb', line 122

def []=(path, data)
  root[path] = data
end

#changed?Boolean

Has our store been changed on disk?

Returns:

  • (Boolean)


162
163
164
# File 'lib/git_store.rb', line 162

def changed?
  head.nil? or head.id != read_head_id
end

#commit(message = '', author = User.from_config, committer = author) ⇒ Object

Write a commit object to disk and set the head of the current branch.

Returns the commit object



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/git_store.rb', line 249

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

#commits(limit = 10, start = head) ⇒ Object

Returns a list of commits starting from head commit.



268
269
270
271
272
273
274
275
276
277
278
# File 'lib/git_store.rb', line 268

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

#delete(path) ⇒ Object

Remove given path from store.



142
143
144
# File 'lib/git_store.rb', line 142

def delete(path)
  root.delete(path)
end

#each(&block) ⇒ Object

Iterate over all key-values pairs found in this store.



127
128
129
# File 'lib/git_store.rb', line 127

def each(&block)
  root.each(&block)
end

#finish_transactionObject

Finish the transaction.

Release the lock file.



239
240
241
242
243
244
# File 'lib/git_store.rb', line 239

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

#get(id) ⇒ Object

Get an object by its id.

Returns a tree, blob, commit or tag object.



283
284
285
286
287
288
289
290
291
292
293
# File 'lib/git_store.rb', line 283

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

#get_object(id) ⇒ Object

Read the raw object with the given id from the repository.

Returns a pair of content and type of the object



321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/git_store.rb', line 321

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

#git_pathObject

Returns the path to the git data directory.



96
97
98
99
100
101
102
# File 'lib/git_store.rb', line 96

def git_path
  if bare
    "#{path}"
  else
    "#{path}/.git"
  end
end

#handler_for(path) ⇒ Object

Return a handler for a given path.



112
113
114
# File 'lib/git_store.rb', line 112

def handler_for(path)
  handler[ path.split('.').last ]
end

#head_pathObject

Returns the path to the current head file.



86
87
88
# File 'lib/git_store.rb', line 86

def head_path
  "#{git_path}/refs/heads/#{branch}"
end

#id_for(type, content) ⇒ Object

Calculate the id for a given type and raw data string.



314
315
316
# File 'lib/git_store.rb', line 314

def id_for(type, content)
  sha "#{type} #{content.length}\0#{content}"
end

#in_transaction?Boolean

Is there any transaction going on?

Returns:

  • (Boolean)


191
192
193
# File 'lib/git_store.rb', line 191

def in_transaction?
  Thread.current['git_store_lock']
end

#inspectObject

Inspect the store.



157
158
159
# File 'lib/git_store.rb', line 157

def inspect
  "#<GitStore #{path} #{branch}>"
end

#load(from_disk = false) ⇒ Object

Load the current head version from repository.



167
168
169
170
171
172
173
174
# File 'lib/git_store.rb', line 167

def load(from_disk = false)
  if id = read_head_id
    @head = get(id)
    @root = @head.tree
  end

  load_from_disk if from_disk
end

#load_from_diskObject



176
177
178
179
180
181
182
183
# File 'lib/git_store.rb', line 176

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

#object_path(id) ⇒ Object

Returns the path to the object file for given id.



91
92
93
# File 'lib/git_store.rb', line 91

def object_path(id)
  "#{git_path}/objects/#{ id[0...2] }/#{ id[2..39] }"
end

#pathsObject

Returns all paths found in this store.



132
133
134
# File 'lib/git_store.rb', line 132

def paths
  root.paths
end

#put(object) ⇒ Object

Save a git object to the store.

Returns the object id.



298
299
300
301
302
303
304
305
306
# File 'lib/git_store.rb', line 298

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

#put_object(type, content) ⇒ Object

Write a raw object to the repository.

Returns the object id.



343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/git_store.rb', line 343

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

#read_head_idObject

Read the id of the head commit.

Returns the object id of the last commit.



107
108
109
# File 'lib/git_store.rb', line 107

def read_head_id
  File.read(head_path).strip if File.exists?(head_path)
end

#refresh!Object

Reload the store, if it has been changed on disk.



186
187
188
# File 'lib/git_store.rb', line 186

def refresh!
  load if changed?
end

#rollbackObject

Restore the state of the store.

Any changes made to the store are discarded.



230
231
232
233
234
# File 'lib/git_store.rb', line 230

def rollback
  objects.clear
  load
  finish_transaction
end

#sha(str) ⇒ Object

Returns the hash value of an object string.



309
310
311
# File 'lib/git_store.rb', line 309

def sha(str)
  Digest::SHA1.hexdigest(str)[0, 40]
end

#start_transactionObject

Start a transaction.

Tries to get lock on lock file, reload the this store if has changed in the repository.



218
219
220
221
222
223
224
225
# File 'lib/git_store.rb', line 218

def start_transaction
  file = open("#{head_path}.lock", "w")
  file.flock(File::LOCK_EX)

  Thread.current['git_store_lock'] = file

  load if changed?
end

#to_hashObject

Returns the store as a hash tree.



152
153
154
# File 'lib/git_store.rb', line 152

def to_hash
  root.to_hash
end

#transaction(message = "", author = User.from_config, committer = author) ⇒ Object

All changes made inside a transaction are atomic. If some exception occurs the transaction will be rolled back.

Example:

store.transaction { store['a'] = 'b' }


201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/git_store.rb', line 201

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

#tree(path) ⇒ Object

Find or create a tree object with given path.



147
148
149
# File 'lib/git_store.rb', line 147

def tree(path)
  root.tree(path)
end

#valuesObject

Returns all values found in this store.



137
138
139
# File 'lib/git_store.rb', line 137

def values
  root.values
end