Projektschnupperer (Linux-Magazin, April 2015)

Der Projekthoster Github beherbergt nicht nur die Code-Repositories vieler bekannter Open-Source-Projekte, sondern bietet auch eine durchdachte API an, mit der sich herrlich in ihnen herumschnüffeln lässt.

Kaum ein Softwareprojekt kommt heute mehr ohne Git aus, wer einmal die Performancevorteile erschnuppert hat, kommt sich auf SVN vor wie im letzten Jahrhundert. Und da Github eine schöne UI drumherum gewickelt hat, kostenlos und zuverlässig die Daten speichert, und die Zusammenarbeit mit Pull Requests so spielend von der Hand geht, schwören viele Entwickler wie auch ich auf den Git-Hoster aus San Francisco. Über die Jahre haben sich so in meinem Account 57 öffentlich sichtbare Repositories angesammelt, die meisten beherbergen irgendwelche CPAN-Module, aber auch die Textdaten für meinen Blog usarundbrief.com lagern dort ([2]). Höchste Zeit, mal mit Perl in den dazugehörigen Metadaten zu schnüffeln!

Nicht nur von Hand

Gerade im Zusammenhang mit automatischen Buildsystemen erfolgt der Zugriff auf die Metadaten der Git-Repositories nicht mehr nur von Hand im Browser. Vielmehr saugen automatisch ablaufende Skripts volles Rohr per API aus dem Datenfüllhorn. Zur Ansteuerung aus Perl heraus findet sich auf dem CPAN unter anderem die Modulsammlung Net::GitHub von Fayland Lam. Mit den erforderlichen Zugriffsrechten ausgestattet kann der API-Nutzer damit nicht nur lesend zugreifen, sondern auch aktiv modifizieren, wie zum Beispiel Pull-Requests absetzen oder gar neuen Code einspeisen.

Abbildung 1: Die Autorenseite auf Github listet eigene Repositories und Beiträge zu anderen auf. Die API bietet ähnliches.

Abbildung 2: Auch ohne Auth-Token gibt die Github-API Informationen zu Repos oder Usern preis.

Damit Github weiß, wer die API-Requests absetzt und notfalls korrigierend einschreiten kann, liegt optional jeder Anfrage an die REST-API ein Authentisierungstoken bei. Diesen erhält der User nicht nur in der Browser-UI, nachdem er sich eingeloggt hat, sondern wahlweise von einem Skript wie in Listing 1, das das Passwort des Users abfragt, die Daten per SSL an den Github-Server schickt und einen Access-Token zurückbekommt. Statt eines Passworts schickt der Client dann in Zukunft den Token mit und wird so zum Server vorgelassen, entsprechend der bei der Erstellung des Tokens vorgegebenen maßgeschneiderten Zugriffsrechte. Damit die weiter unten vorgestellten Testskripts den Token finden, legt Listing 1 ihn in der Datei ~/.githubrc im YAML-Format im Home-Verzeichnis ab. Der Aufruf

    $ ./token-get 
    Password: *****
    /Users/mschilli/.githubrc written

vollzieht die notwendigen Schritte für den in Zeile 11 hartkodierten User.

Listing 1: token-get

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use Net::GitHub;
    04 use Sysadm::Install qw( :all );
    05 use YAML qw( DumpFile );
    06 
    07 my $gh_conf = (glob "~")[0] . "/.githubrc";
    08 
    09 my $pw = password_read("Password:");
    10 my $gh = Net::GitHub::V3->new( 
    11   login => 'mschilli', pass => $pw );
    12 
    13 my $oauth = $gh->oauth;
    14 my $o = $oauth->create_authorization( {
    15   scopes => [],
    16   note   => 'empty scope test',
    17 } );
    18 
    19 umask 0077;
    20 DumpFile $gh_conf, 
    21   { token => $o->{token} };
    22 
    23 print "$gh_conf written\n";

Abbildung 3: Die Github-Seite listet alle bisher generierten Access Tokens auf.

Zu beachten ist, dass das Kommentarfeld note beim Aufruf der Methode create_authorization für jeden neu angeforderten Token anders sein muss, sonst verweigert Github die Herausgabe des Tokens und gibt als Grund eine falsche User/Passwort-Kombination an. Der wahre Grund ist aber, dass Github die Tokens unter diesem Schlüssel auf der Account-Seite des Users auflistet, wo letzterer sie modifizieren oder widerrufen kann (Abbildung 3). Außerdem gibt der Parameter scope an, welche Rechte der Account-Besitzer den tokenschwingenden Clients in Zukunft einräumt. Bleibt er leer, wie in Listing 1, gewährt der Server nach [4] nur lesenden Zugriff auf öffentlich verfügbare Daten. Einer oder mehrere Einträge wie "user" oder "repo" im Scope erlauben dem Client hingegen später Lese- und Schreibzugriffe auf Userdaten (wie zum Beispiel dessen Emailadresse) oder Code-Commits.

Mehr mit Ausweis

Wie aus den Nutzungsbedingungen in [3] hervorgeht, räumt Github Clients, die sich authentifizieren, bis zu 5000 Anfragen pro Stunde ein, während anonyme Anfragen auf 60 pro Stunde und IP begrenzt sind. Die Search-API hingegen, die nach Suchmustern in Repository-Namen oder eingechecktem Code sucht, gibt sich etwas geiziger, sie erlaubt 20 Anfragen pro Minute mit Token, und ohne nur 5. Dass die API auch ohne Einloggen funktioniert, zeigt Abbildung 2 mit einer Abfrage der Metadaten des Github-Repositories mschilli/log4perl.

Wieviele Anfragen noch durchgehen, bis der Server den Hahn zudreht, teilt er jeweils im HTTP-Header der Antwort mit. Abbildung 4 zeigt ein Beispiel von der Kommandozeile ohne Token, bei dem während der angefangenen Stunde schon sieben Abfragen abgeschickt wurden und demnach noch 53 übrig sind, bevor der Server den Zähler zum Stundenschlag wieder auf 60 hochsetzt.

Abbildung 4: Von 60 erlaubten Anfragen pro Stunde hat der Client noch 53 übrig, bevor der Server den Zähler zum angegebenen Zeitpunkt zurücksetzt.

Das Skript in Listing 2 fragt bei Github die Account-Metadaten ab des auf der Kommandozeile angegebenen Users ab. Der User weist sich mittels des CPAN-Moduls YAML aus der YAML-Datei ~/.githubrc extrahierten Access-Tokens aus. Neben einer Riesenlatte von anderen Feldern steht im zurückkommenden JSON-Wust auch die Anzahl der im Account angelegten öffentlich sichtbaren Repositories, und Listing 2 gibt sie in Zeile 17 schlicht aus:

    $ ./repo-user mschilli
    Mike Schilli owns 57 public repos
    $ ./repo-user torvalds
    Mike Schilli owns 2 public repos

Torvalds - Schilli: 2:57

Das Skript repo-user offenbart, dass Linus Torvalds auf Github erstaunlicherweise nur 2 Repositories angelegt hat! Die ersten beiden Zeilen in Listing 2 schnappen sich den vorher von Listing 1 abgelegten Access-Token und die Methodenkette ->user->show() greift auf die Account-Daten des Users zu.

Listing 2: repo-user

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use Net::GitHub;
    04 use YAML qw( LoadFile );
    05 
    06 my( $user ) = @ARGV;
    07 die "usage: $0 user" if !defined $user;
    08 
    09 my $gh_conf = (glob "~")[0] . "/.githubrc";
    10 
    11 my $gh = Net::GitHub->new( access_token => 
    12   LoadFile( $gh_conf )->{ token }
    13 );
    14 
    15 my %data = $gh->user->show( $user );
    16 
    17 print "$data{ name } owns ",
    18   "$data{ public_repos } public repos\n";

Um festzustellen, welche Repositories ein Autor auf Github eingetütet hat, hilft die Methode repos->list() in Listing 3, die aus den zurückkommenden Metadaten jeweils nur den Repository-Namen extrahiert und ausgibt. Alternativ nimmt die Methode list_user() einen Usernamen entgegen, und wer "torvalds" eingibt, sieht, dass die beiden Repositories des Linux-Vaters "linux" und "subsurface" heißen.

Listing 3: repos

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use Net::GitHub;
    04 use YAML qw( LoadFile );
    05 
    06 my $gh_conf = (glob "~")[0] . "/.githubrc";
    07 
    08 my $gh = Net::GitHub->new( access_token => 
    09   LoadFile( $gh_conf )->{ token }
    10 );
    11 
    12 my @repos = $gh->repos->list( );
    13 
    14 for my $repo ( @repos ) {
    15     print $repo->{ name }, "\n";
    16 }

Abbildung 5: Listing 3 spuckt alle Github-Repositories des angegebenen Users aus.

Gewinner nach Punkten

Wer wissen möchte, welche Repositories es zu einem bestimmten Thema gibt, wird oft durch die Search-API fündig. Ich habe zum Beispiel vor vielen Jahren mal ein Projekt namens "Log4perl" ins Leben gerufen und auf Github abgestellt. Listing 4 sucht nun nach allen Repositories auf Github, deren Namen den String "log4perl" enthalten. Erstaunlicherweise liefert das Skript satte 117 Treffer!

    $ ./repo-list-multi
    mschilli/log4perl (84.5)
    cowholio4/log4perl_gelf (15.2)
    TomHamilton/Log4Perl (14.1)
    lammel/moosex-log-log4perl (9.7)
    ...
    $ eg/repo-list-multi  | wc -l
    117

Github begrenzt die Anzahl der vom Server zurückgeschickten Treffer von Haus aus auf 100, und wer mehr erwartet, kann entweder die Paginierungsgröße der Trefferrückgabe mit dem Parameter per_page hochsetzen oder, wie in Listing 4 gezeigt, nach dem Eintrudeln des ersten Hunderterpacks mit has_next_page() nachfragen, ob zum Query noch weitere Ergebnisse vorliegen. Ist das der Fall, holt ein nachfolgender Aufruf von next_page() im alten Search-Objekt wie in Listing 4 gezeigt den nächsten Schub Daten. Den Suchparameter sort setzt das Skript in Zeile 14 auf "stars", und zusammen mit einem Wert von desc (für descending/absteigend) ist das Ergebnis nach Beliebtheit der Repositories sortiert. Wer sich auf Github an einem Projekt interessiert, klickt auf dessen Star-Symbol und wird zukünftig über Neuigkeiten informiert, falls sich im Projekt etwas tut. Viele solcher "Stars" geben einen Hinweis darauf, wie populär ein Projekt ist. Im Ergebnis steht die Anzahl der Stars im Feld stargazers_count und den Namen des zugehörigen Projekts in full_name.

Listing 4: repo-list-multi

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use Net::GitHub;
    04 use YAML qw( LoadFile );
    05 
    06 my $gh_conf = (glob "~")[0] . "/.githubrc";
    07 
    08 my $gh = Net::GitHub->new( access_token => 
    09   LoadFile( $gh_conf )->{ token }
    10 );
    11 
    12 my %data = $gh->search->repositories( {
    13   q     => 'log4perl',
    14   sort  => 'stars',
    15   order => 'desc',
    16 });
    17 
    18 my @items = ();
    19 push @items, @{ $data{ items } };
    20 
    21 while( $gh->search->has_next_page() ) {
    22   my %data = $gh->search->next_page();
    23   push @items, @{ $data{ items } };
    24 }
    25 
    26 for my $item ( @items ) {
    27   printf "%s (%.1f)\n", 
    28     $item->{ full_name }, 
    29     $item->{ stargazers_count };
    30 }

Wer übrigens nicht das offizielle Repo- auf github.com abfragen möchte sondern eine lokale Github-Enterprise-Installation, kann an die Methode new der Klasse Net::GitHub als einleitend drittes Parameterpaar api_url => https://.../api/v3 angeben und bekommt seine Informationen dann von dort.

In den Tiefen des Codes

Wer im eingecheckten Code nach Mustern sucht, dem gibt Listing 5 einen Vorgeschmack davon, was die Github-API zu diesem Thema bietet. Die Methode search() entlockt dem Github-Objekt ein Search-Objekt, dessen Methode code() wiederum eine Suche im Repository-Code einleitet. Der Query

  snickers in:file language:perl repo:mschilli/log4perl

sucht im Repository mschilli/log4perl in Dateien, die Perl-Code enthalten nach dem Wort "snickers". Das Ergebnis

    $ ./repo-search 
    lib/Log/Log4perl.pm
    lib/Log/Log4perl/Config.pm

zeigt, dass im Code des Repositories log4perl in zwei Dateien das Wort "snickers" auftaucht, was nicht nur bestätigt, dass dem Autor Schokoriegel schmecken, sondern auch, dass er gerne mit schräg anmutendem Humor überrascht.

Listing 5: repo-search

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use Net::GitHub;
    04 use YAML qw( LoadFile );
    05 
    06 my $gh_conf = (glob "~")[0] . "/.githubrc";
    07 
    08 my $gh = Net::GitHub->new( access_token => 
    09   LoadFile( $gh_conf )->{ token }
    10 );
    11 
    12 my %data = $gh->search->code( 
    13   { q => 'snickers in:file language:perl' .
    14          ' repo:mschilli/log4perl' 
    15   } );
    16 
    17 for my $item ( @{ $data{ items } } ) {
    18   print $item->{ path }, "\n";
    19 }

Code unterjubeln

Um einem Software-Projekt auf Github per API einen Commit mit einer aufgefrischten README-Datei unterzujubeln, gilt es zunächst, bis ganz nach unten in den Git-Hades abzusteigen. Unter der Haube arbeitet Git nämlich mit ganz einfachen Datenstrukturen, von denen das Kommandozeilentool git normale Anwender normalerweise vollständig abschottet. Eine Datei im Repository stellt Git als "Blob" dar, eine Ansammlung von Blobs in einem Verzeichnis ist ein "Tree", und ein Tree mit einer Check-in-Notiz bildet einen Commit (Abbildung 6). Unter [6] finden Interessierte eine fundierte Einführung in das Thema.

Abbildung 6: Unter Gits Haube bestehen Commits aus Trees und Trees aus Blobs und weiteren Trees.

Bevor das Skript in Listing 6 nun einen Commit in das Test-Projekt "mschilli/apitest" auf Github einspielen kann, muss erst ein neuer Token mit mehr Rechten her. Der in Listing 1 eingeholte Token berechtigt nur zum Lesen der Repository-Daten, um auch noch den Schreibzugriff zu erlangen, muss Zeile 15 in Listing durch

    scopes => ['public_repo'],

ersetzt und der im Parameter note abgelegte Kommentar entsprechend abgewandelt werden. Ein erneuter Aufruf von token-get speichert dann einen neu eingeholten Token mit weitergehenden Rechten in der Tokendatei.

Blobs, Trees, Commits und Refs

Anschließend erzeugt der Aufruf des Skripts readme-upd in Listing 6 einen neuen Blob der README-Datei mit den Zugriffsrechten 0644 und definiert einen Tree dazu, der den Blob enthält. Hinzu kommt noch der Basis-Tree base_tree in Zeile 40, den das Skript aus dem bisher letzten Commit des Master-Branches herausfieselt, damit auch eventuell schon vorhandene weitere Dateien im Repository weiter bestehen bleiben.

Listing 6: readme-upd

    01 #!/usr/local/bin/perl -w
    02 use strict;
    03 use Net::GitHub;
    04 use YAML qw( LoadFile );
    05 use Digest::SHA1 qw( sha1_hex );
    06 
    07 my $gh_conf = (glob "~")[0] . "/.githubrc";
    08 
    09 my $gh = Net::GitHub->new( access_token => 
    10   LoadFile( $gh_conf )->{ token }
    11 );
    12 
    13 $gh->set_default_user_repo( 
    14   "mschilli", "apitest" );
    15 my $gdata = $gh->git_data();
    16 
    17 my $head;
    18 
    19 for my $ref ( @{ $gdata->refs() } ) {
    20   if( $ref->{ ref } eq 
    21       "refs/heads/master" ) {
    22     $head = $ref->{ object }->{ sha };
    23     last;
    24   }
    25 }
    26 
    27 die "Head not found" if !defined $head;
    28 
    29 my $commit_old = $gdata->commit( $head );
    30 my $tree_old = 
    31   $commit_old->{ tree }->{ sha };
    32 
    33 my $content = "Updated by script $0.";
    34 
    35 my $tree = $gdata->create_tree( { 
    36   tree => [ { 
    37     path => "README", mode => "100644",
    38     type => "blob",   content => $content,
    39   } ],
    40   base_tree => $tree_old,
    41 } );
    42 
    43 my $commit = $gdata->create_commit( {
    44   message => "Updated via API",
    45   author => {
    46     name =>  "Mike Schilli",
    47     email => 'm@perlmeister.com',
    48     date =>  "2011-11-11T11:11:11+08:00",
    49   },
    50   parents => [ $head ],
    51   tree => $tree->{ sha },
    52 } );
    53 
    54 $gdata->update_ref( "heads/master", 
    55   { sha => $commit->{ sha } } );

Den Tree wickelt dann Zeile 43 in einen Commit als Abkömmling des bisherigen HEAD-Zeiger des Master-Branches des Projekts, der als Datum den Karnevalsbeginn angibt. Das ist ein schönes Beispiel dafür dass sich Datums- und Autorenangaben völlig willkürlich festsetzen lassen, Github glaubt da wie jedes Remote-Repository blind den erzeugten Daten auf dem lokalen Rechner.

Zeiger zeigen

Eingangs hatte Zeile 19 die Referenzen auf die Entwicklungszweige des Repos eingeholt und Zeile 22 sich den SHA1-Wert des Master-Branches geschnappt, der in Git unter refs/heads/master steht. Am Ende verbiegt nun Zeile 54 den HEAD-Zeiger des Master-Branches des Projekts auf den neuen Commit, was ohne Gewaltanwendung (force => 0)geht, da der neue Commit geradlinig vom alten abstammt. Abbildung 7 zeigt, wie sich schließlich der automatisch eingespielte Commit auf der Github-Webseite des Projekts ausnimmt.

Abbildung 7: Der per API abgesetzte Commit ist kurz darauf auf der Github-Webseite sichtbar.

Bei Redaktionsschluss hatte das Modul Net::GitHub auf dem CPAN in der Version 0.71 noch einen Fehler in der Implementierung von update_ref(), doch ein schnell fabrizierter Pull-Request schuf Abhilfe ([5]), und mit etwas Glück hat der Modulautor den Patch zum Erscheinungstermin dieses Artikels bereits eingespielt und eine neue Version auf dem CPAN freigegeben.

Wer möchte, kann das ein oder andere vorgestellte Abfrageskript einmal am Tag als Cron-Job laufen lassen und erhält dann eine Zeitreihe, die grafisch den Github-Fleiß des Users veranschaulicht. Oder der Boss möchte alarmiert werden, falls sein Mitarbeiter einen plötzlichen Produktivitätsschub durchmacht!

Infos

[1]

Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2015/04/Perl

[2]

"Alternativer Gebrauch", Michael Schilli, Linux-Magazin 2013/01, http://www.linux-magazin.de/Ausgaben/2013/01/Perl-Snapshot

[3]

Github API Rate Limits, https://developer.github.com/v3/search/#rate-limit

[4]

Github API Scopes, https://developer.github.com/v3/oauth/#scopes

[5]

Pull request für einen Bug in update_ref() des Moduls Net::GitData: https://github.com/fayland/perl-net-github/pull/58

[6]

"Ry’s Git Tutorial", http://rypress.com/tutorials/git/plumbing

Michael Schilli

arbeitet als Software-Engineer bei Yahoo in Sunnyvale, Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen der Skriptsprache Perl. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.