Rev Author Line No. Line
185 miho 1 <?php
2 // WebSVN - Subversion repository viewing via the web using PHP
4988 kaklik 3 // Copyright (C) 2004-2006 Tim Armes
185 miho 4 //
5 // This program is free software; you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation; either version 2 of the License, or
8 // (at your option) any later version.
9 //
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
4988 kaklik 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
185 miho 13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with this program; if not, write to the Free Software
4988 kaklik 17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
185 miho 18 //
19 // --
20 //
21 // comp.php
22 //
4988 kaklik 23 // Compare two paths using `svn diff`
185 miho 24 //
25  
4988 kaklik 26 require_once 'include/setup.php';
27 require_once 'include/svnlook.php';
28 require_once 'include/utils.php';
29 require_once 'include/template.php';
185 miho 30  
4988 kaklik 31 function checkRevision($rev)
32 {
33 if (is_numeric($rev) && ((int)$rev > 0))
34 {
35 return $rev;
36 }
37 return 'HEAD';
38 }
185 miho 39  
4988 kaklik 40 // Make sure that we have a repository
41 if (!$rep)
185 miho 42 {
4988 kaklik 43 renderTemplate404('compare','NOREP');
185 miho 44 }
45  
46 $svnrep = new SVNRepository($rep);
47  
48 // Retrieve the request information
4988 kaklik 49 $path1 = @$_REQUEST['compare'][0];
50 $path2 = @$_REQUEST['compare'][1];
51 $rev1 = (int)@$_REQUEST['compare_rev'][0];
52 $rev2 = (int)@$_REQUEST['compare_rev'][1];
53 $manualorder = (@$_REQUEST['manualorder'] == 1);
54 $ignoreWhitespace = $config->getIgnoreWhitespacesInDiff();
185 miho 55  
4988 kaklik 56 if (array_key_exists('ignorews', $_REQUEST))
57 {
58 $ignoreWhitespace = (bool)$_REQUEST['ignorews'];
59 }
60  
185 miho 61 // Some page links put the revision with the path...
4988 kaklik 62 if ($path1 && strpos($path1, '@'))
63 {
64 list($path1, $rev1) = explode('@', $path1);
65 }
66 else if ($path1 && (strpos($path1, '@') === 0))
67 {
68 // Something went wrong. The path is missing.
69 $rev1 = substr($path1, 1);
70 $path1 = '/';
71 }
185 miho 72  
4988 kaklik 73 if ($path2 && strpos($path2, '@'))
74 {
75 list($path2, $rev2) = explode('@', $path2);
76 }
77 else if ($path2 && (strpos($path2, '@') === 0))
78 {
79 $rev2 = substr($path2, 1);
80 $path2 = '/';
81 }
82  
185 miho 83 $rev1 = checkRevision($rev1);
84 $rev2 = checkRevision($rev2);
85  
86 // Choose a sensible comparison order unless told not to
4988 kaklik 87  
88 if (!$manualorder && is_numeric($rev1) && is_numeric($rev2) && $rev1 > $rev2)
185 miho 89 {
4988 kaklik 90 $temppath = $path1;
91 $path1 = $path2;
92 $path2 = $temppath;
93  
94 $temprev = $rev1;
95 $rev1 = $rev2;
96 $rev2 = $temprev;
185 miho 97 }
98  
4988 kaklik 99 $vars['rev1url'] = $config->getURL($rep, $path1, 'dir').createRevAndPegString($rev1, $rev1);
100 $vars['rev2url'] = $config->getURL($rep, $path2, 'dir').createRevAndPegString($rev2, $rev2);
185 miho 101  
4988 kaklik 102 $url = $config->getURL($rep, '', 'comp');
103 $vars['reverselink'] = '<a href="'.$url.'compare%5B%5D='.rawurlencode($path2 ?? '').'@'.$rev2.'&amp;compare%5B%5D='.rawurlencode($path1 ?? '').'@'.$rev1.'&amp;manualorder=1'.($ignoreWhitespace ? '&amp;ignorews=1' : '').'">'.$lang['REVCOMP'].'</a>';
104 $toggleIgnoreWhitespace = '';
185 miho 105  
4988 kaklik 106 if ($ignoreWhitespace == $config->getIgnoreWhitespacesInDiff())
107 {
108 $toggleIgnoreWhitespace = '&amp;ignorews='.($ignoreWhitespace ? '0' : '1');
109 }
185 miho 110  
4988 kaklik 111 if (!$ignoreWhitespace)
112 {
113 $vars['ignorewhitespacelink'] = '<a href="'.$url.'compare%5B%5D='.rawurlencode($path1 ?? '').'@'.$rev1.'&amp;compare%5B%5D='.rawurlencode($path2 ?? '').'@'.$rev2.($manualorder ? '&amp;manualorder=1' : '').$toggleIgnoreWhitespace.'">'.$lang['IGNOREWHITESPACE'].'</a>';
114 }
115 else
116 {
117 $vars['regardwhitespacelink'] = '<a href="'.$url.'compare%5B%5D='.rawurlencode($path1 ?? '').'@'.$rev1.'&amp;compare%5B%5D='.rawurlencode($path2 ?? '').'@'.$rev2.($manualorder ? '&amp;manualorder=1' : '').$toggleIgnoreWhitespace.'">'.$lang['REGARDWHITESPACE'].'</a>';
118 }
185 miho 119  
4988 kaklik 120 if ($rev1 == 0) $rev1 = 'HEAD';
121 if ($rev2 == 0) $rev2 = 'HEAD';
185 miho 122  
4988 kaklik 123 $vars['repname'] = escape($rep->getDisplayName());
124 $vars['action'] = $lang['PATHCOMPARISON'];
125  
126 $hidden = '<input type="hidden" name="manualorder" value="1" />';
127  
128 if ($config->multiViews)
129 {
130 $hidden .= '<input type="hidden" name="op" value="comp"/>';
131 }
132 else
133 {
134 $hidden .= '<input type="hidden" name="repname" value="'.$repname.'" />';
135 }
136  
137 $vars['compare_form'] = '<form method="get" action="'.$url.'" id="compare">'.$hidden;
138 $vars['compare_path1input'] = '<input type="text" size="40" name="compare[0]" value="'.escape($path1).'" />';
139 $vars['compare_path2input'] = '<input type="text" size="40" name="compare[1]" value="'.escape($path2).'" />';
140 $vars['compare_rev1input'] = '<input type="text" size="5" name="compare_rev[0]" value="'.$rev1.'" />';
141 $vars['compare_rev2input'] = '<input type="text" size="5" name="compare_rev[1]" value="'.$rev2.'" />';
142 $vars['compare_submit'] = '<input name="comparesubmit" type="submit" value="'.$lang['COMPAREPATHS'].'" />';
143 $vars['compare_endform'] = '</form>';
144  
145 $vars['safepath1'] = escape($path1);
146 $vars['safepath2'] = escape($path2);
147  
148 $vars['rev1'] = $rev1;
149 $vars['rev2'] = $rev2;
150  
151 $history1 = $svnrep->getLog($path1, $rev1, 0, false, 1);
152 if (!$history1)
153 {
154 renderTemplate404('compare','NOPATH');
155 }
156 else
157 {
158 $history2 = $svnrep->getLog($path2, $rev2, 0, false, 1);
159  
160 if (!$history2)
161 {
162 renderTemplate404('compare','NOPATH');
163 }
164 }
165  
166 // Set variables used for the more recent of the two revisions
167 $history = ($rev1 >= $rev2 ? $history1 : $history2);
168 if ($history && $history->curEntry)
169 {
170 $logEntry = $history->curEntry;
171 $vars['rev'] = $logEntry->rev;
172 $vars['peg'] = $peg;
173 $vars['date'] = $logEntry->date;
174 $vars['age'] = datetimeFormatDuration(time() - strtotime($logEntry->date));
175 $vars['author'] = $logEntry->author;
176 $vars['log'] = xml_entities($logEntry->msg);
177 }
178 else
179 {
180 $vars['warning'] = 'Problem with comparison.';
181 }
182  
185 miho 183 $noinput = empty($path1) || empty($path2);
184  
185 // Generate the diff listing
186  
4988 kaklik 187 $relativePath1 = $path1;
188 $relativePath2 = $path2;
189  
190 $svnpath1 = encodepath($svnrep->getSvnPath(str_replace(DIRECTORY_SEPARATOR, '/', $path1 ?? '')));
191 $svnpath2 = encodepath($svnrep->getSvnPath(str_replace(DIRECTORY_SEPARATOR, '/', $path2 ?? '')));
192  
185 miho 193 $debug = false;
194  
4988 kaklik 195 if (!$noinput)
185 miho 196 {
4988 kaklik 197 $cmd = $config->getSvnCommand().$rep->svnCredentials().' diff '.($ignoreWhitespace ? '-x "-w --ignore-eol-style" ' : '').quote($svnpath1.'@'.$rev1).' '.quote($svnpath2.'@'.$rev2);
185 miho 198 }
199  
4988 kaklik 200 function clearVars()
185 miho 201 {
4988 kaklik 202 global $ignoreWhitespace, $listing, $index;
185 miho 203  
4988 kaklik 204 if ($ignoreWhitespace && $index > 1)
205 {
206 $endBlock = false;
207 $previous = $index - 1;
208 if ($listing[$previous]['endpath']) $endBlock = 'newpath';
209 else if ($listing[$previous]['enddifflines']) $endBlock = 'difflines';
185 miho 210  
4988 kaklik 211 if ($endBlock !== false)
212 {
213 // check if block ending at previous contains real diff data
214 $i = $previous;
215 $containsOnlyEqualDiff = true;
216 $addedLines = array();
217 $removedLines = array();
218 while ($i >= 0 && !$listing[$i - 1][$endBlock])
219 {
220 $diffclass = $listing[$i - 1]['diffclass'];
221  
222 if ($diffclass !== 'diffadded' && $diffclass !== 'diffdeleted')
223 {
224 if ($addedLines !== $removedLines)
225 {
226 $containsOnlyEqualDiff = false;
227 break;
228 }
229 }
230  
231 if (count($addedLines) > 0 && $addedLines === $removedLines)
232 {
233 $addedLines = array();
234 $removedLines = array();
235 }
236  
237 if ($diffclass === 'diff')
238 {
239 $i--;
240 continue;
241 }
242  
243 if ($diffclass === null)
244 {
245 $containsOnlyEqualDiff = false;
246 break;;
247 }
248  
249 if ($diffclass === 'diffdeleted')
250 {
251 if (count($addedLines) <= count($removedLines))
252 {
253 $containsOnlyEqualDiff = false;
254 break;;
255 }
256  
257 array_unshift($removedLines, $listing[$i - 1]['line']);
258 $i--;
259 continue;
260 }
261  
262 if ($diffclass === 'diffadded')
263 {
264 if (count($removedLines) > 0)
265 {
266 $containsOnlyEqualDiff = false;
267 break;;
268 }
269  
270 array_unshift($addedLines, $listing[$i - 1]['line']);
271 $i--;
272 continue;
273 }
274  
275 assert(false);
276 }
277  
278 if ($containsOnlyEqualDiff)
279 {
280 $containsOnlyEqualDiff = $addedLines === $removedLines;
281 }
282  
283 // remove blocks which only contain diffclass=diff and equal removes and adds
284 if ($containsOnlyEqualDiff)
285 {
286 for ($j = $i - 1; $j < $index; $j++)
287 {
288 unset($listing[$j]);
289 }
290  
291 $index = $i - 1;
292 }
293 }
294 }
295  
296 $listvar = &$listing[$index];
297 $listvar['newpath'] = null;
298 $listvar['endpath'] = null;
299 $listvar['info'] = null;
300 $listvar['diffclass'] = null;
301 $listvar['difflines'] = null;
302 $listvar['enddifflines'] = null;
303 $listvar['properties'] = null;
185 miho 304 }
305  
4988 kaklik 306 $vars['success'] = false;
185 miho 307  
4988 kaklik 308 if (!$noinput)
309 {
310 // TODO: Report warning/error if comparison encounters any problems
311 if ($diff = popenCommand($cmd, 'r'))
312 {
313 $listing = array();
314 $index = 0;
315 $indiff = false;
316 $indiffproper = false;
317 $getLine = true;
318 $node = null;
319 $bufferedLine = false;
185 miho 320  
4988 kaklik 321 $vars['success'] = true;
322  
323 while (!feof($diff))
324 {
325 if ($getLine)
326 {
327 if ($bufferedLine === false)
328 {
329 $bufferedLine = rtrim(fgets($diff), "\n\r");
330 }
331  
332 $newlineR = strpos($bufferedLine, "\r");
333 $newlineN = strpos($bufferedLine, "\n");
334 if ($newlineR === false && $newlineN === false)
335 {
336 $line = $bufferedLine;
337 $bufferedLine = false;
338 }
339 else
340 {
341 $newline = ($newlineR < $newlineN ? $newlineR : $newlineN);
342 $line = substr($bufferedLine, 0, $newline);
343 $bufferedLine = substr($bufferedLine, $newline + 1);
344 }
345 }
346  
347 clearVars();
348 $getLine = true;
349 if ($debug) print 'Line = "'.$line.'"<br />';
350 if ($indiff)
351 {
352 // If we're in a diff proper, just set up the line
353 if ($indiffproper)
354 {
355 if (strlen($line) > 0 && ($line[0] == ' ' || $line[0] == '+' || $line[0] == '-'))
356 {
357 $subline = escape(toOutputEncoding(substr($line, 1)));
358 $subline = rtrim($subline, "\n\r");
359 $subline = ($subline) ? expandTabs($subline) : '&nbsp;';
360 $listvar = &$listing[$index];
361 $listvar['line'] = $subline;
362  
363 switch ($line[0])
364 {
365 case ' ':
366 $listvar['diffclass'] = 'diff';
367 if ($debug) print 'Including as diff: '.$subline.'<br />';
368 break;
369  
370 case '+':
371 $listvar['diffclass'] = 'diffadded';
372 if ($debug) print 'Including as added: '.$subline.'<br />';
373 break;
374  
375 case '-':
376 $listvar['diffclass'] = 'diffdeleted';
377 if ($debug) print 'Including as removed: '.$subline.'<br />';
378 break;
379 }
380 $index++;
381 }
382 else if ($line != '\ No newline at end of file')
383 {
384 $indiffproper = false;
385 $listing[$index++]['enddifflines'] = true;
386 $getLine = false;
387 if ($debug) print 'Ending lines<br />';
388 }
389 continue;
390 }
391  
392 // Check for the start of a new diff area
393 if (!strncmp($line, '@@', 2))
394 {
395 $pos = strpos($line, '+');
396 $posline = substr($line, $pos);
397 $sline = 0;
398 $eline = 0;
399 sscanf($posline, '+%d,%d', $sline, $eline);
400  
401 if ($debug) print 'sline = "'.$sline.'", eline = "'.$eline.'"<br />';
402  
403 // Check that this isn't a file deletion
404 if ($sline == 0 && $eline == 0)
405 {
406 $line = fgets($diff);
407 if ($debug) print 'Ignoring: "'.$line.'"<br />';
408  
409 while ($line && ($line[0] == ' ' || $line[0] == '+' || $line[0] == '-'))
410 {
411 $line = fgets($diff);
412 if ($debug) print 'Ignoring: "'.$line.'"<br />';
413 }
414  
415 $getLine = false;
416 if ($debug) print 'Unignoring previous - marking as deleted<br />';
417 $listing[$index++]['info'] = $lang['FILEDELETED'];
418  
419 }
420 else
421 {
422 $listvar = &$listing[$index];
423 $listvar['difflines'] = $line;
424 $sline = 0;
425 $slen = 0;
426 $eline = 0;
427 $elen = 0;
428 sscanf($line, '@@ -%d,%d +%d,%d @@', $sline, $slen, $eline, $elen);
429 $listvar['rev1line'] = $sline;
430 $listvar['rev1len'] = $slen;
431 $listvar['rev2line'] = $eline;
432 $listvar['rev2len'] = $elen;
433  
434 $indiffproper = true;
435  
436 $index++;
437 }
438  
439 continue;
440  
441 }
442 else
443 {
444 $indiff = false;
445 if ($debug) print 'Ending diff';
446 }
447 }
448  
449 // Check for a new node entry
450 if (strncmp(trim($line), 'Index: ', 7) == 0)
451 {
452 // End the current node
453 if ($node)
454 {
455 $listing[$index++]['endpath'] = true;
456 clearVars();
457 }
458  
459 $node = trim(toOutputEncoding($line));
460 $node = substr($node, 7);
461 if ($node == '' || $node[0] != '/') $node = '/'.$node;
462  
463 if (substr($path2, -strlen($node)) === $node)
464 {
465 $absnode = $path2;
466 }
467 else
468 {
469 $absnode = $path2;
470 if (substr($absnode, -1) == '/') $absnode = substr($absnode, 0, -1);
471 $absnode .= $node;
472 }
473  
474 $listvar = &$listing[$index];
475 $listvar['newpath'] = escape($absnode);
476  
477 $listvar['fileurl'] = $config->getURL($rep, $absnode, 'file').'rev='.$rev2;
478  
479 if ($debug) echo 'Creating node '.$node.'<br />';
480  
481 // Skip past the line of ='s
482 $line = fgets($diff);
483 if ($debug) print 'Skipping: '.$line.'<br />';
484  
485 // Check for a file addition
486 $line = fgets($diff);
487 if ($debug) print 'Examining: '.$line.'<br />';
488 if (strpos($line, '(revision 0)'))
489 {
490 $listvar['info'] = $lang['FILEADDED'];
491 }
492  
493 if (strncmp(trim($line), 'Cannot display:', 15) == 0)
494 {
495 $index++;
496 clearVars();
497 $listing[$index++]['info'] = escape(toOutputEncoding(rtrim($line, "\n\r")));
498 continue;
499 }
500  
501 // Skip second file info
502 $line = fgets($diff);
503 if ($debug) print 'Skipping: '.$line.'<br />';
504  
505 $indiff = true;
506 $index++;
507  
508 continue;
509 }
510  
511 if (strncmp(trim($line), 'Property changes on: ', 21) == 0)
512 {
513 $propnode = trim($line);
514 $propnode = substr($propnode, 21);
515 if ($propnode == '' || $propnode[0] != '/') $propnode = '/'.$propnode;
516  
517 if ($debug) print 'Properties on '.$propnode.' (cur node $ '.$node.')';
518 if ($propnode != $node)
519 {
520 if ($node)
521 {
522 $listing[$index++]['endpath'] = true;
523 clearVars();
524 }
525  
526 $node = $propnode;
527  
528 $listing[$index++]['newpath'] = escape(toOutputEncoding($node));
529 clearVars();
530 }
531  
532 $listing[$index++]['properties'] = true;
533 clearVars();
534 if ($debug) echo 'Creating node '.$node.'<br />';
535  
536 // Skip the row of underscores
537 $line = fgets($diff);
538 if ($debug) print 'Skipping: '.$line.'<br />';
539  
540 while ($line = rtrim(fgets($diff), "\n\r"))
541 {
542 if (!strncmp(trim($line), 'Index: ', 7))
543 {
544 break;
545 }
546 if (!strncmp(trim($line), '##', 2) || $line == '\ No newline at end of file')
547 {
548 continue;
549 }
550 $listing[$index++]['info'] = escape(toOutputEncoding($line));
551 clearVars();
552 }
553 $getLine = false;
554  
555 continue;
556 }
557  
558 // Check for error messages
559 if (strncmp(trim($line), 'svn: ', 5) == 0)
560 {
561 $listing[$index++]['info'] = urldecode($line);
562 $vars['success'] = false;
563 continue;
564 }
565  
566 $listing[$index++]['info'] = escape(toOutputEncoding($line));
567  
568 if (strlen($line) === 0)
569 {
570 if (!isset($vars['warning']))
571 {
572 $vars['warning'] = "No changes between revisions";
573 }
574 }
575  
576 }
577  
578 if ($node)
579 {
580 clearVars();
581 $listing[$index++]['endpath'] = true;
582 }
583  
584 if ($debug) print_r($listing);
585  
586 if (!$rep->hasUnrestrictedReadAccess($relativePath1) || !$rep->hasUnrestrictedReadAccess($relativePath2, false))
587 {
588 // check every item for access and remove it if read access is not allowed
589 $restricted = array();
590 $inrestricted = false;
591 foreach ($listing as $i => $item)
592 {
593 if ($item['newpath'] !== null)
594 {
595 $newpath = $item['newpath'];
596 $inrestricted = !$rep->hasReadAccess($newpath, false);
597 }
598  
599 if ($inrestricted)
600 {
601 $restricted[] = $i;
602 }
603  
604 if ($item['endpath'] !== null)
605 {
606 $inrestricted = false;
607 }
608 }
609  
610 foreach ($restricted as $i)
611 {
612 unset($listing[$i]);
613 }
614  
615 if (count($restricted) && !count($listing))
616 {
617 $vars['error'] = $lang['NOACCESS'];
618 sendHeaderForbidden();
619 }
620 }
621  
622 pclose($diff);
623 }
624 }
625  
626 renderTemplate('compare');