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

/glip/git.class.php

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