Subversion Repositories php_utils

Rev

Rev 78 | Rev 80 | Go to most recent revision | 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
6
 * Revision 2023-04-09
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
 
21
function git_get_latest_commit_message($git_dir) {
20 daniel-mar 22
        // First try an official git client
21 daniel-mar 23
        $cmd = "git --git-dir=".escapeshellarg("$git_dir")." log -1 2>&1";
20 daniel-mar 24
        $ec = -1;
25
        $out = array();
26
        @exec($cmd, $out, $ec);
27
        $out = implode("\n",$out);
28
        if (($ec == 0) && ($out != '')) return $out;
29
 
30
        // If that failed, try to decode the binary files outselves
19 daniel-mar 31
        $cont = @file_get_contents($git_dir.'/HEAD');
32
        if (preg_match('@ref: (.+)[\r\n]@', "$cont\n", $m) && file_exists($git_dir.'/'.$m[1])) {
18 daniel-mar 33
                // Example content of a .git folder file:
34
                // 091a5fa6b157be035e88f5d24aa329ba44d20d63
19 daniel-mar 35
                // Not available
18 daniel-mar 36
                $commit_object = trim(file_get_contents($git_dir.'/'.$m[1]));
19 daniel-mar 37
        } else if (file_exists($git_dir.'/refs/heads/master')) {
38
                // Missing at Plesk Git initial checkout, but available on update.
39
                $commit_object = trim(file_get_contents($git_dir.'/refs/heads/master'));
18 daniel-mar 40
        } else if (file_exists($git_dir.'/FETCH_HEAD')) {
19 daniel-mar 41
                // Example content of a Plesk Git folder (fresh):
18 daniel-mar 42
                // 091a5fa6b157be035e88f5d24aa329ba44d20d63     not-for-merge   branch 'master' of https://github.com/danielmarschall/oidplus
43
                // 091a5fa6b157be035e88f5d24aa329ba44d20d63     not-for-merge   remote-tracking branch 'origin/trunk' of https://github.com/danielmarschall/oidplus
44
                $cont = file_get_contents($git_dir.'/FETCH_HEAD');
45
                $commit_object = substr(trim($cont),0,40);
46
        } else {
47
                throw new Exception("Cannot detect last commit object");
48
        }
14 daniel-mar 49
 
50
        $objects_dir = $git_dir . '/objects';
51
 
19 daniel-mar 52
 
53
        // Sometimes, objects are uncompressed, sometimes compressed in a pack file
54
        // Plesk initial checkout is compressed, but pulls via web interface
55
        // save uncompressed files
56
 
57
        $uncompressed_file = $objects_dir . '/' . substr($commit_object,0,2) . '/' . substr($commit_object,2);
58
        if (file_exists($uncompressed_file)) {
59
                // Read compressed data
60
                $compressed = file_get_contents($uncompressed_file);
61
 
62
                // Uncompress
63
                $uncompressed = @gzuncompress($compressed);
64
                if ($uncompressed === false) throw new Exception("Decompression failed");
65
 
20 daniel-mar 66
                // The format is "commit <nnn>\0<Message>" where <nnn> is only a 3 digit number?!
19 daniel-mar 67
                $ary = explode(chr(0), $uncompressed);
68
                $uncompressed = array_pop($ary);
69
 
70
                return $uncompressed;
71
        } else {
22 daniel-mar 72
                $pack_files = @glob($objects_dir.'/pack/pack-*.pack');
19 daniel-mar 73
                $last_exception = 'No pack files found';
22 daniel-mar 74
                if ($pack_files) foreach ($pack_files as $basename) {
19 daniel-mar 75
                        $basename = substr(basename($basename),0,strlen(basename($basename))-5);
76
                        try {
79 daniel-mar 77
                                if (class_exists('ViaThinkSoft\Glip\Git')) {
78
                                        // https://github.com/danielmarschall/glip
79
                                        // composer require danielmarschall/glip
80
                                        $git = new Git($git_dir);
81
                                        $obj = $git->getObject(hex2bin($commit_object));
82
                                        echo $obj->detail;
83
                                } else {
84
                                        // Own implementation (cannot read delta objects yet)
85
                                        return git_read_object($commit_object,
86
                                                $objects_dir.'/pack/'.$basename.'.idx',
87
                                                $objects_dir.'/pack/'.$basename.'.pack'
88
                                        );
89
                                }
19 daniel-mar 90
                        } catch (Exception $e) {
91
                                $last_exception = $e;
92
                        }
14 daniel-mar 93
                }
19 daniel-mar 94
                throw new Exception($last_exception);
14 daniel-mar 95
        }
96
}
97
 
98
function git_read_object($object_wanted, $idx_file, $pack_file, $debug=false) {
99
        // More info about the IDX and PACK format: https://git-scm.com/docs/pack-format
100
 
101
        // Do some checks
102
        if (!preg_match('/^[0-9a-fA-F]{40}$/', $object_wanted, $m)) throw new Exception("Is not a valid object: $object_wanted");
103
        if (!file_exists($idx_file)) throw new Exception("Idx file $idx_file not found");
104
        if (!file_exists($pack_file)) throw new Exception("Pack file $pack_file not found");
105
 
106
        // Open index file
107
        $fp = fopen($idx_file, 'rb');
108
        if (!$fp) throw new Exception("Cannot open index file $idx_file");
109
 
110
        // Read version
111
        fseek($fp, 0);
77 daniel-mar 112
        $unpacked = unpack('H8', fread($fp, 4)); // H8 = 8x "Hex string, high nibble first"
113
        if ($unpacked[1] === bin2hex("\377tOc")) {
114
                $version = unpack('N', fread($fp, 4))[1]; // N = "unsigned long (always 32 bit, big endian byte order)"
14 daniel-mar 115
                $fanout_offset = 8;
116
                if ($version != 2) throw new Exception("Version $version unknown");
117
        } else {
118
                $version = 1;
119
                $fanout_offset = 0;
120
        }
121
        if ($debug) echo "Index file version = $version\n";
122
 
123
        // Read fanout table
124
        fseek($fp, $fanout_offset);
125
        $fanout_ary[0] = 0;
126
        $fanout_ary = unpack('N*', fread($fp, 4*256));
127
        $num_objects = $fanout_ary[256];
128
 
129
        // Find out approximate object number (from fanout table)
130
        $fanout_index = hexdec(substr($object_wanted,0,2));
131
        if ($debug) echo "Fanout index = ".($fanout_index-1)."\n";
132
        $object_no = $fanout_ary[$fanout_index]; // approximate
133
        if ($debug) echo "Object no approx $object_no\n";
134
 
135
        // Find the exact object number
136
        fseek($fp, $fanout_offset + 4*256 + 20*$object_no);
137
        $object_no--;
24 daniel-mar 138
        $pack_offset = -1; // avoid that phpstan complains
14 daniel-mar 139
        do {
140
                $object_no++;
141
                if ($version == 1) {
142
                        $pack_offset = fread($fp, 4);
143
                }
144
                $binary = fread($fp, 20);
145
                if (substr(bin2hex($binary),0,2) != substr(strtolower($object_wanted),0,2)) {
146
                        throw new Exception("Object $object_wanted not found");
147
                }
148
        } while (bin2hex($binary) != strtolower($object_wanted));
149
        if ($debug) echo "Exact object no = $object_no\n";
150
 
151
        if ($version == 2) {
152
                // Get CRC32
153
                fseek($fp, $fanout_offset + 4*256 + 20*$num_objects + 4*$object_no);
79 daniel-mar 154
                $crc32 = unpack('H8', fread($fp,4))[1];
155
                if ($debug) echo "CRC32 = ".$crc32."\n";
14 daniel-mar 156
 
157
                // Get offset (32 bit)
158
                fseek($fp, $fanout_offset + 4*256 + 20*$num_objects + 4*$num_objects + 4*$object_no);
159
                $offset_info = unpack('N', fread($fp,4))[1];
160
                if ($offset_info >= 0x80000000) {
161
                        // MSB set, so the offset is 64 bit
162
                        if ($debug) echo "64 bit pack offset\n";
163
                        $offset_info &= 0x7FFFFFFF;
164
                        fseek($fp, $fanout_offset + 4*256 + 20*$num_objects + 4*$num_objects + 4*$num_objects + 8*$offset_info);
165
                        $pack_offset = unpack('J', fread($fp,8))[1];
166
                } else {
167
                        // MSB is not set, so the offset is 32 bit
168
                        if ($debug) echo "32 bit pack offset\n";
169
                        $offset_info &= 0x7FFFFFFF;
170
                        $pack_offset = $offset_info;
171
                }
172
        }
173
 
174
        if ($debug) echo "Pack file offset = ".sprintf('0x%x',$pack_offset)."\n";
175
 
176
        // Close index file
177
        fclose($fp);
178
 
179
        // Open pack file
180
        $fp = fopen($pack_file, 'rb');
181
        if (!$fp) throw new Exception("Cannot open pack file $pack_file");
182
 
79 daniel-mar 183
        // Read type and first part of the size
14 daniel-mar 184
        fseek($fp, $pack_offset);
185
        $size_info = unpack('C', fread($fp,1))[1];
186
 
79 daniel-mar 187
        // Detect type
78 daniel-mar 188
        $type = ($size_info & 0x70) >> 4; /*0b01110000*/
14 daniel-mar 189
        switch ($type) {
190
                case 1:
191
                        if ($debug) echo "Type = OBJ_COMMIT ($type)\n";
192
                        break;
193
                case 2:
194
                        if ($debug) echo "Type = OBJ_TREE ($type)\n";
195
                        break;
196
                case 3:
197
                        if ($debug) echo "Type = OBJ_BLOB ($type)\n";
198
                        break;
199
                case 4:
200
                        if ($debug) echo "Type = OBJ_TAG ($type)\n";
201
                        break;
202
                case 6:
203
                        if ($debug) echo "Type = OBJ_OFS_DELTA ($type)\n";
204
                        break;
205
                case 7:
206
                        if ($debug) echo "Type = OBJ_REF_DELTA ($type)\n";
207
                        break;
208
                default:
209
                        if ($debug) echo "Type = Invalid ($type)\n";
210
                        break;
211
        }
212
 
79 daniel-mar 213
        // Find out the expected unpacked size
78 daniel-mar 214
        $size = $size_info & 0xF /*0x00001111*/;
215
        $shift_info = 4;
216
        while ($size_info >= 0x80) {
14 daniel-mar 217
                $size_info = unpack('C', fread($fp,1))[1];
218
                $size = (($size_info & 0x7F) << $shift_info) + $size;
78 daniel-mar 219
                $shift_info += 7;
220
        }
79 daniel-mar 221
        if ($debug) echo "Expected unpacked size = $size\n";
14 daniel-mar 222
 
223
        // Read delta base type
79 daniel-mar 224
        // Example implementation: https://github.com/AlexFBP/glip/blob/master/lib/git.class.php#L240
14 daniel-mar 225
        if ($type == 6/*OBJ_OFS_DELTA*/) {
77 daniel-mar 226
                // "a negative relative offset from the delta object's position in the pack if this is an OBJ_OFS_DELTA object"
78 daniel-mar 227
 
228
                // Offset encoding
229
                $offset = 0;
230
                $shift_info = 0;
231
                do {
232
                        $offset_info = unpack('C', fread($fp,1))[1];
233
                        $offset = (($offset_info & 0x7F) << $shift_info) + $offset;
234
                        $shift_info += 7;
235
                } while ($offset_info >= 0x80);
236
 
237
                if ($debug) echo "Delta negative offset: $offset\n";
77 daniel-mar 238
                throw new Exception("OBJ_OFS_DELTA is currently not implemented"); // TODO! Implement OBJ_OFS_DELTA!
14 daniel-mar 239
        }
240
        if ($type == 7/*OBJ_REF_DELTA*/) {
241
                // "base object name if OBJ_REF_DELTA"
77 daniel-mar 242
                $delta_info = bin2hex(fread($fp,20));
14 daniel-mar 243
                if ($debug) echo "Delta base object name: $delta_info\n";
77 daniel-mar 244
                throw new Exception("OBJ_REF_DELTA is currently not implemented"); // TODO! Implement OBJ_REF_DELTA!
14 daniel-mar 245
        }
246
 
79 daniel-mar 247
        // Read and uncompress the compressed data
248
        $compressed = '';
249
        $uncompressed = false;
250
        for ($compressed_size=1; $compressed_size<=32768*$size; $compressed_size++) {
251
                // Since we don't know the compressed size, we need to do trial and error
252
                // TODO: this is a super stupid algorithm! Is there a better way???
253
                $compressed .= fread($fp,1);
254
                $uncompressed = @gzuncompress($compressed);
255
                if (strlen($uncompressed) === $size) {
256
                        if ($debug) echo "Detected compressed size = $compressed_size\n";
257
                        break;
258
                }
259
        }
16 daniel-mar 260
        if ($uncompressed === false) throw new Exception("Decompression failed");
14 daniel-mar 261
        if ($debug) echo "$uncompressed\n";
262
 
263
        // Close pack file
264
        fclose($fp);
265
 
79 daniel-mar 266
        if ($version == 2) {
267
                // Check CRC32
268
                // TODO: Hash does not match. What are we doing wrong?!
269
                // if ($debug) echo "CRC32 found = ".hash('crc32',$compressed)." vs $crc32\n";
270
                // if ($debug) echo "CRC32 found = ".hash('crc32b',$compressed)." vs $crc32\n";
271
        }
14 daniel-mar 272
 
273
        return $uncompressed;
21 daniel-mar 274
}