Freitag, 27. Juni 2014

Rebase, aber richtig

Eine Sache ist klar: Rebase ist Böse, wenn es sich im Branches handelt, die schon publiziert wurden. Aber ab einer bestimmten Anzahl von parallel entwickelten Features (von Entwicklern will ich garnicht erst reden) wird es in der History unübersichtlich, wenn alle Commits zu allen Features nebeneinander stehen.

Es ist aber durchaus nicht abwegig, seine eigenen Branches in der Entwicklung gelegentlich zu rebasen. Wenn man bis dahin mit feature branches und --no-ff merges gearbeitet hat, stellt man aber schnell fest, dass Git die "überflüssigen" Merge-Commits einfach wegbügelt. Inhaltlich mag das sinnvoll sein, schließlich trägt ein Change, der nichts ändert, nichts zum Code bei (alle Ängerungen sind vorher geschehen). Wenn man aber mit kleinteiligen Commits arbeitet, verliert man so den Zusammenhang zwischen den Commits: Was gehörte jetzt zu dieser Implementierung, und was nicht?

Glücklicherweise hat Git ein Feature, welches zumindest beim Rebase das Zusammenfassen der Merge-Commits vermeidet: Mit

git checkout myfeature
git rebase -p master

wird der branch myfeature auf master rebased, wobei die Branch-Struktur im myfeature-Branch erhalten bleibt.

Mit git rebase sind noch mehr praktische Dinge möglich (ich meine hiermit Dinge, die einzelne Commits lesbarer machen). Aber nie vergessen: Einmal publizierte Branches darf man nur noch umschreiben, wenn man mit dem Frust aller Entwickler leben kann, die dann aufwändig ihre Commits neu einsortieren müssen.

Also eigentlich nicht.

Dienstag, 25. März 2014

Klammeraffen

Wenn man ein wenig mit Python rumspielt, stolpert man früher oder später über all die lustigen Dinge, die man mit Funktionen anstellen kann. Über die Vorzüge von funktionaler Programmierung brauche ich mich nicht auslassen, das haben schon andere getan. Jedenfalls hat sich die Python-Gemeinde irgendwann entschieden (ich bin zu faul, den genauen PEP rauszusuchen), die aus anderen Sprachen bekannten @-Dekoratoren einzuführen. (Vielleicht haben sie die auch erfunden, die Historie spielt für diese Anwendung keine Rolle.) Jedenfalls sind diese Dekoratoren in Python sehr einfach:

def decorated(func):
    def wrapper(*args, **kvargs):
        return func(*args, **kvargs)
    return wrapper


@decorated
def my_func():
    """my funky function"""
    pass

Will man dem Dekorator noch Argumente mitgeben, wird es ein wenig umständlicher:

def decorated(msg):
    def decorator(func):
        def wrapper(*args, **kvargs):
            print msg
            return func(*args, **kvargs)
        return wrapper
    return decorator


@decorated("nice function")
def my_func():
    """my funky function"""
    pass

Ich gebe zu, beim ersten Mal lesen erschließt sich das nicht unbedingt gleich: Der Code des Dekorators wird bei der Deklaration der dekorierten Funktion ausgeführt und liefert als Ergebnis die Funktion, die dann später bei Bedarf ausgeführt wird. Mit anderen Worten: Die dekorierte Funktion wird unsichtbar, an ihre Stelle tritt das Ergebnis des Dekorator-Aufrufs. Ach, ich geb's auf, immer wenn ich versuche, das zu erklären, wird es nur verworrener -- es sollte ausreichen zu wissen, dass es dutzende gute und einfache Erklärungen für die Funktionsweise gibt. Dummerweise ist das so einfach, dass man immer geneigt ist zu glauben, da müsse noch irgendwas kompliziertes, magisches im Hintergrund geben. Gibt es nicht.

Oder vielleicht doch: Die dekorierte Funktion heißt jetzt für alle Welt wrapper, und ihr fehlen solche Dinge wie der Docstring, den wir mühsam auf my funct function gesetzt haben. Das kann man jetzt mühsam händisch nachrüsten, oder man verwendet wraps aus den functools. Was wohl die bessere Idee ist?

def haendisch(func)
    def wrapper(*args, **kvargs):
        return func(*args, **kvargs)
    doc = getattr(func, '__doc__', None)
    if doc:
        setattr(wrapper, '__doc__', doc)
    return wrapper


import functools
def bessere_idee(func):
    @functools.wraps(func)
    def wrapper(*args, **kvargs):
        return func(*args, **kvargs)
    return wrapper

Die Funktion bessere_idee sieht schon besser aus, wie ich finde. wraps kümmert sich rührend um Funktionsnamen und Docstring -- einfach immer verwenden, wenn man irgendwelche Funktionen wrapped. Und nicht vergessen: Immer wenn man eine wirklich coole Idee für einen Dekorator hat, der irgendwas wildes mit der Funktion, der Klasse oder dem Objekt anstellen kann -- je weniger wild und cool, desto besser.

Mittwoch, 7. August 2013

Na super() - Freude mit Python

Neulich in PyCharm: Da hacke ich so froh vor mich hin, bastel ein Skriptchen mit zwei Klässelchen, und stelle fest, dass man Teile davon in eine gemeinsame Superklasse ziehen kann. Nichts besonderes, und weil Python ja objektorientiert konzeptioniert ist, geht das auch total einfach:

class Common(object):
  def __init__(self, some):
    self._some = some


class A(Common):
  def __init__(self):
    super(A, self).__init__('some soll hier A sein')


class B(Common):
  def __init__(self):
    super(B, self).__init__('hier soll es B sein')

Ok, ist schon klar, das ist synthetischer Code, tut aber nichts zu Sache. Nach allem, was ich so weiß, funktioniert das theoretisch. In der Praxis funktioniert das auch, beispielsweise auf einem 2.7.x-Python auf Linux.

Bis ich das in einem virtualenv auf meinem Mac laufen lasse (Python 2.7.2). Dann werde ich mit folgender Meldung beglückt:

TypeError: super(type, obj): obj must be an instance or subtype of type

Nicht, was ich erwartet hatte. Google spuckt dazu einen netten Link namens Another super() wrinkle – raising TypeError aus, und wer Freude dran hat, darf sich ruhig in den Artikel vertiefen. Allein: Ich benutze keinen Plugin-Loader, der Fehler tritt bei mir beim ganz normalen Unittest auf (mit einem ganz normalen unittest2, muss ich wohl dazu sagen). Ich überspringe hier mal lange Debugging-Sessions und komme gleich zum Kern: super(self.__class__, self).__init__(...) tut's:

class Common(object):
  def __init__(self, some):
    self._some = some


class A(Common):
  def __init__(self):
    super(self.__class__, self).__init__('some soll hier A sein')


class B(Common):
  def __init__(self):
    super(self.__class__, self).__init__('hier soll es B sein')

Das sieht mir zwar irgendwie wirklich zu schräg aus, und ich kann mir gut vorstellen, dass hier super irgendwie falsch behandelt wird, aber auch, dass man hier noch basteln kann, damit es hübscher aussieht. Andererseits reicht es mir wirklich vollkommen, die Vererbung im Kopf der Klassendefinition anzugeben, also ist diese Schreibweise vielleicht sogar wert, ins Muskelgedächtnis der Finger aufgenommen zu werden.

Ausserdem funktioniert's.

Montag, 5. August 2013

Git Branches auf dem Terminal

Es passiert ja immer wieder mal, dass man auf irgendeinem Rechner, der keine grafische Oberfläche angeschlossen hat, in die History eines Repositories reingucken muss. Das Werkzeug dazu ist natürlich git log:

> git log

commit 123...789

Author: John Doe <john.doe@inter.net>
Date:  Fri Feb 1 13:23:03 2013 +0100

    Add nice stuff

    Today I decided to add nice stuff to this repository.
...

Auf die Art und Weise bekommt man natürlich keinen Überblick über das Repository, das ist ja viel zu ausführlich. Aber git kennt einen Einzeiler-Modus, der praktischerweise gleich so heißt:

> git log --oneline
1234567 Add nice stuff
...

Als History kreuz und quer über alle Branches ist das dann aber etwas unübersichtlich. ASCII-Art sei Dank gibt's auch auch dem Terminal Grafiken:

> git log --graph --oneline
* 1234567 Add nice stuff
...

Schon besser. Wenn wir alle Branches sehen wollen, nicht nur den aktuellen Branch, gibt's dazu noch ein "--all":

> git log --graph --oneline --all
* 1234567 Add nice stuff
...

Ok, der Unterschied ist hier nicht gerade deutlich, aber das sollte man einfach mal mit einem echten Repository probieren. Wenn man das tut, stellt man aber fest: Welcher Commit auf welchen Branch ist, kann man einfach nicht sehen. Das kann schon mal wichtig sein, also lassen wir uns das einfach noch mit anzeigen:

> git log --graph --oneline --decorate=short --all
* 1234567 (HEAD, origin/master, master) Add nice stuff
...

Das ist schon fast perfekt. Damit wir das aber nicht jedes Mal tippen müssen, legen wir dafür einfach noch einen Alias an.

> git config --global alias.lg 'log --graph --oneline --decorate=short'
> git lg --all
* 1234567 (HEAD, origin/master, master) Add nice stuff
...

Den Alias haben wir global für den Nutzer angelegt, damit funktioniert er in allen Repos des Nutzers. Das letzte '--all' habe ich im Beispiel weggelassen, damit mal auch mal einfach nur die Historie des aktuellen Branches sehen kann. Noch schöner wird's übrigens, wenn man die Farbe einschaltet.

So einfach können die kleinen Freuden sein.

Dienstag, 19. Februar 2013

Was ist auf diesem Branch?

Manchmal sitzt man so vor einem Terminal, hat fröhlich-chaotisch vor sich hin gehackt, natürlich alles brav auf einem eigenen chaotisch-vor-sich-hin-hack-Branch, und fühlt, dass die Zeit reif ist, die Früchte seiner Arbeit in den master-Branch zurückfließen zu lassen. Nur: Was sind denn die Früchte besagter Arbeit?

Im grafischen Display von Gitk ist das schnell erkannt: Gitk öffnen, die Commits ansehen, die man auf dem Branch eingebracht hat, und schon kann man entscheiden, ob man wirklich auf den master mergen will oder nicht. Nur was ist, wenn's auf dem Terminal keine grafische Darstellung gibt?

Kein Problem. Gitk ist ja im Grunde nur ein glorifiziertes git log (wenn auch recht gut glorifiziert), also kann man alles, was man mit gitk machen kann, auf mit git log machen, ganz ohne grafische Ausgabe.

In diesem Falle interessieren uns alle Commits auf dem Branch hacking, der von master abgezweigt. Das bedeutet, dass alle Commits, die auf hacking liegen, angezeigt werden sollen, aber keine Commit, die auf master liegen. Und so geht's:

  git log hacking ^master

Eigentlich ganz einfach. Natürlich gibt's noch jede Menge Optionen für git log, mit dem man anpassen kann, was genau wie ausgegeben wird. Beliebt sind in diesem Zusammenhang --oneline und --graph. Aber wenn man wirklich will geht da noch viel mehr...

Dienstag, 12. Februar 2013

Git Status in der Bash, Teil 2

Vor ein paar Jahren hatte habe ich unter Git Status in der Bash meine PS1-Definition abgetippert. Das war eine Kombination aus Bash-Funktionen und einem Prompt-String, die auch lange Zeit ganz gut funktioniert hat. Mit einem der aktuellen Git-Updates (aus der 1.8x-Reihe, wenn ich mich nicht irre) ist das dann umgefallen: Weil Git eine Ausgabezeile geringfügig geändert hat, zeigte das Skript grundsätzlich alle Working Directories als "modified" an. Ärgerlich.

Aber ein Grund, etwas zu verbessern. Die Skripte, die den Status des aktuellen Working Directory anzeigen, gibt's nämlich im Rahmen der normalen Git-Installation unter contrib. (Wer das in seinem Ubuntu nicht findet: In den Git-Sourcen ist das vollständige contrib-Verzeichnis enthalten, einfach einmal clonen und dann contrib/completion/git-prompt.sh einbinden).

Jetzt kommen die folgenden Zeilen in die .profile/.bashrc:

. /contrib/completion/git-prompt.sh
PS1='\[\e]0;\w $(__git_ps1 " %s")\a\]\n\[\e[32m\]\u@\h \
    \[\e[33m\]\w $(__git_ps1 " %s")\[\e[0m\]\n\$ '

Noch ein wenig an den eigenen Geschmack anpassen, und schon ist es fertig. Wer es ein wenig hübscher will, findet in git-prompt.sh noch Hinweise, was man noch so alles tweaken kann.

Das Beste zum Schluss: Da das contrib-Verzeichnis von den Git-Contributors gepflegt wird, bekommt man immer funktionierende für die gerade verwendete Git-Version.

Donnerstag, 24. Januar 2013

Kennt mein Git-Repo das schon?

Hin und wieder muss man tatsächlich nachsehen, ob eine bestimmte Datei schon im Repo ist oder nicht. In der Regel ist ja ein

git status

ausreichend, aber was, wenn man nichts verändert hat? Dann taucht die Datei auch in keiner Liste auf. Und wenn man in ein unbekanntes Repo guckt, hat man ohnehin schlechte Karten: Vielleicht sind die Dateien, die man meint, ja generiert worden und stehen schon in der .gitignore?

Schnelle Antwort liefert diese Kommandozeilenungetüm:

git ls-files --error-unmatch Dateiname

Wenn die Datei bereits im Repo ist, wird sie gelistet, ansonsten gibt's eine Fehlermeldung und, skriptfreundlich, einen Exitcode ungleich Null.

Tip: Kann man auch als Alias einrichten.