PageRenderTime 26ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/git.class.php

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