square-circle-triangle-grit 1.1.2

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