Subversion Repositories php_utils

Rev

Rev 81 | Blame | Compare with Previous | Last modification | View Log | RSS feed

  1. <?php
  2.  
  3. /*
  4.  * PHP git functions
  5.  * Copyright 2021 - 2023 Daniel Marschall, ViaThinkSoft
  6.  * Revision 2023-04-10
  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_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/...")
  23.  
  24.         $cont = @file_get_contents($git_dir . '/HEAD');
  25.         if (preg_match('@ref: (.+)[\r\n]@', "$cont\n", $m) && file_exists($git_dir . '/' . $m[1])) {
  26.                 // Example content of a .git folder file:
  27.                 // 091a5fa6b157be035e88f5d24aa329ba44d20d63
  28.                 return trim(file_get_contents($git_dir . '/' . $m[1]));
  29.         }
  30.  
  31.         if (file_exists($git_dir . '/refs/heads/master')) {
  32.                 // Missing at Plesk Git initial checkout, but available on update.
  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')) {
  60.                 // Example content of a Plesk Git folder (fresh):
  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
  63.                 $cont = file_get_contents($git_dir . '/FETCH_HEAD');
  64.                 return substr(trim($cont), 0, 40);
  65.         }
  66.  
  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);
  81.         $objects_dir = $git_dir . '/objects';
  82.  
  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.  
  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)
  95.  
  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);
  100.  
  101.                         // Uncompress
  102.                         $uncompressed = @gzuncompress($compressed);
  103.                         if ($uncompressed === false) throw new Exception("Decompression failed");
  104.  
  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);
  114.                                         return git_read_object($commit_object,
  115.                                                 $objects_dir . '/pack/' . $basename . '.idx',
  116.                                                 $objects_dir . '/pack/' . $basename . '.pack'
  117.                                         );
  118.                                 }
  119.                         }
  120.                         throw new Exception("No pack files found");
  121.                 }
  122.         }
  123. }
  124.  
  125. function git_read_object(string $object_wanted, string $idx_file, string $pack_file, bool $debug = false): string {
  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);
  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)"
  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;
  153.         $fanout_ary = unpack('N*', fread($fp, 4 * 256));
  154.         $num_objects = $fanout_ary[256];
  155.  
  156.         // Find out approximate object number (from fanout table)
  157.         $fanout_index = hexdec(substr($object_wanted, 0, 2));
  158.         if ($debug) echo "Fanout index = " . ($fanout_index - 1) . "\n";
  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
  163.         fseek($fp, $fanout_offset + 4 * 256 + 20 * $object_no);
  164.         $object_no--;
  165.         $pack_offset = -1; // avoid that phpstan complains
  166.         do {
  167.                 $object_no++;
  168.                 if ($version == 1) {
  169.                         $pack_offset = fread($fp, 4);
  170.                 }
  171.                 $binary = fread($fp, 20);
  172.                 if (substr(bin2hex($binary), 0, 2) != substr(strtolower($object_wanted), 0, 2)) {
  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
  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";
  183.  
  184.                 // Get offset (32 bit)
  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];
  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;
  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];
  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.  
  201.         if ($debug) echo "Pack file offset = " . sprintf('0x%x', $pack_offset) . "\n";
  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.  
  210.         // Read type and first part of the size
  211.         fseek($fp, $pack_offset);
  212.         $size_info = unpack('C', fread($fp, 1))[1];
  213.  
  214.         // Detect type
  215.         $type = ($size_info & 0x70) >> 4; /*0b01110000*/
  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.  
  240.         // Find out the expected unpacked size
  241.         $size = $size_info & 0xF /*0x00001111*/
  242.         ;
  243.         $shift_info = 4;
  244.         while ($size_info >= 0x80) {
  245.                 $size_info = unpack('C', fread($fp, 1))[1];
  246.                 $size = (($size_info & 0x7F) << $shift_info) + $size;
  247.                 $shift_info += 7;
  248.         }
  249.         if ($debug) echo "Expected unpacked size = $size\n";
  250.  
  251.         // Read delta base type
  252.         // Example implementation: https://github.com/AlexFBP/glip/blob/master/lib/git.class.php#L240
  253.         if ($type == 6/*OBJ_OFS_DELTA*/) {
  254.                 // "a negative relative offset from the delta object's position in the pack if this is an OBJ_OFS_DELTA object"
  255.  
  256.                 // Offset encoding
  257.                 $offset = 0;
  258.                 $shift_info = 0;
  259.                 do {
  260.                         $offset_info = unpack('C', fread($fp, 1))[1];
  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";
  266.                 throw new Exception("OBJ_OFS_DELTA is currently not implemented"); // TODO! Implement OBJ_OFS_DELTA!
  267.         }
  268.         if ($type == 7/*OBJ_REF_DELTA*/) {
  269.                 // "base object name if OBJ_REF_DELTA"
  270.                 $delta_info = bin2hex(fread($fp, 20));
  271.                 if ($debug) echo "Delta base object name: $delta_info\n";
  272.                 throw new Exception("OBJ_REF_DELTA is currently not implemented"); // TODO! Implement OBJ_REF_DELTA!
  273.         }
  274.  
  275.         // Read and uncompress the compressed data
  276.         $compressed = '';
  277.         $uncompressed = false;
  278.         for ($compressed_size = 1; $compressed_size <= 32768 * $size; $compressed_size++) {
  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???
  281.                 $compressed .= fread($fp, 1);
  282.                 $uncompressed = @gzuncompress($compressed);
  283.                 if (strlen($uncompressed) === $size) {
  284.                         if ($debug) echo "Detected compressed size = $compressed_size\n";
  285.                         break;
  286.                 }
  287.         }
  288.         if ($uncompressed === false) throw new Exception("Decompression failed");
  289.         if ($debug) echo "$uncompressed\n";
  290.  
  291.         // Close pack file
  292.         fclose($fp);
  293.  
  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.         }
  300.  
  301.         return $uncompressed;
  302. }
  303.