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,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