Es ist ziemlich unkompliziert, Anwendungen mit den APIs der
Bibliotheken von Subversion zu entwickeln. Subversion ist vor
allem eine Menge aus C-Bibliotheken mit Header-Dateien
(.h
), die im Verzeichnis
subversion/include
des Quelltextbaums
liegen. Diese Header werden in Ihre Systemverzeichnisse (z.B.
/usr/local/include
) kopiert, wenn Sie
Subversion aus den Quellen bauen und installieren. Diese Header
repräsentieren die Gesamtheit der Funktionen und Typen, die den
Benutzern der Subversion-Bibliotheken zugänglich gemacht werden
sollen. Die Entwicklergemeinde von Subversion achtet peinlich
genau darauf, dass die öffentliche API gut dokumentiert ist
– diese Dokumentation finden Sie direkt in den
Header-Dateien.
Beim Untersuchen der öffentlichen Header-Dateien wird Ihnen
zunächst auffallen, dass die Datentypen und Funktionen von
Subversion durch Namensräume geschützt sind. Das heißt, jeder
öffentliche Symbolname beginnt mit svn_
,
gefolgt von einem Kürzel der Bibliothek, in der das Symbol
definiert ist (etwa wc
,
client
, fs
usw.), gefolgt
von einem einzelnen Unterstrich (_
) und dem
Rest des Symbolnamens. Halböffentliche Funktionen (die zwischen
Quelldateien einer Bibliothek, jedoch nicht außerhalb davon
verwendet werden und innerhalb der Bibliotheksverzeichnisse zu
finden sind) weichen von diesem Namensschema ab, indem statt
eines einzelnen Unterstrichs nach dem Bibliothekskürzel zwei
Unterstriche stehen (_ _
). Private
Funktionen in einer Quelldatei haben keinen besonderen Präfix
und werden static
deklariert. Einem Compiler
sind diese Konventionen natürlich egal, doch sie helfen, den
Gültigkeitsbereich einer gegebenen Funktion oder eines
Datentypen deutlich zu machen.
Eine weitere gute Informationsquelle zur Programmierung mit den Subversion-APIs sind die Programmierrichtlinien des Projektes, die Sie unter http://subversion.tigris.org/hacking.html finden können. Dieses Dokument enthält nützliche Informationen, die, obwohl sie für Entwickler und angehende Entwickler von Subversion selbst gedacht sind, genauso für Leute geeignet sind, die mit Subversion als ein Satz von Bibliotheken eines Drittanbieters entwickeln. [50]
Neben den eigenen Datentypen von Subversion werden Sie
viele Verweise auf Datentypen entdecken, die mit
apr_
beginnen – Symbole aus der
Bibliothek Apache Portable Runtime (APR). APR ist die
Portabilitätsbibliothek von Apache, ursprünglich aus dem
Server-Code herausgelöst, als ein Versuch, die
betriebssystemspezifischen Teile von den
betriebssystemabhängigen Bereichen des Codes zu trennen. Das
Ergebnis war eine Bibliothek, die ein generisches API für das
Ausführen von Operationen bietet, die sich je nach
Betriebssystem mehr oder weniger stark unterscheiden. Obwohl
der Apache HTTP-Server offensichtlich die APR-Bibliothek als
erster verwendete, erkannten die Subversion-Entwickler sofort
die Vorteile, die durch die Benutzung von APR entstehen. Das
bedeutet, dass es praktisch keinen betriebssystemspezifischen
Code in Subversion gibt. Es bedeutet auch, dass der
Subversion-Client sich auf jedem System übersetzen und
betreiben lässt, auf dem das auch für den Apache HTTP-Server
gilt. Momentan umfasst diese Liste alle Dialekte von Unix,
Win32, BeOS, OS/2 und Mac OS X.
Neben der Bereitstellung konsistenter Implementierungen
von Systemaufrugen, die sich zwischen Betriebssystemen
unterscheiden,
[51]
bietet APR Subversion unmittelbaren Zugriff auf viele
maßgeschneiderte Datentypen, wie etwa dynamische Arrays und
Hashtabellen. Subversion macht von diesen Typen regen
Gebrauch. Der vielleicht am meisten verbreitete APR-Datentyp,
der sich in beinahe jedem Subversion-API-Prototypen
wiederfindet, ist apr_pool_t
— der
APR-Speicherpool. Subversion verwendet Pools intern zum
Zuteilen all seines Speichers (außer wenn eine externe
Bibliothek eine unterschiedliche Speicherverwaltung für die
über ihre API ausgetauschten Daten voraussetzt),
[52]
und obwohl jemand, der mit den Subversion-APIs programmiert
nicht gezwungen ist, dasselbe zu tun,
muss er Pools für die API-Funktionen zur
Verfügung stellen, die sie benötigen. Das heißt, dass Benutzer
der Subversion-API auch gegen die APR linken,
apr_initialize()
zum Initialisieren des
APR-Subsystems aufrufen und Pools zur Verwendung mit
Subversion-API-Aufrufen erzeugen sowie verwalten müssen,
typischerweise unter Benutzung von
svn_pool_create()
,
svn_pool_clear()
und
svn_pool_destroy()
.
Da das Betreiben entfernte Versionskontrolle der Grund für
das Vorhandensein von Subversion ist, ergibt es einen Sinn,
dass der Internationalisierung (i18n) etwas Aufmerksamkeit
gewidmet wurde. Schließlich kann „entfernt“
sowohl „am anderen Ende vom Büro“ als auch
„am anderen Ende der Welt“ bedeuten. Um das zu
ermöglichen, erwarten alle öffentlichen Schnittstellen von
Subversion, die Pfadargumente annehmen, dass diese Pfade im
kanonischen Format vorliegen – was am besten gelingt,
wenn sie durch die Funktion
svn_path_canonicalize()
geschickt werden
– und in UTF-8 kodiert sind. Das bedeutet
beispielsweise, dass ein neuer Client, der die Schnittstelle
libsvn_client
benutzt, zunächst Pfade aus
der sprachabhängigen Kodierung nach UTF-8 konvertieren muss,
bevor sie an die Subversion-Bibliotheken übergeben werden und
anschließend umgekehrt etwaige Ergebnisfade von Subversion
zurück in die sprachabhängige Kodierung umgewandelt werden
müssen, bevor die Pfade für Zwecke verwendet werden, die
nichts mit Subversion zu tun haben. Glücklicherweise stellt
Subversion eine Reihe von Funktionen zur Verfügung (siehe
subversion/include/svn_utf.h
) die jedes
Programm zum Konvertieren benutzen kann.
Ferner ist es für die Subversion APIs erforderlich, dass
alle URL-Parameter richtig URI-kodiert sind. So sollten Sie
statt file:///home/username/My File.txt
als URL
einer Datei namens My File.txt
file:///home/username/My%20File.txt
übergeben. Auch
hierfür stellt Subversion Hilfsfunktionen für Ihre Anwendung
zur Verfügung –
svn_path_uri_encode()
und
svn_path_uri_decode()
zum Kodieren bzw.
Dekodieren von URIs.
Falls Sie daran interessiert sein sollten, die
Subversion-Bibliotheken in Verbindung mit etwas anderem als
ein C-Programm zu benutzen – etwa ein Python- oder ein
Perl-Script – bietet Subversion etwas Unterstützung über
den Simplified Wrapper and Interface
Generator (SWIG). Die SWIG-Bindungen für Subversion
liegen in subversion/bindings/swig
. Sie
reifen zwar noch, können aber verwendet werden. Diese
Bindungen erlauben Ihnen, Subversion-API-Funktionen indirekt
aufzurufen, indem eine Zwischenschicht verwendet wird, die die
Datentypen Ihrer Skriptsprache in die Datentypen umwandeln,
die von Subversions C-Bibliotheken benötigt werden.
Es wurden bedeutende Anstrengungen unternommen, um funktionierende SWIG-erzeugte Bindungen für Python, Perl und Ruby zur Verfügung zu stellen. Bis zu einem gewissen Grad kann die Arbeit zur Vorbereitung der SWIG-Schnittstellen für diese Sprachen wiederverwendet werden, wenn es darum geht, Bindungen für andere von SWIG unterstützte Sprachen zu erzeugen (unter anderem Versionen von C#, Guile, Java, MzScheme, OCaml, PHP und Tcl). Jedoch ist etwas zusätzliche Programmierarbeit für komplizierte APIs erforderlich, bei deren Übersetzung SWIG ein wenig Hilfe benötigt. Weitergehende Informationen zu SWIG finden Sie auf der Projektseite unter http://www.swig.org/.
Subversion verfügt ebenfalls über Sprachbindungen für
Java. Die javahl-Bindungen (zu finden in
subversion/bindings/java
im
Subversion-Quelltext-Baum) sind nicht SWIG-basiert sondern ein
Gemisch aus Java und handgeschriebenem JNI. Javahl deckt die
meisten client-seitigen Subversion-APIs ab und zielt besonders
auf Implementierer Java-basierter Subversion-Clients und
IDE-Integrationen ab.
Zwar gilt den Sprachbindungen von Subversion seitens der Entwickler nicht dieselbe Aufmerksamkeit wie den Subversion-Kernmodulen, doch sie sind durchaus bereit für den Einsatz. Eine Reihe von Skripten und Anwendungen, alternative graphische Subversion-Clients und andere Werkzeuge von Drittanbietern machen heute schon erfolgreich Gebrauch von den Subversion-Sprachbindungen, um ihre Subversion-Integration zustande zu bringen.
An dieser Stelle ist es erwähnenswert, dass es auch andere Optionen gibt, um Subversion-Schnittstellen in anderen Sprachen zu verwenden: alternative Subversion-Bindungen, die gar nicht aus der Subversion-Entwicklergemeinde stammen. Sie können Links zu diesen alternativen Bindungen auf der Linkseite des Subversion-Projektes finden (unter http://subversion.tigris.org/links.html), doch es gibt davon ein paar beliebte, die besonders erwähnenswert sind. Zunächst seien die PySVN-Bindungen von Barry Scott genannt (http://pysvn.tigris.org/), die eine beliebte Alternative zur Python-Bindung darstellen. PySVN rühmt sich, eine Schnittstelle zu liefern, die Python-typischer ist als die eher C-ähnlichen APIs der Subversion-eigenen Python-Bindungen. Und falls Sie eine reine Java-Implementierung suchen, wehen Sie sich SVNKit (http://svnkit.com/) an, das eine vollständige Reimplementierung von Subversion in Java ist.
Beispiel 8.1, „Verwendung der Projektarchiv-Schicht“
enthält einen Codeabschnitt (in C), der einige der erörterten
Konzepte veranschaulicht. Er verwendet sowohl die Projektarchiv-
als auch die Dateisystemschnittstelle (was anhand der Präfixe
svn_repos_
bzw.
svn_fs_
der Funktionsnamen erkennbar ist),
um eine neue Revision zu erzeugen, in der ein Verzeichnis
hinzugefügt wird. Sie können die Verwendung des APR-Pools
erkennen, der zur Speicherzuteilung herumgereicht wird. Der
Code verdeutlicht auch eine etwas undurchsichtige
Angelegenheit bezüglich der Fehlerbehandlung von Subversion
– alle Fehler von Subversion müssen explizit behandelt
werden, um Speicherlöcher zu verhindern (und vereinzelt
Programmabstürze).
Beispiel 8.1. Verwendung der Projektarchiv-Schicht
/* Umwandlung eines Subversion-Fehlers in einen einfachen Boole'schen * Fehlerwert. * * NOTE: Subversion-Fehler müssen zurückgesetzt werden (mit * svn_error_clear()), da sie aus dem globalen Pool zugeteilt * werden, ansonsten treten Speicherlöcher auf. */ #define INT_ERR(expr) \ do { \ svn_error_t *__temperr = (expr); \ if (__temperr) \ { \ svn_error_clear(__temperr); \ return 1; \ } \ return 0; \ } while (0) /* Ein neues Verzeichnis im Pfad NEW_DIRECTORY des * Subversion-Projektarchivs bei REPOS_PATH erzeugen. Sämtliche * Speicherzuteilungen in Pool durchführen. Diese Funktion erzeugt * eine neue Revision, damit NEW_DIRECTORY hinzugefügt werden kann. Im * Erfolgsfall Null, sonst einen Wert ungleich Null zurückgeben. */ static int make_new_directory(const char *repos_path, const char *new_directory, apr_pool_t *pool) { svn_error_t *err; svn_repos_t *repos; svn_fs_t *fs; svn_revnum_t youngest_rev; svn_fs_txn_t *txn; svn_fs_root_t *txn_root; const char *conflict_str; /* Projektarchiv bei REPOS_PATH öffnen. */ INT_ERR(svn_repos_open(&repos, repos_path, pool)); /* Zeiger auf das Dateisystemobjekt in REPOS holen. */ fs = svn_repos_fs(repos); /* Anfrage beim Dateisystem nach der aktuell letzten existierenden * Revision. */ INT_ERR(svn_fs_youngest_rev(&youngest_rev, fs, pool)); /* Starten einer neuen Transaktion basierend auf YOUNGEST_REV. Die * Wahrscheinlichkeit einer später wegen Konflikte abgelehnten * Übergabe sinkt, wenn wir stets versuchen, unsere Änderungen auf * der letzten Momentaufnahme des Dateisystembaums zu machen. */ INT_ERR(svn_repos_fs_begin_txn_for_commit2(&txn, repos, youngest_rev, apr_hash_make(pool), pool)); /* Nach dem Start der Transaktion wird ein Wurzelobjekt geholt, das * diese Transaktion repräsentiert. */ INT_ERR(svn_fs_txn_root(&txn_root, txn, pool)); /* Das neue Verzeichnis unter der Transaktionswurzel im Pfad * NEW_DIRECTORY anlegen. */ INT_ERR(svn_fs_make_dir(txn_root, new_directory, pool)); /* Die Transaktion übergeben, indem eine neue Revision des * Dateisystems erzeugt wird, die unseren hinzugefügten * Verzeichnispfad enthält. */ err = svn_repos_fs_commit_txn(&conflict_str, repos, &youngest_rev, txn, pool); if (! err) { /* Kein Fehler? Ausgezeichnet! Eine kurze Erfolgsnachricht * ausgeben. */ printf("Verzeichnis '%s' ist erfolgreich als neue Revision " "'%ld' hinzugefügt worden.\n", new_directory, youngest_rev); } else if (err->apr_err == SVN_ERR_FS_CONFLICT) { /* Oh-ha. Die Übergabe schlug wegen eines Konfliktes fehl * (jemand anderes scheint im gleichen Bereich des Dateisystems * Änderungen gemacht zu haben, das wir ändern wollten). Eine * Fehlermeldung ausgeben. */ printf("Ein Konflikt trat im Pfad '%s' auf, als versucht wurde, " "das Verzeichnis '%s' dem Projektarchiv bei '%s' hinzuzufügen.\n", conflict_str, new_directory, repos_path); } else { /* Ein anderer Fehler ist aufgetreten. Eine Fehlermeldung * ausgeben. */ printf("Beim Versuch, das Verzeichnis '%s' dem Projektarchiv bei " "'%s' hinzuzufügen, trat ein Fehler auf.\n", new_directory, repos_path); } INT_ERR(err); }
Beachten Sie, dass der Code in Beispiel 8.1, „Verwendung der Projektarchiv-Schicht“ die Transaktion
ebenso einfach mit svn_fs_commit_txn()
hätte übergeben können. Allerdings weiß die Dateisystem-API
nichts über den Hook-Mechanismus der Projektarchiv-Bibliothek.
Falls Sie möchten, dass Ihr Subversion-Projektarchiv bei jeder
Transaktionsübergabe eine Nicht-Subversion-Aufgabe ausführt
(z.B. eine E-Mail, die alle Änderungen in dieser Transaktion
beschreibt, an Ihre Entwickler-Mailingliste senden), müssen
Sie diejenige Version dieser Funktion verwenden, die durch die
Bibliothek libsvn_repos
umschlossen ist
und die Auslösung von Hooks beinhaltet – in diesem Fall
svn_repos_fs_commit_txn()
. (Weitere
Informationen zu Subversions Projektarchiv-Hooks in „Erstellen von Projektarchiv-Hooks“.)
Lassen Sie uns nun die Sprachen wechseln. Beispiel 8.2, „Verwendung der Projektarchiv-Schicht mit Python“ ist ein Beispielprogramm, das die SWIG-Python-Bindungen von Subversion verwendet, um rekursiv die jüngste Projektarchiv-Revision zu traversieren und dabei die zahlreichen Versionspfade auszugeben.
Beispiel 8.2. Verwendung der Projektarchiv-Schicht mit Python
#!/usr/bin/python """Durchwandern eines Projektarchivs mit Ausgabe der Projektarchiv-Pfadnamen.""" import sys import os.path import svn.fs, svn.core, svn.repos def crawl_filesystem_dir(root, directory): """Rekursives durchwandern von DIRECTORY unterhalb von ROOT im Dateisystem und eine Liste aller Pfade unterhalb von DIRECTORY zurückgeben.""" # Ausgabe dieses Pfadnamens. print directory + "/" # Verzeichniseinträge für DIRECTORY holen. entries = svn.fs.svn_fs_dir_entries(root, directory) # Einträge abarbeiten. names = entries.keys() for name in names: # Den vollen Pfadnamen des Eintrags berechnen. full_path = directory + '/' + name # Falls der Eintrag ein Verzeichnis ist, recursiv bearbeiten. # Die Rekursion gibt eine Liste mit dem Eintrag und all seiner # Kinder zurück, die der aktuellen Pfadliste hinzugefügt wird. if svn.fs.svn_fs_is_dir(root, full_path): crawl_filesystem_dir(root, full_path) else: # Else it's a file, so print its path here. print full_path def crawl_youngest(repos_path): """Öffnen des Projektarchivs bei REPOS_PATH, und rekursives Durchwandern seiner jüngsten Revision.""" # Öffnen des Projektarchivs bei REPOS_PATH, und holen einer Referenz # auf sein versioniertes Dateisystem. repos_obj = svn.repos.svn_repos_open(repos_path) fs_obj = svn.repos.svn_repos_fs(repos_obj) # Die aktuell jüngste Revision abfragen. youngest_rev = svn.fs.svn_fs_youngest_rev(fs_obj) # Ein Wurzelobjekt öffnen, das die jüngste (HEAD) Revision # repräsentiert. root_obj = svn.fs.svn_fs_revision_root(fs_obj, youngest_rev) # Rekursiv durchwandern. crawl_filesystem_dir(root_obj, "") if __name__ == "__main__": # Überprüfung auf korrekten Aufruf. if len(sys.argv) != 2: sys.stderr.write("Verwendung: %s REPOS_PATH\n" % (os.path.basename(sys.argv[0]))) sys.exit(1) # Den Projektarchiv-Pfad kanonisieren. repos_path = svn.core.svn_path_canonicalize(sys.argv[1]) # Eigentliche Arbeit machen. crawl_youngest(repos_path)
Das gleiche Programm in C hätte sich noch um das APR-Speicherpoolsystem kümmern müssen. Python jedoch verwaltet den Speicher automatisch, und die Python-Bindungen von Subversion berücksichtigen diese Konvention. In C würden Sie mit angepassten Datentypen (etwa denen aus der APR-Bibliothek) arbeiten, um den Hash mit Einträgen und die Liste der Pfade zu repräsentieren, doch Python verfügt über Hashes („Dictionarys“ genannt) und Listen als eingebaute Datentypen und bietet eine reichhaltige Sammlung aus Funktionen, um mit diesen Datentypen umzugehen. Also übernimmt SWIG (mithilfe einiger Anpassungen in der Sprachbindungsschicht von Subversion) die Abbildung dieser speziellen Datentypen auf die spezifischen Datentypen der Zielsprache. Dies bietet Benutzern dieser Sprache eine intuitivere Schnittstelle.
Auch für Operationen in der Arbeitskopie können die
Python-Bindungen von Subversion verwendet werden. Im
vorangegangenen Abschnitt dieses Kapitels erwähnten wir die
Schnittstelle libsvn_client
und ihren
Zweck zur Vereinfachung der Erstellung eines
Subversion-Clients. Beispiel 8.3, „Status in Python“ ist ein
kurzes Beispiel wie auf diese Bibliothek über die
SWIG-Python-Bindungen zugegriffen werden kann, um eine
abgespeckte Version des Befehls svn status
nachzubauen.
Beispiel 8.3. Status in Python
#!/usr/bin/env python """Durchwandern eines Arbeitskopieverzeichnisses mit Ausgabe von Statusinformation.""" import sys import os.path import getopt import svn.core, svn.client, svn.wc def generate_status_code(status): """Übersetzen eines Stauswerts in einen Ein-Zeichen-Statuscode, wobei dieselbe Logik wie beim Subversion-Kommandozeilen-Client verwendet wird.""" code_map = { svn.wc.svn_wc_status_none : ' ', svn.wc.svn_wc_status_normal : ' ', svn.wc.svn_wc_status_added : 'A', svn.wc.svn_wc_status_missing : '!', svn.wc.svn_wc_status_incomplete : '!', svn.wc.svn_wc_status_deleted : 'D', svn.wc.svn_wc_status_replaced : 'R', svn.wc.svn_wc_status_modified : 'M', svn.wc.svn_wc_status_merged : 'G', svn.wc.svn_wc_status_conflicted : 'C', svn.wc.svn_wc_status_obstructed : '~', svn.wc.svn_wc_status_ignored : 'I', svn.wc.svn_wc_status_external : 'X', svn.wc.svn_wc_status_unversioned : '?', } return code_map.get(status, '?') def do_status(wc_path, verbose): # Ein "Staffelholz" für den Client-Kontext erzeugen. ctx = svn.client.svn_client_ctx_t() def _status_callback(path, status): """Eine Rückruffunktion für svn_client_status.""" # Ausgeben des Pfades, ohne den Teil, der sich mit der Wurzel # des zu durchlaufenden Baums überlappt text_status = generate_status_code(status.text_status) prop_status = generate_status_code(status.prop_status) print '%s%s %s' % (text_status, prop_status, path) # Das Durchlaufen starten, _status_callback() als Rückruffunktion # verwenden. revision = svn.core.svn_opt_revision_t() revision.type = svn.core.svn_opt_revision_head svn.client.svn_client_status2(wc_path, revision, _status_callback, svn.core.svn_depth_infinity, verbose, 0, 0, 1, ctx) def usage_and_exit(errorcode): """Ausgabe des Verwendungshinweises und beenden mit ERRORCODE.""" stream = errorcode and sys.stderr or sys.stdout stream.write("""Verwendung: %s OPTIONS WC-PATH Options: --help, -h : Diesen Hinweis anzeigen --verbose, -v : Zeige alle Status, auch uninteressante """ % (os.path.basename(sys.argv[0]))) sys.exit(errorcode) if __name__ == '__main__': # Parse command-line options. try: opts, args = getopt.getopt(sys.argv[1:], "hv", ["help", "verbose"]) except getopt.GetoptError: usage_and_exit(1) verbose = 0 for opt, arg in opts: if opt in ("-h", "--help"): usage_and_exit(0) if opt in ("-v", "--verbose"): verbose = 1 if len(args) != 1: usage_and_exit(2) # Projektarchivpfad kanonisieren. wc_path = svn.core.svn_path_canonicalize(args[0]) # Eigentliche Arbeit machen. try: do_status(wc_path, verbose) except svn.core.SubversionException, e: sys.stderr.write("Fehler (%d): %s\n" % (e.apr_err, e.message)) sys.exit(1)
Wie in Beispiel 8.2, „Verwendung der Projektarchiv-Schicht mit Python“ verwendet
auch dieses Programm keine Pools und benutzt meist normale
Python-Datentypen. Der Aufruf von
svn_client_ctx_t()
ist irreführend, da
die öffentliche API von Subversion keine derartige Funktion
hat – das passiert nur da, wo die automatische
Spracherzeugung von SWIG eub webig durchscheint (die Funktion
ist eine Art Fabrikfunktion für die Python-Version der
entsprechenden komplizierten C-Struktur). Beachten Sie auch,
dass der an dieses Programm (wie auch beim letzten) übergebene
Pfad durch svn_path_canonicalize()
gefiltert wird, da ein unterlassen dazu
führen kann, dass die Annahmen der darunter liegenden
C-Bibliotheken nicht mehtr zutreffen, was wiederum einen
ziemlich plötzlichen und ungezwungenen Programmabsturz
bedeutet.