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,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