Rev 81 | Details | Compare with Previous | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
14 | daniel-mar | 1 | <?php |
2 | |||
3 | /* |
||
4 | * PHP git functions |
||
77 | daniel-mar | 5 | * Copyright 2021 - 2023 Daniel Marschall, ViaThinkSoft |
81 | daniel-mar | 6 | * Revision 2023-04-10 |
14 | daniel-mar | 7 | * |
8 | * Licensed under the Apache License, Version 2.0 (the "License"); |
||
9 | * you may not use this file except in compliance with the License. |
||
10 | * You may obtain a copy of the License at |
||
11 | * |
||
12 | * http://www.apache.org/licenses/LICENSE-2.0 |
||
13 | * |
||
14 | * Unless required by applicable law or agreed to in writing, software |
||
15 | * distributed under the License is distributed on an "AS IS" BASIS, |
||
16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||
17 | * See the License for the specific language governing permissions and |
||
18 | * limitations under the License. |
||
19 | */ |
||
20 | |||
82 | daniel-mar | 21 | function git_get_latest_commit_id(string $git_dir): string { |
22 | // Note: The method "getTip()" of GLIP only implements "refs/heads/master" and "packed-refs" (but for packed-refs without "refs/remotes/origin/...") |
||
20 | daniel-mar | 23 | |
82 | daniel-mar | 24 | $cont = @file_get_contents($git_dir . '/HEAD'); |
25 | if (preg_match('@ref: (.+)[\r\n]@', "$cont\n", $m) && file_exists($git_dir . '/' . $m[1])) { |
||
18 | daniel-mar | 26 | // Example content of a .git folder file: |
27 | // 091a5fa6b157be035e88f5d24aa329ba44d20d63 |
||
82 | daniel-mar | 28 | return trim(file_get_contents($git_dir . '/' . $m[1])); |
29 | } |
||
30 | |||
31 | if (file_exists($git_dir . '/refs/heads/master')) { |
||
19 | daniel-mar | 32 | // Missing at Plesk Git initial checkout, but available on update. |
82 | daniel-mar | 33 | return trim(file_get_contents($git_dir . '/refs/heads/master')); |
34 | } |
||
35 | |||
36 | if (file_exists($git_dir . '/packed-refs')) { |
||
37 | // Example contents of the file: |
||
38 | // # pack-refs with: peeled fully-peeled sorted |
||
39 | // 5605bd539677494558470234266cb5885343e72b refs/remotes/origin/master |
||
40 | // a3d910dd0cdca30827ae25b0f89045d8403b8843 refs/remotes/origin/patch-1 |
||
41 | $subpaths = ['refs/heads/master', 'refs/remotes/origin/master']; |
||
42 | foreach ($subpaths as $subpath) { |
||
43 | $head = null; |
||
44 | $f = fopen($git_dir . '/packed-refs', 'rb'); |
||
45 | flock($f, LOCK_SH); |
||
46 | while ($head === null && ($line = fgets($f)) !== false) { |
||
47 | if ($line[0] == '#') |
||
48 | continue; |
||
49 | $parts = explode(' ', trim($line)); |
||
50 | if (count($parts) == 2 && $parts[1] == $subpath) |
||
51 | $head = $parts[0]; |
||
52 | } |
||
53 | fclose($f); |
||
54 | if ($head !== null) |
||
55 | return $head; |
||
56 | } |
||
57 | } |
||
58 | |||
59 | if (file_exists($git_dir . '/FETCH_HEAD')) { |
||
19 | daniel-mar | 60 | // Example content of a Plesk Git folder (fresh): |
18 | daniel-mar | 61 | // 091a5fa6b157be035e88f5d24aa329ba44d20d63 not-for-merge branch 'master' of https://github.com/danielmarschall/oidplus |
62 | // 091a5fa6b157be035e88f5d24aa329ba44d20d63 not-for-merge remote-tracking branch 'origin/trunk' of https://github.com/danielmarschall/oidplus |
||
82 | daniel-mar | 63 | $cont = file_get_contents($git_dir . '/FETCH_HEAD'); |
64 | return substr(trim($cont), 0, 40); |
||
18 | daniel-mar | 65 | } |
14 | daniel-mar | 66 | |
82 | daniel-mar | 67 | throw new Exception("Cannot detect latest Commit ID"); |
68 | } |
||
69 | |||
70 | function git_get_latest_commit_message(string $git_dir): string { |
||
71 | // First try an official git client |
||
72 | $cmd = "git --git-dir=" . escapeshellarg("$git_dir") . " log -1 2>&1"; |
||
73 | $ec = -1; |
||
74 | $out = array(); |
||
75 | @exec($cmd, $out, $ec); |
||
76 | $out = implode("\n", $out); |
||
77 | if (($ec == 0) && ($out != '')) return $out; |
||
78 | |||
79 | // If that failed, try to decode the binary files ourselves |
||
80 | $commit_object = git_get_latest_commit_id($git_dir); |
||
14 | daniel-mar | 81 | $objects_dir = $git_dir . '/objects'; |
82 | |||
19 | daniel-mar | 83 | // Sometimes, objects are uncompressed, sometimes compressed in a pack file |
84 | // Plesk initial checkout is compressed, but pulls via web interface |
||
85 | // save uncompressed files |
||
86 | |||
82 | daniel-mar | 87 | if (class_exists('ViaThinkSoft\Glip\Git')) { |
88 | // https://github.com/danielmarschall/glip |
||
89 | // composer require danielmarschall/glip |
||
90 | $git = new ViaThinkSoft\Glip\Git($git_dir); |
||
91 | $obj = $git->getObject(hex2bin($commit_object)); |
||
92 | return $obj->detail; |
||
93 | } else { |
||
94 | // Own implementation (the compressed read cannot handle delta objects yet) |
||
19 | daniel-mar | 95 | |
82 | daniel-mar | 96 | $uncompressed_file = $objects_dir . '/' . substr($commit_object, 0, 2) . '/' . substr($commit_object, 2); |
97 | if (file_exists($uncompressed_file)) { |
||
98 | // Read compressed data |
||
99 | $compressed = file_get_contents($uncompressed_file); |
||
19 | daniel-mar | 100 | |
82 | daniel-mar | 101 | // Uncompress |
102 | $uncompressed = @gzuncompress($compressed); |
||
103 | if ($uncompressed === false) throw new Exception("Decompression failed"); |
||
19 | daniel-mar | 104 | |
82 | daniel-mar | 105 | // The format is "<type> <size>\0<Message>" |
106 | list($hdr, $object_data) = explode("\0", $uncompressed, 2); |
||
107 | // sscanf($hdr, "%s %d", $type, $object_size); |
||
108 | return $object_data; |
||
109 | } else { |
||
110 | $pack_files = @glob($objects_dir . '/pack/pack-*.pack'); |
||
111 | if ($pack_files) { |
||
112 | foreach ($pack_files as $basename) { |
||
113 | $basename = substr(basename($basename), 0, strlen(basename($basename)) - 5); |
||
79 | daniel-mar | 114 | return git_read_object($commit_object, |
82 | daniel-mar | 115 | $objects_dir . '/pack/' . $basename . '.idx', |
116 | $objects_dir . '/pack/' . $basename . '.pack' |
||
79 | daniel-mar | 117 | ); |
118 | } |
||
19 | daniel-mar | 119 | } |
82 | daniel-mar | 120 | throw new Exception("No pack files found"); |
14 | daniel-mar | 121 | } |
122 | } |
||
123 | } |
||
124 | |||
82 | daniel-mar | 125 | function git_read_object(string $object_wanted, string $idx_file, string $pack_file, bool $debug = false): string { |
14 | daniel-mar | 126 | // More info about the IDX and PACK format: https://git-scm.com/docs/pack-format |
127 | |||
128 | // Do some checks |
||
129 | if (!preg_match('/^[0-9a-fA-F]{40}$/', $object_wanted, $m)) throw new Exception("Is not a valid object: $object_wanted"); |
||
130 | if (!file_exists($idx_file)) throw new Exception("Idx file $idx_file not found"); |
||
131 | if (!file_exists($pack_file)) throw new Exception("Pack file $pack_file not found"); |
||
132 | |||
133 | // Open index file |
||
134 | $fp = fopen($idx_file, 'rb'); |
||
135 | if (!$fp) throw new Exception("Cannot open index file $idx_file"); |
||
136 | |||
137 | // Read version |
||
138 | fseek($fp, 0); |
||
77 | daniel-mar | 139 | $unpacked = unpack('H8', fread($fp, 4)); // H8 = 8x "Hex string, high nibble first" |
140 | if ($unpacked[1] === bin2hex("\377tOc")) { |
||
141 | $version = unpack('N', fread($fp, 4))[1]; // N = "unsigned long (always 32 bit, big endian byte order)" |
||
14 | daniel-mar | 142 | $fanout_offset = 8; |
143 | if ($version != 2) throw new Exception("Version $version unknown"); |
||
144 | } else { |
||
145 | $version = 1; |
||
146 | $fanout_offset = 0; |
||
147 | } |
||
148 | if ($debug) echo "Index file version = $version\n"; |
||
149 | |||
150 | // Read fanout table |
||
151 | fseek($fp, $fanout_offset); |
||
152 | $fanout_ary[0] = 0; |
||
82 | daniel-mar | 153 | $fanout_ary = unpack('N*', fread($fp, 4 * 256)); |
14 | daniel-mar | 154 | $num_objects = $fanout_ary[256]; |
155 | |||
156 | // Find out approximate object number (from fanout table) |
||
82 | daniel-mar | 157 | $fanout_index = hexdec(substr($object_wanted, 0, 2)); |
158 | if ($debug) echo "Fanout index = " . ($fanout_index - 1) . "\n"; |
||
14 | daniel-mar | 159 | $object_no = $fanout_ary[$fanout_index]; // approximate |
160 | if ($debug) echo "Object no approx $object_no\n"; |
||
161 | |||
162 | // Find the exact object number |
||
82 | daniel-mar | 163 | fseek($fp, $fanout_offset + 4 * 256 + 20 * $object_no); |
14 | daniel-mar | 164 | $object_no--; |
24 | daniel-mar | 165 | $pack_offset = -1; // avoid that phpstan complains |
14 | daniel-mar | 166 | do { |
167 | $object_no++; |
||
168 | if ($version == 1) { |
||
169 | $pack_offset = fread($fp, 4); |
||
170 | } |
||
171 | $binary = fread($fp, 20); |
||
82 | daniel-mar | 172 | if (substr(bin2hex($binary), 0, 2) != substr(strtolower($object_wanted), 0, 2)) { |
14 | daniel-mar | 173 | throw new Exception("Object $object_wanted not found"); |
174 | } |
||
175 | } while (bin2hex($binary) != strtolower($object_wanted)); |
||
176 | if ($debug) echo "Exact object no = $object_no\n"; |
||
177 | |||
178 | if ($version == 2) { |
||
179 | // Get CRC32 |
||
82 | daniel-mar | 180 | fseek($fp, $fanout_offset + 4 * 256 + 20 * $num_objects + 4 * $object_no); |
181 | $crc32 = unpack('H8', fread($fp, 4))[1]; |
||
182 | if ($debug) echo "CRC32 = " . $crc32 . "\n"; |
||
14 | daniel-mar | 183 | |
184 | // Get offset (32 bit) |
||
82 | daniel-mar | 185 | fseek($fp, $fanout_offset + 4 * 256 + 20 * $num_objects + 4 * $num_objects + 4 * $object_no); |
186 | $offset_info = unpack('N', fread($fp, 4))[1]; |
||
14 | daniel-mar | 187 | if ($offset_info >= 0x80000000) { |
188 | // MSB set, so the offset is 64 bit |
||
189 | if ($debug) echo "64 bit pack offset\n"; |
||
190 | $offset_info &= 0x7FFFFFFF; |
||
82 | daniel-mar | 191 | fseek($fp, $fanout_offset + 4 * 256 + 20 * $num_objects + 4 * $num_objects + 4 * $num_objects + 8 * $offset_info); |
192 | $pack_offset = unpack('J', fread($fp, 8))[1]; |
||
14 | daniel-mar | 193 | } else { |
194 | // MSB is not set, so the offset is 32 bit |
||
195 | if ($debug) echo "32 bit pack offset\n"; |
||
196 | $offset_info &= 0x7FFFFFFF; |
||
197 | $pack_offset = $offset_info; |
||
198 | } |
||
199 | } |
||
200 | |||
82 | daniel-mar | 201 | if ($debug) echo "Pack file offset = " . sprintf('0x%x', $pack_offset) . "\n"; |
14 | daniel-mar | 202 | |
203 | // Close index file |
||
204 | fclose($fp); |
||
205 | |||
206 | // Open pack file |
||
207 | $fp = fopen($pack_file, 'rb'); |
||
208 | if (!$fp) throw new Exception("Cannot open pack file $pack_file"); |
||
209 | |||
79 | daniel-mar | 210 | // Read type and first part of the size |
14 | daniel-mar | 211 | fseek($fp, $pack_offset); |
82 | daniel-mar | 212 | $size_info = unpack('C', fread($fp, 1))[1]; |
14 | daniel-mar | 213 | |
79 | daniel-mar | 214 | // Detect type |
78 | daniel-mar | 215 | $type = ($size_info & 0x70) >> 4; /*0b01110000*/ |
14 | daniel-mar | 216 | switch ($type) { |
217 | case 1: |
||
218 | if ($debug) echo "Type = OBJ_COMMIT ($type)\n"; |
||
219 | break; |
||
220 | case 2: |
||
221 | if ($debug) echo "Type = OBJ_TREE ($type)\n"; |
||
222 | break; |
||
223 | case 3: |
||
224 | if ($debug) echo "Type = OBJ_BLOB ($type)\n"; |
||
225 | break; |
||
226 | case 4: |
||
227 | if ($debug) echo "Type = OBJ_TAG ($type)\n"; |
||
228 | break; |
||
229 | case 6: |
||
230 | if ($debug) echo "Type = OBJ_OFS_DELTA ($type)\n"; |
||
231 | break; |
||
232 | case 7: |
||
233 | if ($debug) echo "Type = OBJ_REF_DELTA ($type)\n"; |
||
234 | break; |
||
235 | default: |
||
236 | if ($debug) echo "Type = Invalid ($type)\n"; |
||
237 | break; |
||
238 | } |
||
239 | |||
79 | daniel-mar | 240 | // Find out the expected unpacked size |
82 | daniel-mar | 241 | $size = $size_info & 0xF /*0x00001111*/ |
242 | ; |
||
78 | daniel-mar | 243 | $shift_info = 4; |
244 | while ($size_info >= 0x80) { |
||
82 | daniel-mar | 245 | $size_info = unpack('C', fread($fp, 1))[1]; |
14 | daniel-mar | 246 | $size = (($size_info & 0x7F) << $shift_info) + $size; |
78 | daniel-mar | 247 | $shift_info += 7; |
248 | } |
||
79 | daniel-mar | 249 | if ($debug) echo "Expected unpacked size = $size\n"; |
14 | daniel-mar | 250 | |
251 | // Read delta base type |
||
79 | daniel-mar | 252 | // Example implementation: https://github.com/AlexFBP/glip/blob/master/lib/git.class.php#L240 |
14 | daniel-mar | 253 | if ($type == 6/*OBJ_OFS_DELTA*/) { |
77 | daniel-mar | 254 | // "a negative relative offset from the delta object's position in the pack if this is an OBJ_OFS_DELTA object" |
78 | daniel-mar | 255 | |
256 | // Offset encoding |
||
257 | $offset = 0; |
||
258 | $shift_info = 0; |
||
259 | do { |
||
82 | daniel-mar | 260 | $offset_info = unpack('C', fread($fp, 1))[1]; |
78 | daniel-mar | 261 | $offset = (($offset_info & 0x7F) << $shift_info) + $offset; |
262 | $shift_info += 7; |
||
263 | } while ($offset_info >= 0x80); |
||
264 | |||
265 | if ($debug) echo "Delta negative offset: $offset\n"; |
||
77 | daniel-mar | 266 | throw new Exception("OBJ_OFS_DELTA is currently not implemented"); // TODO! Implement OBJ_OFS_DELTA! |
14 | daniel-mar | 267 | } |
268 | if ($type == 7/*OBJ_REF_DELTA*/) { |
||
269 | // "base object name if OBJ_REF_DELTA" |
||
82 | daniel-mar | 270 | $delta_info = bin2hex(fread($fp, 20)); |
14 | daniel-mar | 271 | if ($debug) echo "Delta base object name: $delta_info\n"; |
77 | daniel-mar | 272 | throw new Exception("OBJ_REF_DELTA is currently not implemented"); // TODO! Implement OBJ_REF_DELTA! |
14 | daniel-mar | 273 | } |
274 | |||
79 | daniel-mar | 275 | // Read and uncompress the compressed data |
276 | $compressed = ''; |
||
277 | $uncompressed = false; |
||
82 | daniel-mar | 278 | for ($compressed_size = 1; $compressed_size <= 32768 * $size; $compressed_size++) { |
79 | daniel-mar | 279 | // Since we don't know the compressed size, we need to do trial and error |
280 | // TODO: this is a super stupid algorithm! Is there a better way??? |
||
82 | daniel-mar | 281 | $compressed .= fread($fp, 1); |
79 | daniel-mar | 282 | $uncompressed = @gzuncompress($compressed); |
283 | if (strlen($uncompressed) === $size) { |
||
284 | if ($debug) echo "Detected compressed size = $compressed_size\n"; |
||
285 | break; |
||
286 | } |
||
287 | } |
||
16 | daniel-mar | 288 | if ($uncompressed === false) throw new Exception("Decompression failed"); |
14 | daniel-mar | 289 | if ($debug) echo "$uncompressed\n"; |
290 | |||
291 | // Close pack file |
||
292 | fclose($fp); |
||
293 | |||
79 | daniel-mar | 294 | if ($version == 2) { |
295 | // Check CRC32 |
||
296 | // TODO: Hash does not match. What are we doing wrong?! |
||
297 | // if ($debug) echo "CRC32 found = ".hash('crc32',$compressed)." vs $crc32\n"; |
||
298 | // if ($debug) echo "CRC32 found = ".hash('crc32b',$compressed)." vs $crc32\n"; |
||
299 | } |
||
14 | daniel-mar | 300 | |
301 | return $uncompressed; |
||
21 | daniel-mar | 302 | } |