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