Release diogenes-0.9.22
[diogenes.git] / include / VCS / RCS.php
1 <?php
2 /*
3 * Copyright (C) 2003-2004 Polytechnique.org
4 * http://opensource.polytechnique.org/
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 */
20
21
22 require_once 'VCS/Spool.php';
23 // dependency on PEAR
24 require_once 'System.php';
25
26 /** This class handles Diogenes RCS operations.
27 */
28 class Diogenes_VCS_RCS extends Diogenes_VCS_Spool {
29 /** Absolute directory location for the barrel's RCS files. */
30 var $rcsdir;
31
32 /** The username */
33 var $login;
34 /** The constructor.
35 *
36 * @param caller
37 * @param alias
38 * @param login the current user's login
39 * @param init should create this module?
40 */
41 function Diogenes_VCS_RCS(&$caller,$alias,$login,$init = false) {
42 global $globals;
43 $this->Diogenes_VCS_Spool($caller,$alias);
44 $this->rcsdir = "{$globals->rcsroot}/$alias";
45 $this->login = $login;
46
47 // if we were asked to, created directories
48 if ($init) {
49 if (!is_dir($this->rcsdir))
50 mkdir($this->rcsdir, 0700);
51 if (!is_dir($this->datadir))
52 mkdir($this->datadir, 0700);
53 }
54
55 // check RCS directory
56 if (!is_dir($this->rcsdir) || !is_writable($this->rcsdir))
57 $this->kill("'{$this->rcsdir}' is not a writable directory");
58
59 // check spool directory
60 if (!is_dir($this->datadir) || !is_writable($this->datadir))
61 $this->kill("'{$this->datadir}' is not a writable directory");
62 }
63
64
65 /** Return the path of an RCS "item" (file or directory).
66 *
67 * @param parent parent directory (optional)
68 * @param entry the item
69 */
70 function rcsPath($parent="",$entry="") {
71 $this->checkPath($parent,$entry);
72 return $this->rcsdir.($parent ? "/$parent": "") . ($entry ? "/$entry" : "");
73 }
74
75
76 /** Return the path of an RCS file (something,v).
77 *
78 * @param dir parent directory
79 * @param file the RCS entry
80 */
81 function rcsFile($dir,$file) {
82 return $this->rcsPath($dir,$file).",v";
83 }
84
85
86 /** Check whether a file is registered in RCS.
87 *
88 * @param dir parent directory
89 * @param file the RCS entry
90 */
91 function checkFile($dir, $file) {
92 return is_file($this->rcsFile($dir, $file));
93 }
94
95
96 /** Perform sanity check on an RCS directory
97 * and the corresponding checkout in the spool
98 *
99 * @param dir
100 */
101 function checkDir($dir) {
102 return is_dir($this->rcsPath($dir))
103 && is_writable($this->rcsPath($dir))
104 && is_dir($this->spoolPath($dir))
105 && is_writable($this->spoolPath($dir));
106 }
107
108
109 /** Do a checkout of an RCS item to a given location.
110 *
111 * @param dir parent directory
112 * @param file the RCS entry
113 * @param rev the revision to check out
114 * @param output the directory to which we want to perform the checkout
115 */
116 function checkout($dir,$file,$rev,$output)
117 {
118 $this->info("RCS : checkout out $file ($rev)..");
119 $rfile = $this->rcsFile($dir,$file);
120 if ($this->cmdExec("co -q -r".escapeshellarg($rev)." ".escapeshellarg($rfile)." ".escapeshellarg("$output/$file")))
121 {
122 $this->info("RCS : Error, checkout failed!");
123 $this->info($this->cmdStatus());
124 return;
125 }
126 return "$output/$file";
127 }
128
129
130 /** Commit an RCS item. Returns true for succes, false for an error.
131 *
132 * @param dir parent directory
133 * @param file the RCS entry
134 * @param content the contents of the new revision
135 * @param message the log message for this revision
136 */
137 function commit($dir,$file,$content,$message="")
138 {
139 $this->info("RCS : checking in '$file'..");
140
141 // check directories
142 if (!$this->checkDir($dir)) {
143 // error
144 $this->info("RCS : Error, RCS sanity check for '$dir' failed!");
145 return false;
146 }
147
148 // log commit attempt
149 $this->log("rcs_commit","{$this->alias}:$dir/$file:$message");
150
151 $sfile = $this->spoolPath($dir,$file);
152 $rfile = $this->rcsFile($dir,$file);
153
154 // if the RCS file does not exist, create it
155 if (!file_exists($rfile)) {
156 if ($this->cmdExec("echo '' | rcs -q -i ".escapeshellarg($rfile)))
157 {
158 // error
159 $this->info("RCS : Error, could not initialise RCS file '$rfile'!");
160 $this->info($this->cmdStatus());
161 return false;
162 }
163 }
164
165 // lock the spool file
166 if ($this->cmdExec("co -q -l ".escapeshellarg($rfile)." ".escapeshellarg($sfile)))
167 {
168 // error
169 $this->info("RCS : Error, could not get RCS lock on file '$file'!");
170 $this->info($this->cmdStatus());
171 return false;
172 }
173 if ($fp = fopen($sfile,"w")) {
174 fwrite($fp,$content);
175 fclose($fp);
176
177 if ($this->cmdExec("ci -q -w".escapeshellarg($this->login). ($message ? " -m".escapeshellarg($message) : "").
178 " ". escapeshellarg($sfile). " ". escapeshellarg($rfile)))
179 {
180 // error
181 $this->info("RCS : Error, checkin failed!");
182 $this->info($this->cmdStatus());
183 return false;
184 }
185
186 if ($this->cmdExec("co -q ".escapeshellarg($rfile)." ".escapeshellarg($sfile)))
187 {
188 // error
189 $this->info("RCS : Error, checkout after checkin failed!");
190 $this->info($this->cmdStatus());
191 return false;
192 }
193 }
194 return true;
195 }
196
197
198
199 /** Make a copy of an RCS item to a given location.
200 *
201 * @param sdir the source directory
202 * @param sfile the source RCS entry
203 * @param ddir the destination directory
204 * @param dfile the destination RCS entry
205 */
206 function copy($sdir,$sfile,$ddir,$dfile)
207 {
208 $this->info("RCS : copying '$sfile' to '$ddir/$dfile'..");
209
210 $spath = $this->spoolPath($sdir, $sfile);
211 if (!is_file($spath)) {
212 $this->info("Error: source file '$spath' does not exist!");
213 return false;
214 }
215 if (!$this->checkDir($ddir))
216 {
217 $this->info("Error: directory '$ddir' does not exist!");
218 return false;
219 }
220 if ($this->checkFile($ddir, $dfile))
221 {
222 $this->info("Error: file '$dfile' already exists in '$ddir'!");
223 return false;
224 }
225 return $this->commit($ddir,$dfile,
226 file_get_contents($spath),
227 "copied from '$ddir/$sfile'");
228 }
229
230
231 /** Delete an RCS file and its corresponding spool entry.
232 *
233 * @param dir parent directory
234 * @param file the RCS entry
235 */
236 function del($dir,$file) {
237 $this->info("RCS : deleting '$file'..");
238 $this->log("rcs_delete","{$this->alias}:$dir/$file");
239 @unlink($this->spoolPath($dir,$file));
240 @unlink($this->rcsFile($dir,$file));
241 }
242
243
244 /** Retrieve differences between two version of a file.
245 *
246 * @param dir parent directory
247 * @param file the RCS entry
248 * @param r1 the first revision
249 * @param r2 the second revision
250 */
251 function diff($dir,$file,$r1,$r2)
252 {
253 $rfile = $this->rcsFile($dir,$file);
254 $this->info("RCS : diffing '$file' ($r1 to $r2)..");
255 $this->cmdExec("rcsdiff -r".escapeshellarg($r1). " -r".escapeshellarg($r2)." ".escapeshellarg($rfile));
256 return $this->cmd_output;
257 }
258
259
260 /** Converts a Word document to HTML and commits the resulting
261 * HTML and images.
262 *
263 * @param dir
264 * @param htmlfile
265 * @param wordfile
266 */
267 function importWordFile($dir,$htmlfile,$wordfile)
268 {
269 global $globals;
270
271 if (!$globals->word_import) {
272 $this->info("Error : support for word import is disabled!");
273 return false;
274 }
275
276 $func = "importWordFile_{$globals->word_import}";
277
278 if (!method_exists($this, $func))
279 {
280 $this->info("Error : the utility '$globals->word_import' is not supported!");
281 return false;
282 }
283
284 return $this->$func($dir, $htmlfile, $wordfile);
285 }
286
287
288 /** Converts a Word document to HTML using wvHtml and commits the resulting
289 * HTML and images.
290 *
291 * @param dir
292 * @param htmlfile
293 * @param wordfile
294 */
295 function importWordFile_wvHtml($dir,$htmlfile,$wordfile)
296 {
297 $tmphtmlfile = "importWord.html";
298 if (($tmpdir = System::mktemp('-d')) == false) {
299 $this->info("Error : could not create temporary directory!");
300 return false;
301 }
302
303 if ($this->cmdExec("wvHtml --targetdir=".escapeshellarg($tmpdir).
304 " --charset=iso-8859-15 ".
305 escapeshellarg($wordfile)." ".escapeshellarg($tmphtmlfile)))
306 {
307 $this->info("Error : wvHtml returned an error!");
308 $this->info($this->cmdStatus());
309 return false;
310 }
311
312 if (!$dh = opendir($tmpdir)) {
313 $this->info("Error : could not find temporary directory '$tmpdir'!");
314 return false;
315 }
316
317 // process the files generated by wvHtml
318 $ok = true;
319 while (($myentry = readdir($dh)) != false) {
320 if (is_file($myfile = "$tmpdir/$myentry")) {
321 if ($myentry == $tmphtmlfile) {
322 $ok = $ok &&
323 $this->commit($dir,$htmlfile,
324 $this->importHtmlString(file_get_contents($myfile)),
325 "Word file import");
326 } else {
327 $ok = $ok &&
328 $this->commit($dir,$myentry,file_get_contents($myfile),
329 "Word file import");
330 }
331 }
332 }
333 closedir($dh);
334
335 return $ok;
336 }
337
338
339 /** Returns raw log entries for an RCS item.
340 *
341 * @param dir parent directory
342 * @param file the RCS entry
343 */
344 function logEntries($dir,$file) {
345 $rfile = $this->rcsFile($dir,$file);
346 $this->cmdExec("rlog ".escapeshellarg($rfile));
347 return $this->cmd_output;
348 }
349
350
351 /** Parse the log entries for an RCS item into an array.
352 *
353 * @param dir parent directory
354 * @param file the RCS entry
355 */
356 function logParse($dir,$file)
357 {
358 // get the log, drop last 2 lines
359 $lines = $this->logEntries($dir,$file);
360 array_pop($lines);
361 array_pop($lines);
362
363 // split into revision, drop first block
364 $revs = split("----------------------------\n", implode("\n",$lines));
365 array_shift($revs);
366
367 // parse info about the revisions
368 $revinfo = array();
369 foreach ($revs as $rev) {
370 $myrev = array();
371 $lines = explode("\n",$rev);
372 preg_match("/^revision (.+)$/",array_shift($lines),$res);
373 $myrev['rev'] = $res[1];
374 preg_match("/^date: ([^;]+); author: ([^;]+); .*$/",array_shift($lines),$res);
375 $myrev['date'] = $res[1];
376 $myrev['author'] = $res[2];
377 $myrev['log'] = implode("\n",$lines);
378 array_push($revinfo,$myrev);
379 }
380 return $revinfo;
381 }
382
383
384 /** Move an RCS item to a given location.
385 *
386 * @param sdir the source directory
387 * @param sfile the source RCS entry
388 * @param ddir the destination directory
389 * @param dfile the destination RCS entry
390 */
391 function move($sdir,$sfile,$ddir,$dfile)
392 {
393 $this->info("RCS : moving '$sfile' to '$ddir/$dfile'..");
394
395
396 // check source files
397 $spath = $this->spoolPath($sdir, $sfile);
398 $srpath = $this->rcsFile($sdir, $sfile);
399
400 if (!is_file($spath)) {
401 $this->info("Error: source file '$spath' does not exist!");
402 return false;
403 }
404
405 if (!is_file($srpath)) {
406 $this->info("Error: source RCS file '$srpath' does not exist!");
407 return false;
408 }
409
410 // check destination
411 $dpath = $this->spoolPath($ddir, $dfile);
412 $drpath = $this->rcsFile($ddir, $dfile);
413
414 if (!$this->checkDir($ddir))
415 {
416 $this->info("Error: directory '$ddir' does not exist!");
417 return false;
418 }
419
420 if (file_exists($dpath)) {
421 $this->info("Error: file '$dfile' already exists in '$ddir'!");
422 return false;
423 }
424
425 if (file_exists($drpath)) {
426 $this->info("Error: file '".basename($drpath)."' already exists in '$ddir'!");
427 return false;
428 }
429
430 if (!rename($spath, $dpath))
431 {
432 $this->info("Error: failed to move '".basename($spath)."' to '".basename($dpath)."' in '$ddir'!");
433 return false;
434 }
435
436 if (!rename($srpath, $drpath))
437 {
438 $this->info("Error: failed to move '".basename($srpath)."' to '".basename($drpath)."' in '$ddir'!");
439 return false;
440 }
441
442
443 //$this->log("rcs_move","{$this->alias}:$dir/$file");
444
445 return true;
446 }
447
448
449 /** Add a new RCS-managed directory.
450 *
451 * @param parent the parent directory
452 * @param dir the directory to add
453 */
454 function newdir($parent,$dir)
455 {
456 @mkdir($this->rcsPath($parent,$dir),0700);
457 @mkdir($this->spoolPath($parent,$dir),0700);
458 }
459
460
461 /** Retrieve differences between two version of a file and prepare for output.
462 *
463 * @param dir parent directory
464 * @param file the RCS entry
465 * @param r1 the first revision
466 * @param r2 the second revision
467 */
468 function dispDiff($dir,$file,$r1,$r2)
469 {
470 $lns = "[0-9]+|[0-9]+,[0-9]+";
471
472 // get diff, strip any leading comments
473 $lines = $this->diff($dir,$file,$r1,$r2);
474 $line = "";
475 while (!preg_match("/^($lns)([acd])($lns)/",$line))
476 $line = array_shift($lines);
477 array_unshift($lines,$line);
478 $raw = implode("\n",$lines);
479
480 $blocks = preg_split("/($lns)([acd])($lns)\n/",$raw,-1,PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
481 $out = array();
482
483 $lold = array_shift($blocks);
484 while ($lold!='')
485 {
486 $type = array_shift($blocks);
487 $lnew = array_shift($blocks);
488 $diff = array_shift($blocks);
489
490 switch ($type) {
491 case 'c':
492 list($a,$b) = split("---\n",$diff,2);
493 break;
494 case 'a':
495 $a = "";
496 $b = $diff;
497 break;
498 case 'd':
499 $a = $diff;
500 $b = "";
501 break;
502 }
503 array_push($out,array($lold,$type,$lnew,$a,$b));
504 $lold = array_shift($blocks);
505 }
506 //array_push($out,array($type,$a,$b));
507 return $out;
508 }
509
510
511 /** Return an RCS-managed directory.
512 *
513 * @param dir the directory
514 * @param loc the visible location for the directory
515 * @param canedit can we edit files in this directory?
516 */
517 function dispDir($dir,$loc,$canedit) {
518 $entries = array();
519
520 if ($pdir = @opendir($this->rcsPath($dir))) {
521 while ( ($file = readdir($pdir)) !== false) {
522 if ( ($file != ".") && ($file != "..") )
523 {
524 $entry = $this->dispEntry($dir,$loc,$file,$canedit);
525 if (!empty($entry))
526 array_push($entries, $entry);
527 }
528 }
529 closedir($pdir);
530 }
531 return $entries;
532 }
533
534
535 /** Returns an RCS "item" (file or directory).
536 *
537 * @param dir parent directory
538 * @param loc visible location for parent directory
539 * @param file the RCS "item"
540 * @param canedit can we edit files this entry?
541 */
542 function dispEntry($dir,$loc,$file,$canedit) {
543 global $globals;
544
545 $view = $edit = $del = $size = $rev = "";
546
547 $myitem = $this->rcsPath($dir,$file);
548
549 // check the RCS entry exists
550 if (!file_exists($myitem)) {
551 $this->info("RCS entry '$myitem' does not exist!");
552 return;
553 }
554
555 if (is_dir($myitem))
556 {
557 // this is a directory, this should not happen!
558 $this->info("Unexpected directory in RCS, skipping : '$myitem'");
559 return;
560 }
561 else if (substr($file,-2) == ",v")
562 {
563 // this is an RCS file
564 $file = substr($file,0,-2);
565
566 // check we have a working copy of this item
567 $spoolitem = $this->spoolPath($dir,$file);
568 if (!is_file($spoolitem))
569 {
570 $this->info("Could not find working copy '$spoolitem'!");
571 $size = "";
572 $icon = "";
573 } else {
574 $size = $this->dispSize(filesize($spoolitem));
575 $icon = $globals->icons->get_mime_icon($spoolitem);
576 }
577
578 // revision info
579 $myrev = array_shift($tmparr = $this->logParse($dir,$file));
580
581 return array(
582 "icon" => $icon,
583 "file" => $file,
584 "rev" => array($myrev['rev'],$rev),
585 "date" => $myrev['date'],
586 "author" => $myrev['author'],
587 "size" => $size
588 );
589
590 }
591 else
592 {
593 $this->info("Unknown RCS entry type : '$myitem'");
594 return;
595 }
596
597 }
598
599
600 /** Format a file size for display.
601 *
602 * @param size the size, in bytes
603 */
604 function dispSize($size)
605 {
606 if ($size < 1000)
607 return "$size B";
608 else if ($size < 1000000)
609 return floor($size/1000)." kB";
610 else
611 return floor($size/1000000)." MB";
612 }
613
614 }
615
616 ?>