gitgo 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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