amp-git 0.1.0

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