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!
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.
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.
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
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.
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.
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. |
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
.
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.
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.
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 }
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.
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.
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.
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!
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2015/04/Perl
"Alternativer Gebrauch", Michael Schilli, Linux-Magazin 2013/01, http://www.linux-magazin.de/Ausgaben/2013/01/Perl-Snapshot
Github API Rate Limits, https://developer.github.com/v3/search/#rate-limit
Github API Scopes, https://developer.github.com/v3/oauth/#scopes
Pull request für einen Bug in update_ref()
des Moduls Net::GitData:
https://github.com/fayland/perl-net-github/pull/58
"Ry’s Git Tutorial", http://rypress.com/tutorials/git/plumbing