PageRenderTime 55ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/inc/scm/git.php

https://bitbucket.org/yoander/mtrack
PHP | 507 lines | 423 code | 69 blank | 15 comment | 78 complexity | 3a2c1a793f78e6b9c4b1089699b654da MD5 | raw file
Possible License(s): BSD-3-Clause, Apache-2.0
  1. <?php # vim:ts=2:sw=2:et:
  2. /* For licensing and copyright terms, see the file named LICENSE */
  3. /* Git SCM browsing */
  4. class MTrackSCMFileGit extends MTrackSCMFile {
  5. public $name;
  6. public $rev;
  7. public $is_dir;
  8. public $repo;
  9. function __construct(MTrackSCM $repo, $name, $rev, $is_dir = false)
  10. {
  11. $this->repo = $repo;
  12. $this->name = $name;
  13. $this->rev = $rev;
  14. $this->is_dir = $is_dir;
  15. }
  16. public function getChangeEvent()
  17. {
  18. list($ent) = $this->repo->history($this->name, 1, 'rev', $this->rev);
  19. return $ent;
  20. }
  21. function cat()
  22. {
  23. // There may be a better way...
  24. // ls-tree to determine the hash of the file from this change:
  25. $fp = $this->repo->git('ls-tree', $this->rev, $this->name);
  26. $line = fgets($fp);
  27. $fp = null;
  28. list($mode, $type, $hash, $name) = preg_split("/\s+/", $line);
  29. // now we can cat that blob
  30. return $this->repo->git('cat-file', 'blob', $hash);
  31. }
  32. function annotate($include_line_content = false)
  33. {
  34. if ($this->repo->gitdir == $this->repo->repopath) {
  35. // For bare repos, we can't run annotate, so we need to make a clone
  36. // with a work tree. This relies on local clones being a cheap operation
  37. $wc = new MTrackWCGit($this->repo);
  38. $wc->push = false;
  39. $fp = $wc->git('annotate', '-p', $this->name, $this->rev);
  40. } else {
  41. $fp = $this->repo->git('annotate', '-p', $this->name, $this->rev);
  42. }
  43. $i = 1;
  44. $ann = array();
  45. $meta = array();
  46. while ($line = fgets($fp)) {
  47. // echo htmlentities($line), "<br>\n";
  48. if (!strncmp($line, "\t", 1)) {
  49. $A = new MTrackSCMAnnotation;
  50. if (isset($meta['author-mail']) &&
  51. strpos($meta['author-mail'], '@')) {
  52. $A->changeby = $meta['author'] . ' ' . $meta['author-mail'];
  53. } else {
  54. $A->changeby = $meta['author'];
  55. }
  56. $A->rev = $meta['rev'];
  57. if ($include_line_content) {
  58. $A->line = substr($line, 1);
  59. }
  60. $ann[$i++] = $A;
  61. continue;
  62. }
  63. if (preg_match("/^([a-f0-9]+)\s[a-f0-9]+\s[a-f0-9]+\s[a-f0-9]+$/",
  64. $line, $M)) {
  65. $meta['rev'] = $M[1];
  66. } else if (preg_match("/^(\S+)\s*(.*)$/", $line, $M)) {
  67. $name = $M[1];
  68. $value = $M[2];
  69. $meta[$name] = $value;
  70. }
  71. }
  72. return $ann;
  73. }
  74. }
  75. class MTrackWCGit extends MTrackSCMWorkingCopy {
  76. private $repo;
  77. public $push = true;
  78. function __construct(MTrackRepo $repo) {
  79. $this->dir = mtrack_make_temp_dir();
  80. $this->repo = $repo;
  81. mtrack_run_tool('git', 'string',
  82. array('clone', $this->repo->repopath, $this->dir)
  83. );
  84. }
  85. function __destruct() {
  86. if ($this->push) {
  87. echo stream_get_contents($this->git('push', 'origin', 'master'));
  88. }
  89. mtrack_rmdir($this->dir);
  90. }
  91. function getFile($path)
  92. {
  93. return $this->repo->file($path);
  94. }
  95. function addFile($path)
  96. {
  97. $this->git('add', $path);
  98. }
  99. function delFile($path)
  100. {
  101. $this->git('rm', '-f', $path);
  102. }
  103. function commit(MTrackChangeset $CS)
  104. {
  105. if ($CS->when) {
  106. $d = strtotime($CS->when);
  107. putenv("GIT_AUTHOR_DATE=$d -0000");
  108. } else {
  109. putenv("GIT_AUTHOR_DATE=");
  110. }
  111. $reason = trim($CS->reason);
  112. if (!strlen($reason)) {
  113. $reason = 'Changed';
  114. }
  115. MTrackSCMGit::setGitEnvironment($CS->who);
  116. stream_get_contents($this->git('commit', '-a',
  117. '-m', $reason
  118. )
  119. );
  120. }
  121. function git()
  122. {
  123. $args = func_get_args();
  124. $a = array("--git-dir=$this->dir/.git", "--work-tree=$this->dir");
  125. foreach ($args as $arg) {
  126. $a[] = $arg;
  127. }
  128. return mtrack_run_tool('git', 'read', $a);
  129. }
  130. }
  131. class MTrackSCMGit extends MTrackRepo {
  132. protected $branches = null;
  133. protected $tags = null;
  134. public $gitdir = null;
  135. public function getSCMMetaData() {
  136. return array(
  137. 'name' => 'Git',
  138. 'tools' => array('git'),
  139. );
  140. }
  141. function __construct($id = null) {
  142. parent::__construct($id);
  143. if ($id !== null) {
  144. /* transparently handle bare vs. non bare repos */
  145. $this->gitdir = $this->repopath;
  146. if (is_dir("$this->repopath/.git")) {
  147. $this->gitdir .= "/.git";
  148. }
  149. }
  150. }
  151. function getServerURL() {
  152. $url = parent::getServerURL();
  153. if ($url) return $url;
  154. $url = MTrackConfig::get('repos', 'serverurl');
  155. if ($url) {
  156. $pp = MTrackConfig::get('repos', 'serverpathprefix');
  157. if ($pp) {
  158. return "$url:~$pp/" . $this->getBrowseRootName();
  159. }
  160. return "$url:" . $this->getBrowseRootName();
  161. }
  162. return null;
  163. }
  164. /* I've had reports that Git becomes unhappy if it can't find something
  165. * that looks like an email address, so try to normalize towards that */
  166. static function setGitEnvironment($user) {
  167. $userdata = MTrackAuth::getUserData($user);
  168. if (preg_match("/@/", $userdata['email'])) {
  169. $who = $userdata['email'];
  170. } else {
  171. $who = "$user@local";
  172. }
  173. putenv("GIT_AUTHOR_NAME=$who");
  174. putenv("GIT_AUTHOR_EMAIL=$who");
  175. }
  176. public function reconcileRepoSettings(MTrackSCM $r = null) {
  177. if ($r == null) {
  178. $r = $this;
  179. }
  180. if (!is_dir($r->repopath)) {
  181. self::setGitEnvironment(MTrackAuth::whoami());
  182. if ($r->clonedfrom) {
  183. $S = MTrackRepo::loadById($r->clonedfrom);
  184. $stm = mtrack_run_tool('git', 'read',
  185. array('clone', '--bare', $S->repopath, $r->repopath));
  186. $out = stream_get_contents($stm);
  187. if (pclose($stm)) {
  188. throw new Exception("git init failed: $out");
  189. }
  190. } else {
  191. $stm = mtrack_run_tool('git', 'read',
  192. array('init', '--bare', $r->repopath));
  193. $out = stream_get_contents($stm);
  194. if (pclose($stm)) {
  195. throw new Exception("git init failed: $out");
  196. }
  197. }
  198. $php = MTrackConfig::get('tools', 'php');
  199. $hook = realpath(dirname(__FILE__) . '/../../bin/git-commit-hook');
  200. $conffile = realpath(MTrackConfig::getLocation());
  201. foreach (array('pre', 'post') as $step) {
  202. $script = <<<HOOK
  203. #!/bin/sh
  204. exec $php $hook $step $conffile
  205. HOOK;
  206. $target = "$r->repopath/hooks/$step-receive";
  207. if (file_put_contents("$target.mtrack", $script)) {
  208. chmod("$target.mtrack", 0755);
  209. rename("$target.mtrack", $target);
  210. }
  211. }
  212. }
  213. system("chmod -R 02777 $r->repopath");
  214. }
  215. function canFork() {
  216. return true;
  217. }
  218. public function getBranches()
  219. {
  220. if ($this->branches !== null) {
  221. return $this->branches;
  222. }
  223. $this->branches = array();
  224. $fp = $this->git('branch', '--no-color', '--verbose');
  225. while ($line = fgets($fp)) {
  226. // * master 61e7e7d oneliner
  227. $line = substr($line, 2);
  228. list($branch, $rev) = preg_split('/\s+/', $line);
  229. $this->branches[$branch] = $rev;
  230. }
  231. $fp = null;
  232. return $this->branches;
  233. }
  234. public function getTags()
  235. {
  236. if ($this->tags !== null) {
  237. return $this->tags;
  238. }
  239. $this->tags = array();
  240. $fp = $this->git('tag');
  241. while ($line = fgets($fp)) {
  242. $line = trim($line);
  243. $this->tags[$line] = $line;
  244. }
  245. $fp = null;
  246. return $this->tags;
  247. }
  248. public function readdir($path, $object = null, $ident = null)
  249. {
  250. $res = array();
  251. if ($object === null) {
  252. $object = 'branch';
  253. $ident = 'master';
  254. }
  255. $rev = $this->resolveRevision(null, $object, $ident);
  256. if (strlen($path)) {
  257. $path = rtrim($path, '/') . '/';
  258. }
  259. $fp = $this->git('ls-tree', $rev, $path);
  260. $dirs = array();
  261. while ($line = fgets($fp)) {
  262. list($mode, $type, $hash, $name) = preg_split("/\s+/", $line);
  263. $res[] = new MTrackSCMFileGit($this, "$name", $rev, $type == 'tree');
  264. }
  265. return $res;
  266. }
  267. public function file($path, $object = null, $ident = null)
  268. {
  269. if ($object == null) {
  270. $branches = $this->getBranches();
  271. if (isset($branches['master'])) {
  272. $object = 'branch';
  273. $ident = 'master';
  274. } else {
  275. // fresh/empty repo
  276. return null;
  277. }
  278. }
  279. $rev = $this->resolveRevision(null, $object, $ident);
  280. return new MTrackSCMFileGit($this, $path, $rev);
  281. }
  282. public function history($path, $limit = null, $object = null, $ident = null)
  283. {
  284. $res = array();
  285. $args = array();
  286. if ($object == 'rev' && $limit > 1) {
  287. $args[] = $ident;
  288. } else if ($object !== null) {
  289. $rev = $this->resolveRevision(null, $object, $ident);
  290. $args[] = "$rev";
  291. } else {
  292. $args[] = "master";
  293. }
  294. if ($limit !== null) {
  295. if (is_int($limit) || preg_match("/^\d+$/", $limit)) {
  296. $args[] = "--max-count=$limit";
  297. } else {
  298. $args[] = "--since=$limit";
  299. }
  300. }
  301. $args[] = "--no-color";
  302. $args[] = "--name-status";
  303. $args[] = "--date=rfc";
  304. $path = ltrim($path, '/');
  305. $fp = $this->git('log', $args, '--', $path);
  306. $commits = array();
  307. $commit = null;
  308. while (true) {
  309. $line = fgets($fp);
  310. if ($line === false) {
  311. if ($commit !== null) {
  312. $commits[] = $commit;
  313. }
  314. break;
  315. }
  316. if (preg_match("/^commit/", $line)) {
  317. if ($commit !== null) {
  318. $commits[] = $commit;
  319. }
  320. $commit = $line;
  321. continue;
  322. }
  323. $commit .= $line;
  324. }
  325. foreach ($commits as $commit) {
  326. $ent = new MTrackSCMEvent;
  327. $lines = explode("\n", $commit);
  328. $line = array_shift($lines);
  329. if (!preg_match("/^commit\s+(\S+)$/", $line, $M)) {
  330. break;
  331. }
  332. $ent->rev = $M[1];
  333. $ent->branches = array(); // FIXME
  334. $ent->tags = array(); // FIXME
  335. $ent->files = array();
  336. while (count($lines)) {
  337. $line = array_shift($lines);
  338. if (!strlen($line)) {
  339. break;
  340. }
  341. if (preg_match("/^(\S+):\s+(.*)\s*$/", $line, $M)) {
  342. $k = $M[1];
  343. $v = $M[2];
  344. switch ($k) {
  345. case 'Author':
  346. $ent->changeby = $v;
  347. break;
  348. case 'Date':
  349. $ts = strtotime($v);
  350. $ent->ctime = MTrackDB::unixtime($ts);
  351. break;
  352. }
  353. }
  354. }
  355. $ent->changelog = "";
  356. if ($lines[0] == '') {
  357. array_shift($lines);
  358. }
  359. while (count($lines)) {
  360. $line = array_shift($lines);
  361. if (strncmp($line, ' ', 4)) {
  362. array_unshift($lines, $line);
  363. break;
  364. }
  365. $line = substr($line, 4);
  366. $ent->changelog .= $line . "\n";
  367. }
  368. if ($lines[0] == '') {
  369. array_shift($lines);
  370. }
  371. foreach ($lines as $line) {
  372. if (preg_match("/^(.+)\s+(\S+)\s*$/", $line, $M)) {
  373. $f = new MTrackSCMFileEvent;
  374. $f->name = $M[2];
  375. $f->status = $M[1];
  376. $ent->files[] = $f;
  377. }
  378. }
  379. if (!count($ent->branches)) {
  380. $ent->branches[] = 'master';
  381. }
  382. $res[] = $ent;
  383. }
  384. $fp = null;
  385. return $res;
  386. }
  387. public function diff($path, $from = null, $to = null)
  388. {
  389. if ($path instanceof MTrackSCMFile) {
  390. if ($from === null) {
  391. $from = $path->rev;
  392. }
  393. $path = $path->name;
  394. }
  395. if ($to !== null) {
  396. return $this->git('diff', "$from..$to", '--', $path);
  397. }
  398. return $this->git('diff', "$from^..$from", '--', $path);
  399. }
  400. public function getWorkingCopy()
  401. {
  402. return new MTrackWCGit($this);
  403. }
  404. public function getRelatedChanges($revision)
  405. {
  406. $parents = array();
  407. $kids = array();
  408. $fp = $this->git('rev-parse', "$revision^");
  409. while (($line = fgets($fp)) !== false) {
  410. $parents[] = trim($line);
  411. }
  412. // Ugh!: http://stackoverflow.com/questions/1761825/referencing-the-child-of-a-commit-in-git
  413. $fp = $this->git('rev-list', '--all', '--parents');
  414. while (($line = fgets($fp)) !== false) {
  415. $hashes = preg_split("/\s+/", $line);
  416. $kid = array_shift($hashes);
  417. if (in_array($revision, $hashes)) {
  418. $kids[] = $kid;
  419. }
  420. }
  421. return array($parents, $kids);
  422. }
  423. function git()
  424. {
  425. $args = func_get_args();
  426. $a = array(
  427. "--git-dir=$this->gitdir"
  428. );
  429. if ($this->gitdir != $this->repopath) {
  430. $a[] = "--work-tree=$this->repopath";
  431. }
  432. foreach ($args as $arg) {
  433. $a[] = $arg;
  434. }
  435. return mtrack_run_tool('git', 'read', $a);
  436. }
  437. }
  438. MTrackRepo::registerSCM('git', 'MTrackSCMGit');