import
[web.mtrack] / inc / scm / svn.php
1 <?php # vim:ts=2:sw=2:et:
2 /* For licensing and copyright terms, see the file named LICENSE */
3
4 /* Subversion SVN browsing */
5
6 class MTrackSCMFileSVN 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($reponame, $filename, $rev)
21   {
22     $repo = MTrackRepo::loadByName($reponame);
23     list($ent) = $repo->history($filename, 1, 'rev', $rev);
24     return $ent;
25   }
26
27   public function getChangeEvent()
28   {
29     return mtrack_cache(
30       array('MTrackSCMFileSVN', '_determineFileChangeEvent'),
31       array($this->repo->getBrowseRootName(), $this->name, $this->rev),
32       864000);
33   }
34
35   function cat()
36   {
37     return $this->repo->svn('cat', '-r', $this->rev,
38       'file://' . $this->repo->repopath . '/' . $this->name . "@$this->rev");
39   }
40
41   function annotate($include_line_content = false)
42   {
43     $xml = stream_get_contents($this->repo->svn('annotate', '--xml',
44       'file://' . $this->repo->repopath . '/' . $this->name . "@$this->rev"));
45     $ann = array();
46     $xml = @simplexml_load_string($xml);
47     if (!is_object($xml)) {
48       return 'DELETED';
49     }
50     if ($include_line_content) {
51       $cat = $this->cat();
52     }
53     foreach ($xml->target->entry as $ent) {
54       $A = new MTrackSCMAnnotation;
55       $A->rev = (int)$ent->commit['revision'];
56       $A->changeby = (string)$ent->commit->author;
57       if ($include_line_content) {
58         $A->line = fgets($cat);
59       }
60       $ann[(int)$ent['line-number']] = $A;
61     }
62     return $ann;
63
64   }
65 }
66
67 class MTrackWCSVN extends MTrackSCMWorkingCopy {
68   public $repo;
69
70   function __construct(MTrackRepo $repo) {
71     $this->dir = mtrack_make_temp_dir();
72     $this->repo = $repo;
73
74     stream_get_contents($this->repo->svn('checkout',
75       'file://' . $this->repo->repopath . '/trunk',
76       $this->dir));
77   }
78
79   function getFile($path)
80   {
81     return $this->repo->file('trunk/' . $path);
82   }
83
84
85   function addFile($path)
86   {
87     stream_get_contents(
88       $this->repo->svn('add', $this->dir . DIRECTORY_SEPARATOR . $path));
89   }
90
91   function delFile($path)
92   {
93     stream_get_contents(
94       $this->repo->svn('rm', $this->dir . DIRECTORY_SEPARATOR . $path));
95   }
96
97   function commit(MTrackChangeset $CS)
98   {
99     list($proc, $pipes) = mtrack_run_tool('svn', 'proc',
100       array('ci', '--non-interactive', '--username', $CS->who,
101         '-m', $CS->reason, $this->dir));
102 /*
103     $svn = MTrackConfig::get('tools', 'svn');
104     if (!strlen($svn)) $svn = 'svn';
105     $proc = proc_open(
106       "$svn ci --non-interactive " .
107       ' --username ' . escapeshellarg($CS->who) .
108       ' -m ' . escapeshellarg($CS->reason) .
109       ' ' . $this->dir,
110       array(
111         0 => array('pipe', 'r'),
112         1 => array('pipe', 'w'),
113         2 => array('pipe', 'w'),
114       ), $pipes, $this->dir);
115 */
116     $pipes[0] = null;
117     $output = stream_get_contents($pipes[1]);
118     $err = stream_get_contents($pipes[2]);
119
120     if (strlen($err)) {
121       throw new Exception($err);
122     }
123
124     if (preg_match("/Committed revision (\d+)/", $output, $M)) {
125       $rev = $M[1];
126       stream_get_contents(
127           $this->repo->svn('propset', 'svn:date',
128             '--revprop',
129             '-r', $rev, $CS->when, $this->dir
130             ));
131     }
132   }
133 }
134
135 class MTrackSCMSVN extends MTrackRepo {
136   protected $svn = 'svn';
137   static $debug = false;
138
139   public function getSCMMetaData() {
140     return array(
141       'name' => 'Subversion',
142       'tools' => array('svn', 'svnlook', 'svnadmin'),
143     );
144   }
145
146   function getServerURL() {
147     $url = parent::getServerURL();
148     if ($url) return $url;
149     $url = MTrackConfig::get('repos', 'serverurl');
150     if ($url) {
151       return "svn+ssh://$url/" . $this->getBrowseRootName() . '/BRANCHNAME';
152     }
153     return null;
154   }
155
156
157   public function reconcileRepoSettings(MTrackSCM $r = null) {
158     if ($r == null) {
159       $r = $this;
160     }
161     if (!is_dir($r->repopath)) {
162       $stm = mtrack_run_tool('svnadmin', 'read', array('create', $r->repopath));
163       $out = stream_get_contents($stm);
164       if (pclose($stm)) {
165         throw new Exception("failed to create repo: $out");
166       }
167       file_put_contents("$r->repopath/hooks/pre-revprop-change",
168           "#!/bin/sh\nexit 0\n");
169       chmod("$r->repopath/hooks/pre-revprop-change", 0755);
170       $me = mtrack_canon_username(MTrackAuth::whoami());
171       $stm = mtrack_run_tool('svn', 'read', array('mkdir', '-m', 'init',
172         '--username', $me, "file://$r->repopath/trunk"));
173       $out = stream_get_contents($stm);
174       if (pclose($stm)) {
175         throw new Exception("failed to create trunk: $out");
176       }
177       system("chmod -R 02777 $r->repopath/db $r->repopath/locks");
178
179       $authzname = MTrackConfig::get('core', 'vardir') . '/svn.authz';
180       $svnserve = "[general]\nauthz-db = $authzname\n";
181       file_put_contents("$r->repopath/conf/svnserve.conf", $svnserve);
182     }
183   }
184
185   public function getDefaultRoot() {
186     return 'trunk/';
187   }
188
189   public function getBranches()
190   {
191     return null;
192   }
193
194   public function getTags()
195   {
196     return null;
197   }
198
199   public function readdir($path, $object = null, $ident = null)
200   {
201     $res = array();
202
203     if ($object === null) {
204       $object = 'rev';
205       $ident = 'HEAD';
206     }
207     $rev = $this->resolveRevision(null, $object, $ident);
208
209     $rpath = $this->repopath;
210     if (strlen($path)) {
211       $rpath .= "/$path";
212     }
213
214     $fp = $this->svn('ls', '--xml', '-r', $rev,
215           "file://" . $rpath);
216
217     $ls = stream_get_contents($fp);
218     $doc = simplexml_load_string($ls);
219     if (!is_object($doc)) {
220       echo '<pre>', htmlentities($ls, ENT_QUOTES, 'utf-8'), '</pre>';
221     }
222     if (isset($doc->list)) foreach ($doc->list->entry as $le) {
223       $name = $path;
224       $name .= '/';
225       $name .= $le->name;
226       if ($name[0] == '/') {
227         $name = substr($name, 1);
228       }
229       /* Use the revision passed in to readdir rather than the revision
230        * in the entry, as svn can return a revision number that pre-dates
231        * that of the containing tag, and this causes the subsequent
232        * lookup of commit data to fail */
233       $res[] = new MTrackSCMFileSVN($this, $name,
234           //$le->commit['revision'],
235           $rev,
236           $le['kind'] == 'dir');
237     }
238     return $res;
239   }
240
241   public function file($path, $object = null, $ident = null)
242   {
243     if ($object == null) {
244       $object = 'rev';
245       $ident = 'HEAD';
246     }
247     $rev = $this->resolveRevision(null, $object, $ident);
248     return new MTrackSCMFileSVN($this, $path, $rev);
249   }
250
251   public function history($path, $limit = null, $object = null, $ident = null)
252   {
253     $res = array();
254     $args = array();
255     $limit_date = null;
256
257     if ($limit !== null) {
258       if (!is_int($limit)) {
259         $limit_date = strtotime($limit);
260         $limit = null;
261         $limit_date = date('c', $limit_date);
262       }
263     }
264
265     $use_at_rev = false;
266     if ($object !== null) {
267       $rev = $this->resolveRevision(null, $object, $ident);
268       if ($limit_date != null) {
269         $args[] = '-r';
270         $args[] = $rev . ':{' . $limit_date . '}';
271       } else if ($rev == 'HEAD') {
272         $args[] = '-r';
273         $args[] = "$rev:1";
274       } else {
275         $use_at_rev = true;
276       }
277     }
278     if ($limit !== null) {
279       $args[] = '--limit';
280       $args[] = $limit;
281     } else if ($limit_date !== null) {
282       $args[] = '-r';
283       $args[] = '{' . $limit_date . '}:head';
284     }
285
286     $rpath = $this->repopath;
287     if (strlen($path)) {
288       if ($path[0] != '/') {
289         $rpath .= '/';
290       }
291       $rpath .= $path;
292     }
293     $spath = $rpath;
294
295     if ($use_at_rev) {
296       $spath .= "@$rev";
297     }
298
299     $fp = $this->svn('log', '--xml', '-v', $args, "file://$spath");
300
301     $xml = stream_get_contents($fp);
302     $doc = @simplexml_load_string($xml);
303     if (!is_object($doc)) {
304       /* try looking at the parent */
305       $spath = dirname($spath);
306       if ($use_at_rev) {
307         $spath .= "@$rev";
308       }
309       $fp = $this->svn('log', '--xml', '-v', $args, "file://$spath");
310       $xml = stream_get_contents($fp);
311       $doc = @simplexml_load_string($xml);
312     }
313
314     if (!is_object($doc)) {
315 //      echo '<pre>', htmlentities($xml, ENT_QUOTES, 'utf-8'), '</pre>';
316       return null;
317     }
318     if (self::$debug) {
319       if (php_sapi_name() == 'cli') {
320         echo $xml, "\n";
321       } else {
322         echo htmlentities(var_export($xml, true)) . "<br>";
323       }
324     }
325     $origpath = $path;
326     if ($origpath[0] != '/') {
327       $origpath = '/' . $origpath;
328     }
329     if ($doc->logentry) foreach ($doc->logentry as $le) {
330       $matched = false;
331       $ent = new MTrackSCMEvent;
332       $ent->rev = (int)$le['revision'];
333       $ent->branches = array();
334       $ent->tags = array();
335
336       $ent->files = array();
337       foreach ($le->paths->path as $path) {
338         if (strncmp($path, $origpath, strlen($origpath))) {
339           continue;
340         }
341         $matched = true;
342         $f = new MTrackSCMFileEvent;
343         $f->name = (string)$path;
344         $f->status = (string)$path['action'];
345         $ent->files[] = $f;
346       }
347
348       if ($matched) {
349         $ent->changeby = (string)$le->author;
350         $ent->ctime = MTrackDB::unixtime(strtotime($le->date));
351         $ent->changelog = (string)$le->msg;
352
353         $res[] = $ent;
354       }
355     }
356     $fp = null;
357     if (count($res) == 0) {
358       return null;
359     }
360     return $res;
361   }
362
363   function getCheckoutCommand() {
364     $url = $this->getServerURL();
365     if (strlen($url)) {
366       return $this->scmtype . ' checkout ' . $this->getServerURL();
367     }
368     return null;
369   }
370
371   public function diff($path, $from = null, $to = null)
372   {
373     $is_file = null;
374
375     if ($path instanceof MTrackSCMFile) {
376       $is_file = !$path->is_dir;
377       if ($from === null) {
378         $from = $path->rev;
379       }
380       $path = $path->name;
381     } elseif ($path instanceof MTrackSCMFileEvent) {
382       $is_file = true;
383     } else {
384       // http://subversion.tigris.org/issues/show_bug.cgi?id=2873
385       // Essentially, if there are files added in a changeset, you cannot use
386       // diff to show the diff of those newly added files if you explicitly
387       // request the file itself.  So we need to assess whether $path represents
388       // a file and dance around by diffing the parent path.
389
390       $is_file = false;
391       $info = $this->svn('info', "file://$this->repopath$path", '-r', $from);
392       $lines = 0;
393       while (($line = fgets($info)) !== false) {
394         $lines++;
395         if (preg_match("/^Node Kind:\s+file/", $line)) {
396           $is_file = true;
397           break;
398         }
399       }
400       if ($lines == 0) {
401         // no data returned; path doesn't exist at that revision
402         if ($to === null) {
403           $to = $from;
404           $from--;
405         }
406       }
407     }
408     if ($is_file) {
409       $diffpath = dirname($path);
410     } else {
411       $diffpath = $path;
412     }
413
414     if ($to !== null) {
415       $diff = $this->svn('diff', '-r', $from, '-r', $to,
416         "file://$this->repopath$diffpath");
417     } else {
418       $diff = $this->svn('diff', '-c', $from,
419         "file://$this->repopath$diffpath");
420     }
421
422     if ($is_file) {
423       $dir = $diff;
424       $diff = tmpfile();
425       $wanted = basename($path);
426       $in_wanted = false;
427       // search in the diffstream for the file that was originally requested
428       // and copy that through to the tmpfile we're using for the diff we're
429       // returning to the caller
430       while (($line = fgets($dir)) !== false) {
431         if (preg_match("/^Index: $wanted$/", $line)) {
432           $in_wanted = true;
433           fwrite($diff, $line);
434           continue;
435         } else if (preg_match("/^Index: /", $line)) {
436           if ($in_wanted) {
437             break;
438           }
439         }
440         if ($in_wanted) {
441           fwrite($diff, $line);
442         }
443       }
444       fseek($diff, 0);
445     }
446     return $diff;
447   }
448
449   public function getWorkingCopy()
450   {
451     return new MTrackWCSVN($this);
452   }
453
454   public function getRelatedChanges($revision)
455   {
456     return null;
457   }
458
459   function svn()
460   {
461     $args = func_get_args();
462     return mtrack_run_tool('svn', 'read', $args);
463   }
464 }
465
466 MTrackRepo::registerSCM('svn', 'MTrackSCMSVN');