joelmoss-grit 1.1.4

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 (70) hide show
  1. data/API.txt +101 -0
  2. data/History.txt +49 -0
  3. data/LICENSE +22 -0
  4. data/README.md +210 -0
  5. data/VERSION.yml +4 -0
  6. data/examples/ex_add_commit.rb +13 -0
  7. data/examples/ex_index.rb +14 -0
  8. data/lib/grit.rb +68 -0
  9. data/lib/grit/actor.rb +36 -0
  10. data/lib/grit/blame.rb +61 -0
  11. data/lib/grit/blob.rb +126 -0
  12. data/lib/grit/commit.rb +242 -0
  13. data/lib/grit/commit_stats.rb +128 -0
  14. data/lib/grit/config.rb +44 -0
  15. data/lib/grit/diff.rb +70 -0
  16. data/lib/grit/errors.rb +7 -0
  17. data/lib/grit/git-ruby.rb +186 -0
  18. data/lib/grit/git-ruby/commit_db.rb +52 -0
  19. data/lib/grit/git-ruby/file_index.rb +193 -0
  20. data/lib/grit/git-ruby/git_object.rb +350 -0
  21. data/lib/grit/git-ruby/internal/file_window.rb +58 -0
  22. data/lib/grit/git-ruby/internal/loose.rb +137 -0
  23. data/lib/grit/git-ruby/internal/pack.rb +382 -0
  24. data/lib/grit/git-ruby/internal/raw_object.rb +37 -0
  25. data/lib/grit/git-ruby/object.rb +325 -0
  26. data/lib/grit/git-ruby/repository.rb +736 -0
  27. data/lib/grit/git.rb +148 -0
  28. data/lib/grit/index.rb +122 -0
  29. data/lib/grit/lazy.rb +33 -0
  30. data/lib/grit/merge.rb +45 -0
  31. data/lib/grit/ref.rb +99 -0
  32. data/lib/grit/repo.rb +565 -0
  33. data/lib/grit/ruby1.9.rb +7 -0
  34. data/lib/grit/status.rb +151 -0
  35. data/lib/grit/submodule.rb +88 -0
  36. data/lib/grit/tag.rb +66 -0
  37. data/lib/grit/tree.rb +123 -0
  38. data/lib/open3_detach.rb +46 -0
  39. data/test/bench/benchmarks.rb +126 -0
  40. data/test/helper.rb +18 -0
  41. data/test/profile.rb +21 -0
  42. data/test/suite.rb +6 -0
  43. data/test/test_actor.rb +35 -0
  44. data/test/test_blame.rb +32 -0
  45. data/test/test_blame_tree.rb +33 -0
  46. data/test/test_blob.rb +83 -0
  47. data/test/test_commit.rb +207 -0
  48. data/test/test_commit_stats.rb +33 -0
  49. data/test/test_commit_write.rb +20 -0
  50. data/test/test_config.rb +58 -0
  51. data/test/test_diff.rb +18 -0
  52. data/test/test_file_index.rb +56 -0
  53. data/test/test_git.rb +84 -0
  54. data/test/test_grit.rb +32 -0
  55. data/test/test_head.rb +47 -0
  56. data/test/test_index_status.rb +40 -0
  57. data/test/test_merge.rb +17 -0
  58. data/test/test_raw.rb +16 -0
  59. data/test/test_real.rb +19 -0
  60. data/test/test_reality.rb +17 -0
  61. data/test/test_remote.rb +14 -0
  62. data/test/test_repo.rb +347 -0
  63. data/test/test_rubygit.rb +188 -0
  64. data/test/test_rubygit_alt.rb +40 -0
  65. data/test/test_rubygit_index.rb +76 -0
  66. data/test/test_rubygit_iv2.rb +28 -0
  67. data/test/test_submodule.rb +69 -0
  68. data/test/test_tag.rb +67 -0
  69. data/test/test_tree.rb +101 -0
  70. metadata +141 -0
data/lib/grit/repo.rb ADDED
@@ -0,0 +1,565 @@
1
+ module Grit
2
+
3
+ class RemoteError < StandardError; end
4
+ class RemoteNonexistentError < RemoteError; end
5
+ class BranchNonexistentError < RemoteError; end
6
+ class RemoteBranchExistsError < RemoteError; end
7
+ class RemoteUninitializedError < RemoteError; end
8
+ class ConflictError < RemoteError; end
9
+
10
+ class Repo
11
+ DAEMON_EXPORT_FILE = 'git-daemon-export-ok'
12
+
13
+ # The path of the git repo as a String
14
+ attr_accessor :path
15
+ attr_accessor :working_dir
16
+ attr_reader :bare
17
+
18
+ # The git command line interface object
19
+ attr_accessor :git
20
+
21
+ # Create a new Repo instance
22
+ # +path+ is the path to either the root git directory or the bare git repo
23
+ # +options+ :is_bare force to load a bare repo
24
+ #
25
+ # Examples
26
+ # g = Repo.new("/Users/tom/dev/grit")
27
+ # g = Repo.new("/Users/tom/public/grit.git")
28
+ #
29
+ # Returns Grit::Repo
30
+ def initialize(path, options = {})
31
+ epath = File.expand_path(path)
32
+
33
+ if File.exist?(File.join(epath, '.git'))
34
+ self.working_dir = epath
35
+ self.path = File.join(epath, '.git')
36
+ @bare = false
37
+ elsif File.exist?(epath) && (epath =~ /\.git$/ || options[:is_bare])
38
+ self.path = epath
39
+ @bare = true
40
+ elsif File.exist?(epath)
41
+ raise InvalidGitRepositoryError.new(epath)
42
+ else
43
+ raise NoSuchPathError.new(epath)
44
+ end
45
+
46
+ self.git = Git.new(self.path, self.working_dir)
47
+ end
48
+
49
+ def self.init(path, options = {})
50
+ epath = File.expand_path(path)
51
+
52
+ if epath =~ /\.git$/ || options[:is_bare]
53
+ path = epath
54
+ working_dir = nil
55
+ else
56
+ path = File.join(epath, '.git')
57
+ working_dir = epath
58
+ end
59
+
60
+ git = Git.new(path, working_dir)
61
+
62
+ git.init
63
+
64
+ Repo.new(path)
65
+ end
66
+
67
+ def self.clone(remote_repo, path, options = {})
68
+ epath = File.expand_path(path)
69
+
70
+ if epath =~ /\.git$/ || options[:is_bare]
71
+ git_options = { :bare => true }
72
+ else
73
+ git_options = {}
74
+ end
75
+
76
+ git = Git.new(nil)
77
+ git.clone(git_options, remote_repo, epath)
78
+
79
+ Repo.new(path)
80
+ end
81
+
82
+ # The project's description. Taken verbatim from GIT_REPO/description
83
+ #
84
+ # Returns String
85
+ def description
86
+ File.open(File.join(self.path, 'description')).read.chomp
87
+ end
88
+
89
+ def blame(file, commit = nil)
90
+ Blame.new(self, file, commit)
91
+ end
92
+
93
+
94
+ # An array of Head objects representing the branch heads in
95
+ # this repo
96
+ #
97
+ # Returns Grit::Head[] (baked)
98
+ def heads
99
+ Head.find_all(self)
100
+ end
101
+
102
+ alias_method :branches, :heads
103
+
104
+ def get_head(head_name)
105
+ heads.find { |h| h.name == head_name }
106
+ end
107
+
108
+ def is_head?(head_name)
109
+ get_head(head_name)
110
+ end
111
+
112
+ # Object reprsenting the current repo head.
113
+ #
114
+ # Returns Grit::Head (baked)
115
+ def head
116
+ Head.current(self)
117
+ end
118
+
119
+
120
+ # Commits current index
121
+ #
122
+ # Returns true/false if commit worked
123
+ def commit_index(message)
124
+ self.git.commit({}, '-m', message)
125
+ end
126
+
127
+ # Commits all tracked and modified files
128
+ #
129
+ # Returns true/false if commit worked
130
+ def commit_all(message)
131
+ self.git.commit({}, '-a', '-m', message)
132
+ end
133
+
134
+ # Adds files to the index
135
+ def add(*files)
136
+ self.git.add({}, *files.flatten)
137
+ end
138
+
139
+ # Remove files from the index
140
+ def rm(*files)
141
+ self.git.rm({}, *files.flatten)
142
+ end
143
+
144
+ alias :remove :rm
145
+
146
+ def rm_r(*files)
147
+ self.git.rm({}, '-r', *files.flatten)
148
+ end
149
+
150
+ alias :remove_recursively :rm_r
151
+
152
+ def mv(source, destination)
153
+ self.git.mv({}, source, destination)
154
+ end
155
+
156
+ alias :move :mv
157
+
158
+ def checkout_path_commit(commit, path)
159
+ self.git.checkout({}, commit, '--', path)
160
+ end
161
+
162
+ def revert(commit)
163
+ self.git.revert({}, commit)
164
+ end
165
+
166
+
167
+ def blame_tree(commit, path = nil)
168
+ commit_array = self.git.blame_tree(commit, path)
169
+
170
+ final_array = {}
171
+ commit_array.each do |file, sha|
172
+ final_array[file] = commit(sha)
173
+ end
174
+ final_array
175
+ end
176
+
177
+ def status
178
+ Status.new(self)
179
+ end
180
+
181
+
182
+ # An array of Tag objects that are available in this repo
183
+ #
184
+ # Returns Grit::Tag[] (baked)
185
+ def tags
186
+ Tag.find_all(self)
187
+ end
188
+
189
+ # An array of Remote objects representing the remote branches in
190
+ # this repo
191
+ #
192
+ # Returns Grit::Remote[] (baked)
193
+ def remotes
194
+ Remote.find_all(self)
195
+ end
196
+
197
+ # An array of Ref objects representing the refs in
198
+ # this repo
199
+ #
200
+ # Returns Grit::Ref[] (baked)
201
+ def refs
202
+ [ Head.find_all(self), Tag.find_all(self), Remote.find_all(self) ].flatten
203
+ end
204
+
205
+ def commit_stats(start = 'master', max_count = 10, skip = 0)
206
+ options = {:max_count => max_count,
207
+ :skip => skip}
208
+
209
+ CommitStats.find_all(self, start, options)
210
+ end
211
+
212
+ # An array of Commit objects representing the history of a given ref/commit
213
+ # +start+ is the branch/commit name (default 'master')
214
+ # +max_count+ is the maximum number of commits to return (default 10, use +false+ for all)
215
+ # +skip+ is the number of commits to skip (default 0)
216
+ #
217
+ # Returns Grit::Commit[] (baked)
218
+ def commits(start = 'master', max_count = 10, skip = 0)
219
+ options = {:max_count => max_count,
220
+ :skip => skip}
221
+
222
+ Commit.find_all(self, start, options)
223
+ end
224
+
225
+ # The Commits objects that are reachable via +to+ but not via +from+
226
+ # Commits are returned in chronological order.
227
+ # +from+ is the branch/commit name of the younger item
228
+ # +to+ is the branch/commit name of the older item
229
+ #
230
+ # Returns Grit::Commit[] (baked)
231
+ def commits_between(from, to)
232
+ result = Commit.find_all(self, "#{from}..#{to}").reverse
233
+ remote_error
234
+ result
235
+ end
236
+
237
+ # The Commits objects that are newer than the specified date.
238
+ # Commits are returned in chronological order.
239
+ # +start+ is the branch/commit name (default 'master')
240
+ # +since+ is a string represeting a date/time
241
+ # +extra_options+ is a hash of extra options
242
+ #
243
+ # Returns Grit::Commit[] (baked)
244
+ def commits_since(start = 'master', since = '1970-01-01', extra_options = {})
245
+ options = {:since => since}.merge(extra_options)
246
+
247
+ Commit.find_all(self, start, options)
248
+ end
249
+
250
+ # The number of commits reachable by the given branch/commit
251
+ # +start+ is the branch/commit name (default 'master')
252
+ #
253
+ # Returns Integer
254
+ def commit_count(start = 'master')
255
+ Commit.count(self, start)
256
+ end
257
+
258
+ # The Commit object for the specified id
259
+ # +id+ is the SHA1 identifier of the commit
260
+ #
261
+ # Returns Grit::Commit (baked)
262
+ def commit(id)
263
+ options = {:max_count => 1}
264
+
265
+ Commit.find_all(self, id, options).first
266
+ end
267
+
268
+ # Returns a list of commits that is in +other_repo+ but not in self
269
+ #
270
+ # Returns Grit::Commit[]
271
+ def commit_deltas_from(other_repo, ref = "master", other_ref = "master")
272
+ # TODO: we should be able to figure out the branch point, rather than
273
+ # rev-list'ing the whole thing
274
+ repo_refs = self.git.rev_list({}, ref).strip.split("\n")
275
+ other_repo_refs = other_repo.git.rev_list({}, other_ref).strip.split("\n")
276
+
277
+ (other_repo_refs - repo_refs).map do |ref|
278
+ Commit.find_all(other_repo, ref, {:max_count => 1}).first
279
+ end
280
+ end
281
+
282
+ # The Tree object for the given treeish reference
283
+ # +treeish+ is the reference (default 'master')
284
+ # +paths+ is an optional Array of directory paths to restrict the tree (deafult [])
285
+ #
286
+ # Examples
287
+ # repo.tree('master', ['lib/'])
288
+ #
289
+ # Returns Grit::Tree (baked)
290
+ def tree(treeish = 'master', paths = [])
291
+ Tree.construct(self, treeish, paths)
292
+ end
293
+
294
+ # The Blob object for the given id
295
+ # +id+ is the SHA1 id of the blob
296
+ #
297
+ # Returns Grit::Blob (unbaked)
298
+ def blob(id)
299
+ Blob.create(self, :id => id)
300
+ end
301
+
302
+ # The commit log for a treeish
303
+ #
304
+ # Returns Grit::Commit[]
305
+ def log(commit = 'master', path = nil, options = {})
306
+ default_options = {:pretty => "raw"}
307
+ actual_options = default_options.merge(options)
308
+ arg = path ? [commit, '--', path] : [commit]
309
+ commits = self.git.log(actual_options, *arg)
310
+ Commit.list_from_string(self, commits)
311
+ end
312
+
313
+ # The diff from commit +a+ to commit +b+, optionally restricted to the given file(s)
314
+ # +a+ is the base commit
315
+ # +b+ is the other commit
316
+ # +paths+ is an optional list of file paths on which to restrict the diff
317
+ def diff(a, b, *paths)
318
+ self.git.diff({}, a, b, '--', *paths)
319
+ end
320
+
321
+ # The commit diff for the given commit
322
+ # +commit+ is the commit name/id
323
+ #
324
+ # Returns Grit::Diff[]
325
+ def commit_diff(commit)
326
+ Commit.diff(self, commit)
327
+ end
328
+
329
+ # Initialize a bare git repository at the given path
330
+ # +path+ is the full path to the repo (traditionally ends with /<name>.git)
331
+ # +options+ is any additional options to the git init command
332
+ #
333
+ # Examples
334
+ # Grit::Repo.init_bare('/var/git/myrepo.git')
335
+ #
336
+ # Returns Grit::Repo (the newly created repo)
337
+ def self.init_bare(path, git_options = {}, repo_options = {})
338
+ git = Git.new(path)
339
+ git.init(git_options)
340
+ self.new(path, repo_options)
341
+ end
342
+
343
+ # Fork a bare git repository from this repo
344
+ # +path+ is the full path of the new repo (traditionally ends with /<name>.git)
345
+ # +options+ is any additional options to the git clone command (:bare and :shared are true by default)
346
+ #
347
+ # Returns Grit::Repo (the newly forked repo)
348
+ def fork_bare(path, options = {})
349
+ default_options = {:bare => true, :shared => true}
350
+ real_options = default_options.merge(options)
351
+ self.git.clone(real_options, self.path, path)
352
+ Repo.new(path)
353
+ end
354
+
355
+ # Archive the given treeish
356
+ # +treeish+ is the treeish name/id (default 'master')
357
+ # +prefix+ is the optional prefix
358
+ #
359
+ # Examples
360
+ # repo.archive_tar
361
+ # # => <String containing tar archive>
362
+ #
363
+ # repo.archive_tar('a87ff14')
364
+ # # => <String containing tar archive for commit a87ff14>
365
+ #
366
+ # repo.archive_tar('master', 'myproject/')
367
+ # # => <String containing tar archive and prefixed with 'myproject/'>
368
+ #
369
+ # Returns String (containing tar archive)
370
+ def archive_tar(treeish = 'master', prefix = nil)
371
+ options = {}
372
+ options[:prefix] = prefix if prefix
373
+ self.git.archive(options, treeish)
374
+ end
375
+
376
+ # Archive and gzip the given treeish
377
+ # +treeish+ is the treeish name/id (default 'master')
378
+ # +prefix+ is the optional prefix
379
+ #
380
+ # Examples
381
+ # repo.archive_tar_gz
382
+ # # => <String containing tar.gz archive>
383
+ #
384
+ # repo.archive_tar_gz('a87ff14')
385
+ # # => <String containing tar.gz archive for commit a87ff14>
386
+ #
387
+ # repo.archive_tar_gz('master', 'myproject/')
388
+ # # => <String containing tar.gz archive and prefixed with 'myproject/'>
389
+ #
390
+ # Returns String (containing tar.gz archive)
391
+ def archive_tar_gz(treeish = 'master', prefix = nil)
392
+ options = {}
393
+ options[:prefix] = prefix if prefix
394
+ self.git.archive(options, treeish, "| gzip")
395
+ end
396
+
397
+ # Write an archive directly to a file
398
+ # +treeish+ is the treeish name/id (default 'master')
399
+ # +prefix+ is the optional prefix (default nil)
400
+ # +filename+ is the name of the file (default 'archive.tar.gz')
401
+ # +format+ is the optional format (default nil)
402
+ # +pipe+ is the command to run the output through (default 'gzip')
403
+ #
404
+ # Returns nothing
405
+ def archive_to_file(treeish = 'master', prefix = nil, filename = 'archive.tar.gz', format = nil, pipe = "gzip")
406
+ options = {}
407
+ options[:prefix] = prefix if prefix
408
+ options[:format] = format if format
409
+ self.git.archive(options, treeish, "| #{pipe} > #{filename}")
410
+ end
411
+
412
+ # Enable git-daemon serving of this repository by writing the
413
+ # git-daemon-export-ok file to its git directory
414
+ #
415
+ # Returns nothing
416
+ def enable_daemon_serve
417
+ FileUtils.touch(File.join(self.path, DAEMON_EXPORT_FILE))
418
+ end
419
+
420
+ # Disable git-daemon serving of this repository by ensuring there is no
421
+ # git-daemon-export-ok file in its git directory
422
+ #
423
+ # Returns nothing
424
+ def disable_daemon_serve
425
+ FileUtils.rm_f(File.join(self.path, DAEMON_EXPORT_FILE))
426
+ end
427
+
428
+ def gc_auto
429
+ self.git.gc({:auto => true})
430
+ end
431
+
432
+ # The list of alternates for this repo
433
+ #
434
+ # Returns Array[String] (pathnames of alternates)
435
+ def alternates
436
+ alternates_path = File.join(self.path, *%w{objects info alternates})
437
+
438
+ if File.exist?(alternates_path)
439
+ File.read(alternates_path).strip.split("\n")
440
+ else
441
+ []
442
+ end
443
+ end
444
+
445
+ # Sets the alternates
446
+ # +alts+ is the Array of String paths representing the alternates
447
+ #
448
+ # Returns nothing
449
+ def alternates=(alts)
450
+ alts.each do |alt|
451
+ unless File.exist?(alt)
452
+ raise "Could not set alternates. Alternate path #{alt} must exist"
453
+ end
454
+ end
455
+
456
+ if alts.empty?
457
+ File.open(File.join(self.path, *%w{objects info alternates}), 'w') do |f|
458
+ f.write ''
459
+ end
460
+ else
461
+ File.open(File.join(self.path, *%w{objects info alternates}), 'w') do |f|
462
+ f.write alts.join("\n")
463
+ end
464
+ end
465
+ end
466
+
467
+ def config
468
+ @config ||= Config.new(self)
469
+ end
470
+
471
+ def index
472
+ Index.new(self)
473
+ end
474
+
475
+ def update_ref(head, commit_sha)
476
+ return nil if !commit_sha || (commit_sha.size != 40)
477
+
478
+ ref_heads = File.join(self.path, 'refs', 'heads')
479
+ FileUtils.mkdir_p(ref_heads)
480
+ File.open(File.join(ref_heads, head), 'w') do |f|
481
+ f.write(commit_sha)
482
+ end
483
+ commit_sha
484
+
485
+ end
486
+
487
+ # Pretty object inspection
488
+ def inspect
489
+ %Q{#<Grit::Repo "#{@path}">}
490
+ end
491
+
492
+ def push(repository = nil, refspec = nil)
493
+ self.git.push({}, repository, refspec)
494
+ remote_error_or_response
495
+ end
496
+
497
+ def pull(repository = nil, refspec = nil)
498
+ # git-pull seems to ignore the --work-tree setting and only works if you're actually in the directory
499
+ # fatal: .../git-core/git-pull cannot be used without a working tree.
500
+ in_working_dir do
501
+ self.git.pull({}, repository, refspec)
502
+ remote_error_or_response
503
+ end
504
+ end
505
+
506
+ def add_remote(name = nil, url = nil)
507
+ self.git.remote({}, 'add', name, url)
508
+ remote_error_or_response
509
+ end
510
+
511
+ def last_error
512
+ !self.git.last_error.blank? ? self.git.last_error : nil
513
+ end
514
+
515
+ def last_response
516
+ !self.git.last_response.blank? ? self.git.last_response : nil
517
+ end
518
+
519
+ def set_config(key, value = nil)
520
+ self.git.config({}, key, value)
521
+ remote_error_or_response
522
+ end
523
+
524
+ private
525
+
526
+ def in_working_dir(&block)
527
+ cwt = self.git.work_tree
528
+
529
+ Dir.chdir(cwt) do
530
+ self.git.work_tree = nil
531
+ yield
532
+ self.git.work_tree = cwt
533
+ end
534
+ end
535
+
536
+ def remote_error
537
+ if last_error =~ /fatal: '.*': unable to chdir or not a git archive/
538
+ raise RemoteNonexistentError, last_error
539
+ elsif last_error =~ /ssh: Could not resolve hostname .*: nodename nor servname provided, or not known/
540
+ raise RemoteNonexistentError, last_error
541
+ elsif last_error =~ /does not appear to be a git repository/
542
+ raise RemoteNonexistentError, last_error
543
+ elsif last_error =~ /fatal: Couldn't find remote ref .*/
544
+ raise BranchNonexistentError, last_error
545
+ elsif last_error =~ /error: src refspec .* does not match any./
546
+ raise BranchNonexistentError, last_error
547
+ elsif last_error =~ /unknown revision or path not in the working tree/
548
+ raise RemoteUninitializedError, last_error
549
+ elsif last_error =~ /error: Entry '.*' would be overwritten by merge. Cannot merge./
550
+ raise ConflictError, last_error
551
+ elsif last_error =~ /Entry '.*' not uptodate. Cannot merge./
552
+ raise ConflictError, last_error
553
+ elsif last_error =~ /(error|fatal)/
554
+ raise RemoteError, last_error
555
+ end
556
+ end
557
+
558
+ def remote_error_or_response
559
+ remote_error
560
+ last_response
561
+ end
562
+
563
+ end # Repo
564
+
565
+ end # Grit