Subversion Repositories php_utils

Rev

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
}