Subversion Repositories php_utils

Rev

Rev 24 | Rev 78 | 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 {
77
                                return git_read_object($commit_object,
78
                                        $objects_dir.'/pack/'.$basename.'.idx',
77 daniel-mar 79
                                        $objects_dir.'/pack/'.$basename.'.pack'
19 daniel-mar 80
                                );
81
                        } catch (Exception $e) {
82
                                $last_exception = $e;
83
                        }
14 daniel-mar 84
                }
19 daniel-mar 85
                throw new Exception($last_exception);
14 daniel-mar 86
        }
87
}
88
 
89
function git_read_object($object_wanted, $idx_file, $pack_file, $debug=false) {
90
        // More info about the IDX and PACK format: https://git-scm.com/docs/pack-format
91
 
92
        // Do some checks
93
        if (!preg_match('/^[0-9a-fA-F]{40}$/', $object_wanted, $m)) throw new Exception("Is not a valid object: $object_wanted");
94
        if (!file_exists($idx_file)) throw new Exception("Idx file $idx_file not found");
95
        if (!file_exists($pack_file)) throw new Exception("Pack file $pack_file not found");
96
 
97
        // Open index file
98
        $fp = fopen($idx_file, 'rb');
99
        if (!$fp) throw new Exception("Cannot open index file $idx_file");
100
 
101
        // Read version
102
        fseek($fp, 0);
77 daniel-mar 103
        $unpacked = unpack('H8', fread($fp, 4)); // H8 = 8x "Hex string, high nibble first"
104
        if ($unpacked[1] === bin2hex("\377tOc")) {
105
                $version = unpack('N', fread($fp, 4))[1]; // N = "unsigned long (always 32 bit, big endian byte order)"
14 daniel-mar 106
                $fanout_offset = 8;
107
                if ($version != 2) throw new Exception("Version $version unknown");
108
        } else {
109
                $version = 1;
110
                $fanout_offset = 0;
111
        }
112
        if ($debug) echo "Index file version = $version\n";
113
 
114
        // Read fanout table
115
        fseek($fp, $fanout_offset);
116
        $fanout_ary[0] = 0;
117
        $fanout_ary = unpack('N*', fread($fp, 4*256));
118
        $num_objects = $fanout_ary[256];
119
 
120
        // Find out approximate object number (from fanout table)
121
        $fanout_index = hexdec(substr($object_wanted,0,2));
122
        if ($debug) echo "Fanout index = ".($fanout_index-1)."\n";
123
        $object_no = $fanout_ary[$fanout_index]; // approximate
124
        if ($debug) echo "Object no approx $object_no\n";
125
 
126
        // Find the exact object number
127
        fseek($fp, $fanout_offset + 4*256 + 20*$object_no);
128
        $object_no--;
24 daniel-mar 129
        $pack_offset = -1; // avoid that phpstan complains
14 daniel-mar 130
        do {
131
                $object_no++;
132
                if ($version == 1) {
133
                        $pack_offset = fread($fp, 4);
134
                }
135
                $binary = fread($fp, 20);
136
                if (substr(bin2hex($binary),0,2) != substr(strtolower($object_wanted),0,2)) {
137
                        throw new Exception("Object $object_wanted not found");
138
                }
139
        } while (bin2hex($binary) != strtolower($object_wanted));
140
        if ($debug) echo "Exact object no = $object_no\n";
141
 
142
        if ($version == 2) {
143
                // Get CRC32
144
                fseek($fp, $fanout_offset + 4*256 + 20*$num_objects + 4*$object_no);
145
                $crc32 = unpack('N', fread($fp,4))[1];
146
                if ($debug) echo "CRC32 = ".sprintf('0x%08x',$crc32)."\n";
147
 
148
                // Get offset (32 bit)
149
                fseek($fp, $fanout_offset + 4*256 + 20*$num_objects + 4*$num_objects + 4*$object_no);
150
                $offset_info = unpack('N', fread($fp,4))[1];
151
                if ($offset_info >= 0x80000000) {
152
                        // MSB set, so the offset is 64 bit
153
                        if ($debug) echo "64 bit pack offset\n";
154
                        $offset_info &= 0x7FFFFFFF;
155
                        fseek($fp, $fanout_offset + 4*256 + 20*$num_objects + 4*$num_objects + 4*$num_objects + 8*$offset_info);
156
                        $pack_offset = unpack('J', fread($fp,8))[1];
157
                } else {
158
                        // MSB is not set, so the offset is 32 bit
159
                        if ($debug) echo "32 bit pack offset\n";
160
                        $offset_info &= 0x7FFFFFFF;
161
                        $pack_offset = $offset_info;
162
                }
163
        }
164
 
165
        if ($debug) echo "Pack file offset = ".sprintf('0x%x',$pack_offset)."\n";
166
 
167
        // Close index file
168
        fclose($fp);
169
 
170
        // Open pack file
171
        $fp = fopen($pack_file, 'rb');
172
        if (!$fp) throw new Exception("Cannot open pack file $pack_file");
173
 
174
        // Find out type
175
        fseek($fp, $pack_offset);
176
        $size_info = unpack('C', fread($fp,1))[1];
177
 
178
        $type = ($size_info & 0xE0) >> 5; /*0b11100000*/
179
        switch ($type) {
180
                case 1:
181
                        if ($debug) echo "Type = OBJ_COMMIT ($type)\n";
182
                        break;
183
                case 2:
184
                        if ($debug) echo "Type = OBJ_TREE ($type)\n";
185
                        break;
186
                case 3:
187
                        if ($debug) echo "Type = OBJ_BLOB ($type)\n";
188
                        break;
189
                case 4:
190
                        if ($debug) echo "Type = OBJ_TAG ($type)\n";
191
                        break;
192
                case 6:
193
                        if ($debug) echo "Type = OBJ_OFS_DELTA ($type)\n";
194
                        break;
195
                case 7:
196
                        if ($debug) echo "Type = OBJ_REF_DELTA ($type)\n";
197
                        break;
198
                default:
199
                        if ($debug) echo "Type = Invalid ($type)\n";
200
                        break;
201
        }
202
 
203
        // Find out size
204
        $size = $size_info & 0x1F /*0x00011111*/;
205
        $shift_info = 5;
206
        do {
207
                $size_info = unpack('C', fread($fp,1))[1];
208
                $size = (($size_info & 0x7F) << $shift_info) + $size;
209
                $shift_info += 8;
19 daniel-mar 210
        } while ($size_info >= 0x80);
14 daniel-mar 211
 
212
        if ($debug) echo "Packed size = ".sprintf('0x%x',$size)."\n";
213
 
214
        // Read delta base type
215
        if ($type == 6/*OBJ_OFS_DELTA*/) {
77 daniel-mar 216
                // "a negative relative offset from the delta object's position in the pack if this is an OBJ_OFS_DELTA object"
217
                $delta_info = unpack('C*', fread($fp,4))[1];
14 daniel-mar 218
                if ($debug) echo "Delta negative offset: $delta_info\n";
77 daniel-mar 219
                throw new Exception("OBJ_OFS_DELTA is currently not implemented"); // TODO! Implement OBJ_OFS_DELTA!
14 daniel-mar 220
        }
221
        if ($type == 7/*OBJ_REF_DELTA*/) {
222
                // "base object name if OBJ_REF_DELTA"
77 daniel-mar 223
                $delta_info = bin2hex(fread($fp,20));
14 daniel-mar 224
                if ($debug) echo "Delta base object name: $delta_info\n";
77 daniel-mar 225
                throw new Exception("OBJ_REF_DELTA is currently not implemented"); // TODO! Implement OBJ_REF_DELTA!
14 daniel-mar 226
        }
227
 
228
        // Read compressed data
229
        $compressed = fread($fp,$size);
230
 
231
        // Uncompress
16 daniel-mar 232
        $uncompressed = @gzuncompress($compressed);
233
        if ($uncompressed === false) throw new Exception("Decompression failed");
14 daniel-mar 234
        if ($debug) echo "$uncompressed\n";
235
 
236
        // Close pack file
237
        fclose($fp);
238
 
239
        // Check CRC32
19 daniel-mar 240
        // TODO: Does not fit; neither crc32, nor crc32b...
14 daniel-mar 241
        // if ($debug) echo "CRC32 found = 0x".hash('crc32',$compressed)."\n";
242
 
243
        return $uncompressed;
21 daniel-mar 244
}