Der Projekthoster Github beherbergt nicht nur die Code-Repositories vieler bekannter Open-Source-Projekte, sondern bietet auch ein durchdachtes API an, mit dem sich in ihnen herrlich herumschnüffeln lässt.
Kaum ein Softwareprojekt kommt heute ohne Git aus. Wer einmal die Performancevorteile registriert hat, dem kommt SVN vor wie aus der Ära der Postkutsche. Da Github ein schönes UI drum herum gewickelt hat, kostenlos und zuverlässig die Daten speichert und Pull Requests so spielend von der Hand gehen, schwören viele Entwickler wie ich auf den Git-Hoster aus San Francisco.
Über die Jahre haben sich so in meinem Account 57 öffentlich sichtbare Repositories angesammelt, die meisten enthalten irgendwelche CPAN-Module, aber auch die Textdaten für meinen Blog http://usarundbrief.com lagern dort [2]. Höchste Zeit, mal mit Perl in den zugehö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 Skripte alles Nötige per API aus dem Datenfüllhorn. Zur Ansteuerung mit Perl findet sich auf dem CPAN die Modulsammlung Net::GitHub von Fayland Lam. Mit den erforderlichen Zugriffsrechten ausgestattet kann der API-Nutzer damit nicht nur lesend zugreifen (Abbildung 1), sondern auch aktiv modifizieren, etwa neuen Code einspeisen.
Damit Github weiß, wer die API-Requests absetzt und notfalls korrigierend einschreiten kann, liegt optional jeder Anfrage an das REST-API ein Authentisierungstoken bei. Das erhält der User nicht nur im Browser-UI, sondern wahlweise auch von einem Skript wie beispielsweise in Listing 1. Es fragt das Passwort des Users ab, schickt die Daten per SSL an den Github-Server und bekommt ein Access-Token zurück.
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";
Statt eines Passworts schickt der Client dann in Zukunft dieses Token mit und wird zum Server vorgelassen. Dabei erhält er die Zugriffsrechte, die beim Generieren des Token vorgegeben wurden. Damit Skripte das Token finden, legt Listing 1 es in der Datei »~/.githubrc« im Yaml-Format im Homeverzeichnis ab. Der Aufruf
$ ./token-get Password: ***** /Users/mschilli/.githubrc written
vollzieht die notwendigen Schritte für den in Zeile 11 hinterlegten User.
Zu beachten ist, dass das Kommentarfeld »note« beim Aufruf der Methode »create_authorization()« für jedes neu angeforderte Token einen anderen Inhalt haben muss, sonst verweigert Github die Herausgabe des Token und gibt als Ursache 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 dieser sie modifizieren oder widerrufen kann (Abbildung 2).
Außerdem gibt der Parameter »scope« an, welche Rechte der Account-Besitzer den 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 (zum Beispiel auf dessen E-Mail-Adresse) oder Code-Commits.
Mehr mit Ausweis
Nach den Nutzungsbedingungen [3] räumt Github den Clients, die sich authentifizieren, bis zu 5000 Anfragen pro Stunde ein, während anonyme Anfragen auf 60 pro Stunde und IP begrenzt sind. Das Search-API hingegen, das nach Suchmustern in Repositorynamen oder eingechecktem Code sucht, gibt sich etwas geiziger, es erlaubt 20 Anfragen pro Minute mit Token und ohne nur fünf. Dass das API auch ohne Einloggen funktioniert, zeigt Abbildung 3 mit einer Abfrage der Metadaten des Github-Repository »mschilli/log4perl« .
Wie viele Anfragen noch durchgehen, bis der Server den Hahn zudreht, steht jeweils im HTTP-Header der Antwort. 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.
Das Skript in Listing 2 fragt bei Github die Account-Metadaten des auf der Kommandozeile angegebenen Users ab. Der User weist sich mit dem Access-Token aus, das das CPAN-Modul Yaml aus der Yaml-Datei »~/.githubrc« extrahiert hat. Neben einer Riesenlatte von anderen Feldern steht im zurückkommenden Json-Wust auch die Anzahl der im Account angelegten öffentlich sichtbaren Repositories, Listing 2 gibt sie in Zeile 17 einfach aus:
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";
$ ./repo-user mschilli Mike Schilli owns 57 public repos $ ./repo-user torvalds Linus Torvalds owns 2 public repos
Das Skript »repo-user« offenbart ganz nebenbei, dass Linus Torvalds auf Github erstaunlicherweise nur zwei Repositories angelegt hat!
Torvalds : Schilli wie 2 : 57
Die Zeilen 9 und 12 in Listing 2 schnappen sich das zuvor von Listing 1 abgelegte Access-Token und die Methodenkette »->user ->show()« greift hier auf die Accountdaten des Users zu.
Um beispielsweise festzustellen, welche Repositories ein Autor auf Github bereits angelegt hat, dient das Listing 3, das aus den zurückgelieferten Metadaten jeweils nur den Repository-Namen extrahiert und ausgibt.
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 }
Alternativ nimmt die Methode »list_user()« einen Usernamen entgegen (Abbildung 5), und wer »torvalds« eingibt, sieht, dass die beiden Repositories des Linux-Vaters »linux« und »subsurface« heißen.

Listing 3 spuckt alle Github-Repositories des angegebenen Users aus.” width=”234″ height=”300″ />
Abbildung 5: Listing 3 spuckt alle Github-Repositories des angegebenen Users aus.Gewinner nach Punkten
Wer wissen möchte, welche Repositories es zu einem Thema gibt, wird oft mit Hilfe des 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:
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 }
$ ./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. 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 Hunderterpakets mit »has_next_page()« nachfragen, ob zur Query noch weitere Ergebnisse vorliegen. Wenn das der Fall ist, 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« , zusammen mit einem Wert von »desc« (für descending, absteigend) ist das Ergebnis damit nach Beliebtheit der Repositories sortiert. Wer sich auf Github für ein Projekt interessiert, klickt auf dessen Star-Symbol und wird kü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 stehen die Anzahl der Sterne im Feld »stargazers_count« und der Name des Projekts in »full_name« .
Ein Entwickler, der 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 einleitendes drittes Parameterpaar »api_url => https://…/api/v3« anhängen und erhält seine Informationen von dort.
Wer im eingecheckten Code nach Mustern sucht, dem gibt Listing 5 einen Vorgeschmack davon, was das 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. Die Query
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 }
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 Repository Log4perl in zwei Dateien das Wort »snickers« auftaucht.
Code unterjubeln
Um einem Softwareprojekt auf Github per API einen Commit mit einer aufgefrischten Readme-Datei unterzujubeln, gilt es zunächst, abzusteigen. Unter der Haube arbeitet Git nämlich mit einfachen Datenstrukturen, von denen das Kommandozeilentool »git« normale Anwender vollständig abschottet. Eine Datei im Repository stellt Git als Blob dar, eine Ansammlung von Blobs in einem Verzeichnis ist ein Tree, ein Tree mit einer Check-in-Notiz ein Commit (Abbildung 6). Unter [5] finden Interessierte eine fundierte Einführung in das Thema.
Bevor das Skript in Listing 6 nun einen Commit in das Testprojekt »mschilli/apitest« einspielen kann, muss erst ein neues Token mit mehr Rechten her. Das in Listing 1 eingeholte Token berechtigt nur zum Lesen der Repository-Daten, um auch noch den Schreibzugriff zu erlangen, muss Zeile 15 in Listing 1 durch
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 } } );
scopes => ['public_repo'],
ersetzt und der im Parameter »note« abgelegte Kommentar abgewandelt werden. Ein neuer Aufruf von »token-get« speichert dann ein neues Token mit weitergehenden Rechten in der Tokendatei.
Blobs, Trees, Commits
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 Masterbranch herausfieselt, damit auch eventuell bereits vorhandene weitere Dateien im Repository bestehen bleiben.
Den Tree wickelt dann Zeile 43 in einen Commit, 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.
Zeiger zeigen
Eingangs hatte Zeile 19 die Referenzen auf die Entwicklungszweige des Repo eingeholt. Am Ende verbiegt nun Zeile 54 den Head-Zeiger des Masterbranch 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.
Bei Redaktionsschluss hatte das Modul Net::GitHub in der Version 0.71 noch einen Fehler in der Implementierung von »update_ref()« . Doch ein schnell fabrizierter Pull-Request schuf Abhilfe [6] und mit etwas Glück hat der Modul-Autor das Patch zum Erscheinungstermin dieses Artikels bereits eingespielt.
Wer will, kann eines der Abfrageskripte als Cronjob laufen lassen und erhält dann eine Zeitreihe, die grafisch den Github-Fleiß veranschaulicht. Oder der Boss möchte alarmiert werden, falls sein Mitarbeiter einen plötzlichen Produktivitätsschub durchmacht!
Online PLUS
In einem Screencast demonstriert Michael Schilli das Beispiel: https://www.linux-magazin.de/Ausgaben/2015/04/plus
Infos
- Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2015/04/Perl
- Michael Schilli, “Alternativer Gebrauch”: Linux-Magazin 01/13: https://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
- Ry’s Git Tutorial: http://rypress.com/tutorials/git
- Pull-Request für einen Bug in »update_ref()« : https://github.com/fayland/perl-net-github/pull/58












