Subversion Repositories php_utils

Rev

Rev 22 | 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 Daniel Marschall, ViaThinkSoft
  6.  * Revision 2021-12-29
  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.                                         false
  81.                                 );
  82.                         } catch (Exception $e) {
  83.                                 $last_exception = $e;
  84.                         }
  85.                 }
  86.                 throw new Exception($last_exception);
  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--;
  130.         $pack_offset = -1; // avoid that phpstan complains
  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;
  211.         } while ($size_info >= 0x80);
  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
  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.