Subversion Repositories php_utils

Rev

Rev 24 | Rev 78 | Go to most recent revision | 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-09
  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) {
  22.         // First try an official git client
  23.         $cmd = "git --git-dir=".escapeshellarg("$git_dir")." log -1 2>&1";
  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
  31.         $cont = @file_get_contents($git_dir.'/HEAD');
  32.         if (preg_match('@ref: (.+)[\r\n]@', "$cont\n", $m) && file_exists($git_dir.'/'.$m[1])) {
  33.                 // Example content of a .git folder file:
  34.                 // 091a5fa6b157be035e88f5d24aa329ba44d20d63
  35.                 // Not available
  36.                 $commit_object = trim(file_get_contents($git_dir.'/'.$m[1]));
  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'));
  40.         } else if (file_exists($git_dir.'/FETCH_HEAD')) {
  41.                 // Example content of a Plesk Git folder (fresh):
  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.         }
  49.  
  50.         $objects_dir = $git_dir . '/objects';
  51.  
  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.  
  66.                 // The format is "commit <nnn>\0<Message>" where <nnn> is only a 3 digit number?!
  67.                 $ary = explode(chr(0), $uncompressed);
  68.                 $uncompressed = array_pop($ary);
  69.  
  70.                 return $uncompressed;
  71.         } else {
  72.                 $pack_files = @glob($objects_dir.'/pack/pack-*.pack');
  73.                 $last_exception = 'No pack files found';
  74.                 if ($pack_files) foreach ($pack_files as $basename) {
  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.                                 );
  81.                         } catch (Exception $e) {
  82.                                 $last_exception = $e;
  83.                         }
  84.                 }
  85.                 throw new Exception($last_exception);
  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);
  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)"
  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--;
  129.         $pack_offset = -1; // avoid that phpstan complains
  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;
  210.         } while ($size_info >= 0x80);
  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*/) {
  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];
  218.                 if ($debug) echo "Delta negative offset: $delta_info\n";
  219.                 throw new Exception("OBJ_OFS_DELTA is currently not implemented"); // TODO! Implement OBJ_OFS_DELTA!
  220.         }
  221.         if ($type == 7/*OBJ_REF_DELTA*/) {
  222.                 // "base object name if OBJ_REF_DELTA"
  223.                 $delta_info = bin2hex(fread($fp,20));
  224.                 if ($debug) echo "Delta base object name: $delta_info\n";
  225.                 throw new Exception("OBJ_REF_DELTA is currently not implemented"); // TODO! Implement OBJ_REF_DELTA!
  226.         }
  227.  
  228.         // Read compressed data
  229.         $compressed = fread($fp,$size);
  230.  
  231.         // Uncompress
  232.         $uncompressed = @gzuncompress($compressed);
  233.         if ($uncompressed === false) throw new Exception("Decompression failed");
  234.         if ($debug) echo "$uncompressed\n";
  235.  
  236.         // Close pack file
  237.         fclose($fp);
  238.  
  239.         // Check CRC32
  240.         // TODO: Does not fit; neither crc32, nor crc32b...
  241.         // if ($debug) echo "CRC32 found = 0x".hash('crc32',$compressed)."\n";
  242.  
  243.         return $uncompressed;
  244. }
  245.