Zum letzten Mal vorgestellten Modul SDiff.pm
kommt heute
das bislang noch fehlende CGI-Skript hd
hinzu, das die Unterschiede
zwischen Dateiversionen grafisch im Browser darstellt.
Um zwei Dateien im Browser Seite an Seite mittels des
heute vorgestellten Skripts zu vergleichen,
füllt der Benutzer zunächst nur ein paar Formfelder des CGIs aus,
drückt den View-Knopf und schon kalkuliert das Skript im
lokalen
Webserver die
Dateiunterschiede und bringt die farblich unterlegte Darstellung im
Browser hoch.
hd
kennt aber nicht nur normale Dateien, sondern kann auch
mit dem Versionskontrollsystem CVS umgehen.
Wer CVS noch nicht kennt, dem sei es auch für kleinere Softwareprojekte wärmstens ans Herz gelegt: Man kann damit ganze Projektbäume verwalten, mit mehreren Leuten daran arbeiten und trotzdem das Projekt konsistent halten. Geht einem Teammitglied eine Datei oder der ganze Baum verloren, kann man beliebige vorher eingecheckte Versionen wieder hervorzaubern, man sagt einfach: ``Wie sah das Projekt letzten Montag um 9:00 aus?'' und schon zaubert CVS den Stand 100%ig wieder her. Editieren zwei Kollegen versehentlich die gleiche Datei, kann cvs den Konflikt meist automatisch (!) wieder bereinigen. Ein Wahnsinnsteil -- und auch noch frei unter [3] verfügbar. Ein Industriestandard. Das Standardwerk [2] zeigt genau, wie's geht.
Der grafische Dateienvergleicher hd
(in Erweiterung zu [4])
drei verschiedene Eingabemodi:
/pfad/datei1
und /pfad/datei2
(Eingabe Abbildung 1, Ausgabe Abbildung 2).
Pfad zu einer lokalen Datei in einem CVS-Arbeitsverzeichnis
und das CVS-Wurzelverzeichnis
zum Vergleich mit der eingecheckten Version:
CVSROOT, /pfad/arbeitskopie
(Eingabe Abbildung 3, Ausgabe Abbildung 4).
Pfad zu einer Datei relativ zur CVS-Wurzel und zwei
CVS-Versionen dieser Datei, CVSROOT, modul/pfad/datei
, beide
Revisionsnummern (Eingabe siehe Abbildung 5).
Jeder Eingabemodus verfügt der einfachen Bedienbarkeit wegen über eine eigene Eingabeseite, zwischen denen der Benutzer durch Mausklicks auf die Kopfzeilen-Links wechseln kann.
Abbildung 1: Einstellung zum Vergleich zweier Dateien |
Abbildung 2: Einstellung zum Vergleich zweier Dateien |
Abbildung 1 zeigt, wie hd
nach der Installation
und dem Aufruf über den Browser (z.B. als http://localhost/cgi/hd )
im Modus Compare Two Files steht und zwei Dateinamen samt
den zugehörigen Pfaden entgegennimmt. Weiter lässt sich ein
Kommentar angeben, den das Skript in der Ausgabe dann als
Überschrift ausgibt. Und zwei Optionen als anklickbare
Druckknöpfe gibt's noch: stripes, die auch jede zweite
unveränderte Zeile
zur besseren Sichtbarkeit in einem leichten Grau unterlegt (Abbildung 4)
und context, die die beiden Dateien nicht in voller Länge anzeigt,
sondern nur großzügige Fenster um die tatsächlich veränderten Zeilen
(auch Abbildung 4).
Wie Abbildung 2 zeigt, unterlegt das Skript eingefügte oder
gelöschte Zeilen hellgrün und Zeilen, die sich änderten, blau.
Abbildung 3: Eine Datei mit der eingecheckten Version vergleichen |
Abbildung 4: Kontextanzeige |
Am häufigsten tritt die Situation für ein Code-Review ein, bevor ein
Entwickler eine lokal geänderte Datei zurück in den CVS-Projektbaum
stellt. Einmal eingecheckt, beeinflussen etwaige Fehler
das gesamte Entwicklungsteam und deshalb zahlt es sich oft aus, zu diesem
Zeitpunkt ein zweites Augenpaar darauf anzusetzen.
Der Entwickler gibt hierzu im Modus Compare before Check-in (Abbildung 3)
einfach den Pfad zur lokalen Arbeitskopie ein, wählt über die
Auswahlbox die CVS-Wurzel aus, klickt die Anzeigeoptionen nach Wahl und
dann den View-Button. hd
findet nun selbständig heraus, was die letzte
eingecheckte Version der Datei im CVS ist, holt diese hervor, vergleicht
sie mit der lokalen Version und zeigt die Veränderungen an.
Auch zwei eingecheckte Versionen einer Datei kann das Skript vergleichen (Abbildung 5). Im Modus Compare Two CVS Revisions wählt man hierzu zunächst die CVS-Wurzel aus. Ist sie nicht in der Auswahlbox vorhanden, kann man auch einen selbstdefinierten Pfad unter Alternative CVS Root angeben. Im Feld Path to File within CVS gibt man den Pfad zur Datei, ausgehend vom Modulnamen in CVS an und die Felder Rev1 und Rev2 nehmen die beiden zu vergleichenden Versionen (z.B. 1.11 und 1.12) entgegen.
Die meisten Entwickler arbeiten immer nur mit einem CVS-System (das
selbstverständlich beliebig viele Projekte enthalten kann), aber
hd
erlaubt es, mehrere anzugeben. Dabei muss das CVS-System
gar nicht mal auf dem gleichen Server wie hd
laufen. Typischerweise
erwartet CVS die Wurzel des CVS-Systems in der Environmentvariablen
CVSROOT
.
Diese kann entweder, wie in
CVSROOT=/path/to/cvs
auf ein CVS auf der lokalen Festplatte verweisen oder über
CVSROOT=:pserver:mschilli@east.bla.com:/data/cvs
auf einen auf east.bla.com
liegenden Server, auf dem der
Benutzer mschilli
Zugriffsrechte besitzt und auf dem unter
/data/cvs
eine CVS-Wurzel installiert ist.
Da sich diese Werte nur äußerst selten ändern, erlaubt es hd
dem Installierer, den Perl-Code mit den aktuellen CVS-Wurzeln
anzupassen, worauf hd
sie unter sprechenden Namen
in einer Auswahlbox anbietet.
Gibt der Benutzer jedoch eine Alternative CVS Root in das
Formularfeld darunter ein, überschreibt dies den in der Auswahlbox
eingestellten Wert.
Der Zugriff aufs CVS erfolgt über das Programm cvs
, das der Entwickler
normalerweise einfach von der Kommandozeile aus startet. Im CGI-Skript
verwenden wir Perls open()
-Befehl mit der Pipe-Syntax, um Daten
für den Dateivergleich mittels cvs
aus dem CVS zu extrahieren.
CVS
erwartet, dass die Environmentvariable CVSROOT
die
Wurzel des verwendeten CVS-Systems (lokal oder remote) festlegt.
Alternativ kann man CVSROOT
dem Programm cvs
auch mit dem
Parameter -d
zustecken:
$ cvs -d /cvswurzel co \ -r 1.18 -p modul/pfad/datei
extrahiert (co
steht für check out)
die Version 1.18
einer Datei datei
aus
dem Modul (oder Projekt) modul
unter dem Unterverzeichnis pfad
und
gibt deren Inhalt wegen der Option -p
auf der Standardausgabe aus.
Will der Benutzer die lokale Version einer Datei mit der letzten
im aktuellen Projektzweig eingecheckten vergleichen, müssen wir zuerst
ermitteln, welche Version denn die zuletzt eingecheckte ist.
Das ist oft gar nicht trivial, denn CVS erlaubt es Entwicklern, auf
Projektzweigen zu arbeiten, sodass für verschiedene Leute unter
Umständen verschiedene Versionen einer Datei aktuell sind.
Steht der Benutzer im Arbeitsverzeichnis, kann CVS sie
aber ohne Angabe von CVSROOT ermitteln:
$ cd /arbeits/verzeichnis $ cvs status datei.c
File: datei.c Status: Locally Modified Working revision: 1.8 Fri Jan 25 07:18:14 2002 Repository revision: 1.8 /home/mschilli/CVSROOT/ix/95/t.pnd,v
Eingecheckt ist also die Version 1.8
, die mit dem weiter
oben definierten cvs co
-Kommando leicht zu extrahieren ist.
Abbildung 5: Einstellung zum Vergleich zweier CVS-Versionen des Moduls C |
Listing hd
zeigt die Implementierung des CGI-Skripts.
Die Zeilen 10-15 ziehen die benötigten Zusatzmodule herein. Aus
dem letzten Mal vorgestellten SDiff.pm
exportieren wir die
sdiff()
-Funktion, aus dem für CGI-Skripts unentbehrlichen
CGI.pm
so ziemlich alles, einschließlich recht exotischer
Funktionen wie start_font()
und end_pre()
und aus
CGI::Carp
das Tag fatalsToBrowser
, das das Skript anweist, bei
mit die()
ausgelösten Fehlern nicht einfach aufzugeben, sondern
noch eine brauchbare Fehlermeldung für den Browser auszugeben, sodass
dieser statt "500-Server Error"
tatsächlich etwas
der Fehlersuche dienliches ausgibt. Den Schönheitspreis kriegt diesen
Monat wer anderer, Hauptsache ist, es funktioniert.
File::Basename
stellt Funktionen wie basename()
und dirname()
zur praktischen Pfadmanipulation zur Verfügung, Set::IntSpan
verwaltet,
wie letztes Mal schon besprochen, elegant
Integer-Bereiche (z.B. ``1-4,9-12,17-20'') und Text::Wrap
schließlich
bricht Zeilen schön an Wortgrenzen um. Wie immer ist das CPAN die
halbe Miete, man muss es nur zu nutzen wissen.
Zeile 17 setzt $|
auf einen wahren Wert und entpuffert damit die
Standardausgabe -- zum Debuggen eines CGI-Skripts immer eine gute Idee.
Zeile 19 gibt den Pfad zum cvs
-Programm an, das den meisten
Unix-Installationen von Haus aus beiliegt. Wer's nicht hat und es nicht
nutzen will, braucht sich nicht zu sorgen, kann es so stehen lassen
und nur lokale Dateien vergleichen.
In Zeile 20 definiert der Array @CVSROOT
die Anzeige der
Auswahlbox für die CVS-Wurzelverzeichnisse. Mehr dazu im Abschnitt
Installation.
Die Zeilen 30 bis 37 definieren Farben, in denen der Webbrowser
verschiedene Zustände anzeigt, entweder als String (z.B. ``lightgreen''
oder als Hex-RGB-Zahl (z.B. #c0c0c0
). Dort wird ersichtlich, dass
Einfügungen und Löschungen mit der gleichen Farbe ("lightgreen"
),
dargestellt werden -- wer das anders will, kann es gerne ändern.
@MODES
in Zeile 38 ist ein Array mit zweielementigen Unterarrays,
die den drei Modi-Kürzeln file
, cvs
und chi
die entsprechenden
Beschreibungen zuordnen.
$CONTEXT
in Zeile 42 gibt mit dem Wert 5 an, dass wir im context
-Modus
später 5 Zeilen vor und hinter jedem Fenster mit Codeveränderungen sehen
wollen.
$MAX_LINE_LEN
in Zeile 43 definiert die angestrebte Spaltenbreite,
nach der ein Zeilenumbruch erfolgen soll.
Zeile 45 gibt den Header für das CGI-Skript aus -- lebensnotwendig,
sonst leitet der Server einen 500er-Fehler an den Browser weiter.
Anschließend wird in Zeile 47 geprüft, ob wir entweder den Query-Parameter
cfg
erhielten oder ob der Parameter mode
nicht gesetzt ist,
der (wie oben besprochen) einen von drei verschiedenen Modi selektiert,
in denen hd
operiert.
In beiden Fällen stellt hd
kein Ergebnis dar, sondern fordert
den Benutzer dazu auf, Dateien/Versionen zu spezifizieren, die er
später vergleichen will. Die ab Zeile 277 definierte
Funktion config_page()
zeigt entsprechend dem einstellten Modus
entsprechenden Formularfelder an und verlangt Eingaben.
Das CGI-Modul extrahiert vom Benutzer eingegebene/selektierte
und vom Browser übermittelte Parameter einfach praktisch mittels
param(name)
.
Zeile 56 legt die CVS-Wurzel entweder als in der Auswahlbox spezifizierten
Eintrag oder als im Feld ``Alternative CVS Root'' festgelegten Wert
in der Variablen $cvsroot
ab. Eine alternativ festgelegte Wurzel
überstimmt die Auswahlbox.
Das If-Else-Konstrukt zwischen den Zeilen 59 und 96 ermittelt für die
drei Fälle Dateivergleich (file
), Eincheckkontrolle (chi
)
und CVS-Vergleich cvs
die folgenden Werte:
$left
weist auf ein SDiff
-Array (letztes Mal vorgestellt)
der ersten Datei/Revision, die
später in der linken Spalte stehen wird.
$right
weist auf ein SDiff
-Array der zweiten/rechten Datei/Revision.
$h1
und $h2
enthalten die Spaltenüberschriften der linken
und der rechten Datei.
In den drei verschiedenen Modi kommen außerdem die folgenden Parameter vom Benutzer über den Browser herein:
mode
=file f1
(Datei1), f2
(Datei2)
Eincheckkontrolle: mode
=chi,
path
(Pfad zur lokalen Arbeitskopie)
CVS-Vergleich: mode
=cvs,
path
(module/pfad/datei
innerhalb des CVS),
r1
, r2
(Revisionsnummern der ersten und zweiten Datei)
Die weiter unten ab Zeile 217
definierte Funktion readfile
ist ein Tausendsassa,
der Dateien/Revisionen einliest und eine Liste mit zwei
Werten zurückgibt: Eine Referenz auf einen Array mit den Zeilen der
Datei und einen Skalar mit der gefundenen Revisionsnummer. Als
Eingabeparameter fungieren flexible Parameterlisten im Format
name
=> wert
. So liest
readfile(file => "datei");
eine normale Datei ein, während
readfile(cvsroot => "...", lpath => "pfad");
die letzte eingecheckte Version einer Datei holt und
readfile(cvsroot => "...", rpath => "proj/dir/datei"); rev => "1.18");
schließlich schnappt sich genau die angegebene Version des Projekts
proj
der Datei dir/datei
aus dem mit cvsroot
angegebenen
CVS.
Den Zeilen des zurückkommenden Arrays hängen noch Zeilenumbrüche
an, mit den machen die chomp
-Befehle in den Zeilen 98 und 99 kurzen
Prozess.
Wie letztes Mal besprochen, braucht die Funktion sdiff()
, die die
Dateiunterschiede ermittelt, ein Set::IntSpan
-Objekt, um die
interessanten Bereiche zu kennzeichnen. Zeile 101 erzeugt dieses und
103 lässt das SDiff
-Modul den Unterschied zwischen den Zeilen
in $left
und $right
ermitteln und in $sdiff
ablegen.
$CONTEXT
gibt die Breite der Codefenster für die Anzeige der
Veränderungen an.
Das if
-Konstrukt ab Zeile 107 korrigiert die Bereichsangaben mit den
Zeilen, die tatsächlich angezeigt werden sollen. Hat die
Datei 100 Zeilen und in @run_list
steht
@run_list = ("20-30", "50-60");
soll die Ausgabe so aussehen:
Skipping lines 1...19 20 ... ... 30 ... Skipping lines 31...49 50 ... ... 60 ... Skipping lines 61...100
falls der Benutzer die Option context
gewählt hat. Falls nicht,
kommen einfach alle Zeilen untereinander daher.
Der Array @run_list
enthält als Elemente einfach alle
Zeilenbereiche im Format anfang-ende
, die die Methode
run_list()
eines Set::IntSpan
-Objects als einzelnen
String und komma-separiert liefert.
Soll statt der Kontext-Anzeige die ganze Datei ungekürzt erscheinen,
erzeugt Zeile 112 einen Zeilenbereich, der die ganze Datei umfasst.
Ab Zeile 115 startet die HTML-Ausgabe, angefangen mit der Definition
der zweispaltigen Ausgabetabelle und gefolgt vom angegebenen Kommentartext
und den Spaltenüberschriften, die Hinweise auf die dort dargestellten
Dateien geben und vorher in $h1
und $h2
festgelegt wurden.
Die while
-Schleife ab Zeile 139 gibt die Dateidifferenzen farblich
unterlegt aus. Die Show läuft, solange entweder noch Zeilen im
SDiff
-Array sind oder Anweisungen in der Run-List. Steht in letzterer
nichts mehr, sind wir an den im SDiff
-Array verbleibenden Zeilen
nicht mehr interessiert. In Zeile 150 nimmt die
ab Zeile 198 definierte Funktion skip()
eine Referenz auf den
Array, den aktuellen Index (Zeilennummer minus 1) und den
Index der letzten zu überspringenden Zeile entgegen
und liefert den neuen Index zurück,
den wir wieder in $cur_idx
ablegen. Im Falle einer leeren Run-List
bricht last
in Zeile 152 jedoch den Ausgabereigen ab.
Zeile 155 behandelt den Fall, dass der gerade zu behandelnde Bereich
aus genau einer Zeile besteht (sollte selten vorkommen).
Falls der aktuelle Index kleiner ist als der von der neuen
Run-List bestimmte Startwert, müssen noch einige Zeile aus $diff
verschwinden und die entsprechende Skipping...
-Meldung erfolgen,
was skip()
in 159 wie oben gerne übernimmt.
Ab Zeile 163 kommt dann tatsächlich die farblich unterlegte
Diff-Ausgabe dran. Wie letztes Mal besprochen, liefert SDiff
ja
pro Zeile dreielementige Arrays zurück, die die Zeileninhalte
und einen Modifizierungsmerker (c
: Change, i
: Insert, d
: Delete,
u
: Unmodified) enthalten. Das Konstrukt in Zeile 168 weisst den
entsprechenden Merkern die richtigen Farben zu und macht sich dabei
zunutze, dass man Perls ternären ?:
-Operator beliebig
strecken kann, um if-elsif
-ähnliche Funktionalität zu erhalten.
Zeile 175 formatiert die aktuelle Zeilennummer zweispaltig und
Zeile 177 unterlegt jede zweite unmodifizierte Zeile gräulich,
falls der Benutzer den striped
-Modus wählte, der Listings
zur besseren Augenführung gestreift darstellt (Abbildung 2 vs.
Abbildung 4).
Dann wird die Tabelle abgeschlossen und ein Link auf die
Konfigurationsseite ausgegeben, den die Funktion self_url()
automatisch erzeugt, nachdem vorher cfg
mit param()
auf 1
gesetzt wurde.
readfile()
ab Zeile 217 bastelt in $cmd
zunächst das Kommando
für die open()
-Funktion zusammen. Für den Fall einer simplen Datei
ist dies einfach "<datei"
. Für CVS-Zugriffe hingegen
steht dort das CVS-Kommando, gefolgt von einem Pipe (|
)-Zeichen,
damit die später in Zeile 237 aufgerufene open()
-Funktion es
als Shell-Kommando
ausführt und im entsprechend zugeordneten Filehandle
die Ausgabezeilen liefert. Hat der Benutzer nicht die gewünschte
CVS-Revisionsnummer in $opts{rev}
angegeben, muss die Funktion
cvs_fill_in()
diese schlau ermitteln und in $opts{rev}
``einfüllen''. Außerdem wird der relative Pfad vom CVS-Repository
zur Datei gebraucht.
Da der Benutzer mit dem CGI-Parameter path
(lpath
für readfile()
)
nur den absoluten
Pfad zur lokalen Arbeitskopie angibt, muss cvs_fill_in()
zu
einem Trick greifen: CVS legt in jedem Arbeitsverzeichnis ein
Verzeichnis namens CVS
an und legt dort administrative Dateien ab.
Darunter ist auch Repository
, die als einzige Information genau
den gesuchten relativen Pfad enthält. Zeile 255 hängt noch schnell
einen Schrägstrich und den Namen der zu vergleichenden Datei dahinter und
legt das Ergebnis in $opts-<{rpath}
--
fertig. Das ab Zeile 264 zusammengebastelte cvs status
-Kommando
wird die aktuell eingecheckte Version ermitteln und in $opts{rev}
ablegen -- und damit haben wir
alles zusammen, um das in readfile()
ab Zeile 232 zusammengebaute
cvs co
-Kommando auszuführen und die Datei in der geforderten Version
aus dem CVS zu locken. -Q
verdonnert cvs
übrigens dazu,
kein unnützes Geschwätz abzusondern.
config_page()
ab Zeile 277 gibt die verschiedenen Browser-Formulare
aus und nutzt dazu die praktischen HTML-Ausgabebefehle aus Lincoln
Steins CGI-Modul. Der CGI-Parameter mode
bestimmt, welcher
der drei Eingabemodi erscheint. Um Codezeilen formatiert auszugeben,
ersetzt die Funktion type()
ab Zeile 355 alle für HTML gefährlichen
Zeichen wie &
, <
und >
, nutzt das Modul Text::Wrap
,
um zu lange Zeilen an Wortgrenzen umzubrechen (andernfalls werden
die Tabellenspalten bei langen Codezeilen
zu breit), stellt noch einen speziellen Font
mit konstanter Zeichenbreite ein und gibt alle ihm übergebenen
Textstücke HTML-formatiert zurück.
Die Funktion display_cvs_form()
gibt
Formularfelder für zwei verschiedene CVS-Modi aus und wurde deshalb nach
Zeile 374 ausgelagert. Sie greift sich den Array @CVSROOT
und
gibt dem Benutzer ein schönes Drop-Down-Menü für die Auswahl
der CVS-Wurzel und ein Eingabefeld für etwaige andere CVS-Wurzeln.
001 #!/usr/bin/perl 002 ########################################### 003 # hd - Diff files side by side in HTML 004 # Derived from 'hdiff' (Bonsai team) 005 # Mike Schilli, 2002 (mschilli1@aol.com) 006 ########################################### 007 use warnings; 008 use strict; 009 010 use SDiff qw(sdiff); 011 use CGI qw(:all *table *font *pre); 012 use CGI::Carp qw(fatalsToBrowser); 013 use File::Basename; 014 use Set::IntSpan; 015 use Text::Wrap; 016 017 $| = 1; 018 019 my $CVS_COMMAND = "/usr/bin/cvs"; 020 my @CVSROOTS = ( 021 ["/home/mschilli/CVSROOT", 022 "My Local CVS"], 023 [":pserver:anonymous\@anoncvs.gimp.org:" . 024 "/cvs/gnome", 025 "Gimp"], 026 [":pserver:mschilli\@east." . 027 "bla.com:/data/cvs/host", 028 "Server"]); 029 030 my $STABLE_BG_COLOR = "White"; 031 my $ALT_BG_COLOR = "#f0f0f0"; 032 my $SKIPPING_BG_COLOR = "#c0c0c0"; 033 my $HEADER_BG_COLOR = "Orange"; 034 my $CHANGE_BG_COLOR = "LightBlue"; 035 my $ADDITION_BG_COLOR = "LightGreen"; 036 my $DELETION_BG_COLOR = "LightGreen"; 037 my $DIFF_BG_COLOR = "White"; 038 my @MODES = ( 039 [file => "Compare Two Files"], 040 [cvs => "Compare Two CVS revisions"], 041 [chi => "Compare before Check-In"]); 042 my $CONTEXT = 5; 043 my $MAX_LINE_LEN = 80; 044 045 print header(); 046 047 if(param("cfg") or !param("mode")) { 048 # Display config page 049 config_page(); 050 exit 0; 051 } 052 053 my($h1, $h2, $left, $right, $n, 054 @run_list, $nof_lines, $ver); 055 056 my $cvsroot = 057 (param("aroot") || param("cvsroot")); 058 059 if(param("mode") eq "file") { 060 # Compare two files 061 $h1 = basename(param("f1")); 062 $h2 = basename(param("f2")); 063 064 ($left) = readfile(file => 065 param("f1")); 066 ($right) = readfile(file => 067 param("f2")); 068 069 } elsif(param("mode") eq "chi") { 070 # Compare a local version with CVS 071 ($left, $ver) = readfile( 072 lpath => param('path'), 073 cvsroot => $cvsroot); 074 075 my $filename = basename(param("path")); 076 $h1 = "$filename (CVS $ver)"; 077 $h2 = "$filename (local copy)"; 078 079 ($right) = readfile( 080 file => param('path')); 081 082 } elsif(param("mode") eq "cvs") { 083 # Compare two CVS versions 084 my $f = basename(param("path")); 085 $h1 = "$f (CVS " . param("r1") . ")"; 086 $h2 = "$f (CVS " . param("r2") . ")"; 087 088 ($left) = readfile( 089 cvsroot => $cvsroot, 090 rpath => param("path"), 091 rev => param("r1")); 092 ($right) = readfile( 093 cvsroot => $cvsroot, 094 rpath => param("path"), 095 rev => param("r2")); 096 } 097 098 chomp @$left; 099 chomp @$right; 100 101 my $set = Set::IntSpan->new(); 102 103 my $diffs = sdiff($left, $right, \$set, 104 $CONTEXT); 105 $nof_lines = @$diffs; 106 107 if(param("context")) { 108 @run_list = split /,/, 109 $set->run_list(); 110 @run_list = () if $set->empty(); 111 } else { 112 @run_list = ("0-$#$diffs"); 113 } 114 115 print start_html( {BGCOLOR => 116 $STABLE_BG_COLOR, 117 -title => 118 param("comment")}); 119 120 param("cfg" => 1); 121 print a({href => self_url}, "Configure"); 122 123 print start_table( {BGCOLOR => 124 $STABLE_BG_COLOR, 125 RULES => "all", 126 CELLPADDING => 0, 127 CELLSPACING => 0, 128 COLS => 2} 129 ); 130 131 print TR( {BGCOLOR => $DIFF_BG_COLOR}, 132 th( {colspan => 2}, 133 param("comment")) ); 134 print TR( {BGCOLOR => $HEADER_BG_COLOR}, 135 th($h1), th($h2) ); 136 137 my $cur_idx = 0; 138 139 while(@$diffs or @run_list) { 140 141 my($from, $to); 142 143 if(@run_list) { 144 ($from, $to) = split /-/, 145 shift @run_list; 146 } 147 148 if(!defined $from) { 149 # Skip until the end 150 $cur_idx = skip($diffs, $cur_idx, 151 $nof_lines-1); 152 last; 153 } 154 # Just one line? 155 $to = $from unless defined $to; 156 157 if($cur_idx < $from) { 158 # There are lines to skip 159 $cur_idx = skip($diffs, $cur_idx, 160 $from-1); 161 } 162 163 for($cur_idx..$to) { 164 my $e = shift @$diffs; 165 my($left, $right, $mod) = @$e; 166 $cur_idx++; 167 168 my $color = 169 $mod eq "c" ? $CHANGE_BG_COLOR : 170 $mod eq "i" ? $ADDITION_BG_COLOR : 171 $mod eq "d" ? $DELETION_BG_COLOR : 172 $mod eq "u" ? $STABLE_BG_COLOR : 173 "unknown"; 174 175 $n = sprintf "%2d", $cur_idx; 176 177 $color = $ALT_BG_COLOR if 178 $mod eq "u" 179 and param("striped") 180 and $n % 2; 181 182 print TR({BGCOLOR => $color}, 183 td({align => "left"}, 184 type("$n $left")), 185 td({align => "left"}, 186 type("$n $right"))); 187 188 print "\n"; 189 } 190 } 191 192 print end_table(); 193 param("cfg", "1"); 194 print a({href => self_url}, "Configure"); 195 print end_html(); 196 197 ########################################### 198 sub skip { 199 ########################################### 200 my($diffs, $cur_idx, $to) = @_; 201 202 if($to-$cur_idx > 2*$CONTEXT) { 203 print TR( {BGCOLOR => 204 $SKIPPING_BG_COLOR}, 205 td( { COLSPAN => 2 }, 206 b("Skipping lines ", 207 $cur_idx + 1, 208 "...", $to + 1))); 209 splice @$diffs, 0, $to-$cur_idx+1; 210 $cur_idx = $to+1; 211 } 212 213 return $cur_idx; 214 } 215 216 ########################################### 217 sub readfile { 218 ########################################### 219 my (%opts) = @_; 220 221 my $cmd; 222 223 # Local file 224 if(exists $opts{file}) { 225 $cmd = "<$opts{file}"; 226 } else { 227 # Get latest CVS version 228 if(!exists $opts{rev}) { 229 cvs_fill_in(\%opts); 230 } 231 # Get file 232 $cmd = "$CVS_COMMAND -Q -d " . 233 "$opts{cvsroot} co -r " . 234 "$opts{rev} -p $opts{rpath} |"; 235 } 236 237 open F, "$cmd" or 238 die "Cannot open '$cmd'"; 239 my @data = <F>; 240 close F or die "$cmd failed"; 241 return (\@data, $opts{rev}); 242 } 243 244 ########################################### 245 sub cvs_fill_in { 246 ########################################### 247 my ($opts) = @_; 248 249 # Get path within CVS/working path 250 my $rep = dirname($opts->{lpath}) . 251 "/CVS/Repository"; 252 open FILE, "<$rep" or 253 die "Cannot open $rep"; 254 chomp($opts->{rpath} = <FILE>); 255 $opts->{rpath} .= "/" . 256 basename($opts->{lpath}); 257 close FILE; 258 259 # Get cvs version 260 my $wdir = dirname($opts->{lpath}); 261 chdir $wdir or 262 die "Cannot chdir to $wdir"; 263 264 my $cmd = "$CVS_COMMAND -Q -d " . 265 "$opts->{cvsroot} status " . 266 basename($opts->{lpath}) . 267 " 2>/dev/null"; 268 open PIPE, "$cmd |" or 269 die "Cannot open pipe"; 270 my $data = join '', <PIPE>; 271 close PIPE or die "$cmd failed"; 272 ($opts->{rev}) = $data =~ 273 /Repository revision:\s*([\d\.]+)/; 274 } 275 276 ########################################### 277 sub config_page { 278 ########################################### 279 print h1("Configuration"); 280 281 my $checked = ""; 282 my $current_mode = (param("mode") || 283 "file"); 284 for (@MODES) { 285 my ($mode, $text) = @$_; 286 if($mode eq $current_mode) { 287 print b($text); 288 } else { 289 param("mode", $mode); 290 param("cfg", 1); 291 print a({href => self_url}, 292 $text); 293 } 294 print " \n"; 295 } 296 297 # Set it back to current mode 298 param("mode", $current_mode); 299 param("cfg", undef); 300 301 print start_html( {BGCOLOR => 302 $STABLE_BG_COLOR} ); 303 print start_form(-method => "GET"); 304 print start_table({BGCOLOR => 305 $ALT_BG_COLOR}); 306 307 if($current_mode eq "file") { 308 print TR(td("File 1"), 309 td(textfield(-name => "f1", 310 -size => 50))); 311 print TR(td("File 2"), 312 td(textfield(-name => "f2", 313 -size => 50))); 314 315 } elsif($current_mode eq "chi") { 316 display_cvs_form(); 317 318 print TR(td("Local Path and File"), 319 td(textfield(-name => "path", 320 -size => 50))); 321 322 } elsif($current_mode eq "cvs") { 323 324 display_cvs_form(); 325 326 print TR( 327 td("Path to file within CVS"), 328 td(textfield(-name => "path", 329 -size => 50))); 330 331 print TR(td("Rev 1"), 332 td(textfield(-name => "r1", 333 -size => 10))); 334 print TR(td("Rev 2"), 335 td(textfield(-name => "r2", 336 -size => 10))); 337 } 338 339 print TR(td("Comment"), 340 td(textfield(-name => "comment", 341 -size => 50))); 342 print end_table(); 343 print hidden(-name => "mode"); 344 print checkbox(-name => "striped", 345 -value => "on"); 346 print checkbox(-name => "context", 347 -value => "on"); 348 print br(); 349 print submit(value => "View Diff"); 350 351 print end_form(); 352 } 353 354 ########################################### 355 sub type { 356 ########################################### 357 358 @_ = map { s/&/&/g; 359 s/</</g; s/>/>/g; 360 $_ } @_; 361 362 $Text::Wrap::columns = $MAX_LINE_LEN; 363 $_[0] = join "\n", wrap("", "", $_[0]); 364 365 return start_pre() . 366 start_font( 367 { FACE => "Lucida Console", 368 SIZE => 1 }) . 369 join('', @_) . 370 end_font(); 371 } 372 373 ########################################### 374 sub display_cvs_form { 375 ########################################### 376 my %CVSROOTS = map { @$_ } @CVSROOTS; 377 378 print TR(td("CVS Root"), 379 td(popup_menu( 380 -name => 'cvsroot', 381 -values => [map {$_->[0]} @CVSROOTS], 382 -labels => \%CVSROOTS))); 383 384 print TR(td("Alternative CVS Root"), 385 td(textfield(-name => "aroot", 386 -size => 50))); 387 }
Da das Skript hd
im Webserver unter dessen Benutzerkennung
läuft erfolgen auch die Zugriffe aufs CVS unter dieser Kennung,
die folgerichtig einen Zugang
zum CVS benötigt. Falls dieses den Zugriff beschränkt
(was bei Heiminstallationen meist nicht der Fall ist), muss
man unter Umständen den Webserverbenutzer von nobody
auf
eine Kennung mit CVS-Zugang ändern und
vor Benutzung des Skripts dem CVS-Server Nutzername und Passwort
mittels cvs login
mitteilen.
hd
ist allerdings so schlau, dass es
nur eine Kennung zum CVS braucht, während auch andere Benutzer mit
ihrem Browser am Webserver andocken können, um ihre
lokalen Dateien gegen die CVS-Versionen zu vergleichen -- solange letztere
vom Webserver-Benutzer zumindest lesbar sind.
Da das Skript den Inhalt von Dateien auf dem entsprechenden Rechner anzeigt, versteht es sich von selbst, dass es aus Sicherheitsgründen niemals auf einem öffentlich zugänglichen Rechner installiert wird, sondern nur innerhalb der Firewall.
Der Array @CVSROOT
in hd
muss noch an die lokalen Gegebenheiten
angepasst werden -- hier sollten die lokalen CVS-Verzeichnisse bzw.
Server sprechenden Namen zugeordnet werden. Ein Wertepaar wie
["/u/mschilli/CVSROOT", "Mei Gruschtlkistn"],
im Array @CVSROOT
beispielsweise lässt den Browser in der Auswahlbox
Mei Gruschtlkistn anzeigen, während der zugehörige CVSROOT-Wert
-- falls diese Option gewählt wurde -- auf /u/mschilli/CVSROOT
steht.
Die Variable
$CVS_COMMAND
legt den Pfad zum cvs
-Kommando fest, wer will,
kann auch noch immer benutzte Optionen wie z.B. -z3
für
Komprimierung bei der Datenübertragung dahinterhängen.
Vernünftigerweise sollte das Skript unter einer einigermaßen neuen
perl
-Installation mit den nachgerüsteten Modulen Set::IntSpan
,
Text::Wrap
und Algorithm::Diff
laufen.
Letztere kommen über die praktische CPAN-Shell einfach mit
perl -MCPAN -eshell cpan> install Set::IntSpan cpan> install Text::Wrap cpan> install Algorithm::Diff
vom CPAN auf den heimischen Rechner.
Das Skript hd
muss ausführbar
ins cgi-bin
-Verzeichnis des Webservers und
das letztes Mal vorgestellte SDiff.pm
irgendwohin, wo hd
es findet,
also unter Umständen ebenfalls nach cgi-bin
. Richtet man den
Webbrowser anschließend auf http://rechner/cgi-bin/hd
, kommt
das Konfigurationsfenster nach Abbildung 1 hoch. Nach Auswahl des
richtigen Modus (Datei-, lokal/CVS- oder CVS-Vergleich) und der
Eingabe der Pfade/Versionen stellt der Browser die Unterschiede dar
und für das Code-Review schickt man den URL per Email oder
URL zu den Kollegen. Die können sogar noch die Anzeigeparameter
verstellen, in dem sie dem ``Configure''-Link folgen.
Und zum Codereview noch
einen allgemeinen Tipp, den Damian Conway einmal
auf der Perl-Konferenz gab:
Nichts ist so schön, wie einen Experten zu Fall zu bringen!
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |