gitgo 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. data/History +44 -0
  2. data/License.txt +22 -0
  3. data/README +45 -0
  4. data/bin/gitgo +4 -0
  5. data/lib/gitgo.rb +1 -0
  6. data/lib/gitgo/app.rb +63 -0
  7. data/lib/gitgo/controller.rb +89 -0
  8. data/lib/gitgo/controllers/code.rb +198 -0
  9. data/lib/gitgo/controllers/issue.rb +76 -0
  10. data/lib/gitgo/controllers/repo.rb +186 -0
  11. data/lib/gitgo/controllers/wiki.rb +19 -0
  12. data/lib/gitgo/document.rb +680 -0
  13. data/lib/gitgo/document/invalid_document_error.rb +34 -0
  14. data/lib/gitgo/documents/comment.rb +20 -0
  15. data/lib/gitgo/documents/issue.rb +56 -0
  16. data/lib/gitgo/git.rb +941 -0
  17. data/lib/gitgo/git/tree.rb +315 -0
  18. data/lib/gitgo/git/utils.rb +59 -0
  19. data/lib/gitgo/helper.rb +3 -0
  20. data/lib/gitgo/helper/doc.rb +28 -0
  21. data/lib/gitgo/helper/form.rb +88 -0
  22. data/lib/gitgo/helper/format.rb +200 -0
  23. data/lib/gitgo/helper/html.rb +19 -0
  24. data/lib/gitgo/helper/utils.rb +85 -0
  25. data/lib/gitgo/index.rb +421 -0
  26. data/lib/gitgo/index/idx_file.rb +119 -0
  27. data/lib/gitgo/index/sha_file.rb +135 -0
  28. data/lib/gitgo/patches/grit.rb +47 -0
  29. data/lib/gitgo/repo.rb +626 -0
  30. data/lib/gitgo/repo/graph.rb +333 -0
  31. data/lib/gitgo/repo/node.rb +122 -0
  32. data/lib/gitgo/rest.rb +87 -0
  33. data/lib/gitgo/server.rb +114 -0
  34. data/lib/gitgo/version.rb +8 -0
  35. data/public/css/gitgo.css +24 -0
  36. data/public/javascript/gitgo.js +148 -0
  37. data/public/javascript/jquery-1.4.2.min.js +154 -0
  38. data/views/app/index.erb +4 -0
  39. data/views/app/timeline.erb +27 -0
  40. data/views/app/welcome.erb +13 -0
  41. data/views/code/_comment.erb +10 -0
  42. data/views/code/_comment_form.erb +14 -0
  43. data/views/code/_comments.erb +5 -0
  44. data/views/code/_commit.erb +25 -0
  45. data/views/code/_grepnav.erb +5 -0
  46. data/views/code/_treenav.erb +3 -0
  47. data/views/code/blob.erb +6 -0
  48. data/views/code/commit_grep.erb +35 -0
  49. data/views/code/commits.erb +11 -0
  50. data/views/code/diff.erb +10 -0
  51. data/views/code/grep.erb +32 -0
  52. data/views/code/index.erb +17 -0
  53. data/views/code/obj/blob.erb +4 -0
  54. data/views/code/obj/commit.erb +25 -0
  55. data/views/code/obj/tag.erb +25 -0
  56. data/views/code/obj/tree.erb +9 -0
  57. data/views/code/tree.erb +9 -0
  58. data/views/error.erb +19 -0
  59. data/views/issue/_issue.erb +15 -0
  60. data/views/issue/_issue_form.erb +39 -0
  61. data/views/issue/edit.erb +11 -0
  62. data/views/issue/index.erb +28 -0
  63. data/views/issue/new.erb +5 -0
  64. data/views/issue/show.erb +27 -0
  65. data/views/layout.erb +34 -0
  66. data/views/not_found.erb +1 -0
  67. data/views/repo/fsck.erb +29 -0
  68. data/views/repo/help.textile +5 -0
  69. data/views/repo/help/faq.textile +19 -0
  70. data/views/repo/help/howto.textile +31 -0
  71. data/views/repo/help/trouble.textile +28 -0
  72. data/views/repo/idx.erb +29 -0
  73. data/views/repo/index.erb +72 -0
  74. data/views/repo/status.erb +16 -0
  75. data/views/wiki/index.erb +3 -0
  76. metadata +253 -0
@@ -0,0 +1,119 @@
1
+ require 'fileutils'
2
+
3
+ module Gitgo
4
+ class Index
5
+
6
+ # IdxFile is a wrapper providing access to a file of L packed integers.
7
+ class IdxFile
8
+ class << self
9
+
10
+ # Opens and returns an idx file in the specified mode. If a block is
11
+ # given the file is yielded to it and closed afterwards; in this case
12
+ # the return of open is the block result.
13
+ def open(path, mode="r")
14
+ idx_file = new(File.open(path, mode))
15
+
16
+ return idx_file unless block_given?
17
+
18
+ begin
19
+ yield(idx_file)
20
+ ensure
21
+ idx_file.close
22
+ end
23
+ end
24
+
25
+ # Reads the file and returns an array of integers.
26
+ def read(path)
27
+ open(path) {|idx_file| idx_file.read(nil) }
28
+ end
29
+
30
+ # Opens the file and writes the integer; previous contents are
31
+ # replaced. Provide an array of integers to write multiple integers
32
+ # at once.
33
+ def write(path, int)
34
+ dir = File.dirname(path)
35
+ FileUtils.mkdir_p(dir) unless File.exists?(dir)
36
+ open(path, "w") {|idx_file| idx_file.write(int) }
37
+ end
38
+
39
+ # Opens the file and appends the int. Provide an array of integers to
40
+ # append multiple integers at once.
41
+ def append(path, int)
42
+ dir = File.dirname(path)
43
+ FileUtils.mkdir_p(dir) unless File.exists?(dir)
44
+ open(path, "a") {|idx_file| idx_file.write(int) }
45
+ end
46
+
47
+ # Opens the file and removes the integers.
48
+ def rm(path, *ints)
49
+ return unless File.exists?(path)
50
+
51
+ current = read(path)
52
+ write(path, (current-ints))
53
+ end
54
+ end
55
+
56
+ # The pack format
57
+ PACK = "L*"
58
+
59
+ # The unpack format
60
+ UNPACK = "L*"
61
+
62
+ # The size of a packed integer
63
+ PACKED_ENTRY_SIZE = 4
64
+
65
+ # The file being wrapped
66
+ attr_reader :file
67
+
68
+ # Initializes a new ShaFile with the specified file. The file will be
69
+ # set to binary mode.
70
+ def initialize(file)
71
+ file.binmode
72
+ @file = file
73
+ end
74
+
75
+ # Closes file
76
+ def close
77
+ file.close
78
+ end
79
+
80
+ # The index of the current entry.
81
+ def current
82
+ file.pos / PACKED_ENTRY_SIZE
83
+ end
84
+
85
+ # Reads n entries from the start index and returns them as an array. Nil
86
+ # n will read all remaining entries and nil start will read from the
87
+ # current index.
88
+ def read(n=10, start=0)
89
+ if start
90
+ start_pos = start * PACKED_ENTRY_SIZE
91
+ file.pos = start_pos
92
+ end
93
+
94
+ str = file.read(n.nil? ? nil : n * PACKED_ENTRY_SIZE).to_s
95
+ unless str.length % PACKED_ENTRY_SIZE == 0
96
+ raise "invalid packed int length: #{str.length}"
97
+ end
98
+
99
+ str.unpack(UNPACK)
100
+ end
101
+
102
+ # Writes the integers to the file at the current index. Provide an
103
+ # array of integers to write multiple integers at once.
104
+ def write(int)
105
+ int = [int] unless int.respond_to?(:pack)
106
+ file.write int.pack(PACK)
107
+ self
108
+ end
109
+
110
+ # Appends the integers to the file. Provide an array of integers to
111
+ # append multiple integers at once.
112
+ def append(int)
113
+ file.pos = file.size
114
+ write(int)
115
+ self
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,135 @@
1
+ require 'fileutils'
2
+
3
+ module Gitgo
4
+ class Index
5
+
6
+ # ShaFile is a wrapper providing access to a file of H packed shas.
7
+ class ShaFile
8
+ class << self
9
+
10
+ # Opens and returns an sha file in the specified mode. If a block is
11
+ # given the file is yielded to it and closed afterwards; in this case
12
+ # the return of open is the block result.
13
+ def open(path, mode="r")
14
+ sha_file = new(File.open(path, mode))
15
+
16
+ return sha_file unless block_given?
17
+
18
+ begin
19
+ yield(sha_file)
20
+ ensure
21
+ sha_file.close
22
+ end
23
+ end
24
+
25
+ # Reads the file and returns an array of shas.
26
+ def read(path)
27
+ open(path) {|sha_file| sha_file.read(nil) }
28
+ end
29
+
30
+ # Opens the file and writes the sha; previous contents are replaced.
31
+ # Multiple shas may be written at once by providing a string of
32
+ # concatenated shas.
33
+ def write(path, sha)
34
+ dir = File.dirname(path)
35
+ FileUtils.mkdir_p(dir) unless File.exists?(dir)
36
+ open(path, "w") {|sha_file| sha_file.write(sha) }
37
+ end
38
+
39
+ # Opens the file and appends the sha. Multiple shas may be appended
40
+ # at once by providing a string of concatenated shas.
41
+ def append(path, sha)
42
+ dir = File.dirname(path)
43
+ FileUtils.mkdir_p(dir) unless File.exists?(dir)
44
+ open(path, "a") {|sha_file| sha_file.write(sha) }
45
+ end
46
+
47
+ # Opens the file and removes the shas.
48
+ def rm(path, *shas)
49
+ return unless File.exists?(path)
50
+
51
+ current = read(path)
52
+ write(path, (current-shas).join)
53
+ end
54
+ end
55
+
56
+ # The pack format, optimized for packing multiple shas
57
+ PACK = "H*"
58
+
59
+ # The unpacking format
60
+ UNPACK = "H40"
61
+
62
+ # The size of an unpacked sha
63
+ ENTRY_SIZE = 40
64
+
65
+ # The size of a packed sha
66
+ PACKED_ENTRY_SIZE = 20
67
+
68
+ # The file being wrapped
69
+ attr_reader :file
70
+
71
+ # Initializes a new ShaFile with the specified file. The file will be
72
+ # set to binary mode.
73
+ def initialize(file)
74
+ file.binmode
75
+ @file = file
76
+ end
77
+
78
+ # Closes file
79
+ def close
80
+ file.close
81
+ end
82
+
83
+ # The index of the current entry.
84
+ def current
85
+ file.pos / PACKED_ENTRY_SIZE
86
+ end
87
+
88
+ # Reads n entries from the start index and returns them as an array. Nil
89
+ # n will read all remaining entries and nil start will read from the
90
+ # current index.
91
+ def read(n=10, start=0)
92
+ if start
93
+ start_pos = start * PACKED_ENTRY_SIZE
94
+ file.pos = start_pos
95
+ end
96
+
97
+ str = file.read(n.nil? ? nil : n * PACKED_ENTRY_SIZE).to_s
98
+ unless str.length % PACKED_ENTRY_SIZE == 0
99
+ raise "invalid packed sha length: #{str.length}"
100
+ end
101
+ entries = str.unpack(UNPACK * (str.length / PACKED_ENTRY_SIZE))
102
+
103
+ # clear out all missing entries, which will be empty
104
+ while last = entries.last
105
+ if last.empty?
106
+ entries.pop
107
+ else
108
+ break
109
+ end
110
+ end
111
+
112
+ entries
113
+ end
114
+
115
+ # Writes the sha to the file at the current index. Multiple shas may be
116
+ # written at once by providing a string of concatenated shas.
117
+ def write(sha)
118
+ unless sha.length % ENTRY_SIZE == 0
119
+ raise "invalid sha length: #{sha.length}"
120
+ end
121
+
122
+ file.write [sha].pack(PACK)
123
+ self
124
+ end
125
+
126
+ # Appends the sha to the file. Multiple shas may be appended at once by
127
+ # providing a string of concatenated shas.
128
+ def append(sha)
129
+ file.pos = file.size
130
+ write(sha)
131
+ self
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,47 @@
1
+ module Gitgo
2
+ module Patches
3
+ class Grit::Actor
4
+ def <=>(another)
5
+ name <=> another.name
6
+ end
7
+ end
8
+
9
+ class Grit::Commit
10
+
11
+ # This patch allows file add/remove to be detected in diffs. For some
12
+ # reason with the original version (commented out) the diff is missing
13
+ # certain crucial lines in the output:
14
+ #
15
+ # diff --git a/alpha.txt b/alpha.txt
16
+ # index 0000000000000000000000000000000000000000..15db91c38a4cd47235961faa407304bf47ea5d15 100644
17
+ # --- a/alpha.txt
18
+ # +++ b/alpha.txt
19
+ # @@ -1 +1,2 @@
20
+ # +Contents of file alpha.
21
+ #
22
+ # vs
23
+ #
24
+ # diff --git a/alpha.txt b/alpha.txt
25
+ # new file mode 100644
26
+ # index 0000000000000000000000000000000000000000..15db91c38a4cd47235961faa407304bf47ea5d15
27
+ # --- /dev/null
28
+ # +++ b/alpha.txt
29
+ # @@ -0,0 +1 @@
30
+ # +Contents of file alpha.
31
+ #
32
+ # Perhaps the original drops into the pure-ruby version of git?
33
+ def self.diff(repo, a, b = nil, paths = [])
34
+ if b.is_a?(Array)
35
+ paths = b
36
+ b = nil
37
+ end
38
+ paths.unshift("--") unless paths.empty?
39
+ paths.unshift(b) unless b.nil?
40
+ paths.unshift(a)
41
+ # text = repo.git.diff({:full_index => true}, *paths)
42
+ text = repo.git.run('', :diff, '', {:full_index => true}, paths)
43
+ Grit::Diff.list_from_string(repo, text)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,626 @@
1
+ require 'json'
2
+ require 'gitgo/git'
3
+ require 'gitgo/index'
4
+ require 'gitgo/repo/graph'
5
+
6
+ module Gitgo
7
+ # Repo represents the internal data store used by Gitgo. Repos consist of a
8
+ # Git instance for storing documents in the repository, and an Index
9
+ # instance for queries on the documents. The internal workings of Repo are a
10
+ # bit complex; this document provides terminology and details on how
11
+ # documents and associations are stored. See Index and Graph for how
12
+ # document information is accessed.
13
+ #
14
+ # == Terminology
15
+ #
16
+ # Gitgo documents are hashes of attributes that can be serialized as JSON.
17
+ # The Gitgo::Document model adds structure to these hashes and enforces data
18
+ # validity, but insofar as Repo is concerned, a document is a serializable
19
+ # hash. Documents are linked into a document graph -- a directed acyclic
20
+ # graph (DAG) of document nodes that represent, for example, a chain of
21
+ # comments making a conversation. A given repo can be thought of as storing
22
+ # multiple DAGs, each made up of multiple documents.
23
+ #
24
+ # The DAGs used by Gitgo are a little weird because they use some nodes to
25
+ # represent revisions and other nodes to represent the 'current' nodes in a
26
+ # graph (this setup allows documents to be immutable, and thereby to prevent
27
+ # merge conflicts).
28
+ #
29
+ # Normally a DAG has this conceptual structure:
30
+ #
31
+ # head
32
+ # |
33
+ # parent
34
+ # |
35
+ # node
36
+ # |
37
+ # child
38
+ # |
39
+ # tail
40
+ #
41
+ # By contrast, the DAGs used by Gitgo are structured like this:
42
+ #
43
+ # head
44
+ # |
45
+ # parent
46
+ # |
47
+ # original -> previous -> node -> update -> current version
48
+ # |
49
+ # child
50
+ # |
51
+ # tail
52
+ #
53
+ # The extra dimension of updates may be unwound to replace all previous
54
+ # versions of a node with the current version(s), so for example:
55
+ #
56
+ # a a
57
+ # | |
58
+ # b -> b' becomes b'
59
+ # | |
60
+ # c c
61
+ #
62
+ # The full DAG is refered to as the 'convoluted graph' and the current DAG
63
+ # is the 'deconvoluted graph'. The logic performing the deconvolution is
64
+ # encapsulated in Graph and Node.
65
+ #
66
+ # Parent-child associations are referred to as links, while previous-update
67
+ # associations are referred to as updates. Links and updates are
68
+ # collectively referred to as associations.
69
+ #
70
+ # There are two additional types of associations; create and delete. Create
71
+ # associations occur when a sha is associated with the empty sha (ie the sha
72
+ # for an empty document). These associations place new documents along a
73
+ # path in the repo when the new document isn't a child or update. Deletes
74
+ # associate a sha with itself; these act as a break in the DAG such that all
75
+ # subsequent links and updates are omitted.
76
+ #
77
+ # The first member in an association (parent/previous/sha) is a source and
78
+ # the second (child/update/sha) is a target.
79
+ #
80
+ # == Storage
81
+ #
82
+ # Documents are stored on a dedicated git branch in a way that prevents
83
+ # merge conflicts and allows merges to directly add nodes anywhere in a
84
+ # document graph. The branch may be checked out and handled like any other
85
+ # git branch, although typically users manage the gitgo branch through Gitgo
86
+ # itself.
87
+ #
88
+ # Individual documents are stored with their associations along sha-based
89
+ # paths like 'so/urce/target' where the source is split into substrings of
90
+ # length 2 and 38. The mode and the relationship of the source-target shas
91
+ # determine the type of association involved. The logic breaks down like
92
+ # this ('-' refers to the empty sha, and a/b to different shas):
93
+ #
94
+ # source target mode type
95
+ # a - 644 create
96
+ # a b 644 link
97
+ # a b 640 update
98
+ # a a 644 delete
99
+ #
100
+ # Using this system, a traveral of the associations is enough to determine
101
+ # how documents are related in a graph without loading documents into
102
+ # memory.
103
+ #
104
+ # == Implementation Note
105
+ #
106
+ # Repo is organized around an env hash that represents the rack env for a
107
+ # particular request. Objects used by Repo are cached into env for re-use
108
+ # across multiple requests, when possible. The 'gitgo.*' constants are used
109
+ # to identify cached objects.
110
+ #
111
+ # Repo knows how to initialize all the objects it uses. An empty env or a
112
+ # partially filled env may be used to initialize a Repo.
113
+ #
114
+ class Repo
115
+ class << self
116
+
117
+ # Initializes a new Repo to the git repository at the specified path.
118
+ # Options are the same as for Git.init.
119
+ def init(path, options={})
120
+ git = Git.init(path, options)
121
+ new(GIT => git)
122
+ end
123
+
124
+ # Sets env as the thread-specific env and returns the currently set env.
125
+ def set_env(env)
126
+ current = Thread.current[ENVIRONMENT]
127
+ Thread.current[ENVIRONMENT] = env
128
+ current
129
+ end
130
+
131
+ # Sets env for the block.
132
+ def with_env(env)
133
+ begin
134
+ current = set_env(env)
135
+ yield
136
+ ensure
137
+ set_env(current)
138
+ end
139
+ end
140
+
141
+ # The thread-specific env currently in scope (see set_env). The env
142
+ # stores all the objects used by a Repo and typically represents the
143
+ # rack-env for a specific server request.
144
+ #
145
+ # Raises an error if no env is in scope.
146
+ def env
147
+ Thread.current[ENVIRONMENT] or raise("no env in scope")
148
+ end
149
+
150
+ # Returns the current Repo, ie env[REPO]. Initializes and caches a new
151
+ # Repo in env if env[REPO] is not set.
152
+ def current
153
+ env[REPO] ||= new(env)
154
+ end
155
+ end
156
+
157
+ ENVIRONMENT = 'gitgo.env'
158
+ PATH = 'gitgo.path'
159
+ OPTIONS = 'gitgo.options'
160
+ GIT = 'gitgo.git'
161
+ INDEX = 'gitgo.index'
162
+ REPO = 'gitgo.repo'
163
+ CACHE = 'gitgo.cache'
164
+
165
+ # Matches a path -- 'ab/xyz/sha'. After the match:
166
+ #
167
+ # $1:: ab
168
+ # $2:: xyz
169
+ # $3:: sha
170
+ #
171
+ DOCUMENT_PATH = /^(.{2})\/(.{38})\/(.{40})$/
172
+
173
+ # The default blob mode used for added blobs
174
+ DEFAULT_MODE = '100644'.to_sym
175
+
176
+ # The blob mode used to identify updates
177
+ UPDATE_MODE = '100640'.to_sym
178
+
179
+ FILE = 'gitgo'
180
+
181
+ # The repo env, typically the same as a request env.
182
+ attr_reader :env
183
+
184
+ # Initializes a new Repo with the specified env.
185
+ def initialize(env={})
186
+ @env = env
187
+ end
188
+
189
+ def head
190
+ git.head
191
+ end
192
+
193
+ def branch
194
+ git.branch
195
+ end
196
+
197
+ def upstream_branch
198
+ git.upstream_branch
199
+ end
200
+
201
+ def resolve(sha)
202
+ git.resolve(sha) rescue sha
203
+ end
204
+
205
+ # Returns the path to git repository. Path is determined from env[PATH],
206
+ # or inferred and set in env from env[GIT]. The default path is Dir.pwd.
207
+ def path
208
+ env[PATH] ||= (env.has_key?(GIT) ? env[GIT].path : Dir.pwd)
209
+ end
210
+
211
+ # Returns the Git instance set in env[GIT]. If no instance is set then
212
+ # one will be initialized using env[PATH] and env[OPTIONS].
213
+ #
214
+ # Note that given the chain of defaults, git will be initialized to
215
+ # Dir.pwd if the env has no PATH or GIT set.
216
+ def git
217
+ env[GIT] ||= Git.init(path, env[OPTIONS] || {})
218
+ end
219
+
220
+ # Returns the Index instance set in env[INDEX]. If no instance is set then
221
+ # one will be initialized under the git working directory, specific to the
222
+ # git branch. For instance:
223
+ #
224
+ # .git/gitgo/refs/branch/index
225
+ #
226
+ def index
227
+ env[INDEX] ||= Index.new(File.join(git.work_dir, 'refs', git.branch, 'index'))
228
+ end
229
+
230
+ # Returns or initializes a self-populating cache of attribute hashes in
231
+ # env[CACHE]. Attribute hashes are are keyed by sha.
232
+ def cache
233
+ env[CACHE] ||= Hash.new {|hash, sha| hash[sha] = read(sha) }
234
+ end
235
+
236
+ # Returns the cached attrs hash for the specified sha, or nil.
237
+ def [](sha)
238
+ cache[sha]
239
+ end
240
+
241
+ # Sets the cached attrs for the specified sha.
242
+ def []=(sha, attrs)
243
+ cache[sha] = attrs
244
+ end
245
+
246
+ # Returns the sha for an empty string, and ensures the corresponding
247
+ # object is set in the repo.
248
+ def empty_sha
249
+ @empty_sha ||= git.set(:blob, '')
250
+ end
251
+
252
+ # Creates a nested sha path like: ab/xyz/paths
253
+ def sha_path(sha, *paths)
254
+ paths.unshift sha[2,38]
255
+ paths.unshift sha[0,2]
256
+ paths
257
+ end
258
+
259
+ # Returns true if the given commit has an empty 'gitgo' file in it's tree.
260
+ def branch?(sha)
261
+ return false if sha.nil?
262
+ return false unless sha = resolve(sha)
263
+ return false unless commit = git.get(:commit, sha)
264
+
265
+ blob = commit.tree/FILE
266
+ blob && blob.data.empty? ? true : false
267
+ end
268
+
269
+ # Returns an array of refs representing gitgo branches.
270
+ def refs
271
+ select_branches(git.grit.refs)
272
+ end
273
+
274
+ # Returns an array of remotes representing gitgo branches.
275
+ def remotes
276
+ select_branches(git.grit.remotes)
277
+ end
278
+
279
+ # Serializes and sets the attributes as a blob in the git repo and caches
280
+ # the attributes by the blob sha. Returns the blob sha.
281
+ #
282
+ # Note that save does not put the blob along a path in the repo;
283
+ # immediately after save the blob is hanging and will be gc'ed by git
284
+ # unless set into a path by create, link, or update.
285
+ def save(attrs)
286
+ sha = git.set(:blob, JSON.generate(attrs))
287
+ cache[sha] = attrs
288
+ sha
289
+ end
290
+
291
+ # Creates a create association for the sha:
292
+ #
293
+ # sh/a/empty_sha (DEFAULT_MODE, sha)
294
+ #
295
+ def create(sha)
296
+ git[sha_path(sha, empty_sha)] = [DEFAULT_MODE, sha]
297
+ sha
298
+ end
299
+
300
+ # Reads and deserializes the specified hash of attrs. If sha does not
301
+ # indicate a blob that deserializes as JSON then read returns nil.
302
+ def read(sha)
303
+ begin
304
+ JSON.parse(git.get(:blob, sha).data)
305
+ rescue JSON::ParserError, Errno::EISDIR
306
+ nil
307
+ end
308
+ end
309
+
310
+ # Creates a link association for parent and child:
311
+ #
312
+ # pa/rent/child (DEFAULT_MODE, child)
313
+ #
314
+ def link(parent, child)
315
+ git[sha_path(parent, child)] = [DEFAULT_MODE, child]
316
+ self
317
+ end
318
+
319
+ # Creates an update association for old and new shas:
320
+ #
321
+ # ol/d_sha/new_sha (UPDATE_MODE, new_sha)
322
+ #
323
+ def update(old_sha, new_sha)
324
+ git[sha_path(old_sha, new_sha)] = [UPDATE_MODE, new_sha]
325
+ self
326
+ end
327
+
328
+ # Creates a delete association for the sha:
329
+ #
330
+ # sh/a/sha (DEFAULT_MODE, empty_sha)
331
+ #
332
+ def delete(sha)
333
+ git[sha_path(sha, sha)] = [DEFAULT_MODE, empty_sha]
334
+ self
335
+ end
336
+
337
+ # Returns the operative sha in an association, ie the source in a
338
+ # head/delete association and the target in a link/update association.
339
+ def assoc_sha(source, target)
340
+ case target
341
+ when source then source
342
+ when empty_sha then source
343
+ else target
344
+ end
345
+ end
346
+
347
+ # Returns the mode of the specified association.
348
+ def assoc_mode(source, target)
349
+ tree = git.tree.subtree(sha_path(source))
350
+ return nil unless tree
351
+
352
+ mode, sha = tree[target]
353
+ mode
354
+ end
355
+
356
+ # Returns the association type given the source, target, and mode.
357
+ def assoc_type(source, target, mode=assoc_mode(source, target))
358
+ case mode
359
+ when DEFAULT_MODE
360
+ case target
361
+ when empty_sha then :create
362
+ when source then :delete
363
+ else :link
364
+ end
365
+ when UPDATE_MODE
366
+ :update
367
+ else
368
+ :invalid
369
+ end
370
+ end
371
+
372
+ # Returns a hash of associations for the source, mainly used as a
373
+ # convenience method during testing.
374
+ def associations(source, sort=true)
375
+ associations = {}
376
+ links = []
377
+ updates = []
378
+
379
+ each_assoc(source) do |sha, type|
380
+ case type
381
+ when :create, :delete
382
+ associations[type] = true
383
+ when :link
384
+ links << sha
385
+ when :update
386
+ updates << sha
387
+ end
388
+ end
389
+
390
+ unless links.empty?
391
+
392
+ associations[:links] = links
393
+ end
394
+
395
+ unless updates.empty?
396
+ updates.sort! if sort
397
+ associations[:updates] = updates
398
+ end
399
+
400
+ associations
401
+ end
402
+
403
+ # Yield each association for source to the block, with the association sha
404
+ # and type. Returns self.
405
+ def each_assoc(source) # :yields: sha, type
406
+ return self if source.nil?
407
+
408
+ target_tree = git.tree.subtree(sha_path(source))
409
+ target_tree.each_pair do |target, (mode, sha)|
410
+ yield assoc_sha(source, target), assoc_type(source, target, mode)
411
+ end if target_tree
412
+
413
+ self
414
+ end
415
+
416
+ # Yields the sha of each document in the repo, in no particular order and
417
+ # with duplicates for every link/update that has multiple association
418
+ # sources.
419
+ def each
420
+ git.tree.each_pair(true) do |ab, xyz_tree|
421
+ next unless ab.length == 2
422
+
423
+ xyz_tree.each_pair(true) do |xyz, target_tree|
424
+ source = "#{ab}#{xyz}"
425
+
426
+ target_tree.keys.each do |target|
427
+ doc_sha = assoc_sha(source, target)
428
+ yield(doc_sha) if doc_sha
429
+ end
430
+ end
431
+ end
432
+ end
433
+
434
+ # Initializes a Graph for the sha.
435
+ def graph(sha)
436
+ Graph.new(self, sha)
437
+ end
438
+
439
+ # Returns an array of shas representing recent documents added.
440
+ def timeline(options={})
441
+ options = {:n => 10, :offset => 0}.merge(options)
442
+ offset = options[:offset]
443
+ n = options[:n]
444
+
445
+ shas = []
446
+ return shas if n <= 0
447
+
448
+ dates = index.values('date').sort.reverse
449
+ index.each_sha('date', dates) do |sha|
450
+ if block_given?
451
+ next unless yield(sha)
452
+ end
453
+
454
+ if offset > 0
455
+ offset -= 1
456
+ else
457
+ shas << sha
458
+ break if n && shas.length == n
459
+ end
460
+ end
461
+
462
+ shas
463
+ end
464
+
465
+ # Returns an array of revisions (commits) reachable from the sha. These
466
+ # revisions are cached for quick retreival.
467
+ def rev_list(sha)
468
+ sha = sha.to_sym
469
+ unless cache.has_key?(sha)
470
+ cache[sha] = git.rev_list(sha.to_s)
471
+ end
472
+
473
+ cache[sha]
474
+ end
475
+
476
+ # Returns a list of document shas that have been added ('A') between a and
477
+ # b. Deleted ('D') or modified ('M') documents can be specified using
478
+ # type.
479
+ def diff(a, b, type='A')
480
+ if a == b || b.nil?
481
+ return []
482
+ end
483
+
484
+ paths = a.nil? ? git.ls_tree(b) : git.diff_tree(a, b)[type]
485
+ paths.collect! do |path|
486
+ ab, xyz, target = path.split('/', 3)
487
+ assoc_sha("#{ab}#{xyz}", target)
488
+ end
489
+
490
+ paths.compact!
491
+ paths
492
+ end
493
+
494
+ # Generates a status message based on currently uncommitted changes.
495
+ def status
496
+ unless block_given?
497
+ return status {|sha| sha}
498
+ end
499
+
500
+ lines = []
501
+ git.status.each_pair do |path, state|
502
+ ab, xyz, target = path.split('/', 3)
503
+ source = "#{ab}#{xyz}"
504
+
505
+ sha = assoc_sha(source, target)
506
+ type = assoc_type(source, target)
507
+
508
+ status = case assoc_type(source, target)
509
+ when :create
510
+ type = self[sha]['type']
511
+ [type || 'doc', yield(sha)]
512
+ when :link
513
+ ['link', "#{yield(source)} to #{yield(target)}"]
514
+ when :update
515
+ ['update', "#{yield(target)} was #{yield(source)}"]
516
+ when :delete
517
+ ['delete', yield(sha)]
518
+ else
519
+ ['unknown', path]
520
+ end
521
+
522
+ if status
523
+ status.unshift state_str(state)
524
+ lines << status
525
+ end
526
+ end
527
+
528
+ indent = lines.collect {|(state, type, msg)| type.length }.max
529
+ format = "%s %-#{indent}s %s"
530
+ lines.collect! {|ary| format % ary }
531
+ lines.sort!
532
+ lines.join("\n")
533
+ end
534
+
535
+ # Commits any changes to git and writes the index to disk. The commit
536
+ # message is inferred from the status, if left unspecified. Commit will
537
+ # raise an error if there are no changes to commit.
538
+ def commit(msg=status)
539
+ setup unless head
540
+
541
+ sha = git.commit(msg)
542
+ index.write(sha)
543
+ sha
544
+ end
545
+
546
+ # Same as commit but does not check if there are changes to commit, useful
547
+ # when you know there are changes to commit and don't want the overhead of
548
+ # checking for changes.
549
+ def commit!(msg=status)
550
+ setup unless head
551
+
552
+ sha = git.commit!(msg)
553
+ index.write(sha)
554
+ sha
555
+ end
556
+
557
+ def setup(upstream_branch=nil)
558
+ if head
559
+ raise "already setup on: #{branch} (#{head})"
560
+ end
561
+
562
+ if upstream_branch.nil? || upstream_branch.empty?
563
+ tree = Git::Tree.new
564
+ tree[FILE] = [git.default_blob_mode, empty_sha]
565
+ mode, sha = tree.write_to(git)
566
+ git.commit!("setup gitgo", :tree => sha)
567
+
568
+ current_tree = git.tree
569
+ git.reset
570
+ git.tree.merge!(current_tree)
571
+
572
+ return self
573
+ end
574
+
575
+ unless branch?(upstream_branch)
576
+ raise "not a gitgo branch: #{upstream_branch.inspect}"
577
+ end
578
+
579
+ if git.tracking_branch?(upstream_branch)
580
+ git.track(upstream_branch)
581
+ end
582
+ git.merge(upstream_branch)
583
+
584
+ cache.clear
585
+ index.reset
586
+ self
587
+ end
588
+
589
+ def checkout(branch)
590
+ git.checkout(branch)
591
+ self
592
+ end
593
+
594
+ def reset(full=false)
595
+ git.reset(full)
596
+ cache.clear
597
+ index.reset
598
+ self
599
+ end
600
+
601
+ # Sets self as the current Repo for the duration of the block.
602
+ def scope
603
+ Repo.with_env(REPO => self) { yield }
604
+ end
605
+
606
+ protected
607
+
608
+ def select_branches(refs) # :nodoc:
609
+ results = {}
610
+ refs.select do |ref|
611
+ sha = ref.commit.id
612
+ results[sha] ||= (branch?(sha) ? ref : nil)
613
+ end
614
+
615
+ results.values.compact
616
+ end
617
+
618
+ def state_str(state) # :nodoc:
619
+ case state
620
+ when :add then '+'
621
+ when :rm then '-'
622
+ else '~'
623
+ end
624
+ end
625
+ end
626
+ end