Dienstag, 4. Januar 2011

Micro-Versionierung

Vor kurzem wäre ich beinahe Opfer eines schlimmen Datenverlustes geworden [1]: Durch den Upload eines falschen JavaScript-Files hätte ich die Frucht von fast zwei Tagen intensiver Programmierarbeit verloren. Zwar ist es meine Erfahrung, dass das erneute Schreiben des Programms in solchen Fällen ein besseres Ergebnis liefert, auch wenn das erste Produkt doch wieder auftaucht oder wiederhergestellt werden kann. Auch Briefe gewinnen deutlich an Qualität, wenn man sie nach Abfassung zerreisst und komplett neu schreibt!

Dennoch versuche ich natürlich vernünftigerweise, Datenverlust möglichst zu vermeiden. Im SAP-System gelingt mir dies durch häufige Freigabe meiner Änderungen ins Testsystem. Dadurch entstehen zwar viele kleine Transporte - aber die Arbeit wird dadurch besser dokumentiert und nachvollziehbar. Die Kurztexte der Transporte kann ich darüberhinaus zur Dokumentation der kleinen Änderungen nutzen, die ich jeweils vorgenommen habe. So enthält die Versionsverwaltung schliesslich eine Dokumentation der an einem Quelltext vorgenommenen Arbeitsschritte.

Nur für Arbeiten an "Wegwerfprototypen" verzichte ich auf Transporte und verwende lokale Pakete. Genau daher bekam ich mein Problem. Dabei haben Prototypen in der Entwicklungsarbeit durchaus einen hohen Stellenwert: sie können - wie in diesem Fall - als Proof Of Concept die Vergabe eines Entwicklungsprojekts legitimieren.

Für Entwicklungen in JavaScript, Perl, C/C++ und Java benutze ich häufig den Editor UltraEdit. Dieser erlaubt es zwar, automatisch numerierte Backups der letzten Änderungen zu erstellen. Diese Einstellung ist aber pauschal, gilt dann für jedes File, das ich im Editor überhaupt ändere. Mir ist es lieber, die Versionierung gezielt selbst anstossen zu können. Ausserdem gefällt mir die Verschwendung nicht, jede dieser Versionen als Vollversion zu speichern (statt nur die Differenzen).

Daher habe ich UltraEdit mit einem Versionierungswerkzeug auf Basis von CVS ausgestattet (CVS ist zwar ein bisschen aus der Mode gekommen, aber zweifellos "gut abgehangen"). Wer CVS nicht mag, sondern lieber mit GIT oder SVN arbeitet, kann meine Lösung leicht abändern, da die Kommandozeilenbefehle für das Ein- und Auschecken in allen diesen Systemen ähnlich sind.

Wie funktioniert es? Wenn ich eine Version der aktuell bearbeiteten Datei ziehen möchte, wähle ich mein neues Werkzeug "Versionieren":



Die Versionen, ihre Diffs und Patches kann ich mir nachher mit einem GUI für CVS ansehen, z.B. TortoiseCVS oder wincvs.

Der Vollständigkeit halber füge ich hier noch das Batchfile an, das die Versionierung ausführt. Es bekommt von Ultraedit zwei Argumente, den Verzeichnisnamen und den Dateinamen (im Werkzeugkommando durch die Symbole %p und %n%e bezeichnet). Die Definition des Werkzeugs lautet also
makeVersion.bat "%p" %n%e

Das Batchfile muss mit seiner mittelalterlichen Stringverarbeitung diese Anführungszeichen wieder entfernen, um Name und Pfad in eigenen Kommandos verwenden zu können. Es richtet dann ein temporäres Arbeitsverzeichnis ein, checkt die Datei gleichen Namens aus einem lokalen Repository aus, kopiert den aktuellen Stand in das Arbeitsverzeichnis und führt schliesslich den commit aus. Auf Dokumentationen der Änderungen mit dem -m Parameter verzichte ich. Denn es geht mir ja nur um Microversionen - die viele kleinsten Versionen, die während der Arbeit an einem Programm entstehen. Aus dem gleichen Grunde reicht es mir, die Versionierung nur anhand des Dateinamens auszuführen. Wenn ich vor vielen Monaten schon einmal eine ganz andere Datei desselben Namens bearbeitet hatte, stören mich die von dieser Zeit noch vorliegenden Versionen nicht: Ich schaue ja gar nicht so weit zurück, mir geht es um Änderungen in einem Zeitrahmen von Minuten oder Stunden.

In meiner IDE der Zukunft sind all diese Dinge in den Editor integriert. Für die Fehlersuche steht mir dann eine Zeitlinie zur Verfügung, in der auch bei der gleichzeitigen Arbeit an mehreren Dateien die letzten konsistenten Codestände testhalber wiederhergestellt können, so dass ich ohne aufwendige Analyse direkt ermitteln kann, ab welcher Änderung sich das Fehlerchen eingeschlichen hat.

@ECHO OFF

rem SYNTAX:
rem makeVersion <Verzeichnisname> <Dateiname>
rem Versioniert die angegebene Datei, wobei für jeden Dateinamen
rem ein neuer Modul im Wurzelverzeichnis ue32work angelegt wird.

set CVSWORKDIR=\UltraEdit-32\cvswork
set WTEMP=\Temp
set UE32WORK=%WTEMP%\ue32work
set CVSCMD=\cvsnt\cvs -d %CVSWORKDIR%
set FILEPATH=%1%
for /f "useback tokens=*" %%a in ('%FILEPATH%') do set FILEPATH=%%~a
set FILENAME=%2%
for /f "useback tokens=*" %%a in ('%FILENAME%') do set FILENAME=%%~a
set FULLFILENAME="%FILEPATH%%FILENAME%"

rmdir /S /Q %UE32WORK%
mkdir %UE32WORK%
if exist %CVSWORKDIR%\ue32work\%FILENAME% (
@ECHO ON
cd %WTEMP%
%CVSCMD% co ue32work\%FILENAME%
copy /Y %FULLFILENAME% %UE32WORK%\%FILENAME%
cd %UE32WORK%\%FILENAME%
%CVSCMD% commit -m "Dummy" %FILENAME%
) else (
@ECHO ON
mkdir %UE32WORK%\%FILENAME%
copy /Y %FULLFILENAME% %UE32WORK%\%FILENAME%
cd %UE32WORK%\%FILENAME%
%CVSCMD% import -m "Dummy" ue32work\%FILENAME% "UE32WORK" "r1"
)


Nachtrag am 23.3.2013

Hier eine zweite Version mit JavaScript und SVN. Das Kommando in der Werkzeugleiste lautet nun
cscript //nologo //e:jscript H:\uedit32\tools\make_version.js "%p" "%n%e"
Mit Subversion kann man mit einem kleinen Trick auch einzelne Versionen auschecken: Man führt den checkout-Befehl mit der Option --depth empty aus und macht danach gezielt den update für das File, das allein man haben möchte. Mit dem folgenden Script verwende ich daher nur noch ein Repository für alle Files. Das Kommando svn add produziert bei Ausführung mit einem bereits versionierten File eine Warnung, ohne sonst etwas zu tun. Daher ist der Existenzcheck ("Existiert das File im Repository") überflüssig, und man kann pauschal den svn add ausführen.
// Werkzeug zum Ziehen einer Version - hier mit SVN

// Das Repo, das sich sonst so wichtig macht ("Master"), 
// ist nur slave, um Versionen abzuspeichern


generateVersion(
  getFilenameAndPathFromCommandLine()
  );

function generateVersion(file) {

  var work = "C:\\temp\\ue32work",
      svn = Subversion({
              repoURL:"file:///H:/svn_general",
              work:work
              }),
      fs = FileSystem();       

  fs.createCleanDirectory(work);
  svn.checkoutSingleFile(file.name);
  fs.copy(file,work);
  svn.commitNewVersion(file.name);
  
  }

  
function Subversion(args) {
  var 
    repoURL = args.repoURL,
    work    = args.work;

  return {
    checkoutSingleFile:function(filename) {
      svn('checkout ' + repoURL + ' ' + work + ' --depth empty ' );
      svn('update "'   + filename + '"', work ); 
      },
    commitNewVersion:function(filename) {
      svn('add "' + filename + '"', work);
      svn('commit -m "Dummy" "' + filename + '"', work ); 
      }
    };    

  function svn(cmd, currentDirectory) {
    var SVN = 'C:\\Users\\rplantik\\Portables\\svnportable\\SVN\\svn.exe';  
    execute(SVN+" "+cmd,currentDirectory);
    };    
  
  function execute(cmd,currentDirectory) {
    var sh = WScript.CreateObject("WScript.Shell");
    if (currentDirectory) sh.CurrentDirectory = currentDirectory;  
    WScript.echo(cmd);
    var exec =  sh.Exec(cmd)
    while (exec.Status == 0) {
      WScript.Sleep(100);
      WScript.StdOut.Write(exec.StdOut.ReadAll());
      WScript.StdErr.Write(exec.StdErr.ReadAll());
      }  
    }  
  }

function FileSystem() {
  var fso     = new ActiveXObject("Scripting.FileSystemObject");
  return {
    createCleanDirectory:function(path) {
      if (fso.FolderExists(path)) {
        deleteFolder(path); // ... with all its possible contents
        }
      fso.createFolder( path );
      },
    copy:function(file,dest) {
      if (!dest.match(/\\$/)) dest += "\\"; 
      fso.CopyFile(file.fullName,dest);
      }
    };
  function deleteFolder(path) {
    var folder = fso.GetFolder(path);
    if (folder.Attributes !== 0) {
      folder.Attributes = 0;
      }
    folder.Delete(true);  
    }
  }

function getFilenameAndPathFromCommandLine() {
  var file = {
    path:WScript.Arguments(0),
    name:WScript.Arguments(1)
    };
  file.fullName = file.path + "\\" + file.name;
  return file;  
  }  
Für die Anzeige aller Versionen einer aktuell im Editor geöffneten Datei habe ich mir ein weiteres Werkzeug konfiguriert. Es ruft den Dialog Show Log von TortoiseSVN auf, der alle Versionen anzeigt. Von dort kann man bequem in die Delta-Anzeige zwischen verschiedenen Ständen verzweigen. Die Befehlszeile in UltraEdit hierfür ist (für mein Repository und meinen TortoiseSVN-Installationsort):
C:\programme\tortoisesvn\bin\tortoiseproc.exe 
  /command:log 
  /path:"file:///H:/svn_general/%n%e"




[1] Wieso wurde ich nur beinahe ein Datenverlust-Opfer? Dank eines wahnsinnigen Zufalls hatte mein Kollege eine Version des Files in seinem Browser-Cache, die immerhin nur eine halbe Stunde alt war. Das genügte.

Keine Kommentare :