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.
- data/.document +5 -0
- data/.gitignore +23 -0
- data/Gemfile +15 -0
- data/LICENSE +20 -0
- data/README.rdoc +17 -0
- data/Rakefile +68 -0
- data/VERSION +1 -0
- data/features/amp-git.feature +9 -0
- data/features/step_definitions/amp-git_steps.rb +0 -0
- data/features/support/env.rb +4 -0
- data/lib/amp-git/encoding/binary_delta.rb +171 -0
- data/lib/amp-git/repo_format/changeset.rb +348 -0
- data/lib/amp-git/repo_format/commit_object.rb +87 -0
- data/lib/amp-git/repo_format/index.rb +169 -0
- data/lib/amp-git/repo_format/loose_object.rb +78 -0
- data/lib/amp-git/repo_format/packfile.rb +263 -0
- data/lib/amp-git/repo_format/packfile_index.rb +196 -0
- data/lib/amp-git/repo_format/raw_object.rb +56 -0
- data/lib/amp-git/repo_format/staging_area.rb +215 -0
- data/lib/amp-git/repo_format/tag_object.rb +87 -0
- data/lib/amp-git/repo_format/tree_object.rb +98 -0
- data/lib/amp-git/repo_format/versioned_file.rb +133 -0
- data/lib/amp-git/repositories/local_repository.rb +192 -0
- data/lib/amp-git/repository.rb +57 -0
- data/lib/amp-git.rb +49 -0
- data/lib/amp_plugin.rb +1 -0
- data/spec/amp-git_spec.rb +15 -0
- data/spec/repository_spec.rb +74 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +29 -0
- data/test/index_tests/index +0 -0
- data/test/index_tests/test_helper.rb +16 -0
- data/test/index_tests/test_index.rb +69 -0
- data/test/packfile_tests/hasindex.idx +0 -0
- data/test/packfile_tests/hasindex.pack +0 -0
- data/test/packfile_tests/pack-4e1941122fd346526b0a3eee2d92f3277a0092cd.pack +0 -0
- data/test/packfile_tests/pack-d23ff2538f970371144ae7182c28730b11eb37c1.idx +0 -0
- data/test/packfile_tests/test_helper.rb +16 -0
- data/test/packfile_tests/test_packfile.rb +75 -0
- data/test/packfile_tests/test_packfile_index_v2.rb +90 -0
- data/test/packfile_tests/test_packfile_with_index.rb +76 -0
- data/test/test_commit_object.rb +60 -0
- data/test/test_git_delta.rb +67 -0
- data/test/test_helper.rb +71 -0
- data/test/test_loose_object.rb +51 -0
- data/test/test_tag_object.rb +72 -0
- data/test/test_tree_object.rb +55 -0
- 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
|