amp-git 0.1.0

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 (48) hide show
  1. data/.document +5 -0
  2. data/.gitignore +23 -0
  3. data/Gemfile +15 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +17 -0
  6. data/Rakefile +68 -0
  7. data/VERSION +1 -0
  8. data/features/amp-git.feature +9 -0
  9. data/features/step_definitions/amp-git_steps.rb +0 -0
  10. data/features/support/env.rb +4 -0
  11. data/lib/amp-git/encoding/binary_delta.rb +171 -0
  12. data/lib/amp-git/repo_format/changeset.rb +348 -0
  13. data/lib/amp-git/repo_format/commit_object.rb +87 -0
  14. data/lib/amp-git/repo_format/index.rb +169 -0
  15. data/lib/amp-git/repo_format/loose_object.rb +78 -0
  16. data/lib/amp-git/repo_format/packfile.rb +263 -0
  17. data/lib/amp-git/repo_format/packfile_index.rb +196 -0
  18. data/lib/amp-git/repo_format/raw_object.rb +56 -0
  19. data/lib/amp-git/repo_format/staging_area.rb +215 -0
  20. data/lib/amp-git/repo_format/tag_object.rb +87 -0
  21. data/lib/amp-git/repo_format/tree_object.rb +98 -0
  22. data/lib/amp-git/repo_format/versioned_file.rb +133 -0
  23. data/lib/amp-git/repositories/local_repository.rb +192 -0
  24. data/lib/amp-git/repository.rb +57 -0
  25. data/lib/amp-git.rb +49 -0
  26. data/lib/amp_plugin.rb +1 -0
  27. data/spec/amp-git_spec.rb +15 -0
  28. data/spec/repository_spec.rb +74 -0
  29. data/spec/spec.opts +1 -0
  30. data/spec/spec_helper.rb +29 -0
  31. data/test/index_tests/index +0 -0
  32. data/test/index_tests/test_helper.rb +16 -0
  33. data/test/index_tests/test_index.rb +69 -0
  34. data/test/packfile_tests/hasindex.idx +0 -0
  35. data/test/packfile_tests/hasindex.pack +0 -0
  36. data/test/packfile_tests/pack-4e1941122fd346526b0a3eee2d92f3277a0092cd.pack +0 -0
  37. data/test/packfile_tests/pack-d23ff2538f970371144ae7182c28730b11eb37c1.idx +0 -0
  38. data/test/packfile_tests/test_helper.rb +16 -0
  39. data/test/packfile_tests/test_packfile.rb +75 -0
  40. data/test/packfile_tests/test_packfile_index_v2.rb +90 -0
  41. data/test/packfile_tests/test_packfile_with_index.rb +76 -0
  42. data/test/test_commit_object.rb +60 -0
  43. data/test/test_git_delta.rb +67 -0
  44. data/test/test_helper.rb +71 -0
  45. data/test/test_loose_object.rb +51 -0
  46. data/test/test_tag_object.rb +72 -0
  47. data/test/test_tree_object.rb +55 -0
  48. metadata +215 -0
@@ -0,0 +1,87 @@
1
+ ##################################################################
2
+ # Licensing Information #
3
+ # #
4
+ # The following code is licensed, as standalone code, under #
5
+ # the Ruby License, unless otherwise directed within the code. #
6
+ # #
7
+ # For information on the license of this code when distributed #
8
+ # with and used in conjunction with the other modules in the #
9
+ # Amp project, please see the root-level LICENSE file. #
10
+ # #
11
+ # © Michael J. Edgar and Ari Brown, 2009-2010 #
12
+ # #
13
+ ##################################################################
14
+
15
+ # This was written by reading the Git Book. No source code was
16
+ # examined to produce this code. It is the original work of its
17
+ # creators, Michael Edgar and Ari Brown.
18
+ #
19
+ # http://book.git-scm.com/7_how_git_stores_objects.html
20
+
21
+ module Amp
22
+ module Core
23
+ module Repositories
24
+ module Git
25
+ ##
26
+ # = CommitObject
27
+ #
28
+ # This is a commit object in the git system. This contains a reference to
29
+ # a tree, one or more parents, an author, a committer, and a message. This
30
+ # object is all you need to know everything about a commit.
31
+ class CommitObject < RawObject
32
+ attr_reader :tree_ref, :parent_refs, :author, :committer, :date, :message
33
+
34
+ ##
35
+ # Initializes the CommitObject. Needs a hash to identify it and
36
+ # an opener. The opener should point to the .git directory. Immediately
37
+ # parses the object.
38
+ #
39
+ # @param [String] hsh the hash to use to find the object
40
+ # @param [Support::RootedOpener] opener the opener to use to open the
41
+ # object file
42
+ # @param [String] content if the content is known already, use
43
+ # the provided content instead
44
+ def initialize(hsh, opener, content = nil)
45
+ if content
46
+ @hash_id, @opener = hsh, opener
47
+ @type = 'commit'
48
+ @content = content
49
+ else
50
+ super(hsh, opener)
51
+ end
52
+ @parent_refs = []
53
+ parse!
54
+ end
55
+
56
+ private
57
+
58
+ ##
59
+ # Parses the commit object into our attributes.
60
+ def parse!
61
+ lines = @content.split("\n")
62
+ last_idx = 0
63
+ lines.each_with_index do |line, idx|
64
+ case line
65
+ when /^tree (.{40})/
66
+ @tree_ref = Support::StringUtils.unhexlify($1)
67
+ when /^parent (.{40})/
68
+ @parent_refs << Support::StringUtils.unhexlify($1)
69
+ when /^author #{AUTHOR_MATCH}/
70
+ @author = "#{$1} <#{$2}>"
71
+ @date = Time.at($3.to_i)
72
+ when /^committer #{AUTHOR_MATCH}/
73
+ @committer = "#{$1} <#{$2}>"
74
+ @date = Time.at($3.to_i)
75
+ when ""
76
+ last_idx = idx + 1
77
+ break
78
+ end
79
+ end
80
+ @message = lines[last_idx..-1].join("\n")
81
+ end
82
+
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,169 @@
1
+ ##################################################################
2
+ # Licensing Information #
3
+ # #
4
+ # The following code is licensed, as standalone code, under #
5
+ # the Ruby License, unless otherwise directed within the code. #
6
+ # #
7
+ # For information on the license of this code when distributed #
8
+ # with and used in conjunction with the other modules in the #
9
+ # Amp project, please see the root-level LICENSE file. #
10
+ # #
11
+ # © Michael J. Edgar and Ari Brown, 2009-2010 #
12
+ # #
13
+ ##################################################################
14
+
15
+ # This was written by reading the Git Book. No source code was
16
+ # examined to produce this code. It is the original work of its
17
+ # creators, Michael Edgar and Ari Brown.
18
+ #
19
+ # http://book.git-scm.com/7_the_packfile.html
20
+ # http://repo.or.cz/w/git.git?a=blob;f=Documentation/technical/pack-format.txt;h=1803e64e465fa4f8f0fe520fc0fd95d0c9def5bd;hb=HEAD
21
+ # http://git.rsbx.net/Documents/Git_Data_Formats.txt
22
+
23
+ module Amp
24
+ module Core
25
+ module Repositories
26
+ module Git
27
+ ##
28
+ # = Index
29
+ #
30
+ # The Index is essentially a cache of the working directory. It tracks
31
+ # which files have been added to the staging area and which have not, and
32
+ # can be used to check if a file has been modified or not. It is a relatively
33
+ # complex binary format and there are two versions of it we also have to
34
+ # support.
35
+ module Index
36
+
37
+ class IndexParseError < StandardError; end
38
+
39
+ ##
40
+ # Parses the given file as an Index, and returns the appropriate subclass of Index.
41
+ # There are two versions that are supported and each needs to be able to handle
42
+ # status lookups and so on.
43
+ #
44
+ # @param [String] file the name of the file to open
45
+ # @param [Support::RootedOpener] opener an opener to scope the opening of files
46
+ # @return [AbstractIndex] the index subclass this file represents
47
+ def self.parse(file, opener)
48
+ opener.open(file, "r") do |fp|
49
+ if fp.read(4) != "DIRC"
50
+ raise IndexParseError.new("#{file} is not an index file.")
51
+ end
52
+ version = fp.read(4).unpack("N").first
53
+ case version
54
+ when 1
55
+ IndexVersion1.new(fp)
56
+ when 2
57
+ IndexVersion2.new(fp)
58
+ end
59
+ end
60
+ end
61
+
62
+ ##
63
+ # The format of each index entry is as follows:
64
+ # create_time, 32-bits # in seconds, least-significant bits if rollover
65
+ # create_time_nanoseconds, 32-bits
66
+ # modify_time, 32-bits # in seconds, least-significant bits if rollover
67
+ # modify_time_nanoseconds, 32-bits
68
+ # device, 32-bits # device id
69
+ # inode, 32-bits # inode from the filesystem
70
+ # mode, 32-bits # permissions/mode from the FS
71
+ # uid, 32-bits # user ID from the FS
72
+ # gid, 32-bits # group ID from the FS
73
+ # size, 32-bits # filesize, least-significant-bits, from FS
74
+ # hash_id, 20 bytes # sha-1 hash of the data
75
+ # assume_valid, 1 bit # flag for whether this file should be assumed to be unchanged
76
+ # update_needed, 1 bit # flag saying the file needs to be refreshed
77
+ # stage, 2 bits # two flags used for merging
78
+ # filename_size, 12 bits # the size of the upcoming filename in bytes
79
+ # filename, N bytes # the name of the file in the index
80
+ # padding, N bytes # null padding. At least 1 byte, enough to make the block's size a
81
+ # multiple of 8 bytes
82
+ #
83
+ # This class is a big effing struct for this.
84
+ class IndexEntry < Struct.new(:ctime, :ctime_ns, :mtime, :mtime_ns, :dev, :inode, :mode, :uid, :gid, :size,
85
+ :hash_id, :assume_valid, :update_needed, :stage, :name)
86
+ ENTRY_HEADER_FORMAT = "NNNNNNNNNNa20n"
87
+ ENTRY_HEADER_SIZE = 62
88
+ def initialize(*args)
89
+ if args.size > 0 && args[0].kind_of?(IO)
90
+ fp = args.first
91
+ header = fp.read(ENTRY_HEADER_SIZE).unpack(ENTRY_HEADER_FORMAT)
92
+ self.ctime, self.ctime_ns, self.mtime, self.mtime_ns, self.dev, self.inode,
93
+ self.mode, self.uid, self.gid, self.size, self.hash_id, flags = header
94
+ self.assume_valid = flags & 0x8000 > 0
95
+ self.update_needed = flags & 0x4000 > 0
96
+ self.stage = (flags & 0x3000) >> 12
97
+ namesize = flags & 0x0FFF
98
+ self.name = fp.read(namesize)
99
+ mod = (ENTRY_HEADER_SIZE + namesize) & 0x7
100
+ padding_len = mod == 0 ? 8 : 8 - mod
101
+ fp.read(padding_len)
102
+ else
103
+ super
104
+ end
105
+ end
106
+ end
107
+
108
+ ##
109
+ # Generic Index class, handles common initialization and generic methods
110
+ # that aren't different between different versions of the index
111
+ class AbstractIndex
112
+ def initialize(fp)
113
+ @entry_map = {}
114
+ @entry_count = fp.read(4).unpack("N").first
115
+ end
116
+
117
+ ##
118
+ # @return [Integer] the number of entries in the Index.
119
+ def size
120
+ @entry_count
121
+ end
122
+
123
+ ##
124
+ # Returns an IndexEntry for the file with the given name.
125
+ # Returns nil on failure, and this should not be used by end-users
126
+ #
127
+ # @param [String] name the name of the object/file to look up
128
+ # @return [IndexEntry, NilClass] the entry with the given name, or nil
129
+ def [](name)
130
+ @entry_map[name]
131
+ end
132
+
133
+ def read_entries(fp)
134
+ @entries = []
135
+ @entry_count.times do
136
+ new_entry = IndexEntry.new(fp)
137
+ @entries << new_entry
138
+ @entry_map[new_entry.name] = new_entry
139
+ end
140
+ end
141
+
142
+ def inspect
143
+ "<Git Index, entries: #{@entry_count}>"
144
+ end
145
+ end
146
+
147
+ ##
148
+ # Older version of the index. Not used anymore by git.
149
+ class IndexVersion1 < AbstractIndex
150
+ def initialize(fp)
151
+ super
152
+ @checksum = fp.read(20)
153
+ read_entries(fp)
154
+ end
155
+ end
156
+
157
+ ##
158
+ # Newer version of the index - default format of the index.
159
+ class IndexVersion2 < AbstractIndex
160
+ def initialize(fp)
161
+ super
162
+ read_entries(fp)
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,78 @@
1
+ ##################################################################
2
+ # Licensing Information #
3
+ # #
4
+ # The following code is licensed, as standalone code, under #
5
+ # the Ruby License, unless otherwise directed within the code. #
6
+ # #
7
+ # For information on the license of this code when distributed #
8
+ # with and used in conjunction with the other modules in the #
9
+ # Amp project, please see the root-level LICENSE file. #
10
+ # #
11
+ # © Michael J. Edgar and Ari Brown, 2009-2010 #
12
+ # #
13
+ ##################################################################
14
+
15
+ # This was written by reading the Git Book. No source code was
16
+ # examined to produce this code. It is the original work of its
17
+ # creators, Michael Edgar and Ari Brown.
18
+ #
19
+ # http://book.git-scm.com/7_how_git_stores_objects.html
20
+
21
+ module Amp
22
+ module Core
23
+ module Repositories
24
+ module Git
25
+ ##
26
+ # = LooseObject
27
+ #
28
+ # A single loose object (tree, tag, commit, etc.) in the Git system.
29
+ # Its type and content will be determined after we read the file.
30
+ #
31
+ # It is uniquely identified by a SHA1 hash.
32
+ class LooseObject < RawObject
33
+
34
+ class << self
35
+
36
+ def lookup(hsh, opener)
37
+ require 'scanf'
38
+ path = File.join("objects", hsh[0..1], hsh[2..40])
39
+ mode = "r"
40
+ type, content = nil, nil
41
+ begin
42
+ opener.open(path, mode) do |fp|
43
+ type, content_size = fp.scanf("%s %d")
44
+ fp.seek(type.size + 1 + content_size.to_s.size + 1, IO::SEEK_SET)
45
+ content = fp.read(content_size)
46
+ end
47
+ rescue SystemCallError
48
+ if create
49
+ FileUtils.mkdir_p(opener.join("objects", hsh[0..1]))
50
+ mode = "w+"
51
+ retry
52
+ else
53
+ raise
54
+ end
55
+ end
56
+
57
+ RawObject.construct(hsh, opener, type, content)
58
+ end
59
+ end
60
+
61
+ attr_accessor :type
62
+
63
+ ##
64
+ # Initializes the RawObject. Needs a hash to identify it and
65
+ # an opener. The opener should point to the .git directory.
66
+ #
67
+ # @param [String] hsh the hash to use to find the object
68
+ # @param [Support::RootedOpener] opener the opener to use to open the
69
+ # object file
70
+ def initialize(hsh, opener, content = nil)
71
+ @hash_id, @opener, @content = hsh, opener, content
72
+ end
73
+
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,263 @@
1
+ ##################################################################
2
+ # Licensing Information #
3
+ # #
4
+ # The following code is licensed, as standalone code, under #
5
+ # the Ruby License, unless otherwise directed within the code. #
6
+ # #
7
+ # For information on the license of this code when distributed #
8
+ # with and used in conjunction with the other modules in the #
9
+ # Amp project, please see the root-level LICENSE file. #
10
+ # #
11
+ # © Michael J. Edgar and Ari Brown, 2009-2010 #
12
+ # #
13
+ ##################################################################
14
+
15
+ # This was written by reading the Git Book. No source code was
16
+ # examined to produce this code. It is the original work of its
17
+ # creators, Michael Edgar and Ari Brown.
18
+ #
19
+ # http://book.git-scm.com/7_the_packfile.html
20
+ # http://repo.or.cz/w/git.git?a=blob;f=Documentation/technical/pack-format.txt;h=1803e64e465fa4f8f0fe520fc0fd95d0c9def5bd;hb=HEAD
21
+ # http://git.rsbx.net/Documents/Git_Data_Formats.txt
22
+
23
+ module Amp
24
+ module Core
25
+ module Repositories
26
+ module Git
27
+ ##
28
+ # = PackFile
29
+ #
30
+ # Git uses it's "gc" command to pack loose objects into PackFiles.
31
+ # This is one file, preferably with an index (though not requiring one),
32
+ # which stores a number of objects in a very simple raw form.
33
+ #
34
+ # The index is *not* necessary. It is simply preferable because otherwise
35
+ # you have to uncompress each object in a raw, then calculate the hash of
36
+ # the object, just to find out where each object is and what its hash is.
37
+ class PackFile
38
+ include Amp::Core::Support
39
+ ##
40
+ # A single entry in a packfile. Dumb struct. However, it has some smart
41
+ # class methods for parsing these bad boys in from a packfile. Take a
42
+ # look at {#at} and {#read}.
43
+ class PackFileEntry < Struct.new(:type, :size, :content, :hash_id, :reference, :offset, :delta_offset)
44
+ include Amp::Core::Support
45
+ class << self
46
+
47
+ ##
48
+ # Reads a {PackFileEntry} from the given file at a given offset.
49
+ # This is a helper method for the entry point for reading from an actual PackFile,
50
+ # since often, you'll know where the entry will be located in the PackFile.
51
+ #
52
+ # @param [IO, #read] fp the file to read from
53
+ # @return [PackFileEntry] an entry, decompressed and with its hash calculated.
54
+ def at(fp, pos)
55
+ fp.seek pos, IO::SEEK_SET
56
+ read fp
57
+ end
58
+
59
+ ##
60
+ # Reads a {PackFileEntry} from the given file. This is the entry point for
61
+ # reading from an actual PackFile.
62
+ #
63
+ # @param [IO, #read] fp the file to read from
64
+ # @return [PackFileEntry] an entry, decompressed and with its hash calculated.
65
+ def read(fp)
66
+ result = PackFileEntry.new
67
+ result.offset = fp.pos
68
+ result.type, result.size = read_header(fp)
69
+ if result.type == OBJ_REF_DELTA
70
+ result.reference = fp.read(20)
71
+ elsif result.type == OBJ_OFS_DELTA
72
+ result.delta_offset = result.offset - read_offset(fp)
73
+ end
74
+ result.content = read_data(fp, result.size)
75
+ if result.type == OBJ_REF_DELTA
76
+
77
+ elsif result.type == OBJ_OFS_DELTA
78
+ cur = fp.tell
79
+ patch = Amp::Core::Repositories::Git::Encoding::BinaryDelta.new(result.content)
80
+ previous = self.at(fp, result.delta_offset)
81
+ result.content = patch.apply(previous.content)
82
+ result.size = result.content.size
83
+ result.type = previous.type
84
+ fp.seek(cur, IO::SEEK_SET)
85
+ end
86
+ result.calculate_hash!
87
+ result
88
+ end
89
+
90
+ ##
91
+ # Reads an OBJ_OFS_DELTA offset. N-bytes, encoded as a series of
92
+ # bytes. Each byte is shifted by (7 * n) bits, and added to the
93
+ # total. If the high bit (MSB) of a byte is 1, then another byte is
94
+ # read, If it's 0, it stops.
95
+ #
96
+ # @param [IO, #read] fp the IO stream to read from
97
+ # @return [Integer] the offset read
98
+ def read_offset(fp)
99
+ byte = Support::StringUtils.ord(fp.read(1))
100
+ tot = byte & 0x7f
101
+ while (byte & 0x80) > 0
102
+ byte = Support::StringUtils.ord(fp.read(1))
103
+ tot = ((tot + 1) << 7) | (byte & 0x7f)
104
+ break if (byte & 0x80) == 0
105
+ end
106
+ tot
107
+ end
108
+
109
+ ##
110
+ # Reads in a PackFileEntry header from the file. This will get us the
111
+ # type of the entry, as well as the size of its uncompressed data.
112
+ #
113
+ # @param [IO, #read] fp the file to read the header from
114
+ # @return [Array(Integer, Integer)] the type and size of the entry packed
115
+ # into a tuple.
116
+ def read_header(fp)
117
+ tags = Support::StringUtils.ord(fp.read(1))
118
+ type = (tags & 0x70) >> 4
119
+ size = tags & 0xF
120
+ shift = 4
121
+ while tags & 0x80 > 0
122
+ tags = Support::StringUtils.ord(fp.read(1))
123
+ size += (tags & 0x7F) << shift
124
+ shift += 7
125
+ end
126
+ [type, size]
127
+ end
128
+
129
+ ##
130
+ # Reads data from the file, uncompressing along the way, until +size+ bytes
131
+ # have been decompressed. Since we don't know how much that will be ahead of time,
132
+ # this is annoying slow. Oh wells.
133
+ #
134
+ # @param [IO, #read] fp the IO source to read compressed data from
135
+ # @param [Integer] size the amount of uncompressed data to expect
136
+ # @return [String] the uncompressed data
137
+ def read_data(fp, size)
138
+ result = ""
139
+ z = Zlib::Inflate.new
140
+ start = fp.tell
141
+ while result.size < size && !z.stream_end?
142
+ result += z.inflate(fp.read(1))
143
+ end
144
+ # final bytes... can't predict this yet though it's usually 5 bytes
145
+ while !fp.eof?
146
+ begin
147
+ result += z.finish
148
+ break
149
+ rescue Zlib::BufError
150
+ result += z.inflate(fp.read(1))
151
+ end
152
+ end
153
+ z.close
154
+ result
155
+ end
156
+ end
157
+
158
+ ##
159
+ # Calculates the hash of this particular entry. We need to reconstruct the loose object
160
+ # header to do this.
161
+ def calculate_hash!
162
+ prefix = PREFIX_NAME_LOOKUP[self.type]
163
+ # add special cases for refs
164
+ self.hash_id = StringUtils.sha1("#{prefix} #{self.size}\0#{self.content}").digest
165
+ self.hash_id.force_encoding("ASCII-8BIT") if RUBY_VERSION >= "1.9"
166
+ end
167
+
168
+ ##
169
+ # Converts to an actual raw object.
170
+ #
171
+ # @param [Support::RootedOpener] an opener in case this object references other things....
172
+ # should usually be set
173
+ # @return [RawObject] this entry in raw object form
174
+ def to_raw_object(opener = nil)
175
+ RawObject.construct(hash_id, opener, PREFIX_NAME_LOOKUP[type], content)
176
+ end
177
+ end
178
+
179
+ attr_reader :index, :version, :size, :name
180
+
181
+ OBJ_COMMIT = 1
182
+ OBJ_TREE = 2
183
+ OBJ_BLOB = 3
184
+ OBJ_TAG = 4
185
+ OBJ_OFS_DELTA = 6
186
+ OBJ_REF_DELTA = 7
187
+
188
+ DATA_START_OFFSET = 12
189
+
190
+ PREFIX_NAME_LOOKUP = {OBJ_COMMIT => 'commit', OBJ_TREE => 'tree', OBJ_BLOB => 'blob', OBJ_TAG => 'tag'}
191
+ ##
192
+ # Initializes a PackFile. Parses the header for some information but that's about it. It will
193
+ # however determine if there is an index file, and if so, it will load that for
194
+ # fast lookups later. It also verifies the fourcc of the packfile.
195
+ #
196
+ # @param [String] name the name of the packfile. This is relative to the directory it's in.
197
+ # @param [Support::RootedOpener] opener an opener that should be relative to the .git directory.
198
+ def initialize(name, opener)
199
+ @name = name
200
+ @opener = opener
201
+ opener.open(name, "r") do |fp|
202
+ # Check signature
203
+ unless fp.read(4) == "PACK"
204
+ raise ArgumentError.new("#{name} is not a packfile.")
205
+ end
206
+ @version = fp.read(4).unpack("N").first
207
+ @size = fp.read(4).unpack("N").first
208
+ cur = fp.tell
209
+ fp.seek(0, IO::SEEK_END)
210
+ @end_of_data = fp.tell - 20
211
+ end
212
+ possible_index_path = name[0..(name.size - File.extname(name).size - 1)] + ".idx"
213
+ if File.exist? possible_index_path
214
+ # use a persistent file pointer
215
+ fp = File.open(possible_index_path, "r")
216
+ @index = PackFileIndex.parse(fp)
217
+ end
218
+ @offset_cache = {}
219
+ end
220
+
221
+ def cached_offset(given_hash)
222
+ @offset_cache[given_hash]
223
+ end
224
+
225
+ def cache_entry(entry)
226
+ @offset_cache[entry.hash_id] = entry.offset
227
+ end
228
+
229
+ ##
230
+ # Gets an object in the Git system with the provided SHA1 hash identifier.
231
+ # If this packfile has an associated index file, that will be used. Otherwise,
232
+ # the packfile can be scanned from the beginning to the end, caching offsets as
233
+ # it goes, enabling easy lookup later. Either way, a RawObject or a subclass of it
234
+ # will be returned, or nil if no matching object is found.
235
+ #
236
+ # @param [String] given_hash the SHA-1 of the desired object
237
+ # @return [RawObject] the object with the given ID. Nil if the object is not in the
238
+ # packfile.
239
+ def object_for_hash(given_hash)
240
+ @opener.open(name, "r") do |fp|
241
+ given_hash.force_encoding("ASCII-8BIT") if RUBY_VERSION >= "1.9"
242
+ entry = nil
243
+ if index
244
+ starting_at = index.offset_for_hash(given_hash)
245
+ return PackFileEntry.at(starting_at, fp).to_raw_object
246
+ else
247
+ starting_at = cached_offset(given_hash) || DATA_START_OFFSET
248
+ fp.seek(starting_at, IO::SEEK_SET)
249
+ while fp.tell < @end_of_data
250
+ entry = PackFileEntry.read(fp)
251
+ cache_entry(entry)
252
+ return entry.to_raw_object if entry.hash_id == given_hash
253
+ end
254
+ end
255
+ end
256
+ nil
257
+ end
258
+
259
+ end
260
+ end
261
+ end
262
+ end
263
+ end