Subversion Repositories oidplus

Rev

Rev 1193 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

  1. <?php
  2.  
  3. /*
  4.  * Copyright (C) 2008, 2009 Patrik Fimml
  5.  * Copyright (c) 2023 Daniel Marschall
  6.  *
  7.  * This file is part of glip.
  8.  *
  9.  * glip is free software: you can redistribute it and/or modify
  10.  * it under the terms of the GNU General Public License as published by
  11.  * the Free Software Foundation, either version 2 of the License, or
  12.  * (at your option) any later version.
  13.  
  14.  * glip is distributed in the hope that it will be useful,
  15.  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16.  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  17.  * GNU General Public License for more details.
  18.  *
  19.  * You should have received a copy of the GNU General Public License
  20.  * along with glip.  If not, see <http://www.gnu.org/licenses/>.
  21.  */
  22.  
  23. namespace ViaThinkSoft\Glip;
  24.  
  25. class Git
  26. {
  27.         public $dir;
  28.  
  29.         private $packs;
  30.  
  31.         const OBJ_NONE = 0;
  32.         const OBJ_COMMIT = 1;
  33.         const OBJ_TREE = 2;
  34.         const OBJ_BLOB = 3;
  35.         const OBJ_TAG = 4;
  36.         const OBJ_OFS_DELTA = 6;
  37.         const OBJ_REF_DELTA = 7;
  38.  
  39.         static public function getTypeID($name) {
  40.                 if ($name == 'commit')
  41.                         return Git::OBJ_COMMIT;
  42.                 else if ($name == 'tree')
  43.                         return Git::OBJ_TREE;
  44.                 else if ($name == 'blob')
  45.                         return Git::OBJ_BLOB;
  46.                 else if ($name == 'tag')
  47.                         return Git::OBJ_TAG;
  48.                 throw new \Exception(sprintf('unknown type name: %s', $name));
  49.         }
  50.  
  51.         static public function getTypeName($type) {
  52.                 if ($type == Git::OBJ_COMMIT)
  53.                         return 'commit';
  54.                 else if ($type == Git::OBJ_TREE)
  55.                         return 'tree';
  56.                 else if ($type == Git::OBJ_BLOB)
  57.                         return 'blob';
  58.                 else if ($type == Git::OBJ_TAG)
  59.                         return 'tag';
  60.                 throw new \Exception(sprintf('no string representation of type %d', $type));
  61.         }
  62.  
  63.         public function __construct($dir) {
  64.                 $this->dir = realpath($dir);
  65.                 if ($this->dir === false || !@is_dir($this->dir))
  66.                         throw new \Exception(sprintf('not a directory: %s', $dir));
  67.  
  68.                 $this->packs = array();
  69.                 $dh = opendir(sprintf('%s/objects/pack', $this->dir));
  70.                 if ($dh !== false) {
  71.                         while (($entry = readdir($dh)) !== false)
  72.                                 if (preg_match('#^pack-([0-9a-fA-F]{40})\.idx$#', $entry, $m))
  73.                                         $this->packs[] = pack('H40', $m[1]);
  74.                         closedir($dh);
  75.                 }
  76.         }
  77.  
  78.         /**
  79.          * @brief Tries to find $object_name in the fanout table in $f at $offset.
  80.          *
  81.          * @returns array The range where the object can be located (first possible
  82.          * location and past-the-end location)
  83.          */
  84.         protected function readFanout($f, $object_name, $offset) {
  85.                 if ($object_name[0] == "\x00") {
  86.                         $cur = 0;
  87.                         fseek($f, $offset);
  88.                         $after = Binary::fuint32($f);
  89.                 } else {
  90.                         fseek($f, $offset + (ord($object_name[0]) - 1) * 4);
  91.                         $cur = Binary::fuint32($f);
  92.                         $after = Binary::fuint32($f);
  93.                 }
  94.  
  95.                 return array($cur, $after);
  96.         }
  97.  
  98.         /**
  99.          * @brief Try to find an object in a pack.
  100.          *
  101.          * @param $object_name (string) name of the object (binary SHA1)
  102.          * @returns (array) an array consisting of the name of the pack (string) and
  103.          * the byte offset inside it, or NULL if not found
  104.          */
  105.         protected function findPackedObject($object_name) {
  106.                 foreach ($this->packs as $pack_name) {
  107.                         $index = fopen(sprintf('%s/objects/pack/pack-%s.idx', $this->dir, bin2hex($pack_name)), 'rb');
  108.                         flock($index, LOCK_SH);
  109.  
  110.                         /* check version */
  111.                         $magic = fread($index, 4);
  112.                         if ($magic != "\xFFtOc") {
  113.                                 /* version 1 */
  114.                                 /* read corresponding fanout entry */
  115.                                 list($cur, $after) = $this->readFanout($index, $object_name, 0);
  116.  
  117.                                 $n = $after - $cur;
  118.                                 if ($n == 0)
  119.                                         continue;
  120.  
  121.                                 /*
  122.                                  * TODO: do a binary search in [$offset, $offset+24*$n)
  123.                                  */
  124.                                 fseek($index, 4 * 256 + 24 * $cur);
  125.                                 for ($i = 0; $i < $n; $i++) {
  126.                                         $off = Binary::fuint32($index);
  127.                                         $name = fread($index, 20);
  128.                                         if ($name == $object_name) {
  129.                                                 /* we found the object */
  130.                                                 fclose($index);
  131.                                                 return array($pack_name, $off);
  132.                                         }
  133.                                 }
  134.                         } else {
  135.                                 /* version 2+ */
  136.                                 $version = Binary::fuint32($index);
  137.                                 if ($version == 2) {
  138.                                         list($cur, $after) = $this->readFanout($index, $object_name, 8);
  139.  
  140.                                         if ($cur == $after)
  141.                                                 continue;
  142.  
  143.                                         fseek($index, 8 + 4 * 255);
  144.                                         $total_objects = Binary::fuint32($index);
  145.  
  146.                                         /* look up sha1 */
  147.                                         fseek($index, 8 + 4 * 256 + 20 * $cur);
  148.                                         for ($i = $cur; $i < $after; $i++) {
  149.                                                 $name = fread($index, 20);
  150.                                                 if ($name == $object_name)
  151.                                                         break;
  152.                                         }
  153.                                         if ($i == $after)
  154.                                                 continue;
  155.  
  156.                                         fseek($index, 8 + 4 * 256 + 24 * $total_objects + 4 * $i);
  157.                                         $off = Binary::fuint32($index);
  158.                                         if ($off & 0x80000000) {
  159.                                                 /* packfile > 2 GB. Gee, you really want to handle this
  160.                                                  * much data with PHP?
  161.                                                  */
  162.                                                 throw new \Exception('64-bit packfiles offsets not implemented');
  163.                                         }
  164.  
  165.                                         fclose($index);
  166.                                         return array($pack_name, $off);
  167.                                 } else
  168.                                         throw new \Exception('unsupported pack index format');
  169.                         }
  170.                         fclose($index);
  171.                 }
  172.                 /* not found */
  173.                 return null;
  174.         }
  175.  
  176.         /**
  177.          * @brief Apply the git delta $delta to the byte sequence $base.
  178.          *
  179.          * @param $delta (string) the delta to apply
  180.          * @param $base (string) the sequence to patch
  181.          * @returns (string) the patched byte sequence
  182.          */
  183.         protected function applyDelta($delta, $base) {
  184.                 $pos = 0;
  185.  
  186.                 $base_size = Binary::git_varint($delta, $pos);
  187.                 $result_size = Binary::git_varint($delta, $pos);
  188.  
  189.                 $r = '';
  190.                 while ($pos < strlen($delta)) {
  191.                         $opcode = ord($delta[$pos++]);
  192.                         if ($opcode & 0x80) {
  193.                                 /* copy a part of $base */
  194.                                 $off = 0;
  195.                                 if ($opcode & 0x01) $off = ord($delta[$pos++]);
  196.                                 if ($opcode & 0x02) $off |= ord($delta[$pos++]) << 8;
  197.                                 if ($opcode & 0x04) $off |= ord($delta[$pos++]) << 16;
  198.                                 if ($opcode & 0x08) $off |= ord($delta[$pos++]) << 24;
  199.                                 $len = 0;
  200.                                 if ($opcode & 0x10) $len = ord($delta[$pos++]);
  201.                                 if ($opcode & 0x20) $len |= ord($delta[$pos++]) << 8;
  202.                                 if ($opcode & 0x40) $len |= ord($delta[$pos++]) << 16;
  203.                                 if ($len == 0) $len = 0x10000;
  204.                                 $r .= substr($base, $off, $len);
  205.                         } else {
  206.                                 /* take the next $opcode bytes as they are */
  207.                                 $r .= substr($delta, $pos, $opcode);
  208.                                 $pos += $opcode;
  209.                         }
  210.                 }
  211.                 return $r;
  212.         }
  213.  
  214.         /**
  215.          * @brief Unpack an object from a pack.
  216.          *
  217.          * @param $pack (resource) open .pack file
  218.          * @param $object_offset (integer) offset of the object in the pack
  219.          * @returns (array) an array consisting of the object type (int) and the
  220.          * binary representation of the object (string)
  221.          */
  222.         protected function unpackObject($pack, $object_offset) {
  223.                 fseek($pack, $object_offset);
  224.  
  225.                 /* read object header */
  226.                 $c = ord(fgetc($pack));
  227.                 $type = ($c >> 4) & 0x07;
  228.                 $size = $c & 0x0F;
  229.                 for ($i = 4; $c & 0x80; $i += 7) {
  230.                         $c = ord(fgetc($pack));
  231.                         $size |= (($c & 0x7F) << $i);
  232.                 }
  233.  
  234.                 /* compare sha1_file.c:1608 unpack_entry */
  235.                 if ($type == Git::OBJ_COMMIT || $type == Git::OBJ_TREE || $type == Git::OBJ_BLOB || $type == Git::OBJ_TAG) {
  236.                         /*
  237.                          * We don't know the actual size of the compressed
  238.                          * data, so we'll assume it's less than
  239.                          * $object_size+512.
  240.                          *
  241.                          * FIXME use PHP stream filter API as soon as it behaves
  242.                          * consistently
  243.                          */
  244.                         $data = gzuncompress(fread($pack, $size + 512), $size);
  245.                 } else if ($type == Git::OBJ_OFS_DELTA) {
  246.                         /* 20 = maximum varint length for offset */
  247.                         $buf = fread($pack, $size + 512 + 20);
  248.  
  249.                         /*
  250.                          * contrary to varints in other places, this one is big endian
  251.                          * (and 1 is added each turn)
  252.                          * see sha1_file.c (get_delta_base)
  253.                          */
  254.                         $pos = 0;
  255.                         $offset = -1;
  256.                         do {
  257.                                 $offset++;
  258.                                 $c = ord($buf[$pos++]);
  259.                                 $offset = ($offset << 7) + ($c & 0x7F);
  260.                         } while ($c & 0x80);
  261.  
  262.                         $delta = gzuncompress(substr($buf, $pos), $size);
  263.                         unset($buf);
  264.  
  265.                         $base_offset = $object_offset - $offset;
  266.                         assert($base_offset >= 0);
  267.                         list($type, $base) = $this->unpackObject($pack, $base_offset);
  268.  
  269.                         $data = $this->applyDelta($delta, $base);
  270.                 } else if ($type == Git::OBJ_REF_DELTA) {
  271.                         $base_name = fread($pack, 20);
  272.                         list($type, $base) = $this->getRawObject($base_name);
  273.  
  274.                         // $size is the length of the uncompressed delta
  275.                         $delta = gzuncompress(fread($pack, $size + 512), $size);
  276.  
  277.                         $data = $this->applyDelta($delta, $base);
  278.                 } else
  279.                         throw new \Exception(sprintf('object of unknown type %d', $type));
  280.  
  281.                 return array($type, $data);
  282.         }
  283.  
  284.         /**
  285.          * @brief Fetch an object in its binary representation by name.
  286.          *
  287.          * Throws an exception if the object cannot be found.
  288.          *
  289.          * @param $object_name (string) name of the object (binary SHA1)
  290.          * @returns (array) an array consisting of the object type (int) and the
  291.          * binary representation of the object (string)
  292.          */
  293.         protected function getRawObject($object_name) {
  294.                 static $cache = array();
  295.                 /* FIXME allow limiting the cache to a certain size */
  296.  
  297.                 if (isset($cache[$object_name]))
  298.                         return $cache[$object_name];
  299.                 $sha1 = bin2hex($object_name);
  300.                 $path = sprintf('%s/objects/%s/%s', $this->dir, substr($sha1, 0, 2), substr($sha1, 2));
  301.                 if (file_exists($path)) {
  302.                         list($hdr, $object_data) = explode("\0", gzuncompress(file_get_contents($path)), 2);
  303.  
  304.                         sscanf($hdr, "%s %d", $type, $object_size);
  305.                         $object_type = Git::getTypeID($type);
  306.                         $r = array($object_type, $object_data);
  307.                 } else if ($x = $this->findPackedObject($object_name)) {
  308.                         list($pack_name, $object_offset) = $x;
  309.  
  310.                         $pack = fopen(sprintf('%s/objects/pack/pack-%s.pack', $this->dir, bin2hex($pack_name)), 'rb');
  311.                         flock($pack, LOCK_SH);
  312.  
  313.                         /* check magic and version */
  314.                         $magic = fread($pack, 4);
  315.                         $version = Binary::fuint32($pack);
  316.                         if ($magic != 'PACK' || $version != 2)
  317.                                 throw new \Exception('unsupported pack format');
  318.  
  319.                         $r = $this->unpackObject($pack, $object_offset);
  320.                         fclose($pack);
  321.                 } else
  322.                         throw new \Exception(sprintf('object not found: %s', bin2hex($object_name)));
  323.                 $cache[$object_name] = $r;
  324.                 return $r;
  325.         }
  326.  
  327.         /**
  328.          * @brief Fetch an object in its PHP representation.
  329.          *
  330.          * @param $name (string) name of the object (binary SHA1)
  331.          * @returns (GitObject) the object
  332.          */
  333.         public function getObject($name) {
  334.                 list($type, $data) = $this->getRawObject($name);
  335.                 $object = GitObject::create($this, $type);
  336.                 $object->unserialize($data);
  337.                 assert($name == $object->getName());
  338.                 return $object;
  339.         }
  340.  
  341.         /**
  342.          * @brief Look up a branch.
  343.          *
  344.          * @param $branch (string) The branch to look up, defaulting to @em master.
  345.          * @returns (string) The tip of the branch (binary sha1).
  346.          */
  347.         public function getTip($branch = 'master') {
  348.                 $subpath = sprintf('refs/heads/%s', $branch);
  349.                 $path = sprintf('%s/%s', $this->dir, $subpath);
  350.                 if (file_exists($path))
  351.                         return pack('H40', trim(file_get_contents($path)));
  352.                 $path = sprintf('%s/packed-refs', $this->dir);
  353.                 if (file_exists($path)) {
  354.                         $head = null;
  355.                         $f = fopen($path, 'rb');
  356.                         flock($f, LOCK_SH);
  357.                         while ($head === null && ($line = fgets($f)) !== false) {
  358.                                 if ($line[0] == '#')
  359.                                         continue;
  360.                                 $parts = explode(' ', trim($line));
  361.                                 if (count($parts) == 2 && $parts[1] == $subpath)
  362.                                         $head = pack('H40', $parts[0]);;
  363.                         }
  364.                         fclose($f);
  365.                         if ($head !== null)
  366.                                 return $head;
  367.                 }
  368.                 throw new \Exception(sprintf('no such branch: %s', $branch));
  369.         }
  370. }
  371.