joelmoss-grit 1.1.4

Sign up to get free protection for your applications and to get access to all the features.
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