Subversion Repositories php_utils

Rev

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