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,34 @@
1
+ module Gitgo
2
+ class Document
3
+ # Raised by Document#validate for an invalid document.
4
+ class InvalidDocumentError < StandardError
5
+ attr_reader :doc
6
+ attr_reader :errors
7
+
8
+ def initialize(doc, errors)
9
+ @doc = doc
10
+ @errors = errors
11
+ super format_errors
12
+ end
13
+
14
+ def format_errors
15
+ lines = []
16
+ errors.keys.sort.each do |key|
17
+ error = errors[key]
18
+ lines << "#{key}: #{error.message} (#{error.class})"
19
+ end
20
+
21
+ lines.unshift header(lines.length)
22
+ lines.join("\n")
23
+ end
24
+
25
+ def header(n)
26
+ case n
27
+ when 0 then "unknown errors"
28
+ when 1 then "found 1 error:"
29
+ else "found #{n} errors:"
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,20 @@
1
+ require 'gitgo/document'
2
+
3
+ module Gitgo
4
+ module Documents
5
+ class Comment < Document
6
+ define_attributes do
7
+ attr_accessor(:content) {|content| validate_not_blank(content) }
8
+ attr_accessor(:re) {|re| validate_format_or_nil(re, SHA) }
9
+ end
10
+
11
+ def normalize!
12
+ if re = attrs['re']
13
+ attrs['re'] = repo.resolve(re)
14
+ end
15
+
16
+ super
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,56 @@
1
+ require 'gitgo/document'
2
+
3
+ module Gitgo
4
+ module Documents
5
+ class Issue < Document
6
+ class << self
7
+ def find(all={}, any=nil, update_index=true)
8
+ self.update_index if update_index
9
+ repo.index.select(
10
+ :basis => basis,
11
+ :all => all,
12
+ :any => any,
13
+ :shas => true,
14
+ :map => true
15
+ ).collect! {|sha| self[sha] }
16
+ end
17
+
18
+ protected
19
+
20
+ def basis
21
+ repo.index['type'][type] - repo.index['filter']['tail']
22
+ end
23
+ end
24
+
25
+ define_attributes do
26
+ attr_accessor(:title)
27
+ attr_accessor(:content)
28
+ end
29
+
30
+ def graph_heads
31
+ graph[graph_head].versions.collect {|head| Issue[head] or raise "missing head: #{head.inspect} (#{sha})" }
32
+ end
33
+
34
+ def graph_titles
35
+ graph_heads.collect {|head| head.title }
36
+ end
37
+
38
+ def graph_tags
39
+ graph_tails.collect {|tail| tail.tags }.flatten.uniq
40
+ end
41
+
42
+ def graph_active?(commit=nil)
43
+ graph_tails.any? {|tail| tail.active?(commit) }
44
+ end
45
+
46
+ def graph_tails
47
+ graph.tails.collect {|tail| Issue[tail] or raise "missing tail: #{tail.inspect} (#{sha})" }
48
+ end
49
+
50
+ def inherit(attrs={})
51
+ attrs['tags'] ||= graph_tags
52
+ self.class.new(attrs, repo)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,941 @@
1
+ require 'grit'
2
+ require 'gitgo/patches/grit'
3
+ require 'gitgo/git/tree'
4
+ require 'gitgo/git/utils'
5
+
6
+ module Gitgo
7
+
8
+ # A wrapper to a Grit::Repo that allows access and modification of a git
9
+ # repository without checking files out (under most circumstances). The api
10
+ # is patterned after the git command line interface.
11
+ #
12
+ # == Usage
13
+ #
14
+ # Checkout, add, and commit new content:
15
+ #
16
+ # git = Git.init("example", :author => "John Doe <jdoe@example.com>")
17
+ # git.add(
18
+ # "README" => "New Project",
19
+ # "lib/project.rb" => "module Project\nend",
20
+ # "remove_this_file" => "won't be here long...")
21
+ #
22
+ # git.commit("setup a new project")
23
+ #
24
+ # Content may be removed as well:
25
+ #
26
+ # git.rm("remove_this_file")
27
+ # git.commit("removed extra file")
28
+ #
29
+ # Now access the content:
30
+ #
31
+ # git["/"] # => ["README", "lib"]
32
+ # git["/lib/project.rb"] # => "module Project\nend"
33
+ # git["/remove_this_file"] # => nil
34
+ #
35
+ # You can go back in time if you wish:
36
+ #
37
+ # git.branch = "gitgo^"
38
+ # git["/remove_this_file"] # => "won't be here long..."
39
+ #
40
+ # For direct access to the Grit objects, use get:
41
+ #
42
+ # git.get("/lib").id # => "cad0dc0df65848aa8f3fee72ce047142ec707320"
43
+ # git.get("/lib/project.rb").id # => "636e25a2c9fe1abc3f4d3f380956800d5243800e"
44
+ #
45
+ # === The Working Tree
46
+ #
47
+ # Changes to the repo are tracked by an in-memory working tree until being
48
+ # committed. Trees can be thought of as a hash of (path, [:mode, sha]) pairs
49
+ # representing the contents of a directory.
50
+ #
51
+ # git = Git.init("example", :author => "John Doe <jdoe@example.com>")
52
+ # git.add(
53
+ # "README" => "New Project",
54
+ # "lib/project.rb" => "module Project\nend"
55
+ # ).commit("added files")
56
+ #
57
+ # git.tree
58
+ # # => {
59
+ # # "README" => [:"100644", "73a86c2718da3de6414d3b431283fbfc074a79b1"],
60
+ # # "lib" => {
61
+ # # "project.rb" => [:"100644", "636e25a2c9fe1abc3f4d3f380956800d5243800e"]
62
+ # # }
63
+ # # }
64
+ #
65
+ # Trees can be collapsed using reset. Afterwards subtrees are only expanded
66
+ # as needed; before expansion they appear as a [:mode, sha] pair and after
67
+ # expansion they appear as a hash. Symbol paths are used to differentiate
68
+ # subtrees (which can be expanded) from blobs (which cannot be expanded).
69
+ #
70
+ # git.reset
71
+ # git.tree
72
+ # # => {
73
+ # # "README" => [:"100644", "73a86c2718da3de6414d3b431283fbfc074a79b1"],
74
+ # # :lib => [:"040000", "cad0dc0df65848aa8f3fee72ce047142ec707320"]
75
+ # # }
76
+ #
77
+ # git.add("lib/project/utils.rb" => "module Project\n module Utils\n end\nend")
78
+ # git.tree
79
+ # # => {
80
+ # # "README" => [:"100644", "73a86c2718da3de6414d3b431283fbfc074a79b1"],
81
+ # # "lib" => {
82
+ # # "project.rb" => [:"100644", "636e25a2c9fe1abc3f4d3f380956800d5243800e"],
83
+ # # "project" => {
84
+ # # "utils.rb" => [:"100644", "c4f9aa58d6d5a2ebdd51f2f628b245f9454ff1a4"]
85
+ # # }
86
+ # # }
87
+ # # }
88
+ #
89
+ # git.rm("README")
90
+ # git.tree
91
+ # # => {
92
+ # # "lib" => {
93
+ # # "project.rb" => [:"100644", "636e25a2c9fe1abc3f4d3f380956800d5243800e"],
94
+ # # "project" => {
95
+ # # "utils.rb" => [:"100644", "c4f9aa58d6d5a2ebdd51f2f628b245f9454ff1a4"]
96
+ # # }
97
+ # # }
98
+ # # }
99
+ #
100
+ # The working tree can be compared with the commit tree to produce a list of
101
+ # files that have been added and removed using the status method:
102
+ #
103
+ # git.status
104
+ # # => {
105
+ # # "README" => :rm
106
+ # # "lib/project/utils.rb" => :add
107
+ # # }
108
+ #
109
+ # == Tracking, Push/Pull
110
+ #
111
+ # Git provides limited support for setting a tracking branch and doing
112
+ # push/pull from tracking branches without checking the gitgo branch out.
113
+ # More complicated operations can are left to the command line, where the
114
+ # current branch can be directly manipulated by the git program.
115
+ #
116
+ # Unlike git (the program), Git (the class) requires the upstream branch
117
+ # setup by 'git branch --track' to be an existing tracking branch. As an
118
+ # example, if you were to setup this:
119
+ #
120
+ # % git branch --track remote/branch
121
+ #
122
+ # Or equivalently this:
123
+ #
124
+ # git = Git.init
125
+ # git.track "remote/branch"
126
+ #
127
+ # Then Git would assume:
128
+ #
129
+ # * the upstream branch is 'remote/branch'
130
+ # * the tracking branch is 'remotes/remote/branch'
131
+ # * the 'branch.name.remote' config is 'remote'
132
+ # * the 'branch.name.merge' config is 'refs/heads/branch'
133
+ #
134
+ # If ever these assumptions are broken, for instance if the gitgo branch is
135
+ # manually set up to track a local branch, methods like pull/push could
136
+ # cause odd failures. To help check:
137
+ #
138
+ # * track will raise an error if the upstream branch is not a tracking
139
+ # branch
140
+ # * upstream_branch raises an error if the 'branch.name.merge' config
141
+ # doesn't follow the 'ref/heads/branch' pattern
142
+ # * pull/push raise an error given a non-tracking branch
143
+ #
144
+ # Under normal circumstances, all these assumptions will be met.
145
+ class Git
146
+ class << self
147
+ # Creates a Git instance for path, initializing the repo if necessary.
148
+ def init(path=Dir.pwd, options={})
149
+ unless File.exists?(path)
150
+ FileUtils.mkdir_p(path)
151
+
152
+ Dir.chdir(path) do
153
+ bare = options[:is_bare] ? true : false
154
+ gitdir = bare || path =~ /\.git$/ ? path : File.join(path, ".git")
155
+
156
+ Utils.with_env('GIT_DIR' => gitdir) do
157
+ git = Grit::Git.new(gitdir)
158
+ git.init({:bare => bare})
159
+ end
160
+ end
161
+ end
162
+
163
+ new(path, options)
164
+ end
165
+
166
+ # Sets up Grit to log to the device for the duration of the block.
167
+ # Primarily useful during debugging, and inadvisable otherwise because
168
+ # the logger must be shared among all Grit::Repo instances (this is a
169
+ # consequence of how logging is implemented in Grit).
170
+ def debug(dev=$stdout)
171
+ current_logger = Grit.logger
172
+ current_debug = Grit.debug
173
+ begin
174
+ Grit.logger = Logger.new(dev)
175
+ Grit.debug = true
176
+ yield
177
+ ensure
178
+ Grit.logger = current_logger
179
+ Grit.debug = current_debug
180
+ end
181
+ end
182
+
183
+ # Returns the git version as an array of integers like [1,6,4,2]. The
184
+ # version query performed once and then cached.
185
+ def version
186
+ @version ||= `git --version`.split(/\s/).last.split(".").collect {|i| i.to_i}
187
+ end
188
+
189
+ # Checks if the git version is compatible with GIT_VERSION. This check is
190
+ # performed once and then cached.
191
+ def version_ok?
192
+ @version_ok ||= ((GIT_VERSION <=> version) <= 0)
193
+ end
194
+ end
195
+
196
+ include Enumerable
197
+ include Utils
198
+
199
+ # The default branch
200
+ DEFAULT_BRANCH = 'gitgo'
201
+
202
+ # The default upstream branch for push/pull
203
+ DEFAULT_UPSTREAM_BRANCH = 'origin/gitgo'
204
+
205
+ # The default directory for gitgo-related files
206
+ DEFAULT_WORK_DIR = 'gitgo'
207
+
208
+ # The default blob mode used for added blobs
209
+ DEFAULT_BLOB_MODE = '100644'.to_sym
210
+
211
+ # The default tree mode used for added trees
212
+ DEFAULT_TREE_MODE = '40000'.to_sym
213
+
214
+ # A regexp matching a valid sha sum
215
+ SHA = /\A[A-Fa-f\d]{40}\z/
216
+
217
+ # The minimum required version of git (see Git.version_ok?)
218
+ GIT_VERSION = [1,6,4,2]
219
+
220
+ # The internal Grit::Repo
221
+ attr_reader :grit
222
+
223
+ # The gitgo branch
224
+ attr_reader :branch
225
+
226
+ # The in-memory working tree tracking any adds and removes
227
+ attr_reader :tree
228
+
229
+ # Returns the sha for the branch
230
+ attr_reader :head
231
+
232
+ # The path to the instance working directory
233
+ attr_reader :work_dir
234
+
235
+ # The path to the temporary working tree
236
+ attr_reader :work_tree
237
+
238
+ # The path to the temporary index_file
239
+ attr_reader :index_file
240
+
241
+ # The default blob mode for self (see DEFAULT_BLOB_MODE)
242
+ attr_reader :default_blob_mode
243
+
244
+ # The default tree mode for self (see DEFAULT_TREE_MODE)
245
+ attr_reader :default_tree_mode
246
+
247
+ # Initializes a new Git bound to the repository at the specified path.
248
+ # Raises an error if no such repository exists. Options can specify the
249
+ # following:
250
+ #
251
+ # :branch the branch for self
252
+ # :author the author for self
253
+ # + any Grit::Repo options
254
+ #
255
+ def initialize(path=Dir.pwd, options={})
256
+ @grit = path.kind_of?(Grit::Repo) ? path : Grit::Repo.new(path, options)
257
+ @sandbox = false
258
+ @branch = nil
259
+ @work_dir = path(options[:work_dir] || DEFAULT_WORK_DIR)
260
+ @work_tree = options[:work_tree] || File.join(work_dir, 'tmp', object_id.to_s)
261
+ @index_file = options[:index_file] || File.join(work_dir, 'tmp', "#{object_id}.index")
262
+
263
+ self.author = options[:author] || nil
264
+ self.checkout options[:branch] || DEFAULT_BRANCH
265
+ self.default_blob_mode = options[:default_blob_mode] || DEFAULT_BLOB_MODE
266
+ self.default_tree_mode = options[:default_tree_mode] || DEFAULT_TREE_MODE
267
+ end
268
+
269
+ # Sets the default blob mode
270
+ def default_blob_mode=(mode)
271
+ @default_blob_mode = mode.to_sym
272
+ end
273
+
274
+ # Sets the default tree mode
275
+ def default_tree_mode=(mode)
276
+ @default_tree_mode = mode.to_sym
277
+ end
278
+
279
+ # Returns the specified path relative to the git repository (ie the .git
280
+ # directory). With no arguments path returns the repository path.
281
+ def path(*segments)
282
+ segments.collect! {|segment| segment.to_s }
283
+ File.join(grit.path, *segments)
284
+ end
285
+
286
+ # Returns the configured author (which should be a Grit::Actor, or similar).
287
+ # If no author is is currently set, a default author will be determined from
288
+ # the git configurations.
289
+ def author
290
+ @author ||= begin
291
+ name = grit.config['user.name']
292
+ email = grit.config['user.email']
293
+ Grit::Actor.new(name, email)
294
+ end
295
+ end
296
+
297
+ # Sets the author. The input may be a Grit::Actor, an array like [author,
298
+ # email], a git-formatted author string, or nil.
299
+ def author=(input)
300
+ @author = case input
301
+ when Grit::Actor, nil then input
302
+ when Array then Grit::Actor.new(*input)
303
+ when String then Grit::Actor.from_string(*input)
304
+ else raise "could not convert to Grit::Actor: #{input.class}"
305
+ end
306
+ end
307
+
308
+ # Returns a full sha for the identifier, as determined by rev_parse. All
309
+ # valid sha string are returned immediately; there is no guarantee the sha
310
+ # will point to an object currently in the repo.
311
+ #
312
+ # Returns nil the identifier cannot be resolved to an sha.
313
+ def resolve(id)
314
+ case id
315
+ when SHA, nil then id
316
+ else rev_parse(id).first
317
+ end
318
+ end
319
+
320
+ # Returns the type of the object identified by sha; the output of:
321
+ #
322
+ # % git cat-file -t sha
323
+ #
324
+ def type(sha)
325
+ grit.git.cat_file({:t => true}, sha)
326
+ end
327
+
328
+ # Gets the specified object, returning an instance of the appropriate Grit
329
+ # class. Raises an error for unknown types.
330
+ def get(type, id)
331
+ case type.to_sym
332
+ when :blob then grit.blob(id)
333
+ when :tree then grit.tree(id)
334
+ when :commit then grit.commit(id)
335
+ when :tag
336
+
337
+ object = grit.git.ruby_git.get_object_by_sha1(id)
338
+ if object.type == :tag
339
+ Grit::Tag.new(object.tag, grit.commit(object.object))
340
+ else
341
+ nil
342
+ end
343
+
344
+ else raise "unknown type: #{type}"
345
+ end
346
+ end
347
+
348
+ # Sets an object of the specified type into the git repository and returns
349
+ # the object sha.
350
+ def set(type, content)
351
+ grit.git.put_raw_object(content, type.to_s)
352
+ end
353
+
354
+ # Gets the content for path; either the blob data or an array of content
355
+ # names for a tree. Returns nil if path doesn't exist.
356
+ def [](path, entry=false, committed=false)
357
+ tree = committed ? commit_tree : @tree
358
+
359
+ segments = split(path)
360
+ unless basename = segments.pop
361
+ return entry ? tree : tree.keys
362
+ end
363
+
364
+ unless tree = tree.subtree(segments)
365
+ return nil
366
+ end
367
+
368
+ obj = tree[basename]
369
+ return obj if entry
370
+
371
+ case obj
372
+ when Array then get(:blob, obj[1]).data
373
+ when Tree then obj.keys
374
+ else nil
375
+ end
376
+ end
377
+
378
+ # Sets content for path. The content can either be:
379
+ #
380
+ # * a string of content
381
+ # * a symbol sha, translated to [default_blob_mode, sha]
382
+ # * an array like [mode, sha]
383
+ # * a nil, to remove content
384
+ #
385
+ # Note that set content is immediately stored in the repo and tracked in
386
+ # the in-memory working tree but not committed until commit is called.
387
+ def []=(path, content=nil)
388
+ segments = split(path)
389
+ unless basename = segments.pop
390
+ raise "invalid path: #{path.inspect}"
391
+ end
392
+
393
+ tree = @tree.subtree(segments, true)
394
+ tree[basename] = convert_to_entry(content)
395
+ end
396
+
397
+ # Sets branch to track the specified upstream_branch. The upstream_branch
398
+ # must be an existing tracking branch; an error is raised if this
399
+ # requirement is not met (see the Tracking, Push/Pull notes above).
400
+ def track(upstream_branch)
401
+ if upstream_branch.nil?
402
+ # currently grit.config does not support unsetting (grit-2.0.0)
403
+ grit.git.config({:unset => true}, "branch.#{branch}.remote")
404
+ grit.git.config({:unset => true}, "branch.#{branch}.merge")
405
+ else
406
+ unless tracking_branch?(upstream_branch)
407
+ raise "the upstream branch is not a tracking branch: #{upstream_branch}"
408
+ end
409
+
410
+ remote, remote_branch = upstream_branch.split('/', 2)
411
+ grit.config["branch.#{branch}.remote"] = remote
412
+ grit.config["branch.#{branch}.merge"] = "refs/heads/#{remote_branch}"
413
+ end
414
+ end
415
+
416
+ # Returns the upstream_branch as setup by track. Raises an error if the
417
+ # 'branch.name.merge' config doesn't follow the pattern 'ref/heads/branch'
418
+ # (see the Tracking, Push/Pull notes above).
419
+ def upstream_branch
420
+ remote = grit.config["branch.#{branch}.remote"]
421
+ merge = grit.config["branch.#{branch}.merge"]
422
+
423
+ # No remote, no merge, no tracking.
424
+ if remote.nil? || merge.nil?
425
+ return nil
426
+ end
427
+
428
+ unless merge =~ /^refs\/heads\/(.*)$/
429
+ raise "invalid upstream branch"
430
+ end
431
+
432
+ "#{remote}/#{$1}"
433
+ end
434
+
435
+ # Returns the remote as setup by track, or origin if tracking has not been
436
+ # setup.
437
+ def remote
438
+ grit.config["branch.#{branch}.remote"] || 'origin'
439
+ end
440
+
441
+ # Returns true if the specified ref is a tracking branch, ie it is the
442
+ # name of an existing remote ref.
443
+ def tracking_branch?(ref)
444
+ ref && grit.remotes.find {|remote| remote.name == ref }
445
+ end
446
+
447
+ #########################################################################
448
+ # Git API
449
+ #########################################################################
450
+
451
+ # Adds a hash of (path, content) pairs (see AGET for valid content).
452
+ def add(paths)
453
+ paths.each_pair do |path, content|
454
+ self[path] = content
455
+ end
456
+
457
+ self
458
+ end
459
+
460
+ # Removes the content at each of the specified paths
461
+ def rm(*paths)
462
+ paths.each {|path| self[path] = nil }
463
+ self
464
+ end
465
+
466
+ # Commits the in-memory working tree to branch with the specified message
467
+ # and returns the sha for the new commit. The branch is created if it
468
+ # doesn't already exist. Options can specify (as symbols):
469
+ #
470
+ # tree:: The sha of the tree this commit points to (default the
471
+ # sha for tree, the in-memory working tree)
472
+ # parents:: An array of shas representing parent commits (default the
473
+ # current commit)
474
+ # author:: A Grit::Actor, or similar representing the commit author
475
+ # (default author)
476
+ # authored_date:: The authored date (default now)
477
+ # committer:: A Grit::Actor, or similar representing the user
478
+ # making the commit (default author)
479
+ # committed_date:: The authored date (default now)
480
+ #
481
+ # Raises an error if there are no changes to commit.
482
+ def commit(message, options={})
483
+ raise "no changes to commit" if status.empty?
484
+ commit!(message, options)
485
+ end
486
+
487
+ # Same as commit but does not check if there are changes to commit, useful
488
+ # when you know there are changes to commit and don't want the overhead of
489
+ # checking for changes.
490
+ def commit!(message, options={})
491
+ now = Time.now
492
+
493
+ sha = options.delete(:tree) || tree.write_to(self).at(1)
494
+ parents = options.delete(:parents) || (head ? [head] : [])
495
+ author = options[:author] || self.author
496
+ authored_date = options[:authored_date] || now
497
+ committer = options[:committer] || author
498
+ committed_date = options[:committed_date] || now
499
+
500
+ # commit format:
501
+ #---------------------------------------------------
502
+ # tree sha
503
+ # parent sha
504
+ # author name <email> time_as_int zone_offset
505
+ # committer name <email> time_as_int zone_offset
506
+ #
507
+ # messsage
508
+ #
509
+ #---------------------------------------------------
510
+ # Note there is a trailing newline after the message.
511
+ #
512
+ lines = []
513
+ lines << "tree #{sha}"
514
+ parents.each do |parent|
515
+ lines << "parent #{parent}"
516
+ end
517
+ lines << "author #{author.name} <#{author.email}> #{authored_date.strftime("%s %z")}"
518
+ lines << "committer #{committer.name} <#{committer.email}> #{committed_date.strftime("%s %z")}"
519
+ lines << ""
520
+ lines << message
521
+ lines << ""
522
+
523
+ @head = set('commit', lines.join("\n"))
524
+ grit.update_ref(branch, head)
525
+
526
+ head
527
+ end
528
+
529
+ # Resets the working tree. Also reinitializes grit if full is specified;
530
+ # this can be useful after operations that change configurations or the
531
+ # cached packs (see gc).
532
+ def reset(full=false)
533
+ @grit = Grit::Repo.new(path, :is_bare => grit.bare) if full
534
+ commit = grit.commits(branch, 1).first
535
+ @head = commit ? commit.sha : nil
536
+ @tree = commit_tree
537
+
538
+ self
539
+ end
540
+
541
+ # Returns a hash of (path, state) pairs indicating paths that have been
542
+ # added or removed. States are add/rm/mod only -- renames, moves, and
543
+ # copies are not detected.
544
+ def status(full=false)
545
+ a = commit_tree.flatten
546
+ b = tree.flatten
547
+
548
+ diff = {}
549
+ (a.keys | b.keys).collect do |key|
550
+ a_entry = a.has_key?(key) ? a[key] : nil
551
+ b_entry = b.has_key?(key) ? b[key] : nil
552
+
553
+ change = case
554
+ when a_entry && b_entry
555
+ next unless a_entry != b_entry
556
+ :mod
557
+ when a_entry
558
+ :rm
559
+ when b_entry
560
+ :add
561
+ end
562
+
563
+ diff[key] = full ? [change, a_entry || [], b_entry || []] : change
564
+ end
565
+ diff
566
+ end
567
+
568
+ # Sets the current branch and updates tree.
569
+ #
570
+ # Checkout does not actually checkout any files unless a block is given.
571
+ # In that case, the current branch will be checked out for the duration of
572
+ # the block into work_tree; a gitgo-specific directory distinct from the
573
+ # user's working directory. Checkout with a block permits the execution of
574
+ # git commands that must be performed in a working directory.
575
+ #
576
+ # Returns self.
577
+ def checkout(branch=self.branch) # :yields: working_dir
578
+ if branch != @branch
579
+ @branch = branch
580
+ reset
581
+ end
582
+
583
+ if block_given?
584
+ sandbox do |git, work_tree, index_file|
585
+ git.read_tree({:index_output => index_file}, branch)
586
+ git.checkout_index({:a => true})
587
+ yield(work_tree)
588
+ end
589
+ end
590
+
591
+ self
592
+ end
593
+
594
+ # Fetches from the remote.
595
+ def fetch(remote=self.remote)
596
+ sandbox {|git,w,i| git.fetch({}, remote) }
597
+ self
598
+ end
599
+
600
+ # Returns true if a merge update is available for branch.
601
+ def merge?(treeish=upstream_branch)
602
+ sandbox do |git, work_tree, index_file|
603
+ des, src = safe_rev_parse(branch, treeish)
604
+
605
+ case
606
+ when src.nil? then false
607
+ when des.nil? then true
608
+ else des != src && git.merge_base({}, des, src).chomp("\n") != src
609
+ end
610
+ end
611
+ end
612
+
613
+ # Merges the specified reference with the current branch, fast-forwarding
614
+ # when possible. This method does not need to checkout the branch into a
615
+ # working directory to perform the merge.
616
+ def merge(treeish=upstream_branch)
617
+ sandbox do |git, work_tree, index_file|
618
+ des, src = safe_rev_parse(branch, treeish)
619
+ base = des.nil? ? nil : git.merge_base({}, des, src).chomp("\n")
620
+
621
+ case
622
+ when base == src
623
+ break
624
+ when base == des
625
+ # fast forward situation
626
+ grit.update_ref(branch, src)
627
+ else
628
+ # todo: add rebase as an option
629
+
630
+ git.read_tree({
631
+ :m => true, # merge
632
+ :i => true, # without a working tree
633
+ :trivial => true, # only merge if no file-level merges are required
634
+ :aggressive => true, # allow resolution of removes
635
+ :index_output => index_file
636
+ }, base, branch, src)
637
+
638
+ commit!("gitgo merge of #{treeish} into #{branch}",
639
+ :tree => git.write_tree.chomp("\n"),
640
+ :parents => [des, src]
641
+ )
642
+ end
643
+
644
+ reset
645
+ end
646
+
647
+ self
648
+ end
649
+
650
+ # Pushes branch to the tracking branch. No other branches are pushed.
651
+ # Raises an error if given a non-tracking branch (see the Tracking,
652
+ # Push/Pull notes above).
653
+ def push(tracking_branch=upstream_branch)
654
+ sandbox do |git, work_tree, index_file|
655
+ remote, remote_branch = parse_tracking_branch(tracking_branch)
656
+ git.push({}, remote, "#{branch}:#{remote_branch}") unless head.nil?
657
+ end
658
+ end
659
+
660
+ # Fetches the tracking branch and merges with branch. No other branches
661
+ # are fetched. Raises an error if given a non-tracking branch (see the
662
+ # Tracking, Push/Pull notes above).
663
+ def pull(tracking_branch=upstream_branch)
664
+ sandbox do |git, work_tree, index_file|
665
+ remote, remote_branch = parse_tracking_branch(tracking_branch)
666
+ git.fetch({}, remote, "#{remote_branch}:remotes/#{tracking_branch}")
667
+ merge(tracking_branch)
668
+ end
669
+ reset
670
+ end
671
+
672
+ # Clones self into the specified path and sets up tracking of branch in
673
+ # the new grit. Clone was primarily implemented for testing; normally
674
+ # clones are managed by the user.
675
+ def clone(path, options={})
676
+ with_env do
677
+ grit.git.clone(options, grit.path, path)
678
+ clone = Grit::Repo.new(path)
679
+
680
+ if options[:bare]
681
+ # bare origins directly copy branch heads without mapping them to
682
+ # 'refs/remotes/origin/' (see git-clone docs). this maps the branch
683
+ # head so the bare grit can checkout branch
684
+ clone.git.remote({}, "add", "origin", grit.path)
685
+ clone.git.fetch({}, "origin")
686
+ clone.git.branch({}, "-D", branch)
687
+ end
688
+
689
+ # sets up branch to track the origin to enable pulls
690
+ clone.git.branch({:track => true}, branch, "origin/#{branch}")
691
+ self.class.new(clone, :branch => branch, :author => author)
692
+ end
693
+ end
694
+
695
+ # Returns an array of shas identified by the args (ex a sha, short-sha, or
696
+ # treeish). Raises an error if not all args can be converted into a valid
697
+ # sha.
698
+ #
699
+ # Note there is no guarantee the resulting shas indicate objects in the
700
+ # repository; not even 'git rev-parse' will do that.
701
+ def rev_parse(*args)
702
+ return args if args.empty?
703
+
704
+ sandbox do |git,w,i|
705
+ shas = git.run('', :rev_parse, '', {}, args).split("\n")
706
+
707
+ # Grit::Git#run only makes stdout available, not stderr, and so this
708
+ # wonky check relies on the fact that git rev-parse will print the
709
+ # unresolved ref to stdout and quit if it can't succeed. That means
710
+ # the last printout will not look like a sha in the event of an error.
711
+ unless shas.last.to_s =~ SHA
712
+ raise "could not resolve to a sha: #{shas.last}"
713
+ end
714
+
715
+ shas
716
+ end
717
+ end
718
+
719
+ # Same as rev_parse but always returns an array. Arguments that cannot be
720
+ # converted to a valid sha will be represented by nil. This method is
721
+ # slower than rev_parse because it converts arguments one by one
722
+ def safe_rev_parse(*args)
723
+ args.collect! {|arg| rev_parse(arg).at(0) rescue nil }
724
+ end
725
+
726
+ # Returns an array of revisions (commits) reachable from the treeish.
727
+ def rev_list(*treeishs)
728
+ return treeishs if treeishs.empty?
729
+ sandbox {|git,w,i| git.run('', :rev_list, '', {}, treeishs).split("\n") }
730
+ end
731
+
732
+ # Retuns an array of added, deleted, and modified files keyed by 'A', 'D',
733
+ # and 'M' respectively.
734
+ def diff_tree(a, b="^#{a}")
735
+ sandbox do |git,w,i|
736
+ output = git.run('', :diff_tree, '', {:r => true, :name_status => true}, [a, b])
737
+
738
+ diff = {'A' => [], 'D' => [], 'M' => []}
739
+ output.split("\n").each do |line|
740
+ mode, path = line.split(' ', 2)
741
+ array = diff[mode] or raise "unexpected diff output:\n#{output}"
742
+ array << path
743
+ end
744
+
745
+ diff
746
+ end
747
+ end
748
+
749
+ # Returns an array of paths at the specified treeish.
750
+ def ls_tree(treeish)
751
+ sandbox do |git,w,i|
752
+ git.run('', :ls_tree, '', {:r => true, :name_only => true}, [treeish]).split("\n")
753
+ end
754
+ end
755
+
756
+ # Greps for paths matching the pattern, at the specified treeish. Each
757
+ # matching path and blob are yielded to the block.
758
+ #
759
+ # Instead of a pattern, a hash of grep options may be provided. The
760
+ # following options are allowed:
761
+ #
762
+ # :ignore_case
763
+ # :invert_match
764
+ # :fixed_strings
765
+ # :e
766
+ #
767
+ def grep(pattern, treeish=head) # :yields: path, blob
768
+ options = pattern.respond_to?(:merge) ? pattern.dup : {:e => pattern}
769
+ options.delete_if {|key, value| nil_or_empty?(value) }
770
+ options = options.merge!(
771
+ :cached => true,
772
+ :name_only => true,
773
+ :full_name => true
774
+ )
775
+
776
+ unless commit = grit.commit(treeish)
777
+ raise "unknown commit: #{treeish}"
778
+ end
779
+
780
+ sandbox do |git, work_tree, index_file|
781
+ git.read_tree({:index_output => index_file}, commit.id)
782
+ git.grep(options).split("\n").each do |path|
783
+ yield(path, (commit.tree / path))
784
+ end
785
+ end
786
+ self
787
+ end
788
+
789
+ # Greps for trees matching the pattern, at the specified treeish. Each
790
+ # matching path and tree are yielded to the block.
791
+ #
792
+ # Instead of a pattern, a hash of grep options may be provided. The
793
+ # following options are allowed:
794
+ #
795
+ # :ignore_case
796
+ # :invert_match
797
+ # :fixed_strings
798
+ # :e
799
+ #
800
+ def tree_grep(pattern, treeish=head) # :yields: path, tree
801
+ options = pattern.respond_to?(:merge) ? pattern.dup : {:e => pattern}
802
+ options.delete_if {|key, value| nil_or_empty?(value) }
803
+
804
+ unless commit = grit.commit(treeish)
805
+ raise "unknown commit: #{treeish}"
806
+ end
807
+
808
+ sandbox do |git, work_tree, index_file|
809
+ postfix = options.empty? ? '' : begin
810
+ grep_options = git.transform_options(options)
811
+ " | grep #{grep_options.join(' ')}"
812
+ end
813
+
814
+ stdout, stderr = git.sh("#{Grit::Git.git_binary} ls-tree -r --name-only #{git.e(commit.id)} #{postfix}")
815
+ stdout.split("\n").each do |path|
816
+ yield(path, commit.tree / path)
817
+ end
818
+ end
819
+ self
820
+ end
821
+
822
+ # Greps for commits with messages matching the pattern, starting at the
823
+ # specified treeish. Each matching commit yielded to the block.
824
+ #
825
+ # Instead of a pattern, a hash of git-log options may be provided.
826
+ def commit_grep(pattern, treeish=head) # :yields: commit
827
+ options = pattern.respond_to?(:merge) ? pattern.dup : {:grep => pattern}
828
+ options.delete_if {|key, value| nil_or_empty?(value) }
829
+ options[:format] = "%H"
830
+
831
+ sandbox do |git, work_tree, index_file|
832
+ git.log(options, treeish).split("\n").each do |sha|
833
+ yield grit.commit(sha)
834
+ end
835
+ end
836
+ self
837
+ end
838
+
839
+ # Peforms 'git prune' and returns self.
840
+ def prune
841
+ sandbox {|git,w,i| git.prune }
842
+ self
843
+ end
844
+
845
+ # Performs 'git gc' and resets self so that grit will use the updated pack
846
+ # files. Returns self.
847
+ def gc
848
+ sandbox {|git,w,i| git.gc }
849
+ reset(true)
850
+ end
851
+
852
+ # Performs 'git fsck' and returns the output.
853
+ def fsck
854
+ sandbox do |git, work_tree, index_file|
855
+ stdout, stderr = git.sh("#{Grit::Git.git_binary} fsck")
856
+ "#{stdout}#{stderr}"
857
+ end
858
+ end
859
+
860
+ # Returns a hash of repo statistics parsed from 'git count-objects
861
+ # --verbose'.
862
+ def stats
863
+ sandbox do |git, work_tree, index_file|
864
+ stdout, stderr = git.sh("#{Grit::Git.git_binary} count-objects --verbose")
865
+ stats = YAML.load(stdout)
866
+
867
+ unless stats.kind_of?(Hash)
868
+ raise stderr
869
+ end
870
+
871
+ stats
872
+ end
873
+ end
874
+
875
+ # Creates and sets a work tree and index file so that git will have an
876
+ # environment it can work in. Specifically sandbox creates an empty
877
+ # work_tree and index_file, the sets these ENV variables:
878
+ #
879
+ # GIT_DIR:: set to the repo path
880
+ # GIT_WORK_TREE:: work_tree,
881
+ # GIT_INDEX_FILE:: index_file
882
+ #
883
+ # Once these are set, sandbox yields grit.git, the work_tree, and
884
+ # index_file to the block. After the block returns, the work_tree and
885
+ # index_file are removed. Nested calls to sandbox will reuse the previous
886
+ # sandbox and yield immediately to the block.
887
+ #
888
+ # Note that no content is checked out into work_tree or index_file by this
889
+ # method; that must be done as needed within the block.
890
+ def sandbox # :yields: git, work_tree, index_file
891
+ if @sandbox
892
+ return yield(grit.git, work_tree, index_file)
893
+ end
894
+
895
+ FileUtils.rm_r(work_tree) if File.exists?(work_tree)
896
+ FileUtils.rm(index_file) if File.exists?(index_file)
897
+
898
+ begin
899
+ FileUtils.mkdir_p(work_tree)
900
+ @sandbox = true
901
+
902
+ with_env(
903
+ 'GIT_DIR' => grit.path,
904
+ 'GIT_WORK_TREE' => work_tree,
905
+ 'GIT_INDEX_FILE' => index_file
906
+ ) do
907
+
908
+ yield(grit.git, work_tree, index_file)
909
+ end
910
+ ensure
911
+ FileUtils.rm_r(work_tree) if File.exists?(work_tree)
912
+ FileUtils.rm(index_file) if File.exists?(index_file)
913
+ @sandbox = false
914
+ end
915
+ end
916
+
917
+ protected
918
+
919
+ def parse_tracking_branch(ref) # :nodoc:
920
+ unless tracking_branch?(ref)
921
+ raise "not a tracking branch: #{ref.inspect}"
922
+ end
923
+
924
+ ref.split('/', 2)
925
+ end
926
+
927
+ def convert_to_entry(content) # :nodoc:
928
+ case content
929
+ when String then [default_blob_mode, set(:blob, content)]
930
+ when Symbol then [default_blob_mode, content]
931
+ when Array, nil then content
932
+ else raise "invalid content: #{content.inspect}"
933
+ end
934
+ end
935
+
936
+ def commit_tree # :nodoc:
937
+ tree = head ? get(:commit, head).tree : nil
938
+ Tree.new(tree)
939
+ end
940
+ end
941
+ end