import
[web.mtrack] / inc / scm / hg.php
1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
3
4 /* Mercurial SCM browsing */
5
6 class MTrackSCMFileHg extends MTrackSCMFile {
7   public $name;
8   public $rev;
9   public $is_dir;
10   public $repo;
11
12   function __construct(MTrackSCM $repo, $name, $rev, $is_dir = false)
13   {
14     $this->repo = $repo;
15     $this->name = $name;
16     $this->rev = $rev;
17     $this->is_dir = $is_dir;
18   }
19
20   public function _determineFileChangeEvent($repoid, $filename, $rev)
21   {
22     $repo = MTrackRepo::loadById($repoid);
23     $ents = $repo->history($filename, 1, 'rev', "$rev:0");
24     if (!count($ents)) {
25       throw new Exception("$filename is invalid");
26     }
27     return $ents[0];
28   }
29
30   public function getChangeEvent()
31   {
32     return mtrack_cache(
33       array('MTrackSCMFileHg', '_determineFileChangeEvent'),
34       array($this->repo->repoid, $this->name, $this->rev),
35       864000);
36   }
37
38   function cat()
39   {
40     return $this->repo->hg('cat', '-r', $this->rev, $this->name);
41   }
42
43   function annotate($include_line_content = false)
44   {
45     $i = 1;
46     $ann = array();
47     $fp = $this->repo->hg('annotate', '-r', $this->rev, '-uvc', $this->name);
48     while ($line = fgets($fp)) {
49       preg_match("/^\s*([^:]*)\s+([0-9a-fA-F]+): (.*)$/", $line, $M);
50       $A = new MTrackSCMAnnotation;
51       $A->changeby = $M[1];
52       $A->rev = $M[2];
53       if ($include_line_content) {
54         $A->line = $M[3];
55       }
56       $ann[$i++] = $A;
57     }
58     return $ann;
59   }
60 }
61
62 class MTrackWCHg extends MTrackSCMWorkingCopy {
63   private $repo;
64
65   function __construct(MTrackRepo $repo) {
66     $this->dir = mtrack_make_temp_dir();
67     $this->repo = $repo;
68
69     stream_get_contents($this->hg('init', $this->dir));
70     stream_get_contents($this->hg('pull', $this->repo->repopath));
71     stream_get_contents($this->hg('up'));
72   }
73
74   function __destruct() {
75
76     $a = array("-y", "--cwd", $this->dir, 'push', $this->repo->repopath);
77
78     list($proc, $pipes) = mtrack_run_tool('hg', 'proc', $a);
79
80     $out = stream_get_contents($pipes[1]);
81     $err = stream_get_contents($pipes[2]);
82     $st = proc_close($proc);
83
84     if ($st) {
85       throw new Exception("push failed with status $st: $err $out");
86     }
87     mtrack_rmdir($this->dir);
88   }
89
90   function getFile($path)
91   {
92     return $this->repo->file($path);
93   }
94
95   function addFile($path)
96   {
97     // nothing to do; we use --addremove
98   }
99
100   function delFile($path)
101   {
102     // we use --addremove when we commit for this to take effect
103     unlink($this->dir . DIRECTORY_SEPARATOR . $path);
104   }
105
106   function commit(MTrackChangeset $CS)
107   {
108     $hg_date = (int)strtotime($CS->when) . ' 0';
109     $reason = trim($CS->reason);
110     if (!strlen($reason)) {
111       $reason = 'Changed';
112     }
113     $out = $this->hg('ci', '--addremove',
114       '-m', $reason,
115       '-d', $hg_date,
116       '-u', $CS->who);
117     $data = stream_get_contents($out);
118     $st = pclose($out);
119     if ($st != 0) {
120       throw new Exception("commit failed $st $data");
121     }
122   }
123
124   function hg()
125   {
126     $args = func_get_args();
127     $a = array("-y", "--cwd", $this->dir);
128     foreach ($args as $arg) {
129       $a[] = $arg;
130     }
131
132     return mtrack_run_tool('hg', 'read', $a);
133   }
134 }
135
136 class MTrackSCMHg extends MTrackRepo {
137   protected $hg = 'hg';
138   protected $branches = null;
139   protected $tags = null;
140
141   public function getSCMMetaData() {
142     return array(
143       'name' => 'Mercurial',
144       'tools' => array('hg'),
145     );
146   }
147
148   public function reconcileRepoSettings(MTrackSCM $r = null) {
149     if ($r == null) {
150       $r = $this;
151     }
152     $description = substr(preg_replace("/\r?\n/m", ' ', $r->description), 0, 64);
153     $description = trim($description);
154     if (!is_dir($r->repopath)) {
155       if ($r->clonedfrom) {
156         $S = MTrackRepo::loadById($r->clonedfrom);
157         $stm = mtrack_run_tool('hg', 'read', array(
158           'clone', $S->repopath, $r->repopath));
159       } else {
160         $stm = mtrack_run_tool('hg', 'read', array('init', $r->repopath));
161       }
162       $out = stream_get_contents($stm);
163       $st = pclose($stm);
164       if ($st) {
165         throw new Exception("hg: failed $out");
166       }
167     }
168
169     $php = MTrackConfig::get('tools', 'php');
170     $conffile = realpath(MTrackConfig::getLocation());
171
172     $install = realpath(dirname(__FILE__) . '/../../');
173
174     /* fixup config */
175     $apply = array(
176       "hooks" => array(
177         "changegroup.mtrack" =>
178           "$php $install/bin/hg-commit-hook changegroup $conffile",
179         "commit.mtrack" =>
180           "$php $install/bin/hg-commit-hook commit $conffile",
181         "pretxncommit.mtrack" =>
182           "$php $install/bin/hg-commit-hook pretxncommit $conffile",
183         "pretxnchangegroup.mtrack" =>
184           "$php $install/bin/hg-commit-hook pretxnchangegroup $conffile",
185       ),
186       "web" => array(
187         "description" => $description,
188       )
189     );
190
191     $cfg = @file_get_contents("$r->repopath/.hg/hgrc");
192     $adds = array();
193
194     foreach ($apply as $sect => $opts) {
195       foreach ($opts as $name => $value) {
196         if (preg_match("/^$name\s*=/m", $cfg)) {
197           $cfg = preg_replace("/^$name\s*=.*$/m", "$name = $value", $cfg);
198         } else {
199           $adds[$sect][$name] = $value;
200         }
201       }
202     }
203
204     foreach ($adds as $sect => $opts) {
205       $cfg .= "[$sect]\n";
206       foreach ($opts as $name => $value) {
207         $cfg .= "$name = $value\n";
208       }
209     }
210     file_put_contents("$r->repopath/.hg/hgrc", $cfg, LOCK_EX);
211     system("chmod -R 02777 $r->repopath");
212   }
213
214   function canFork() {
215     return true;
216   }
217
218   function getServerURL() {
219     $url = parent::getServerURL();
220     if ($url) return $url;
221     $url = MTrackConfig::get('repos', 'serverurl');
222     if ($url) {
223       return "ssh://$url/" . $this->getBrowseRootName();
224     }
225     return null;
226   }
227
228   public function getBranches()
229   {
230     if ($this->branches !== null) {
231       return $this->branches;
232     }
233     $this->branches = array();
234     $fp = $this->hg('branches');
235     while ($line = fgets($fp)) {
236       list($branch, $revstr) = preg_split('/\s+/', $line);
237       list($num, $rev) = explode(':', $revstr, 2);
238       $this->branches[$branch] = $rev;
239     }
240     $fp = null;
241     return $this->branches;
242   }
243
244   public function getTags()
245   {
246     if ($this->tags !== null) {
247       return $this->tags;
248     }
249     $this->tags = array();
250     $fp = $this->hg('tags');
251     while ($line = fgets($fp)) {
252       list($tag, $revstr) = preg_split('/\s+/', $line);
253       list($num, $rev) = explode(':', $revstr, 2);
254       $this->tags[$tag] = $rev;
255     }
256     $fp = null;
257     return $this->tags;
258   }
259
260   public function readdir($path, $object = null, $ident = null)
261   {
262     $res = array();
263
264     if ($object === null) {
265       $object = 'branch';
266       $ident = 'default';
267     }
268     $rev = $this->resolveRevision(null, $object, $ident);
269
270     $fp = $this->hg('manifest', '-r', $rev);
271
272     if (strlen($path)) {
273       $path .= '/';
274     }
275     $plen = strlen($path);
276
277     $dirs = array();
278     $exists = false;
279
280     while ($line = fgets($fp)) {
281       $name = trim($line);
282
283       if (!strncmp($name, $path, $plen)) {
284         $exists = true;
285         $ent = substr($name, $plen);
286         if (strpos($ent, '/') === false) {
287           $res[] = new MTrackSCMFileHg($this, "$path$ent", $rev);
288         } else {
289           list($d) = explode('/', $ent, 2);
290           if (!isset($dirs[$d])) {
291             $dirs[$d] = $d;
292             $res[] = new MTrackSCMFileHg($this, "$path$d", $rev, true);
293           }
294         }
295       }
296     }
297
298     if (!$exists) {
299       throw new Exception("location $path does not exist");
300     }
301     return $res;
302   }
303
304   public function file($path, $object = null, $ident = null)
305   {
306     if ($object == null) {
307       $branches = $this->getBranches();
308       if (isset($branches['default'])) {
309         $object = 'branch';
310         $ident = 'default';
311       } else {
312         // fresh/empty repo
313         $object = 'tag';
314         $ident = 'tip';
315       }
316     }
317     $rev = $this->resolveRevision(null, $object, $ident);
318     return new MTrackSCMFileHg($this, $path, $rev);
319   }
320
321   public function history($path, $limit = null, $object = null, $ident = null)
322   {
323     $res = array();
324
325     $args = array();
326     if ($object !== null) {
327       $rev = $this->resolveRevision(null, $object, $ident);
328       $args[] = '-r';
329       $args[] = $rev;
330     }
331     if ($limit !== null) {
332       if (is_int($limit)) {
333         $args[] = '-l';
334         $args[] = $limit;
335       } else {
336         $t = strtotime($limit);
337         $args[] = '-d';
338         $args[] = ">$t 0";
339       }
340     }
341
342     $sep = uniqid();
343     $fp = $this->hg('log',
344       '--template', $sep . '\n{node|short}\n{branches}\n{tags}\n{file_adds}\n{file_copies}\n{file_mods}\n{file_dels}\n{author|email}\n{date|hgdate}\n{desc}\n', $args,
345       $path);
346
347     fgets($fp); # discard leading $sep
348
349     // corresponds to the file_adds, file_copies, file_modes, file_dels
350     // in the template above
351     static $file_status_order = array('A', 'C', 'M', 'D');
352
353     while (true) {
354       $ent = new MTrackSCMEvent;
355       $ent->rev = trim(fgets($fp));
356       if (!strlen($ent->rev)) {
357         break;
358       }
359
360       $ent->branches = array();
361       foreach (preg_split('/\s+/', trim(fgets($fp))) as $b) {
362         if (strlen($b)) {
363           $ent->branches[] = $b;
364         }
365       }
366       if (!count($ent->branches)) {
367         $ent->branches[] = 'default';
368       }
369
370       $ent->tags = array();
371       foreach (preg_split('/\s+/', trim(fgets($fp))) as $t) {
372         if (strlen($t)) {
373           $ent->tags[] = $t;
374         }
375       }
376
377       $ent->files = array();
378
379       foreach ($file_status_order as $status) {
380         foreach (preg_split('/\s+/', trim(fgets($fp))) as $t) {
381           if (strlen($t)) {
382             $f = new MTrackSCMFileEvent;
383             $f->name = $t;
384             $f->status = $status;
385             $ent->files[] = $f;
386           }
387         }
388       }
389
390       $ent->changeby = trim(fgets($fp));
391       list($ts) = preg_split('/\s+/', fgets($fp));
392       $ent->ctime = MTrackDB::unixtime((int)$ts);
393       $changelog = array();
394       while (($line = fgets($fp)) !== false) {
395         $line = rtrim($line, "\r\n");
396         if ($line == $sep) {
397           break;
398         }
399         $changelog[] = $line;
400       }
401       $ent->changelog = join("\n", $changelog);
402
403       $res[] = $ent;
404
405       if ($line === false) {
406         break;
407       }
408     }
409     $fp = null;
410     return $res;
411   }
412
413   public function diff($path, $from = null, $to = null)
414   {
415     if ($path instanceof MTrackSCMFile) {
416       if ($from === null) {
417         $from = $path->rev;
418       }
419       $path = $path->name;
420     }
421     if ($to !== null) {
422       return $this->hg('diff', '-r', $from, '-r', $to,
423         '--git', $path);
424     }
425     return $this->hg('diff', '-c', $from, '--git', $path);
426   }
427
428   public function getWorkingCopy()
429   {
430     return new MTrackWCHg($this);
431   }
432
433   public function getRelatedChanges($revision)
434   {
435     $parents = array();
436     $kids = array();
437
438     foreach (preg_split('/\s+/',
439           stream_get_contents($this->hg('parents', '-r', $revision,
440               '--template', '{node|short}\n'))) as $p) {
441       if (strlen($p)) {
442         $parents[] = $p;
443       }
444     }
445
446     foreach (preg_split('/\s+/',
447         stream_get_contents($this->hg('--config',
448           'extensions.children=',
449           'children', '-r', $revision,
450           '--template', '{node|short}\n'))) as $p) {
451       if (strlen($p)) {
452         $kids[] = $p;
453       }
454     }
455     return array($parents, $kids);
456   }
457
458   function hg()
459   {
460     $args = func_get_args();
461     $a = array("-y", "-R", $this->repopath, "--cwd", $this->repopath);
462     foreach ($args as $arg) {
463       $a[] = $arg;
464     }
465
466     return mtrack_run_tool('hg', 'read', $a);
467   }
468 }
469
470 MTrackRepo::registerSCM('hg', 'MTrackSCMHg');
471