Störendes knacken der Lautsprecher bei Ubuntu 9.10

Bei Ubuntu 9.10 kommt es gelegentlich vor, dass die Boxen ein Knacken von sich geben. Dies geschieht, wenn man für längere Zeit die Soundkarte nicht benutzt. Wenn es auch kein großes Problem ist, kann das ganze doch ein wenig störend sein. Die Ursache des Problems sind die Energiespareinstellungen von ALSA. Auch wenn sie für ein Notebook recht nützlich sind, kann ich auf meinem Desktop darauf verzichten.

Um dieses Verhalten zu deaktivieren muss man in der Datei /etc/modprobe.d/alsa-base.conf die folgende Zeile abändern:

options snd-hda-intel power_save=10 power_save_controller=N

Das ganze sollte so aussehen:

options snd-hda-intel power_save=0 power_save_controller=N

Dies sollte die Energiesparmaßnahme abschalten und somit das Knacken verhindern.

Webservices verwenden mit Oracle 10gR2 und UTL_DBWS

Auf Grund meiner Arbeit musste ich mich in letzter Zeit mit Service Oriented Architecture (SOA) und der Verarbeitung von Services beschäftigen. In den letzten Tagen hatte ich die Aufgabe mit einer Oracle 10gR2 Datenbank einen Webservice zu verwenden. Hörte sich für mich leicht an, irgendwie wurde das ganze doch ein wenig komplizierter 😉

Zunächst gab es ein paar Verzögerungen bei der Installation des UTL_DBWS Paketes, welches für die Interaktion zwischen Webservice und Datenbank nötig war. Nachdem das Paket dann endlich einsatzbereit war, machte ich mich daran die Dokumentation zu verstehen. Leider konnte ich keine Beispiele finden, die direkt von Oracle zur Verfügung gestellt wurden. Daher machte ich mich auf die Suche nach Foren und Blogs. Interessant war dabei, dass Funktionen zu existieren scheinen, die nicht in der Dokumentation vorhanden sind. Da ich kein komplett funktionierendes Beispiel finden konnte, verbrachte ich die meiste Zeit mit Ausprobieren und Fehlersuche…

Nach recht viel Frust läuft meine Funktion nun jedoch und ich werde sie hier kurz präsentieren und hoffe, dass sie anderen Programmierern bei ihrer Suche helfen kann 😉

Der Service

Neben dem eigentlichen Service, den ich verarbeiten sollte, machte ich mich nach einiger Zeit auf die Suche nach anderen die ich ebenfalls zum testen verwenden konnte. Dabei fand ich die Seite WebserviceX.NET welche recht viele Webservices bereitstellt. Das folgende Beispiel wird die GetWeather Funktion des GlobalWeather Services verwenden. Diesem Webservice werden Stadt und Land übergeben und er gibt die aktuelle Wettervorhersage zurück.

Das Beispiel

Mein Ansatz beschreibt eine Datenbankfunktion, welche Stadt und Land als Parameter übergeben bekommt und die Antwort des Webservices als Oracle Xmltype zurück gibt.  Meine Implementation dieser Funktion sieht wie folgt aus:

create or replace function getWeather
 (city in varchar2, country in varchar2)
 return xmltype
is
 dbws_service sys.utl_dbws.SERVICE;
 dbws_call sys.utl_dbws.CALL;

 dbws_wsdl_url varchar2(128);
 dbws_namespace varchar2(128);
 dbws_service_qname sys.utl_dbws.QNAME;
 dbws_port_qname sys.utl_dbws.QNAME;
 dbws_operation_qname sys.utl_dbws.QNAME;

 request sys.Xmltype;
 result sys.Xmltype;
begin
 dbws_wsdl_url := 'http://www.webservicex.net/globalweather.asmx?wsdl';
 dbws_namespace := 'http://www.webserviceX.NET';

 dbws_service_qname := sys.utl_dbws.to_QName(dbws_namespace, 'GlobalWeather');
 dbws_port_qname := sys.utl_dbws.to_QName(dbws_namespace, 'GlobalWeatherSoap');
 dbws_operation_qname := sys.utl_dbws.to_QName(dbws_namespace, 'GetWeather');

 dbws_service := sys.utl_dbws.create_service(
 service_Name => dbws_service_qname,
 wsdl_document_location => URIFACTORY.getUri(dbws_wsdl_url));

 dbws_call := sys.utl_dbws.create_call(
 service_Handle => dbws_service,
 port_name => dbws_port_qname,
 operation_name => dbws_operation_qname);

 sys.utl_dbws.set_property(dbws_call, 'SOAPACTION_USE', 'TRUE');
 sys.utl_dbws.set_property(dbws_call, 'SOAPACTION_URI', 'http://www.webserviceX.NET/GetWeather');
 sys.utl_dbws.set_property(dbws_call, 'OPERATION_STYLE', 'document');

 request := xmltype('<GetWeather xmlns="http://www.webserviceX.NET">
 <CityName>' || city || '</CityName>
 <CountryName>' || country || '</CountryName>
 </GetWeather>');

 result := sys.utl_dbws.invoke(dbws_call, request);

 sys.utl_dbws.release_call(call_Handle => dbws_call);
 sys.utl_dbws.release_service(service_Handle => dbws_service);

 return result;
end getWeather;

Zunächst einmal kann man sehen, dass einige Variablen definiert wurden. Diese werden später für die Konfiguration des Serviceaufrufs verwendet. Zuerst muss der Speicherort der WSDL Datei bekannt gegeben werden. Diese Datei enthält Informationen, die für den Aufruf des Services benötigt werden. Außerdem muss man den Service Namespace angeben, den man in der WSDL Datei im targetNamespace Attribut des wsdl:definitions Knotens finden kann.

dbws_wsdl_url := 'http://www.webservicex.net/globalweather.asmx?wsdl';
dbws_namespace := 'http://www.webserviceX.NET';

Die folgenden drei Variablen Beschreiben den Service, Port und die Operation die aufgerufen werden sollen. Die ersten beiden Werte findet man gegen ende der WSDL Datei in der wsdl:service Sektion. Diese Sektion definiert den Service und seine Ports. In meinem Fall habe ich den ersten Port verwendet. Hier der entsprechende Auszug aus der WSDL Datei (siehe unterstrichene Attribute):

<wsdl:service name="GlobalWeather">
  <wsdl:port name="GlobalWeatherSoap" binding="tns:GlobalWeatherSoap">
    <soap:address location="http://www.webservicex.net/globalweather.asmx"/>
  </wsdl:port>
  ...
</wsdl:service>

Die Definition der Funktion findet man in der WSDL Datei unter wsdl:PortType. Hier werden alle Funktionen die der jeweilige Port anbietet aufgelistet. Hier die Definition der Operation die dieses Beispiel verwendet (siehe unterstrichenes Attribut):

<wsdl:operation name="GetWeather">
  <wsdl:documentation>
    Get weather report for all major cities around the world.
  </wsdl:documentation>
  <wsdl:input message="tns:GetWeatherSoapIn"/>
  <wsdl:output message="tns:GetWeatherSoapOut"/>
</wsdl:operation>

Nachdem diese Vorkehrungen getroffen sind, müssen jeweils eine Service und Call Instanz unter Verwendung dieser Parameter erstellt werden. Hierfür muss man die Funktionen UTL_DBWS.create_service() und UTT_DBWS.create_call() verwenden:

dbws_service := sys.utl_dbws.create_service(
 service_Name => dbws_service_qname,
 wsdl_document_location => URIFACTORY.getUri(dbws_wsdl_url));

 dbws_call := sys.utl_dbws.create_call(
 service_Handle => dbws_service,
 port_name => dbws_port_qname,
 operation_name => dbws_operation_qname);

Als nächstes müssen noch ein paar Einstellungen vorgenommen werden. Die Optionen die für dieses Beispiel interessant waren, konnte ich in der wsdl:binding Sektion der WSDL Datei finden:

<wsdl:operation name="GetWeather">
  <soap:operation soapAction="http://www.webserviceX.NET/GetWeather" style="document"/>
  ...
</wsdl:operation>

Nachdem somit alle Einstellungen vorgenommen sind, fehlt nur noch die eigentliche Nachricht, die wir an den Webservice senden wollen. In meinem Beispiel habe ich sie direkt als Xmltype definiert. Als Vorlage habe ich die Body Sektion genommen, die ich in der Beschreibung der GetWeather Funktion gefunden habe:

request := xmltype('<GetWeather xmlns="http://www.webserviceX.NET">
 <CityName>' || city || '</CityName>
 <CountryName>' || country || '</CountryName>
 </GetWeather>');

Nun kann man den Service mit dieser Nachricht aufrufen:

result := sys.utl_dbws.invoke(dbws_call, request);

Ein Funktionsaufruf um das ganze zu testen kann wie folgt aussehen:

begin
 dbms_output.put_line(getWeather('Southampton', 'United Kingdom').getStringVal());
end;
/

Und liefert bei Erfolg etwa folgende Ausgabe:

SQL> begin
 2      dbms_output.put_line(getWeather('Southampton', 'United Kingdom').getStringVal());
 3  end;
 4  /

<GetWeatherResponse xmlns="http://www.webserviceX.NET">
 <GetWeatherResult>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-16&quot;?&gt;
&lt;CurrentWeather&gt;
 &lt;Location&gt;Southampton / Weather Centre, United Kingdom (EGHI) 50-54N 001-24W 0M&lt;/Location&gt;
 &lt;Time&gt;Feb 23, 2010 - 10:50 AM EST / 2010.02.23 1550 UTC&lt;/Time&gt;
 &lt;Wind&gt; from the E (100 degrees) at 8 MPH (7 KT) (direction variable):0&lt;/Wind&gt;
 &lt;Visibility&gt; greater than 7 mile(s):0&lt;/Visibility&gt;
 &lt;SkyConditions&gt; mostly cloudy&lt;/SkyConditions&gt;
 &lt;Temperature&gt; 39 F (4 C)&lt;/Temperature&gt;
 &lt;DewPoint&gt; 37 F (3 C)&lt;/DewPoint&gt;
 &lt;RelativeHumidity&gt; 93%&lt;/RelativeHumidity&gt;
 &lt;Pressure&gt; 29.26 in. Hg (0991 hPa)&lt;/Pressure&gt;
 &lt;Status&gt;Success&lt;/Status&gt;
&lt;/CurrentWeather&gt;</GetWeatherResult>
</GetWeatherResponse>

PL/SQL procedure successfully completed

Ich hoffe dieses Beispiel hilft anderen mehr als diejenigen, die ich gefunden habe 😉

Ubuntu: Configs von gelöschten Paketen entfernen

Ich mag es allgemein wenn meine Systeme sauber installiert sind und kein unnötiger Ballast mitgeschleppt wird. Die Paketverwaltungssysteme, welche von Linux Distributionen verwendet werden, leisten hier meist sehr gute Dienste. Wenn man sich auf sie stützt kann man sich meistens auf ein stabiles und sauberes System verlassen.

Wenn man jedoch des Öfteren mal neue Programme ausprobiert, sollte man sich Gedanken darüber machen, wie man sie am besten installiert und deinstalliert. Dabei kann man entweder auf grafische Verwaltungssysteme wie die Synaptic-Paketverwaltung oder Kommandozeilen Tools wie apt-get zurückgreifen.

Beim entfernen von Paketen sollte man immer überlegen, ob die Möglichkeit besteht, dass man diese Anwendung noch einmal verwendet. Sollte dies der Fall sein kann es nützlich sein die Konfigurationsdateien zu behalten. Dazu kann man die Programme einfach nur mit der bevorzugten Anwendung entfernen, bei apt-get sähe dies zum Beispiel so aus:

apt-get remove <Paketliste>

Damit würden die Programme in der Paketliste vom System entfernt, die Konfigurationsdateien jedoch beibehalten. Nun kann es jedoch sein, dass man die Konfigurationsdateien nicht behalten möchte. Mögliche Situationen hierfür sind, wenn man das Programm vermutlich nie wieder benutzt oder aber bei der Konfiguration ein paar Probleme hatte und von vorne beginnen möchte 😉

Um Programme inklusive der Konfigurationsdateien zu entfernen muss man in der Synaptic-Paketverwaltunge Zum vollständigen Entfernen vormerken auswählen. Mit apt-get verwendet man einfach die Aktion purge anstatt remove:

apt-get purge <Paketliste>

Die Synaptic-Paketverwaltung erlaubt es auch nachträglich die Konfigurationsdateien von deinstallierten Paketen zu entfernen. Dazu kann man den Filter Nicht installiert (zurückgebliebene Konfiguration) verwenden. Dieser zeigt die entsprechenden Pakete an und man kann sie mittels Zum vollständigen Entfernen vormerken komplett entfernen. Auf meinem Server habe ich jedoch keine grafische Oberfläche installiert und deshalb ist das ganze etwas komplizierter gewesen. Allerdings konnte ich mir durch ein paar Nachforschungen eine einfache Kommandozeile zusammenbauen die den selben Effekt hat:

dpkg --purge $(dpkg -l | grep ^rc | cut -d " " -f 3 | cut -d " " -f 1)

Dabei gebe ich mit dpkg -l eine Liste der installierten Pakete aus und beschränke sie durch grep ^rc auf die deinstallierten (r = removed) Pakete mit noch immer vorhandenen Konfigurationsdateien (c = config-files). Das ganze wird dann mit dem Programm cut noch etwas gestutzt und schon erhält man eine Liste dieser Pakete. Auf meinem System hat die Kommandozeile funktioniert und alle zurückgebliebenen Konfigurationsdateien entfernt. Benutzen allerdings auf eigene Gefahr 😉

1&1 Webspace und Probleme mit WordPress

Seit ich den Blog auf meinem 1&1 Webspace installiert habe ärgerten mich anhaltende Probleme. Die Installation von Plug-Ins endete oft in Problemen die sie unbenutzbar machten und wenn mehrere Plug-Ins aktiviert waren streikte oft der visuelle Editor. Als Resultat musste ich um neue Posts zu erstellen erst einmal einige Plug-Ins deaktivieren um überhaupt etwas machen zu können. Hinzu kamen noch immer wieder auftretende Fehler der Marke “500 Internal Server Error”.

Nach reichlich probieren und viel Frustration überwand ich mich gestern eine Nachricht an den 1&1 Support zu schreiben. Heute kam die Antwort und was soll ich sagen…Sie scheinen wirklich Ahnung von den Servern und ihren Problemen zu haben 😉

Mir wurde geraten WordPress im PHP5 Kontext auszuführen, was nicht die Standardeinstellung zu sein scheint. Dabei folgte ich dem Tipp eine .htaccess Datei in dem betroffenen Verzeichnis zu erstellen und die folgenden magischen Zeilen darin zu verewigen:

AddType x-mapp-php5 .php
AddHandler x-mapp-php5 .php

Seither scheint alles zu funktionieren. Man soll den Tag zwar nicht vor dem Abend loben, aber so macht bloggen doch viel mehr Spaß 😉

Hier nochmal ein Danke an den Support 😛

SSH Verbindung vereinfachen mit ~/.ssh/config

Wer etwas häufiger mit SSH (Secure Shell) auf unterschiedlichen Systemen arbeitet wird schnell feststellen, dass es nervig wird jedes mal die komplette Kommandozeile in die Shell einzugeben. Meist sehen einfache Kommandozeilen wie folgt aus:

ssh foo@bar.server.com

Nicht so schwierig, allerdings wird es nervig, wenn man den entsprechenden Server nur per IP Adresse ansprechen kann und viele weiter Optionen verwenden möchte um z.B. die Art der Anmeldung zu definieren oder etwas schwierigere Aufgaben wie SSH Tunneling zu verwenden.

SSH bietet hierfür jedoch eine sehr einfache Lösung um den Aufwand etwas zu verringern, nämlich die Dateien ~/.ssh/config sowie das globale Gegenstück, welches bei Ubuntu unter /etc/ssh/ssh_config liegt. In diesen können Verbindungskonfigurationen abgelegt werden. So können z.B. Synonyme für bestimme Konfigurationen festgelegt werden die dann anstelle der gesamten Parameterliste verwendet werden können. Eine mögliche Konfiguration könnte wie folgt aussehen:

Host Bar
    Hostname bar.server.com
    User foo
    RSAAuthentication yes
    PasswordAuthentication no

Dieser Konfigurationsblock erstellt das Synonym Bar. Die Optionen beschreiben den Verbindungsaufbau zum Server bar.server.com als Benutzer foo und es soll eine Public Key Authentisierung verwendet werden. Aufgerufen werden kann das ganze dann einfach mit:

ssh Bar

Das ganze funktioniert auch mit Autocompletion, was eine Vervollständigung mit der Tabulatortaste ermöglicht. Neben der einfachen Anmeldung verwende ich diese Methode um meinen Server als SOCKS-Proxy zu verwenden. Dazu kann man die folgende Konfiguration verwenden:

Host Bar-Proxy
    HostName bar.server.com
    User foo
    RSAAuthentication yes
    PasswordAuthentication no
    DynamicForward 4321

Wobei nun localhost:4321 von Anwendungen wie Browsern als Proxy verwendet werden kann, was den gesamten Traffic über den Server leitet. Weitere Optionen die verwendet werden können findet man in der Manpage ssh_config.

Neues Theme

Auch wenn ich das standard WordPress Theme nicht verkehrt fand, habe ich mich in der recht umfangreichen Auswahl umgesehen. Da ich eigentlich eher ein Minimalist bin fand ich recht viele der Themes ziemlich überladen. Allerdings gibt es auch andere Vertreter meiner Zunft und so habe ich doch ein paar recht minimalistische gefunden.

Ich habe mich letztendlich fuer das monochrome Theme entschieden. Wie ich finde ein recht gutes, nicht zu überladenes Theme.

Linux Logs überwachen mit phpLogCon

Wenn es zu Fehlern auf einem Server kommt ist die erste Anlaufstelle immer das Auswerten der Log Dateien die von dem Betriebssystem des Servers erstellt werden. Linux ist in dieser Hinsicht ein sehr geschwätziger Vertreter 😉

Während ich viele Serverstatistiken, die größtenteils auf Hardware ausgelegt sind, mit Munin überwachen kann, fehlte mir noch eine komfortable Art um die Serverlogs auszuwerten. Bisher beschränkte ich mich dabei auf eine Verbindung zum Server per SSH und das manuelle Auswerten der Logs. Allerdings ist nicht immer die Möglichkeit gegeben eine SSH Verbindung aufzubauen und deshalb habe ich mich nach einer einfacheren Variante umgesehen.

Was ich gefunden habe ist phpLogCon. Es ist ein in PHP geschriebenes System um die Systemlogs auszulesen und zu analysieren. Dabei kann es sowohl simple Dateien als auch Datenbanken als Quelle verwenden. Die Logs werden aufbereitet, Statistiken erstellt und man kann einfach in den Lognachrichten suchen.

Die Installation gestaltet sich recht einfach. In meiner Installation musste ich noch PHP5-cgi und PHP5-GD hinzufügen, wobei die GD Erweiterung für die Statistikfunktion benötigt wird. Danach einfach den beiliegenden Installationsanweisungen folgen. Kurz und knapp:

  1. src Verzeichnis auf den Webserver kopieren
  2. configure.sh und secure.sh in das Verzeichnis kopieren, Rechte zum ausführen setzen und ausführen
  3. phpLogCon aufrufen und Konfigurationsanweisungen folgen

Ich habe das Verzeichnis noch mit beschränkten Zugangsrechten belegt, damit nicht jeder meine Logs einsehen kann. Soweit sieht das ganze recht gut aus, mal sehen wie viel es bringt, wenn ich die Logs wirklich einmal benötige 😉

Kontaktformular und der visuelle Editor

In den letzten Tagen hatte ich ein paar Probleme mit dem Kontaktformular Plugin Contact Form 7 und dem visuellen Editor für Artikel unter WordPress 2.9.1. Jedes mal, wenn ich den visuellen Editor verwenden wollte startete er nicht korrekt und ich bekam eine ähnliche Ansicht wie beim HTML Editor. Nach ein paar frustrierenden Versuchen das ganze zu beheben probierte ich unterschiedliche andere Plugins aus. Nach mehreren Plugins die das selbe Problem verursachten, habe ich nun eines gefunden das funktioniert ohne den visuellen Editor in die Knie zu zwingen.

Die neue Formularseite verwendet jetzt das Fast and Secure Contact Form. Bisher habe ich damit keine Probleme.

wget Rekursion

Ich habe gestern einen ziemlich ärgerlichen Fehler in meinem Backupskript gefunden der natürlich erst auffiel als ich das Backup dann wirklich brauchte… Der Fehler hängt mit wget zusammen welches ich benutze um von einem anderen Server die Daten über FTP zu sichern.  Bisher verwendete ich die folgende wget Zeile:

wget -rq 'ftp://example.org'

Zu dieser kam ich durch das lesen der Manpage von wget:

-r
 --recursive
 Turn on recursive retrieving.

In Verbindung mit Maschinen und Macro Definitionen in der Datei .netrc wurde die Verbindung zum Server aufgebaut und der Verzeichnisbaum rekursiv durchlaufen… so der Plan 😉

Dummerweise hatte ich die nächste Option der Manpage überlesen:

-l depth
 --level=depth
 Specify recursion maximum depth level depth.  The default maximum
 depth is 5.

Zeigt mal wieder, dass man Manpages immer sehr gewissenhaft und gründlich lesen sollte, auch wenn ich mir einen Hinweis auf die Option -l gewünscht hätte. Um das ganze aber noch zu übertreffen fand ich natürlich noch die option -m die irgendwie genau das erreicht was ich wollte…

-m
 --mirror
 Turn on options suitable for mirroring.  This option turns on
 recursion and time-stamping, sets infinite recursion depth and
 keeps FTP directory listings.  It is currently equivalent to -r -N
 -l inf --no-remove-listing.

So entsteht eine neue wget Zeile:

wget -mq 'ftp://example.org'

Schon klappt auch das Update mit dem benachbarten Server 😉

Synchronisieung mit Git

Wenn man mehrere Systeme hat an denen man arbeitet kommt öfters mal die Frage auf wie man sie miteinander synchronisieren kann. Natürlich ist das Kopieren per Hand dabei nicht wirklich eine nutzbare Alternative, da dabei immer ein paar Änderungen und Dateien untergehen können. Allerdings gibt es einige Tools die das ganze erleichtern sollen. Dabei reicht die Palette von einfachen Skripten über Programme wie rsync und rsnapshot bis zu Versionsverwaltungssystemen wie Subversion, CVS oder Git.

Obwohl rsync oder rsnapshot durchaus zur Synchronisierung von Verzeichnissen und Erstellung von Backups gedacht sind, fand ich es etwas umständlich mit ihnen meine beiden Systeme auf dem selben Stand zu halten. Deshalb habe ich mich für die Synchronisationsstrategie mit Versionsverwaltungssystemen entschieden.

Ich hatte zwar bereits Erfahrungen mit CVS und Subversion gesammelt, allerdings auch viel über die Vorzüge von Git gehört. Aus diesem Grund wollte ich es einmal ausprobieren. Bisher bereue ich diese Entscheidung nicht. Die Synchronisation läuft sehr schnell, es ist einfach zu bedienen und die dezentrale Architektur ermöglicht es zu jeder Zeit ein Projekt auf dem lokalen Rechner zu starten.

In meinem Fall habe ich natürlich meinen Homeserver in die ganze Geschichte mit einbezogen, indem ich meine Repositories auf ihm ablege und meine beiden Rechner mit dem Tracking feature synchronisiere. Ein Vorteil von diesem Vorgehen ist natürlich, dass meine gesamten Daten in einer weiteren Kopie auf dem Server vorliegen. Außerdem erleichtert es das Erstellen von Backups an einem zentralen Punkt, wobei sich für diese Aufgabe natürlich rsync oder rsnapshot anbieten 😉

Neben diesen eher sicherheitsrelevanten Aspekten ermöglicht mir gitweb noch den einfachen Zugriff auf meine Daten von jedem PC mit Internet. Da es sich bei gitweb um ein CGI Skript handelt musste ich bei lighttpd das CGI Modul aktivieren. Das geht recht einfach mit:

sudo lighttpd-enable-mod cgi
sudo /etc/init.d/lighttpd force-reload

Damit ich meine Repositories vom Internet jedoch durchstöbern konnte musste ich dies zunächst noch erlauben. Dafür ist ein Eintrag in der Datei /etc/lighttpd/conf-enabled/10-cgi.conf nötig:

$HTTP["host"] =~ "virtual.host.example.org" {
 alias.url += ( "/cgi-bin/" => "/pfad/zum/cgi/verzeichnis" )
 $HTTP["url"] =~ "^/cgi-bin/" {
 cgi.assign = ( "" => "" )
 }
}

Das sollte den Zugriff von außen erlauben. Alles in allem kann ich dieses Vorgehen nur empfehlen. Es erleichtert mir das Synchronisieren ungemein und ich kann sogar zu älteren Versionen zurückkehren. Außerdem ist Git ein perfektes Tool für Programmierer um ihren Code zu sichern und alle Änderungen beizubehalten.