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,196 @@
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://git.rsbx.net/Documents/Git_Data_Formats.txt
21
+ # http://repo.or.cz/w/git.git?a=blob;f=Documentation/technical/pack-format.txt;h=1803e64e465fa4f8f0fe520fc0fd95d0c9def5bd;hb=HEAD
22
+
23
+ module Amp
24
+ module Core
25
+ module Repositories
26
+ module Git
27
+ class PackFileIndexLookupError < StandardError; end
28
+ ##
29
+ # = PackFile
30
+ #
31
+ # Git uses it's "gc" command to pack loose objects into PackFiles.
32
+ # This is one file, preferably with an index (though not requiring one),
33
+ # which stores a number of objects in a very simple raw form.
34
+ #
35
+ # This class handles the index file, which is used for fast lookups of
36
+ # entries in packfiles. They use fanouts to make searching very simple.
37
+ #
38
+ # The fanout values are interesting - they are the number of entries with a first sha1
39
+ # byte less than or equal to the byte provided. So if the only sha1s you can
40
+ # find in a given packfile are 00112233.... and 020304.... then the first few
41
+ # fanouts are:
42
+ # fanout[0] = 1
43
+ # fanout[1] = 1
44
+ # fanout[2] = 2
45
+ # ....
46
+ # fanout[254] = 2
47
+ class PackFileIndex
48
+
49
+ class << self
50
+ ##
51
+ # Initializes the index file by reading from an input source. Does not
52
+ # automatically read in any actual data, but instead finds the necessary
53
+ # values to make searching by SHA1 quick.
54
+ #
55
+ # @param [IO, #read] fp an input stream at the beginning of the index
56
+ def parse(fp)
57
+ fourcc = fp.read(4)
58
+ if fourcc == "\xfftOc"
59
+ version = fp.read(4).unpack("N").first
60
+ PackFileIndexV2.new(fp) if version == 2
61
+ else
62
+ fp.seek(-4, IO::SEEK_CUR)
63
+ PackFileIndexV1.new(fp)
64
+ end
65
+ end
66
+ end
67
+
68
+ FANOUT_TABLE_SIZE = 4 * 256
69
+
70
+ # Common PackFileIndex methods
71
+ attr_reader :size
72
+ attr_reader :fanout
73
+
74
+ # Common initialization code
75
+ # @param [IO, #read] fp input stream to read the file from
76
+ def initialize(fp)
77
+ @fp = fp
78
+ @fanout = @fp.read(FANOUT_TABLE_SIZE).unpack("N256")
79
+ @size = @fanout[255]
80
+ end
81
+
82
+ ##
83
+ # uses fanout logic to determine the indices in which the desired
84
+ # hash might be found. This range can be searched to find the hash.
85
+ def search_range_for_hash(hash)
86
+ byte = Support::StringUtils.ord(hash[0,1])
87
+ min = byte > 0 ? (fanout[byte - 1]) : 0
88
+ max = fanout[byte]
89
+ min...max
90
+ end
91
+ end
92
+
93
+ ##
94
+ # This class is for the older version of index files. They are
95
+ # structured as simply a fanout table and a list of [offset][sha1] entries.
96
+ # There's a checksum at the bottom for verification.
97
+ #
98
+ # Initialization only reads in the fanout table, and the file is left open.
99
+ class PackFileIndexV1 < PackFileIndex
100
+ ENTRY_TABLE_START = 256 * 4
101
+ ENTRY_TABLE_ENTRY_SIZE = 20 + 4
102
+ ENTRY_TABLE_FORMAT = "Na20"
103
+
104
+ ##
105
+ # Looks up the offset in a packfile of a given hash. nil is returned if
106
+ # the sha1 isn't found.
107
+ #
108
+ # @param [String] hsh a binary, sha-1 hash of the desired object
109
+ # @return [Fixnum, nil] the offset into the corresponding packfile at which you
110
+ # can find the object
111
+ def offset_for_hash(hsh)
112
+ range = search_range_for_hash hsh
113
+ @fp.seek(FANOUT_TABLE_START + range.begin * ENTRY_TABLE_ENTRY_SIZE, IO::SEEK_SET)
114
+ # linear search for now.
115
+ # TODO: binary search!
116
+ range.each do |_|
117
+ offset, sha1 = @fp.read(ENTRY_TABLE_ENTRY_SIZE).unpack(ENTRY_TABLE_FORMAT)
118
+ return offset if sha1 == hsh
119
+ end
120
+ raise PackFileIndexLookupError.new("Couldn't find the hash #{hsh.inspect} in the packfile index.")
121
+ end
122
+ end
123
+
124
+ ##
125
+ # This class is for the older version of index files. They are
126
+ # structured as simply a fanout table and a list of [offset][sha1] entries.
127
+ # There's a checksum at the bottom for verification.
128
+ #
129
+ # Initialization only reads in the fanout table, and the file is left open.
130
+ class PackFileIndexV2 < PackFileIndex
131
+ ENTRY_TABLE_START = 256 * 4 + 8
132
+ SHA1_FORMAT = "a20"
133
+ SHA1_SIZE = 20
134
+
135
+ def sha1_table_offset
136
+ ENTRY_TABLE_START
137
+ end
138
+
139
+ def crc_table_offset
140
+ ENTRY_TABLE_START + size * SHA1_SIZE
141
+ end
142
+
143
+ def offset_table_offset
144
+ crc_table_offset + size * 4
145
+ end
146
+
147
+ def big_offset_table_offset
148
+ offset_table_offset + size * 4
149
+ end
150
+
151
+ ##
152
+ # Looks up the offset in a packfile of a given hash. nil is returned if
153
+ # the sha1 isn't found.
154
+ #
155
+ # @param [String] hsh a binary, sha-1 hash of the desired object
156
+ # @return [Fixnum, nil] the offset into the corresponding packfile at which you
157
+ # can find the object
158
+ def offset_for_hash(hsh)
159
+ range = search_range_for_hash hsh
160
+ @fp.seek(sha1_table_offset + range.begin * SHA1_SIZE, IO::SEEK_SET)
161
+ # linear search for now.
162
+ # TODO: binary search!
163
+ range.each do |idx|
164
+ sha1 = @fp.read(SHA1_SIZE).unpack(SHA1_FORMAT).first # sha1s are 20 bytes
165
+ return offset_for_index(idx) if sha1 == hsh
166
+ end
167
+ raise PackFileIndexLookupError.new("Couldn't find the hash #{hsh.inspect} in the packfile index.")
168
+ end
169
+
170
+ ##
171
+ # Looks up the offset of an object given its index in the file. This index
172
+ # is assumed to have been determined by matching the sha1 of the object to
173
+ # a sha1 in the SHA1 table of the index.
174
+ #
175
+ # This requires a little more advanced logic because there is an offset table,
176
+ # and a 64-bit offset table. The normal offset table only supports 31-bit offsets,
177
+ # if the high bit is set, then the 31-bit value is actually an index into the big
178
+ # offset table.
179
+ #
180
+ # @param [Integer] idx the index into the offset table
181
+ # @return [Integer] the offset in the packfile where you can find the object with
182
+ # the given index into the index
183
+ def offset_for_index(idx)
184
+ @fp.seek(offset_table_offset + 4 * idx, IO::SEEK_SET)
185
+ offset = @fp.read(4).unpack("N").first
186
+ if offset & 0x80000000 != 0
187
+ @fp.seek(big_offset_table_offset + 8 * (offset & 0x7fffffff))
188
+ offset = Support::EncodingUtils.network_to_host_64(@fp.read(8).unpack("Q").first)
189
+ end
190
+ offset
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,56 @@
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 RawObject
33
+ attr_reader :type, :content, :hash_id
34
+ AUTHOR_MATCH = /([^<]*) <(.*)> (\d+) ([-+]?\d+)/
35
+ class << self
36
+
37
+
38
+ def for_hash(hsh, git_opener)
39
+ # no way to handle packed objects yet
40
+ LooseObject.lookup(hsh, git_opener)
41
+ end
42
+
43
+ def construct(hsh, opener, type, content)
44
+ # type, content should both be set now
45
+ type_lookup = {'tree' => TreeObject, 'commit' => CommitObject, 'tag' => TagObject}
46
+ object_klass = type_lookup[type] || LooseObject
47
+ result = object_klass.new(hsh, opener, content)
48
+ result.type = type if object_klass == LooseObject
49
+ result
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,215 @@
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
+ module Amp
16
+ module Core
17
+ module Repositories
18
+ module Git
19
+ class StagingArea < Amp::Core::Repositories::AbstractStagingArea
20
+
21
+ attr_accessor :repo
22
+
23
+ def initialize(repo)
24
+ @repo = repo
25
+ end
26
+
27
+ ##
28
+ # Marks a file to be added to the repository upon the next commit.
29
+ #
30
+ # @param [[String]] filenames a list of files to add in the next commit
31
+ # @return [Boolean] true for success, false for failure
32
+ def add(*filenames)
33
+ `git add #{filenames.join ' '} 2> /dev/null`
34
+ true
35
+ end
36
+
37
+ ##
38
+ # The directory used by the VCS to store magical information (.hg, .git, etc.).
39
+ #
40
+ # @api
41
+ # @return [String] relative to root
42
+ def vcs_dir
43
+ '.hg'
44
+ end
45
+
46
+ ##
47
+ # Marks a file to be removed from the repository upon the next commit. Last argument
48
+ # can be a hash, which can take an :unlink key, specifying whether the files should actually
49
+ # be removed or not.
50
+ #
51
+ # @param [String, Array<String>] filenames a list of files to remove in the next commit
52
+ # @return [Boolean] true for success, false for failure
53
+ def remove(*filenames)
54
+ `git rm #{filenames.join ' '} 2> /dev/null`
55
+ true
56
+ end
57
+
58
+ ##
59
+ # Set +file+ as normal and clean. Un-removes any files marked as removed, and
60
+ # un-adds any files marked as added.
61
+ #
62
+ # @param [String, Array<String>] files the name of the files to mark as normal
63
+ # @return [Boolean] success marker
64
+ def normal(*files)
65
+ # Do nothing...
66
+ true
67
+ end
68
+
69
+ ##
70
+ # Mark the files as untracked.
71
+ #
72
+ # @param [Array<String>] files the name of the files to mark as untracked
73
+ # @return [Boolean] success marker
74
+ def forget(*files)
75
+ `git rm --cached #{files.join ' '} 2> /dev/null`
76
+ true
77
+ end
78
+
79
+ ##
80
+ # Marks a file to be copied from the +from+ location to the +to+ location
81
+ # in the next commit, while retaining history.
82
+ #
83
+ # @param [String] from the source of the file copy
84
+ # @param [String] to the destination of the file copy
85
+ # @return [Boolean] true for success, false for failure
86
+ def copy(from, to)
87
+ `git cp #{from} #{to} 2> /dev/null`
88
+ true
89
+ end
90
+
91
+ ##
92
+ # Marks a file to be moved from the +from+ location to the +to+ location
93
+ # in the next commit, while retaining history.
94
+ #
95
+ # @param [String] from the source of the file move
96
+ # @param [String] to the destination of the file move
97
+ # @return [Boolean] true for success, false for failure
98
+ def move(from, to)
99
+ `git mv #{from} #{to} 2> /dev/null`
100
+ true
101
+ end
102
+
103
+ ##
104
+ # Marks a modified file to be included in the next commit.
105
+ # If your VCS does this implicitly, this should be defined as a no-op.
106
+ #
107
+ # @param [String, Array<String>] filenames a list of files to include for committing
108
+ # @return [Boolean] true for success, false for failure
109
+ def include(*filenames)
110
+ add filenames
111
+ end
112
+ alias_method :stage, :include
113
+
114
+ ##
115
+ # Mark a modified file to not be included in the next commit.
116
+ # If your VCS does not include this idea because staging a file is implicit, this should
117
+ # be defined as a no-op.
118
+ #
119
+ # @param [[String]] filenames a list of files to remove from the staging area for committing
120
+ # @return [Boolean] true for success, false for failure
121
+ def exclude(*filenames)
122
+ `git rm --cached #{filenames.join ' '} 2> /dev/null`
123
+ true
124
+ end
125
+ alias_method :unstage, :exclude
126
+
127
+ ##
128
+ # Returns a Symbol.
129
+ #
130
+ # If you call localrepo#status from this method... well...
131
+ # I DARE YOU!
132
+ def file_status(filename)
133
+ parse!
134
+ inverted = @status.inject({}) do |h, (k, v)|
135
+ v.each {|v_| h[v_] = k }
136
+ h
137
+ end
138
+
139
+ # lame hack, i know
140
+ case val = inverted[filename]
141
+ when :modified
142
+ :normal
143
+ else
144
+ val
145
+ end
146
+ end
147
+
148
+ # modified, lookup, or clean
149
+ # in this case, since we're shelling out,
150
+ # only modified or clean
151
+ def file_precise_status(filename, st)
152
+ parse!
153
+ inverted = @status.inject({}) do |h, (k, v)|
154
+ v.each {|v_| h[v_] = k }
155
+ h
156
+ end
157
+
158
+ # bleh this code sucks
159
+ if inverted[filename] == :modified
160
+ :modified
161
+ else
162
+ :clean
163
+ end
164
+ end
165
+
166
+ ##
167
+ # Calculates the difference (in bytes) between a file and its last tracked state.
168
+ #
169
+ # Supplements the built-in #status method so that its output will include deltas.
170
+ #
171
+ # @apioptional
172
+ # @param [String] file the filename to look up
173
+ # @param [File::Stats] st the current results of File.lstat(file)
174
+ # @return [Fixnum] the number of bytes difference between the file and
175
+ # its last tracked state.
176
+ def calculate_delta(file, st)
177
+ 0
178
+ end
179
+
180
+ def parse!
181
+ return if @parsed
182
+
183
+ @status = {}
184
+ data = `git status 2> /dev/null`.split("\n")
185
+ data.inject @status do |h, line|
186
+ case line
187
+ when /^#\s+(\w+):\s(.+)$/
188
+ h[$1.to_sym] = $2.strip
189
+ when /^#\s+new file:\s(.+)$/
190
+ h[:added] = $1.strip
191
+ when /^#\s+([^ ]+)$/
192
+ h[:untracked] = $1.strip
193
+ else
194
+ h
195
+ end
196
+ end
197
+
198
+ @parsed = true
199
+ end
200
+
201
+ ##
202
+ # Returns all files tracked by the repository *for the working directory* - not
203
+ # to be confused with the most recent changeset.
204
+ #
205
+ # @api
206
+ # @return [Array<String>] all files tracked by the repository at this moment in
207
+ # time, including just-added files (for example) that haven't been committed yet.
208
+ def all_files
209
+ Amp::Git::WorkingDirectoryChangeset.new(@repo).all_files
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
@@ -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
+ # = TagObject
27
+ #
28
+ # This is a formal tag object in the commit system. Most tags actually don't
29
+ # involve one of these objects - they just create a ref (alias) to a commit
30
+ # object. This tag will also reference a (usually) commit object, but they
31
+ # can contain their own messages, including PGP signatures. And honestly,
32
+ # we just have to suck it up and support them.
33
+ class TagObject < RawObject
34
+ attr_reader :object_ref, :reffed_type, :tag, :tagger, :date, :message
35
+
36
+ ##
37
+ # Initializes the TagObject. Needs a hash to identify it and
38
+ # an opener. The opener should point to the .git directory. Immediately
39
+ # parses the object.
40
+ #
41
+ # @param [String] hsh the hash to use to find the object
42
+ # @param [Support::RootedOpener] opener the opener to use to open the
43
+ # object file
44
+ # @param [String] content if the content is known already, use
45
+ # the provided content instead
46
+ def initialize(hsh, opener, content = nil)
47
+ if content
48
+ @hash_id, @opener = hsh, opener
49
+ @type = 'tag'
50
+ @content = content
51
+ else
52
+ super(hsh, opener)
53
+ end
54
+ parse!
55
+ end
56
+
57
+ private
58
+
59
+ ##
60
+ # Parses the commit object into our attributes.
61
+ def parse!
62
+ lines = @content.split("\n")
63
+ last_idx = 0
64
+ lines.each_with_index do |line, idx|
65
+ case line
66
+ when /^object (.{40})/
67
+ @object_ref = Support::StringUtils.unhexlify($1)
68
+ when /^type (\S+)/
69
+ @reffed_type = $1
70
+ when /^tag\s+(.*)\s*$/
71
+ @tag = $1
72
+ when /^tagger #{AUTHOR_MATCH}/
73
+ @tagger = "#{$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,98 @@
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
+ # = TreeObject
27
+ #
28
+ # This is a tree object representing a versioned directory. It has
29
+ # a list of
30
+ class TreeObject < RawObject
31
+ TreeEntry = Struct.new(:name, :mode, :ref)
32
+
33
+ ##
34
+ # Initializes the RawObject. Needs a hash to identify it and
35
+ # an opener. The opener should point to the .git directory.
36
+ #
37
+ # @param [String] hsh the hash to use to find the object
38
+ # @param [Support::RootedOpener] opener the opener to use to open the
39
+ # object file
40
+ # @param [String] content if the content is known already, use
41
+ # the provided content instead
42
+ def initialize(hsh, opener, content = nil)
43
+ if content
44
+ @hash_id, @opener = hsh, opener
45
+ @type = 'tree'
46
+ @content = content
47
+ else
48
+ super(hsh, opener)
49
+ end
50
+ parse!
51
+ end
52
+
53
+ ##
54
+ # Returns a list of the names of the files/directories (blobs/trees) in the
55
+ # tree
56
+ #
57
+ # @return [Array<String>] a (possibly unsorted) list of file and
58
+ # and directory names in this tree
59
+ def entry_names
60
+ @pairs.keys
61
+ end
62
+
63
+ ##
64
+ # Returns the number of entries in the directory.
65
+ #
66
+ # @return [Fixnum] the number of entries in this directory
67
+ def size
68
+ @pairs.size
69
+ end
70
+
71
+ ##
72
+ # Returns a TreeObject for the given filename in this level of
73
+ # the tree.
74
+ #
75
+ # @param [String] name the name of the file to look up
76
+ # @return [TreeEntry] the treeobject representing the file
77
+ def tree_lookup(name)
78
+ @pairs[name]
79
+ end
80
+
81
+ private
82
+
83
+ def parse!
84
+ require 'strscan'
85
+ @pairs = {}
86
+ scanner = StringScanner.new(@content)
87
+ until scanner.eos?
88
+ break unless scanner.scan(/(\d+) (\S+?)\x00(.{20})/m)
89
+ new_entry = TreeEntry.new(scanner[2], scanner[1].to_i(8), scanner[3])
90
+ @pairs[new_entry.name] = new_entry
91
+ end
92
+ end
93
+
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end