1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
6 class MTrackSCMFileGit extends MTrackSCMFile {
12 function __construct(MTrackSCM $repo, $name, $rev, $is_dir = false)
17 $this->is_dir = $is_dir;
20 public function getChangeEvent()
22 list($ent) = $this->repo->history($this->name, 1, 'rev', $this->rev);
28 // There may be a better way...
29 // ls-tree to determine the hash of the file from this change:
30 $fp = $this->repo->git('ls-tree', $this->rev, $this->name);
33 list($mode, $type, $hash, $name) = preg_split("/\s+/", $line);
35 // now we can cat that blob
36 return $this->repo->git('cat-file', 'blob', $hash);
39 function annotate($include_line_content = false)
41 if ($this->repo->gitdir == $this->repo->repopath) {
42 // For bare repos, we can't run annotate, so we need to make a clone
43 // with a work tree. This relies on local clones being a cheap operation
44 $wc = new MTrackWCGit($this->repo);
46 $fp = $wc->git('annotate', '-p', $this->name, $this->rev);
48 $fp = $this->repo->git('annotate', '-p', $this->name, $this->rev);
53 while ($line = fgets($fp)) {
54 // echo htmlentities($line), "<br>\n";
55 if (!strncmp($line, "\t", 1)) {
56 $A = new MTrackSCMAnnotation;
57 if (isset($meta['author-mail']) &&
58 strpos($meta['author-mail'], '@')) {
59 $A->changeby = $meta['author'] . ' ' . $meta['author-mail'];
61 $A->changeby = $meta['author'];
63 $A->rev = $meta['rev'];
64 if ($include_line_content) {
65 $A->line = substr($line, 1);
70 if (preg_match("/^([a-f0-9]+)\s[a-f0-9]+\s[a-f0-9]+\s[a-f0-9]+$/",
73 } else if (preg_match("/^(\S+)\s*(.*)$/", $line, $M)) {
76 $meta[$name] = $value;
83 class MTrackWCGit extends MTrackSCMWorkingCopy {
87 function __construct(MTrackRepo $repo) {
88 $this->dir = mtrack_make_temp_dir();
91 mtrack_run_tool('git', 'string',
92 array('clone', $this->repo->repopath, $this->dir)
96 function __destruct() {
98 echo stream_get_contents($this->git('push'));
100 mtrack_rmdir($this->dir);
103 function getFile($path)
105 return $this->repo->file($path);
108 function addFile($path)
110 $this->git('add', $path);
113 function delFile($path)
115 $this->git('rm', '-f', $path);
118 function commit(MTrackChangeset $CS)
121 $d = strtotime($CS->when);
122 putenv("GIT_AUTHOR_DATE=$d -0000");
124 putenv("GIT_AUTHOR_DATE=");
126 $reason = trim($CS->reason);
127 if (!strlen($reason)) {
130 putenv("GIT_AUTHOR_NAME=$CS->who");
131 putenv("GIT_AUTHOR_EMAIL=$CS->who");
132 stream_get_contents($this->git('commit', '-a',
140 $args = func_get_args();
141 $a = array("--git-dir=$this->dir/.git", "--work-tree=$this->dir");
142 foreach ($args as $arg) {
146 return mtrack_run_tool('git', 'read', $a);
150 class MTrackSCMGit extends MTrackRepo {
151 protected $branches = null;
152 protected $tags = null;
153 public $gitdir = null;
155 public function getSCMMetaData() {
158 'tools' => array('git'),
162 function __construct($id = null) {
163 parent::__construct($id);
165 /* transparently handle bare vs. non bare repos */
166 $this->gitdir = $this->repopath;
167 if (is_dir("$this->repopath/.git")) {
168 $this->gitdir .= "/.git";
173 function getServerURL() {
174 $url = parent::getServerURL();
175 if ($url) return $url;
176 $url = MTrackConfig::get('repos', 'serverurl');
178 return "$url:" . $this->getBrowseRootName();
183 public function reconcileRepoSettings(MTrackSCM $r = null) {
188 if (!is_dir($r->repopath)) {
189 $userdata = MTrackAuth::getUserData(MTrackAuth::whoami());
190 $who = $userdata['email'];
191 putenv("GIT_AUTHOR_NAME=$who");
192 putenv("GIT_AUTHOR_EMAIL=$who");
194 if ($r->clonedfrom) {
195 $S = MTrackRepo::loadById($r->clonedfrom);
197 $stm = mtrack_run_tool('git', 'read',
198 array('clone', '--bare', $S->repopath, $r->repopath));
199 $out = stream_get_contents($stm);
201 throw new Exception("git init failed: $out");
205 /* a little peculiar, but bear with it.
206 * We need to have a bare repo so that git doesn't mess around
207 * trying to deal with a checkout in the repo dir.
208 * So we need to create two repos; one bare, one not bare.
209 * We populate the non-bare repo with a dummy file just to have
210 * something to commit, then push non-bare -> bare, and remove non-bare.
213 $stm = mtrack_run_tool('git', 'read',
214 array('init', '--bare', $r->repopath));
215 $out = stream_get_contents($stm);
217 throw new Exception("git init failed: $out");
220 $alt = "$r->repopath.MTRACKINIT";
222 $stm = mtrack_run_tool('git', 'read',
223 array('init', $alt));
224 $out = stream_get_contents($stm);
226 throw new Exception("git init failed: $out");
232 file_put_contents("$alt/.gitignore", "#\n");
233 $stm = mtrack_run_tool('git', 'read',
234 array('add', '.gitignore'));
235 $out = stream_get_contents($stm);
237 throw new Exception("git add .gitignore failed: $out");
239 $stm = mtrack_run_tool('git', 'read',
240 array('commit', '-a', '-m', 'init'));
241 $out = stream_get_contents($stm);
243 throw new Exception("git commit failed: $out");
245 $stm = mtrack_run_tool('git', 'read',
246 array('push', $r->repopath, 'master'));
247 $out = stream_get_contents($stm);
249 throw new Exception("git push failed: $out");
252 system("rm -rf $alt");
255 $php = MTrackConfig::get('tools', 'php');
256 $hook = realpath(dirname(__FILE__) . '/../../bin/git-commit-hook');
257 $conffile = realpath(MTrackConfig::getLocation());
258 foreach (array('pre', 'post') as $step) {
261 exec $php $hook $step $conffile
264 $target = "$r->repopath/hooks/$step-receive";
265 if (file_put_contents("$target.mtrack", $script)) {
266 chmod("$target.mtrack", 0755);
267 rename("$target.mtrack", $target);
272 system("chmod -R 02777 $r->repopath");
280 public function getBranches()
282 if ($this->branches !== null) {
283 return $this->branches;
285 $this->branches = array();
286 $fp = $this->git('branch', '--no-color', '--verbose');
287 while ($line = fgets($fp)) {
288 // * master 61e7e7d oneliner
289 $line = substr($line, 2);
290 list($branch, $rev) = preg_split('/\s+/', $line);
291 $this->branches[$branch] = $rev;
294 return $this->branches;
297 public function getTags()
299 if ($this->tags !== null) {
302 $this->tags = array();
303 $fp = $this->git('tag');
304 while ($line = fgets($fp)) {
306 $this->tags[$line] = $line;
312 public function readdir($path, $object = null, $ident = null)
316 if ($object === null) {
320 $rev = $this->resolveRevision(null, $object, $ident);
323 $path = rtrim($path, '/') . '/';
326 $fp = $this->git('ls-tree', $rev, $path);
330 while ($line = fgets($fp)) {
331 list($mode, $type, $hash, $name) = preg_split("/\s+/", $line);
333 $res[] = new MTrackSCMFileGit($this, "$name", $rev, $type == 'tree');
338 public function file($path, $object = null, $ident = null)
340 if ($object == null) {
341 $branches = $this->getBranches();
342 if (isset($branches['master'])) {
350 $rev = $this->resolveRevision(null, $object, $ident);
351 return new MTrackSCMFileGit($this, $path, $rev);
354 public function history($path, $limit = null, $object = null, $ident = null)
359 if ($object !== null) {
360 $rev = $this->resolveRevision(null, $object, $ident);
365 if ($limit !== null) {
366 if (is_int($limit)) {
367 $args[] = "--max-count=$limit";
369 $args[] = "--since=$limit";
372 $args[] = "--no-color";
373 $args[] = "--name-status";
374 $args[] = "--date=rfc";
376 $path = ltrim($path, '/');
378 $fp = $this->git('log', $args, '--', $path);
384 if ($line === false) {
385 if ($commit !== null) {
386 $commits[] = $commit;
390 if (preg_match("/^commit/", $line)) {
391 if ($commit !== null) {
392 $commits[] = $commit;
400 foreach ($commits as $commit) {
401 $ent = new MTrackSCMEvent;
402 $lines = explode("\n", $commit);
403 $line = array_shift($lines);
405 if (!preg_match("/^commit\s+(\S+)$/", $line, $M)) {
410 $ent->branches = array(); // FIXME
411 $ent->tags = array(); // FIXME
412 $ent->files = array();
414 while (count($lines)) {
415 $line = array_shift($lines);
416 if (!strlen($line)) {
419 if (preg_match("/^(\S+):\s+(.*)\s*$/", $line, $M)) {
429 $ent->ctime = MTrackDB::unixtime($ts);
435 $ent->changelog = "";
437 if ($lines[0] == '') {
441 while (count($lines)) {
442 $line = array_shift($lines);
443 if (strncmp($line, ' ', 4)) {
444 array_unshift($lines, $line);
447 $line = substr($line, 4);
448 $ent->changelog .= $line . "\n";
451 if ($lines[0] == '') {
454 foreach ($lines as $line) {
455 if (preg_match("/^(.+)\s+(\S+)\s*$/", $line, $M)) {
456 $f = new MTrackSCMFileEvent;
463 if (!count($ent->branches)) {
464 $ent->branches[] = 'master';
473 public function diff($path, $from = null, $to = null)
475 if ($path instanceof MTrackSCMFile) {
476 if ($from === null) {
482 return $this->git('diff', "$from..$to", '--', $path);
484 return $this->git('diff', "$from^..$from", '--', $path);
487 public function getWorkingCopy()
489 return new MTrackWCGit($this);
492 public function getRelatedChanges($revision)
497 $fp = $this->git('rev-parse', "$revision^");
498 while (($line = fgets($fp)) !== false) {
499 $parents[] = trim($line);
502 // Ugh!: http://stackoverflow.com/questions/1761825/referencing-the-child-of-a-commit-in-git
503 $fp = $this->git('rev-list', '--all', '--parents');
504 while (($line = fgets($fp)) !== false) {
505 $hashes = preg_split("/\s+/", $line);
506 $kid = array_shift($hashes);
507 if (in_array($revision, $hashes)) {
512 return array($parents, $kids);
517 $args = func_get_args();
519 "--git-dir=$this->gitdir"
522 if ($this->gitdir != $this->repopath) {
523 $a[] = "--work-tree=$this->repopath";
525 foreach ($args as $arg) {
529 return mtrack_run_tool('git', 'read', $a);
533 MTrackRepo::registerSCM('git', 'MTrackSCMGit');