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