mojombo-grit 0.8.1 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. data/History.txt +7 -0
  2. data/Manifest.txt +18 -1
  3. data/grit.gemspec +56 -10
  4. data/lib/{mojombo-grit.rb → grit.rb} +20 -4
  5. data/lib/grit/commit.rb +32 -11
  6. data/lib/grit/commit_stats.rb +104 -0
  7. data/lib/grit/git-ruby.rb +182 -0
  8. data/lib/grit/git-ruby/commit_db.rb +52 -0
  9. data/lib/grit/git-ruby/file_index.rb +186 -0
  10. data/lib/grit/git-ruby/git_object.rb +344 -0
  11. data/lib/grit/git-ruby/internal/loose.rb +136 -0
  12. data/lib/grit/git-ruby/internal/mmap.rb +58 -0
  13. data/lib/grit/git-ruby/internal/pack.rb +382 -0
  14. data/lib/grit/git-ruby/internal/raw_object.rb +37 -0
  15. data/lib/grit/git-ruby/object.rb +319 -0
  16. data/lib/grit/git-ruby/repository.rb +729 -0
  17. data/lib/grit/git.rb +33 -15
  18. data/lib/grit/head.rb +6 -15
  19. data/lib/grit/index.rb +121 -0
  20. data/lib/grit/ref.rb +95 -0
  21. data/lib/grit/repo.rb +95 -6
  22. data/lib/grit/status.rb +151 -0
  23. data/lib/grit/tree.rb +3 -2
  24. data/test/test_blob.rb +5 -0
  25. data/test/test_commit.rb +7 -5
  26. data/test/test_diff.rb +1 -1
  27. data/test/test_git.rb +20 -2
  28. data/test/test_grit.rb +32 -0
  29. data/test/test_head.rb +30 -5
  30. data/test/test_real.rb +8 -6
  31. data/test/test_remote.rb +14 -0
  32. data/test/test_repo.rb +86 -79
  33. data/test/test_tag.rb +2 -6
  34. data/test/test_tree.rb +5 -0
  35. metadata +40 -40
  36. data/test/fixtures/blame +0 -131
  37. data/test/fixtures/cat_file_blob +0 -1
  38. data/test/fixtures/cat_file_blob_size +0 -1
  39. data/test/fixtures/diff_2 +0 -54
  40. data/test/fixtures/diff_2f +0 -19
  41. data/test/fixtures/diff_f +0 -15
  42. data/test/fixtures/diff_i +0 -201
  43. data/test/fixtures/diff_mode_only +0 -1152
  44. data/test/fixtures/diff_new_mode +0 -17
  45. data/test/fixtures/diff_p +0 -610
  46. data/test/fixtures/for_each_ref +0 -0
  47. data/test/fixtures/for_each_ref_tags +0 -0
  48. data/test/fixtures/ls_tree_a +0 -7
  49. data/test/fixtures/ls_tree_b +0 -2
  50. data/test/fixtures/ls_tree_commit +0 -3
  51. data/test/fixtures/rev_list +0 -26
  52. data/test/fixtures/rev_list_count +0 -655
  53. data/test/fixtures/rev_list_single +0 -7
  54. data/test/fixtures/rev_parse +0 -1
  55. data/test/fixtures/show_empty_commit +0 -6
  56. data/test/fixtures/simple_config +0 -2
  57. data/test/helper.rb +0 -17
  58. data/test/profile.rb +0 -21
  59. data/test/suite.rb +0 -6
@@ -0,0 +1,37 @@
1
+ #
2
+ # converted from the gitrb project
3
+ #
4
+ # authors:
5
+ # Matthias Lederhofer <matled@gmx.net>
6
+ # Simon 'corecode' Schubert <corecode@fs.ei.tum.de>
7
+ #
8
+ # provides native ruby access to git objects and pack files
9
+ #
10
+
11
+ require 'digest/sha1'
12
+
13
+ module Grit
14
+ module GitRuby
15
+ module Internal
16
+ OBJ_NONE = 0
17
+ OBJ_COMMIT = 1
18
+ OBJ_TREE = 2
19
+ OBJ_BLOB = 3
20
+ OBJ_TAG = 4
21
+
22
+ OBJ_TYPES = [nil, :commit, :tree, :blob, :tag].freeze
23
+
24
+ class RawObject
25
+ attr_accessor :type, :content
26
+ def initialize(type, content)
27
+ @type = type
28
+ @content = content
29
+ end
30
+
31
+ def sha1
32
+ Digest::SHA1.digest("%s %d\0" % [@type, @content.length] + @content)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,319 @@
1
+ #
2
+ # converted from the gitrb project
3
+ #
4
+ # authors:
5
+ # Matthias Lederhofer <matled@gmx.net>
6
+ # Simon 'corecode' Schubert <corecode@fs.ei.tum.de>
7
+ # Scott Chacon <schacon@gmail.com>
8
+ #
9
+ # provides native ruby access to git objects and pack files
10
+ #
11
+
12
+ # These classes translate the raw binary data kept in the sha encoded files
13
+ # into parsed data that can then be used in another fashion
14
+ require 'stringio'
15
+
16
+ module Grit
17
+ module GitRuby
18
+
19
+ # class for author/committer/tagger lines
20
+ class UserInfo
21
+ attr_accessor :name, :email, :date, :offset
22
+
23
+ def initialize(str)
24
+ m = /^(.*?) <(.*)> (\d+) ([+-])0*(\d+?)$/.match(str)
25
+ if !m
26
+ raise RuntimeError, "invalid header '%s' in commit" % str
27
+ end
28
+ @name = m[1]
29
+ @email = m[2]
30
+ @date = Time.at(Integer(m[3]))
31
+ @offset = (m[4] == "-" ? -1 : 1)*Integer(m[5])
32
+ end
33
+
34
+ def to_s
35
+ "%s <%s> %s %+05d" % [@name, @email, @date.to_i, @offset]
36
+ end
37
+ end
38
+
39
+ # base class for all git objects (blob, tree, commit, tag)
40
+ class Object
41
+ attr_accessor :repository
42
+
43
+ def Object.from_raw(rawobject, repository = nil)
44
+ case rawobject.type
45
+ when :blob
46
+ return Blob.from_raw(rawobject, repository)
47
+ when :tree
48
+ return Tree.from_raw(rawobject, repository)
49
+ when :commit
50
+ return Commit.from_raw(rawobject, repository)
51
+ when :tag
52
+ return Tag.from_raw(rawobject, repository)
53
+ else
54
+ raise RuntimeError, "got invalid object-type"
55
+ end
56
+ end
57
+
58
+ def initialize
59
+ raise NotImplemented, "abstract class"
60
+ end
61
+
62
+ def type
63
+ raise NotImplemented, "abstract class"
64
+ end
65
+
66
+ def raw_content
67
+ raise NotImplemented, "abstract class"
68
+ end
69
+
70
+ def sha1
71
+ Digest::SHA1.hexdigest("%s %d\0" % \
72
+ [self.type, self.raw_content.length] + \
73
+ self.raw_content)
74
+ end
75
+ end
76
+
77
+ class Blob < Object
78
+ attr_accessor :content
79
+
80
+ def self.from_raw(rawobject, repository)
81
+ new(rawobject.content)
82
+ end
83
+
84
+ def initialize(content, repository=nil)
85
+ @content = content
86
+ @repository = repository
87
+ end
88
+
89
+ def type
90
+ :blob
91
+ end
92
+
93
+ def raw_content
94
+ @content
95
+ end
96
+ end
97
+
98
+ class DirectoryEntry
99
+ S_IFMT = 00170000
100
+ S_IFLNK = 0120000
101
+ S_IFREG = 0100000
102
+ S_IFDIR = 0040000
103
+
104
+ attr_accessor :mode, :name, :sha1
105
+ def initialize(mode, filename, sha1o)
106
+ @mode = 0
107
+ mode.each_byte do |i|
108
+ @mode = (@mode << 3) | (i-'0'[0])
109
+ end
110
+ @name = filename
111
+ @sha1 = sha1o
112
+ if ![S_IFLNK, S_IFDIR, S_IFREG].include?(@mode & S_IFMT)
113
+ raise RuntimeError, "unknown type for directory entry"
114
+ end
115
+ end
116
+
117
+ def type
118
+ case @mode & S_IFMT
119
+ when S_IFLNK
120
+ @type = :link
121
+ when S_IFDIR
122
+ @type = :directory
123
+ when S_IFREG
124
+ @type = :file
125
+ else
126
+ raise RuntimeError, "unknown type for directory entry"
127
+ end
128
+ end
129
+
130
+ def type=(type)
131
+ case @type
132
+ when :link
133
+ @mode = (@mode & ~S_IFMT) | S_IFLNK
134
+ when :directory
135
+ @mode = (@mode & ~S_IFMT) | S_IFDIR
136
+ when :file
137
+ @mode = (@mode & ~S_IFMT) | S_IFREG
138
+ else
139
+ raise RuntimeError, "invalid type"
140
+ end
141
+ end
142
+
143
+ def format_type
144
+ case type
145
+ when :link
146
+ 'link'
147
+ when :directory
148
+ 'tree'
149
+ when :file
150
+ 'blob'
151
+ end
152
+ end
153
+
154
+ def format_mode
155
+ "%06o" % @mode
156
+ end
157
+
158
+ def raw
159
+ "%o %s\0%s" % [@mode, @name, [@sha1].pack("H*")]
160
+ end
161
+ end
162
+
163
+
164
+ def self.read_bytes_until(io, char)
165
+ string = ''
166
+ while ((next_char = io.getc.chr) != char) && !io.eof
167
+ string += next_char
168
+ end
169
+ string
170
+ end
171
+
172
+
173
+ class Tree < Object
174
+ attr_accessor :entry
175
+
176
+ def self.from_raw(rawobject, repository=nil)
177
+ raw = StringIO.new(rawobject.content)
178
+
179
+ entries = []
180
+ while !raw.eof?
181
+ mode = Grit::GitRuby.read_bytes_until(raw, ' ')
182
+ file_name = Grit::GitRuby.read_bytes_until(raw, "\0")
183
+ raw_sha = raw.read(20)
184
+ sha = raw_sha.unpack("H*").first
185
+
186
+ entries << DirectoryEntry.new(mode, file_name, sha)
187
+ end
188
+ new(entries, repository)
189
+ end
190
+
191
+ def initialize(entries=[], repository = nil)
192
+ @entry = entries
193
+ @repository = repository
194
+ end
195
+
196
+ def type
197
+ :tree
198
+ end
199
+
200
+ def raw_content
201
+ # TODO: sort correctly
202
+ #@entry.sort { |a,b| a.name <=> b.name }.
203
+ @entry.collect { |e| [[e.format_mode, e.format_type, e.sha1].join(' '), e.name].join("\t") }.join("\n")
204
+ end
205
+
206
+ def actual_raw
207
+ #@entry.collect { |e| e.raw.join(' '), e.name].join("\t") }.join("\n")
208
+ end
209
+ end
210
+
211
+ class Commit < Object
212
+ attr_accessor :author, :committer, :tree, :parent, :message, :headers
213
+
214
+ def self.from_raw(rawobject, repository=nil)
215
+ parent = []
216
+ tree = author = committer = nil
217
+
218
+ headers, message = rawobject.content.split(/\n\n/, 2)
219
+ all_headers = headers.split(/\n/).map { |header| header.split(/ /, 2) }
220
+ all_headers.each do |key, value|
221
+ case key
222
+ when "tree"
223
+ tree = value
224
+ when "parent"
225
+ parent.push(value)
226
+ when "author"
227
+ author = UserInfo.new(value)
228
+ when "committer"
229
+ committer = UserInfo.new(value)
230
+ else
231
+ warn "unknown header '%s' in commit %s" % \
232
+ [key, rawobject.sha1.unpack("H*")[0]]
233
+ end
234
+ end
235
+ if not tree && author && committer
236
+ raise RuntimeError, "incomplete raw commit object"
237
+ end
238
+ new(tree, parent, author, committer, message, headers, repository)
239
+ end
240
+
241
+ def initialize(tree, parent, author, committer, message, headers, repository=nil)
242
+ @tree = tree
243
+ @author = author
244
+ @parent = parent
245
+ @committer = committer
246
+ @message = message
247
+ @headers = headers
248
+ @repository = repository
249
+ end
250
+
251
+ def type
252
+ :commit
253
+ end
254
+
255
+ def raw_content
256
+ "tree %s\n%sauthor %s\ncommitter %s\n\n" % [
257
+ @tree,
258
+ @parent.collect { |i| "parent %s\n" % i }.join,
259
+ @author, @committer] + @message
260
+ end
261
+
262
+ def raw_log(sha)
263
+ output = "commit #{sha}\n"
264
+ output += @headers + "\n\n"
265
+ output += @message.split("\n").map { |l| ' ' + l }.join("\n") + "\n\n"
266
+ end
267
+
268
+ end
269
+
270
+ class Tag < Object
271
+ attr_accessor :object, :type, :tag, :tagger, :message
272
+
273
+ def self.from_raw(rawobject, repository=nil)
274
+ headers, message = rawobject.content.split(/\n\n/, 2)
275
+ headers = headers.split(/\n/).map { |header| header.split(/ /, 2) }
276
+ headers.each do |key, value|
277
+ case key
278
+ when "object"
279
+ object = value
280
+ when "type"
281
+ if !["blob", "tree", "commit", "tag"].include?(value)
282
+ raise RuntimeError, "invalid type in tag"
283
+ end
284
+ type = value.to_sym
285
+ when "tag"
286
+ tag = value
287
+ when "tagger"
288
+ tagger = UserInfo.new(value)
289
+ else
290
+ warn "unknown header '%s' in tag" % \
291
+ [key, rawobject.sha1.unpack("H*")[0]]
292
+ end
293
+ if not object && type && tag && tagger
294
+ raise RuntimeError, "incomplete raw tag object"
295
+ end
296
+ end
297
+ new(object, type, tag, tagger, repository)
298
+ end
299
+
300
+ def initialize(object, type, tag, tagger, repository=nil)
301
+ @object = object
302
+ @type = type
303
+ @tag = tag
304
+ @tagger = tagger
305
+ @repository = repository
306
+ end
307
+
308
+ def raw_content
309
+ "object %s\ntype %s\ntag %s\ntagger %s\n\n" % \
310
+ [@object, @type, @tag, @tagger] + @message
311
+ end
312
+
313
+ def type
314
+ :tag
315
+ end
316
+ end
317
+
318
+ end
319
+ end
@@ -0,0 +1,729 @@
1
+ #
2
+ # converted from the gitrb project
3
+ #
4
+ # authors:
5
+ # Matthias Lederhofer <matled@gmx.net>
6
+ # Simon 'corecode' Schubert <corecode@fs.ei.tum.de>
7
+ # Scott Chacon <schacon@gmail.com>
8
+ #
9
+ # provides native ruby access to git objects and pack files
10
+ #
11
+ require 'grit/git-ruby/internal/raw_object'
12
+ require 'grit/git-ruby/internal/pack'
13
+ require 'grit/git-ruby/internal/loose'
14
+ require 'grit/git-ruby/git_object'
15
+
16
+ require 'rubygems'
17
+ require 'diff/lcs'
18
+ require 'diff/lcs/hunk'
19
+
20
+ # have to do this so it doesn't interfere with Grit::Diff
21
+ module Difference
22
+ include Diff
23
+ end
24
+
25
+ module Grit
26
+ module GitRuby
27
+ class Repository
28
+
29
+ class NoSuchShaFound < StandardError
30
+ end
31
+
32
+ class NoSuchPath < StandardError
33
+ end
34
+
35
+ attr_accessor :git_dir, :options
36
+
37
+ def initialize(git_dir, options = {})
38
+ @git_dir = git_dir
39
+ @options = options
40
+ @packs = []
41
+ end
42
+
43
+ # returns the loose objects object lazily
44
+ def loose
45
+ @loose ||= initloose
46
+ end
47
+
48
+ # returns the array of pack list objects
49
+ def packs
50
+ @packs ||= initpacks
51
+ end
52
+
53
+
54
+ # prints out the type, shas and content of all of the pack files
55
+ def show
56
+ packs.each do |p|
57
+ puts p.name
58
+ puts
59
+ p.each_sha1 do |s|
60
+ puts "**#{p[s].type}**"
61
+ if p[s].type.to_s == 'commit'
62
+ puts s.unpack('H*')
63
+ puts p[s].content
64
+ end
65
+ end
66
+ puts
67
+ end
68
+ end
69
+
70
+
71
+ # returns a raw object given a SHA1
72
+ def get_raw_object_by_sha1(sha1o)
73
+ sha1 = [sha1o.chomp].pack("H*")
74
+ # try packs
75
+ packs.each do |pack|
76
+ o = pack[sha1]
77
+ return pack[sha1] if o
78
+ end
79
+
80
+ # try loose storage
81
+ loose.each do |lsobj|
82
+ o = lsobj[sha1]
83
+ return o if o
84
+ end
85
+
86
+ # try packs again, maybe the object got packed in the meantime
87
+ initpacks
88
+ packs.each do |pack|
89
+ o = pack[sha1]
90
+ return o if o
91
+ end
92
+
93
+ # puts "*#{sha1o}*"
94
+ raise NoSuchShaFound
95
+ end
96
+
97
+ def cached(key, object, do_cache = true)
98
+ object
99
+ end
100
+
101
+ # returns GitRuby object of any type given a SHA1
102
+ def get_object_by_sha1(sha1)
103
+ r = get_raw_object_by_sha1(sha1)
104
+ return nil if !r
105
+ GitObject.from_raw(r)
106
+ end
107
+
108
+ # writes a raw object into the git repo
109
+ def put_raw_object(content, type)
110
+ loose.first.put_raw_object(content, type)
111
+ end
112
+
113
+ # returns true or false if that sha exists in the db
114
+ def object_exists?(sha1)
115
+ sha_hex = [sha1].pack("H*")
116
+ return true if in_packs?(sha_hex)
117
+ return true if in_loose?(sha_hex)
118
+ initpacks
119
+ return true if in_packs?(sha_hex) #maybe the object got packed in the meantime
120
+ false
121
+ end
122
+
123
+ # returns true if the hex-packed sha is in the packfiles
124
+ def in_packs?(sha_hex)
125
+ # try packs
126
+ packs.each do |pack|
127
+ return true if pack[sha_hex]
128
+ end
129
+ false
130
+ end
131
+
132
+ # returns true if the hex-packed sha is in the loose objects
133
+ def in_loose?(sha_hex)
134
+ loose.each do |lsobj|
135
+ return true if lsobj[sha_hex]
136
+ end
137
+ false
138
+ end
139
+
140
+
141
+ # returns the file type (as a symbol) of this sha
142
+ def cat_file_type(sha)
143
+ get_raw_object_by_sha1(sha).type
144
+ end
145
+
146
+ # returns the file size (as an int) of this sha
147
+ def cat_file_size(sha)
148
+ get_raw_object_by_sha1(sha).content.size
149
+ end
150
+
151
+ # returns the raw file contents of this sha
152
+ def cat_file(sha)
153
+ get_object_by_sha1(sha).raw_content
154
+ end
155
+
156
+ # returns a 2-d hash of the tree
157
+ # ['blob']['FILENAME'] = {:mode => '100644', :sha => SHA}
158
+ # ['tree']['DIRNAME'] = {:mode => '040000', :sha => SHA}
159
+ def list_tree(sha)
160
+ data = {'blob' => {}, 'tree' => {}, 'link' => {}, 'commit' => {}}
161
+ get_object_by_sha1(sha).entry.each do |e|
162
+ data[e.format_type][e.name] = {:mode => e.format_mode, :sha => e.sha1}
163
+ end
164
+ data
165
+ end
166
+
167
+ # returns the raw (cat-file) output for a tree
168
+ # if given a commit sha, it will print the tree of that commit
169
+ # if given a path limiter array, it will limit the output to those
170
+ def ls_tree(sha, paths = [])
171
+ if paths.size > 0
172
+ # pathing
173
+ part = []
174
+ paths.each do |path|
175
+ part += ls_tree_path(sha, path)
176
+ end
177
+ return part.join("\n")
178
+ else
179
+ get_raw_tree(sha)
180
+ end
181
+ end
182
+
183
+ def get_raw_tree(sha)
184
+ o = get_raw_object_by_sha1(sha)
185
+ if o.type == :commit
186
+ tree = cat_file(get_object_by_sha1(sha).tree)
187
+ elsif o.type == :tag
188
+ commit_sha = get_object_by_sha1(sha).object
189
+ tree = cat_file(get_object_by_sha1(commit_sha).tree)
190
+ else
191
+ tree = cat_file(sha)
192
+ end
193
+ return tree
194
+ end
195
+
196
+ # return array of tree entries
197
+ ## TODO : refactor this to remove the fugly
198
+ def ls_tree_path(sha, path, append = nil)
199
+ tree = get_raw_tree(sha)
200
+ if path =~ /\//
201
+ paths = path.split('/')
202
+ last = path[path.size - 1, 1]
203
+ if (last == '/') && (paths.size == 1)
204
+ append = append ? File.join(append, paths.first) : paths.first
205
+ dir_name = tree.split("\n").select { |p| p.split("\t")[1] == paths.first }.first
206
+ raise NoSuchPath if !dir_name
207
+ next_sha = dir_name.split(' ')[2]
208
+ tree = get_raw_tree(next_sha)
209
+ tree = tree.split("\n")
210
+ if append
211
+ mod_tree = []
212
+ tree.each do |ent|
213
+ (info, fpath) = ent.split("\t")
214
+ mod_tree << [info, File.join(append, fpath)].join("\t")
215
+ end
216
+ mod_tree
217
+ else
218
+ tree
219
+ end
220
+ else
221
+ next_path = paths.shift
222
+ dir_name = tree.split("\n").select { |p| p.split("\t")[1] == next_path }.first
223
+ raise NoSuchPath if !dir_name
224
+ next_sha = dir_name.split(' ')[2]
225
+ next_path = append ? File.join(append, next_path) : next_path
226
+ if (last == '/')
227
+ ls_tree_path(next_sha, paths.join("/") + '/', next_path)
228
+ else
229
+ ls_tree_path(next_sha, paths.join("/"), next_path)
230
+ end
231
+ end
232
+ else
233
+ tree = tree.split("\n")
234
+ tree = tree.select { |p| p.split("\t")[1] == path }
235
+ if append
236
+ mod_tree = []
237
+ tree.each do |ent|
238
+ (info, fpath) = ent.split("\t")
239
+ mod_tree << [info, File.join(append, fpath)].join("\t")
240
+ end
241
+ mod_tree
242
+ else
243
+ tree
244
+ end
245
+ end
246
+ end
247
+
248
+ # returns an array of GitRuby Commit objects
249
+ # [ [sha, raw_output], [sha, raw_output], [sha, raw_output] ... ]
250
+ #
251
+ # takes the following options:
252
+ # :since - Time object specifying that you don't want commits BEFORE this
253
+ # :until - Time object specifying that you don't want commit AFTER this
254
+ # :first_parent - tells log to only walk first parent
255
+ # :path_limiter - string or array of strings to limit path
256
+ # :max_count - number to limit the output
257
+ def log(sha, options = {})
258
+ @already_searched = {}
259
+ walk_log(sha, options)
260
+ end
261
+
262
+ def truncate_arr(arr, sha)
263
+ new_arr = []
264
+ arr.each do |a|
265
+ if a[0] == sha
266
+ return new_arr
267
+ end
268
+ new_arr << a
269
+ end
270
+ return new_arr
271
+ end
272
+
273
+ def rev_list(sha, options)
274
+ if sha.is_a? Array
275
+ (end_sha, sha) = sha
276
+ end
277
+
278
+ log = log(sha, options)
279
+ log = log.sort { |a, b| a[2] <=> b[2] }.reverse
280
+
281
+ if end_sha
282
+ log = truncate_arr(log, end_sha)
283
+ end
284
+
285
+ # shorten the list if it's longer than max_count (had to get everything in branches)
286
+ if options[:max_count]
287
+ if (opt_len = options[:max_count].to_i) < log.size
288
+ log = log[0, opt_len]
289
+ end
290
+ end
291
+
292
+ if options[:pretty] == 'raw'
293
+ log.map {|k, v| v }.join('')
294
+ else
295
+ log.map {|k, v| k }.join("\n")
296
+ end
297
+ end
298
+
299
+ # called by log() to recursively walk the tree
300
+ def walk_log(sha, opts, total_size = 0)
301
+ return [] if @already_searched[sha] # to prevent rechecking branches
302
+ @already_searched[sha] = true
303
+
304
+ array = []
305
+ if (sha)
306
+ o = get_raw_object_by_sha1(sha)
307
+ if o.type == :tag
308
+ commit_sha = get_object_by_sha1(sha).object
309
+ c = get_object_by_sha1(commit_sha)
310
+ else
311
+ c = GitObject.from_raw(o)
312
+ end
313
+
314
+ return [] if c.type != :commit
315
+
316
+ add_sha = true
317
+
318
+ if opts[:since] && opts[:since].is_a?(Time) && (opts[:since] > c.committer.date)
319
+ add_sha = false
320
+ end
321
+ if opts[:until] && opts[:until].is_a?(Time) && (opts[:until] < c.committer.date)
322
+ add_sha = false
323
+ end
324
+
325
+ # follow all parents unless '--first-parent' is specified #
326
+ subarray = []
327
+
328
+ if !c.parent.first && opts[:path_limiter] # check for the last commit
329
+ add_sha = false
330
+ end
331
+
332
+ if (!opts[:max_count] || ((array.size + total_size) < opts[:max_count]))
333
+
334
+ if !opts[:path_limiter]
335
+ output = c.raw_log(sha)
336
+ array << [sha, output, c.committer.date]
337
+ end
338
+
339
+ if (opts[:max_count] && (array.size + total_size) >= opts[:max_count])
340
+ return array
341
+ end
342
+
343
+ c.parent.each do |psha|
344
+ if psha && !files_changed?(c.tree, get_object_by_sha1(psha).tree,
345
+ opts[:path_limiter])
346
+ add_sha = false
347
+ end
348
+ subarray += walk_log(psha, opts, (array.size + total_size))
349
+ next if opts[:first_parent]
350
+ end
351
+
352
+ if opts[:path_limiter] && add_sha
353
+ output = c.raw_log(sha)
354
+ array << [sha, output, c.committer.date]
355
+ end
356
+
357
+ if add_sha
358
+ array += subarray
359
+ end
360
+ end
361
+
362
+ end
363
+
364
+ array
365
+ end
366
+
367
+ def diff(commit1, commit2, options = {})
368
+ patch = ''
369
+
370
+ commit_obj1 = get_object_by_sha1(commit1)
371
+ tree1 = commit_obj1.tree
372
+ if commit2
373
+ tree2 = get_object_by_sha1(commit2).tree
374
+ else
375
+ tree2 = get_object_by_sha1(commit_obj1.parent.first).tree
376
+ end
377
+
378
+ qdiff = quick_diff(tree1, tree2)
379
+
380
+ qdiff.sort.each do |diff_arr|
381
+ format, lines, output = :unified, 3, ''
382
+ file_length_difference = 0
383
+
384
+ fileA = (diff_arr[2]) ? cat_file(diff_arr[2]) : ''
385
+ fileB = (diff_arr[3]) ? cat_file(diff_arr[3]) : ''
386
+
387
+ sha1 = (diff_arr[2]) ? diff_arr[2] : '0000000000000000000000000000000000000000'
388
+ sha2 = (diff_arr[3]) ? diff_arr[3] : '0000000000000000000000000000000000000000'
389
+
390
+ data_old = fileA.split(/\n/).map! { |e| e.chomp }
391
+ data_new = fileB.split(/\n/).map! { |e| e.chomp }
392
+
393
+ diffs = Difference::LCS.diff(data_old, data_new)
394
+ next if diffs.empty?
395
+
396
+ header = 'diff --git a/' + diff_arr[0].gsub('./', '') + ' b/' + diff_arr[0].gsub('./', '')
397
+ if options[:full_index]
398
+ header << "\n" + 'index ' + sha1 + '..' + sha2
399
+ header << ' 100644' if diff_arr[3] # hard coding this because i don't think we use it
400
+ else
401
+ header << "\n" + 'index ' + sha1[0,7] + '..' + sha2[0,7]
402
+ header << ' 100644' if diff_arr[3] # hard coding this because i don't think we use it
403
+ end
404
+ header << "\n--- " + 'a/' + diff_arr[0].gsub('./', '')
405
+ header << "\n+++ " + 'b/' + diff_arr[0].gsub('./', '')
406
+ header += "\n"
407
+
408
+ oldhunk = hunk = nil
409
+
410
+ diffs.each do |piece|
411
+ begin
412
+ hunk = Difference::LCS::Hunk.new(data_old, data_new, piece, lines, file_length_difference)
413
+ file_length_difference = hunk.file_length_difference
414
+
415
+ next unless oldhunk
416
+
417
+ if lines > 0 && hunk.overlaps?(oldhunk)
418
+ hunk.unshift(oldhunk)
419
+ else
420
+ output << oldhunk.diff(format)
421
+ end
422
+ ensure
423
+ oldhunk = hunk
424
+ output << "\n"
425
+ end
426
+ end
427
+
428
+ output << oldhunk.diff(format)
429
+ output << "\n"
430
+
431
+ patch << header + output.lstrip
432
+ end
433
+ patch
434
+ rescue
435
+ '' # one of the trees was bad or lcs isn't there - no diff
436
+ end
437
+
438
+ # takes 2 tree shas and recursively walks them to find out what
439
+ # files or directories have been modified in them and returns an
440
+ # array of changes
441
+ # [ [full_path, 'added', tree1_hash, nil],
442
+ # [full_path, 'removed', nil, tree2_hash],
443
+ # [full_path, 'modified', tree1_hash, tree2_hash]
444
+ # ]
445
+ def quick_diff(tree1, tree2, path = '.', recurse = true)
446
+ # handle empty trees
447
+ changed = []
448
+ return changed if tree1 == tree2
449
+
450
+ t1 = list_tree(tree1) if tree1
451
+ t2 = list_tree(tree2) if tree2
452
+
453
+ # finding files that are different
454
+ t1['blob'].each do |file, hsh|
455
+ t2_file = t2['blob'][file] rescue nil
456
+ full = File.join(path, file)
457
+ if !t2_file
458
+ changed << [full, 'added', hsh[:sha], nil] # not in parent
459
+ elsif (hsh[:sha] != t2_file[:sha])
460
+ changed << [full, 'modified', hsh[:sha], t2_file[:sha]] # file changed
461
+ end
462
+ end if t1
463
+ t2['blob'].each do |file, hsh|
464
+ if !t1 || !t1['blob'][file]
465
+ changed << [File.join(path, file), 'removed', nil, hsh[:sha]]
466
+ end
467
+ end if t2
468
+
469
+ t1['tree'].each do |dir, hsh|
470
+ t2_tree = t2['tree'][dir] rescue nil
471
+ full = File.join(path, dir)
472
+ if !t2_tree
473
+ if recurse
474
+ changed += quick_diff(hsh[:sha], nil, full, true)
475
+ else
476
+ changed << [full, 'added', hsh[:sha], nil] # not in parent
477
+ end
478
+ elsif (hsh[:sha] != t2_tree[:sha])
479
+ if recurse
480
+ changed += quick_diff(hsh[:sha], t2_tree[:sha], full, true)
481
+ else
482
+ changed << [full, 'modified', hsh[:sha], t2_tree[:sha]] # file changed
483
+ end
484
+ end
485
+ end if t1
486
+ t2['tree'].each do |dir, hsh|
487
+ t1_tree = t1['tree'][dir] rescue nil
488
+ full = File.join(path, dir)
489
+ if !t1_tree
490
+ if recurse
491
+ changed += quick_diff(nil, hsh[:sha], full, true)
492
+ else
493
+ changed << [full, 'removed', nil, hsh[:sha]]
494
+ end
495
+ end
496
+ end if t2
497
+
498
+ changed
499
+ end
500
+
501
+ # returns true if the files in path_limiter were changed, or no path limiter
502
+ # used by the log() function when passed with a path_limiter
503
+ def files_changed?(tree_sha1, tree_sha2, path_limiter = nil)
504
+ if path_limiter
505
+ mod = quick_diff(tree_sha1, tree_sha2)
506
+ files = mod.map { |c| c.first }
507
+ path_limiter.to_a.each do |filepath|
508
+ if files.include?(filepath)
509
+ return true
510
+ end
511
+ end
512
+ return false
513
+ end
514
+ true
515
+ end
516
+
517
+ def get_subtree(commit_sha, path)
518
+ tree_sha = get_object_by_sha1(commit_sha).tree
519
+
520
+ if path && !(path == '' || path == '.' || path == './')
521
+ paths = path.split('/')
522
+ paths.each do |path|
523
+ tree = get_object_by_sha1(tree_sha)
524
+ if entry = tree.entry.select { |e| e.name == path }.first
525
+ tree_sha = entry.sha1 rescue nil
526
+ else
527
+ return false
528
+ end
529
+ end
530
+ end
531
+
532
+ tree_sha
533
+ end
534
+
535
+ def blame_tree(commit_sha, path)
536
+ # find subtree
537
+ tree_sha = get_subtree(commit_sha, path)
538
+ return {} if !tree_sha
539
+
540
+ looking_for = []
541
+ get_object_by_sha1(tree_sha).entry.each do |e|
542
+ looking_for << File.join('.', e.name)
543
+ end
544
+
545
+ @already_searched = {}
546
+ commits = look_for_commits(commit_sha, path, looking_for)
547
+
548
+ # cleaning up array
549
+ arr = {}
550
+ commits.each do |commit_array|
551
+ key = commit_array[0].gsub('./', '')
552
+ arr[key] = commit_array[1]
553
+ end
554
+ arr
555
+ end
556
+
557
+ def look_for_commits(commit_sha, path, looking_for, options = {})
558
+ return [] if @already_searched[commit_sha] # to prevent rechecking branches
559
+
560
+ @already_searched[commit_sha] = true
561
+
562
+ commit = get_object_by_sha1(commit_sha)
563
+ tree_sha = get_subtree(commit_sha, path)
564
+
565
+ found_data = []
566
+
567
+ # at the beginning of the branch
568
+ if commit.parent.size == 0
569
+ looking_for.each do |search|
570
+ # prevents the rare case of multiple branch starting points with
571
+ # files that have never changed
572
+ if found_data.assoc(search)
573
+ found_data << [search, commit_sha]
574
+ end
575
+ end
576
+ return found_data
577
+ end
578
+
579
+ # go through the parents recursively, looking for somewhere this has been changed
580
+ commit.parent.each do |pc|
581
+ diff = quick_diff(tree_sha, get_subtree(pc, path), '.', false)
582
+
583
+ # remove anything found
584
+ looking_for.each do |search|
585
+ if match = diff.assoc(search)
586
+ found_data << [search, commit_sha, match]
587
+ looking_for.delete(search)
588
+ end
589
+ end
590
+
591
+ if looking_for.size <= 0 # we're done
592
+ return found_data
593
+ end
594
+
595
+ found_data += look_for_commits(pc, path, looking_for) # recurse into parent
596
+ return found_data if options[:first_parent]
597
+ end
598
+
599
+ ## TODO : find most recent commit with change in any parent
600
+ found_data
601
+ end
602
+
603
+ # initialize a git repository
604
+ def self.init(dir, bare = false)
605
+
606
+ FileUtils.mkdir_p(dir) if !File.exists?(dir)
607
+
608
+ FileUtils.cd(dir) do
609
+ if(File.exists?('objects'))
610
+ return false # already initialized
611
+ else
612
+ # initialize directory
613
+ create_initial_config(bare)
614
+ FileUtils.mkdir_p('refs/heads')
615
+ FileUtils.mkdir_p('refs/tags')
616
+ FileUtils.mkdir_p('objects/info')
617
+ FileUtils.mkdir_p('objects/pack')
618
+ FileUtils.mkdir_p('branches')
619
+ add_file('description', 'Unnamed repository; edit this file to name it for gitweb.')
620
+ add_file('HEAD', "ref: refs/heads/master\n")
621
+ FileUtils.mkdir_p('hooks')
622
+ FileUtils.cd('hooks') do
623
+ add_file('applypatch-msg', '# add shell script and make executable to enable')
624
+ add_file('post-commit', '# add shell script and make executable to enable')
625
+ add_file('post-receive', '# add shell script and make executable to enable')
626
+ add_file('post-update', '# add shell script and make executable to enable')
627
+ add_file('pre-applypatch', '# add shell script and make executable to enable')
628
+ add_file('pre-commit', '# add shell script and make executable to enable')
629
+ add_file('pre-rebase', '# add shell script and make executable to enable')
630
+ add_file('update', '# add shell script and make executable to enable')
631
+ end
632
+ FileUtils.mkdir_p('info')
633
+ add_file('info/exclude', "# *.[oa]\n# *~")
634
+ end
635
+ end
636
+ end
637
+
638
+ def self.create_initial_config(bare = false)
639
+ bare ? bare_status = 'true' : bare_status = 'false'
640
+ config = "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = #{bare_status}\n\tlogallrefupdates = true"
641
+ add_file('config', config)
642
+ end
643
+
644
+ def self.add_file(name, contents)
645
+ File.open(name, 'w') do |f|
646
+ f.write contents
647
+ end
648
+ end
649
+
650
+ def close
651
+ @packs.each do |pack|
652
+ pack.close
653
+ end if @packs
654
+ end
655
+
656
+ protected
657
+
658
+ def git_path(path)
659
+ return "#@git_dir/#{path}"
660
+ end
661
+
662
+ private
663
+
664
+ def initloose
665
+ @loose = []
666
+ load_loose(git_path('objects'))
667
+ load_alternate_loose(git_path('objects'))
668
+ @loose
669
+ end
670
+
671
+ def load_alternate_loose(path)
672
+ # load alternate loose, too
673
+ alt = File.join(path, 'info/alternates')
674
+ if File.exists?(alt)
675
+ File.readlines(alt).each do |line|
676
+ if line[0, 2] == '..'
677
+ line = File.expand_path(File.join(@git_dir, line))
678
+ end
679
+ load_loose(line.chomp)
680
+ load_alternate_loose(line.chomp)
681
+ end
682
+ end
683
+ end
684
+
685
+ def load_loose(path)
686
+ return if !File.exists?(path)
687
+ @loose << Grit::GitRuby::Internal::LooseStorage.new(path)
688
+ end
689
+
690
+ def initpacks
691
+ close
692
+ @packs = []
693
+ load_packs(git_path("objects/pack"))
694
+ load_alternate_packs(git_path('objects'))
695
+ @packs
696
+ end
697
+
698
+ def load_alternate_packs(path)
699
+ alt = File.join(path, 'info/alternates')
700
+ if File.exists?(alt)
701
+ File.readlines(alt).each do |line|
702
+ if line[0, 2] == '..'
703
+ line = File.expand_path(File.join(@git_dir, line))
704
+ end
705
+ full_pack = File.join(line.chomp, 'pack')
706
+ load_packs(full_pack)
707
+ load_alternate_packs(File.join(line.chomp))
708
+ end
709
+ end
710
+ end
711
+
712
+ def load_packs(path)
713
+ return if !File.exists?(path)
714
+ Dir.open(path) do |dir|
715
+ dir.each do |entry|
716
+ next if !(entry =~ /\.pack$/i)
717
+ pack = Grit::GitRuby::Internal::PackStorage.new(File.join(path,entry))
718
+ if @options[:map_packfile]
719
+ pack.cache_objects
720
+ end
721
+ @packs << pack
722
+ end
723
+ end
724
+ end
725
+
726
+ end
727
+
728
+ end
729
+ end