Git Kommandos bei Trunk-Based Development


Git ist mächtig, Git ist vielseitig. Daher reicht es oft nicht aus, einfach drauf los zu committen und irgendwas daraus dann zu veröffentlichen. Um einen absoluten Top-Film zu zitieren (Tremors) “Du musst anfangen im Voraus zu planen!”

Daher sollte ein Branching Model gewählt werden, dass mit der Produktumwelt am besten harmoniert. Ein paar Vorschläge dazu liefert Paul Hammant.

Da neulich ein Freund mit genau der selben Frage für ein neues Projekt auf mich zukam, habe ich ihn an Hammants Artikel verwiesen. Für ihn, und seine Projektsituation passend, wählte er als (noch) Einzelentwickler das Trunk-Based Development Modell. Als Unterstützung bot ich ihm an, die gängigsten Aktionen die hierfür in Git notwendig sind aufzuzeigen.

Trunk-Based Development

Dazu vorab nochmal kurz das Modell erklärt:

trunkbased

Alle Teammitglieder (oder, im Falle meines Freundes, nur eine Person) arbeiten am Trunk. Ein Branch wird nur für ein Major-Release angelegt. Fehlerbehebungen werden per Cherry-Pick aus dem Entwicklungsbranch gezogen. Minor-Releases kennzeichnen weitere Auslieferungen auf den Release-Branches.

Mögliche Aktionen in diesem Modell können nun sein:

  • Neues Projekt anlegen
  • Alltägliches Commit
  • Dem Remote-Branch hinzufügen
  • Major-Release & Tagging
  • Branchwechsel
  • Hotfix in einem Release
  • Minor-Release
  • Neues Major-Release
  • Branch umbenennen
  • Alle Änderungen in das letzte Release nachziehen
  • Änderungen an einem alten Release mit nicht committeten Dateien im Trunk

Zur besseren Darstellung habe ich das ganze in einem Github Repository zur Visuellen Unterstützung veröffentlicht.

https://github.com/tpummer/trunk-based-dev

Neues Projekt anlegen

Das sollte ja eigentlich die einfachste Übung sein. Trotzdem möchte ich es der Vollständigkeit halber anführen:

$ git init
 Initialized empty Git repository in /your/path/to/project/trunk-based-dev/.git/
$ git remote add origin git@github.com/tpummer/trunk-based-dev.git
$ touch README.md
$ git add .
$ git commit -m "First commit"
 [master (root-commit) ed857cc] First commit
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 README.md
$ git push -u origin master
 Counting objects: 3, done.
 Writing objects: 100% (3/3), 217 bytes | 0 bytes/s, done.
 Total 3 (delta 0), reused 0 (delta 0)
 To git@github.com:tpummer/trunk-based-dev.git
 * [new branch]      master -> master
 Branch master set up to track remote branch master from origin.

 

Alltägliches Commit

Wie gerade eben bei der Anlage des initialen Projekts gesehen ist ein normaler Commit keine Hexerei. Mittels

$ git status

kann man sich eine Auflistung der geänderten Dateien anzeigen lassen, welche man einzeln

$ git add README.md

oder einfach alle

$ git add .

dem nächsten Commit hinzufügen kann. Hat man dann alle Dateien für einen Commit gewählt, checkt man diese in die lokale Versionsverwaltung ein.

$ git commit –m “<your commit message>”

Das Ergebnis kann mittels

$ git log

verifiziert werden.

Dem Remote-Branch hinzufügen

Will man dies dann auch noch auf den Remote Branch publishen greift man zu:

$ git push

Da wir vorher einen Upstream für den master Branch festgelegt hatten müssen wir hierbei keine genaueren Pfade angeben. Dies erfolgte mittels

$ git push -u origin master

Major-Release & Tagging

Hurra es ist soweit, wir wollen unser erstes Release an den Mann bringen!

Also müssen wir als Erstes einen Branch anlegen.

$ git checkout –b release1.0.x
 Switched to a new branch 'release1.0.x'

Da aber zukünftig weitere Minor-Releases erfolgen könnten wollen wir jedes einzelne davon taggen, um es zukünftig bequem wiederzufinden.

$ git tag –a v1.0.0 –m “Release Version 1.0.0”

Dies müssen wir natürlich dem Remote Repository wieder mitteilen. Dazu übertragen wir den neuen Branch mit

$ git push –u origin release1.0.x
 Total 0 (delta 0), reused 0 (delta 0)
 To git@github.com:tpummer/trunk-based-dev.git
 * [new branch]      release1.0.x -> release1.0.x
 Branch release1.0.x set up to track remote branch release1.0.x from origin.

Und nicht nur der neue Branch gehört bekanntgegeben, die Tags müssen extra transferiert werden! Wieder einzeln:

$ git push origin v1.0.0
 Counting objects: 1, done.
 Writing objects: 100% (1/1), 174 bytes | 0 bytes/s, done.
 Total 1 (delta 0), reused 0 (delta 0)
 To git@github.com:tpummer/trunk-based-dev.git
 * [new tag]         v1.0.0 -> v1.0.0

oder alle auf einmal

$ git push origin --tag
 Counting objects: 1, done.
 Writing objects: 100% (1/1), 174 bytes | 0 bytes/s, done.
 Total 1 (delta 0), reused 0 (delta 0)
 To git@github.com:tpummer/trunk-based-dev.git
 * [new tag]         v1.0.0 -> v1.0.0

Branchwechsel

Wir wechseln zum Abschluss noch in unseren Entwicklungsbranch um weiterarbeiten zu können:

$git checkout master

Wo wir uns befinden können wir jederzeit mittels

$ git branch
 * master
 release1.0.x

nachsehen. Das Sternchen markiert unseren Standort.

Hotfix in einem Release

Oh nein, es hat sich ein Fehler eingeschlichen! Korrigiert ist er ja flott, aber wie kriege ich das in das Release rüber? Nachdem der Fehler im master Branch ausgebessert und committet wurde, wollen wir nur einen speziellen Commit auch in den Release Branch übertragen.

Dazu sehen wir uns zuerst die Commit ID an

$ git log
 commit 026ec4023e9e55f290b12711918c7c512fb38350
 Author: Thomas Pummer dev@example.org
 Date:   Mon Apr 28 15:08:38 2014 +0200
Fuer Release 1.0.x
commit b5f3a9aeb48fcb0cbaef286d7569f28829192041
 Author: Thomas Pummer dev@example.org
 Date:   Mon Apr 28 15:08:25 2014 +0200
Nicht fuer Release 1.0.x

 

Wir sind also mit commit 026ec4023e an der richtigen Adresse.  Als Nächstes wechseln wir nun in den Release Branch.

$ git checkout release1.0.x
 Switched to branch 'release1.0.x'

Dannach holen wir uns mittels Cherry-Pick genau dieses eine Commit und stecken es abschließend auch in unseren Remote Branch.

$ git cherry-pick 026ec4023e
 [release1.0.x 189c204] Fuer Release 1.0.x
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git push
 Counting objects: 5, done.
 Delta compression using up to 4 threads.
 Compressing objects: 100% (2/2), done.
 Writing objects: 100% (3/3), 307 bytes | 0 bytes/s, done.
 Total 3 (delta 0), reused 0 (delta 0)
 To git@github.com:tpummer/trunk-based-dev.git
 0a648f8..189c204  release1.0.x -> release1.0.x

 

Mittels git log sehen wir, dass der Commit eine neue Commit ID bekommen hat. Um zu identifizieren woher der Commit kam, muss man mit –x einen zusätzlichen Parameter nutzen.

$ git cherry-pick -x c9ed634
 [release1.0.x bd1bee9] cherry with x
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log
 commit bd1bee920ebee7a0ada2e5723086e8178e15d3c0
 Author: Thomas Pummer dev@example,org
 Date:   Mon Apr 28 15:16:55 2014 +0200
cherry with x
(cherry picked from commit c9ed634612f9f5f31db666d348b88a1563aa31fe)

Minor-Release

Wollen wir nun erneut veröffentlichen funktioniert es wie vorher beim Major Release: In den Branch wechseln

$ git checkout release1.0.x
 Switched to branch 'release1.0.x'

und taggen

$ git tag -a v1.0.1 -m "Release Version 1.0.1"

Auf das Hochladen in das Remote Repository nicht vergessen!

$ git push --tags
 Counting objects: 1, done.
 Writing objects: 100% (1/1), 171 bytes | 0 bytes/s, done.
 Total 1 (delta 0), reused 0 (delta 0)
 To git@github.com:tpummer/trunk-based-dev.git
 * [new tag]         v1.0.1 -> v1.0.1

Minor-Release auschecken

Was müssen wir also tun um an unser Release ranzukommen? Als erstes das Repository klonen:

$ git clone git@github.com:tpummer/trunk-based-dev.git
 Cloning into 'trunk-based-dev'...
 remote: Reusing existing pack: 28, done.
 remote: Total 28 (delta 0), reused 0 (delta 0)
 Receiving objects: 100% (28/28), done.
 Resolving deltas: 100% (1/1), done.

Dannach checken wir direkt den Tag aus:

$ git checkout v1.0.1
 Note: checking out 'v1.0.1'.
You are in 'detached HEAD' state. You can look around, make experimental
 changes and commit them, and you can discard any commits you make in this
 state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
 do so (now or later) by using -b with the checkout command again. Example:
git checkout -b new_branch_name
HEAD is now at bd1bee9... cherry with x

Jetzt haben wir direkt den Stand eines spezifischen getaggten Releases. Wie der Message zu entnehmen ist dies jedoch kein geeigneter Punkt weiterzuarbeiten.  Daher sollte entweder einer der Release-Branches ausgecheckt werden oder wie oben beschrieben ein neuer Branch eröffnet werden. Allerdings würde ein neuer Branch  nicht in unser Trunk-Based Modell passen.

Neues Major-Release

Haben wir nun am master Branch soweit weiterentwickelt dass wir eine neue Major Version herausbringen wollen gehen wir wie bei der ersten Version vor:

$ git checkout -b release1.1.0
 Switched to a new branch 'release1.1.0'
$ git tag -a v1.1.0 -m "Release Version 1.1.0"
$ git push -u origin release1.1.0
 Total 0 (delta 0), reused 0 (delta 0)
 To git@github.com:tpummer/trunk-based-dev.git
 * [new branch]      release1.1.0 -> release1.1.0
 Branch release1.1.0 set up to track remote branch release1.1.0 from origin.
$ git push origin v1.1.0
 Counting objects: 1, done.
 Writing objects: 100% (1/1), 174 bytes | 0 bytes/s, done.
 Total 1 (delta 0), reused 0 (delta 0)
 To git@github.com:tpummer/trunk-based-dev.git
 * [new tag]         v1.1.0 -> v1.1.0

Branch umbenennen

Oh nein der falsche Branch Name! Ist es aufgefallen? Wir wollen ja mehrere Minor Releases in einen Branch unterbringen, weshalb eine Bezeichnung mit einer Wildcard besser gewesen wäre. Also statt release1.1.0 lieber release1.1.x

Nun Lokal geht das ja flott

$ git branch -m release1.1.0 release1.1.x
$ git branch
 master
 release1.0.x
 * release1.1.x

Remote ist die Lösung schon aufwendiger, da dies ein externer Branch ist. Hier hilft nur vorheriges löschen und erneutes pushen

$ git push origin :release1.1.0
 To git@github.com:tpummer/trunk-based-dev.git
 - [deleted]         release1.1.0
$ git push -u origin release1.1.x
 Total 0 (delta 0), reused 0 (delta 0)
 To git@github.com:tpummer/trunk-based-dev.git
 * [new branch]      release1.1.x -> release1.1.x
 Branch release1.1.x set up to track remote branch release1.1.x from origin.

Alle Änderungen in das letzte Release nachziehen

Sind alle Änderungen am master Branch auch für das letzte Release vorgesehen, muss man nicht per Cherry-Pick vorgehen. Man kann den gesamten Branch mergen. Dazu wechselt man zuerst in den Ziel-Branch und holt dann mittels merge Kommando alle Änderungen.

$ git checkout release1.1.x
 Switched to branch 'release1.1.x'
$ git merge master
 Updating 4131cac..5687e2c
 Fast-forward
 README.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

Änderungen an einem alten Release mit nicht committeten Dateien im Trunk

Manchmal hat man Datein bereits einem zukünftigen Commit hinzugefügt, dieses aber noch nicht eingecheckt. Möchte man nun den Branch wechseln kommt es zum Fehler, oder die Änderung wird mitgenommen. Zur Vermeidung dessen gibt es ein paar Optionen.

Die simpelste, und am wenigsten Git Magic-lastige, wäre ein Checkout in einem neuen Verzeichnis.

Alternativ kann man die Änderungen auch einfach wegschmeißen

$ git reset –-hard HEAD

Variante Nummer drei  ist der git stash befehl. Damit bunkert man die noch nicht versionierten Änderungen für später und kann eine andere Änderung vorziehen.

Eine mögliche Ausgangslage dafür wären die zwei Datein switch und new:

$ git status
 # On branch master
 # Changes to be committed:
 #   (use "git reset HEAD <file>..." to unstage)
 #
 #       new file:   switch
 #
 # Untracked files:
 #   (use "git add <file>..." to include in what will be committed)
 #
 #       new

Wenn wir nun git stash aufrufen kümmert sich git nur um für ein commit vorgesehene Dateien:

$ git stash
 Saved working directory and index state WIP on master: 5687e2c ready for merge
 HEAD is now at 5687e2c ready for merge
$ git status
 # On branch master
 # Untracked files:
 #   (use "git add <file>..." to include in what will be committed)
 #
 #       new
 nothing added to commit but untracked files present (use "git add" to track)

 

Wollen wir die Datei new also auch stashen müssen wir sie zuerst unter Versionskontrolle stellen:

$ git add .

Dannach können wir auch diese Datei stashen

$ git stash
 Saved working directory and index state WIP on master: 5687e2c ready for merge
 HEAD is now at 5687e2c ready for merge

Wir haben nun 2 stash Objekte bereitliegen, um nach unserer Änderung wieder hervorgeholt zu werden

$ git stash list
 stash@{0}: WIP on master: 5687e2c ready for merge
 stash@{1}: WIP on master: 5687e2c ready for merge
$ git stash apply
 # On branch master
 # Changes to be committed:
 #   (use "git reset HEAD <file>..." to unstage)
 #
 #       new file:   new
 #

 

Ein simples apply holt nur das oberste Element aus dem Stack hervor. Da wir jedoch 2 mal Datein gestashed haben müssen wir diese direkt ansprechen.

$ git stash list
 stash@{0}: WIP on master: 5687e2c ready for merge
 stash@{1}: WIP on master: 5687e2c ready for merge
$ git stash apply stash@{0}
 # On branch master
 # Changes to be committed:
 #   (use "git reset HEAD <file>..." to unstage)
 #
 #       new file:   new
 #
$ git stash apply stash@{1}
 # On branch master
 # Changes to be committed:
 #   (use "git reset HEAD <file>..." to unstage)
 #
 #       new file:   new
 #       new file:   switch
 #

 

Wie zu sehen sind die gebunkerten Änderungen durch ein apply nicht aus der stashing Liste entfernt. Das kann man mit

$ git stash drop

für jeweils wiederum das stash Objekt mit der höchsten Nummer nachholen. Oder man entfernt es direkt mittels der Referen

$ git stash drop stash@{0}

Natürlich hat Stashing eine gewisse Komplexität, und je nach bereits vollzogenen Änderungen will man auch nicht alles Wegschmeißen, weshalb ich im Bedarfsfalle zu erneutem Auschecken (wenn kein Netzwerk dann eben von der lokalen Kopie) tendieren würde. Auch wenn dies eher als Workaround anzusehen ist um Git etwas Komplexität zu nehmen.

Fehlt euch etwas? Dann bitte ab in die Comments, ich werde versuchen es zu ergänzen.
Viel Erfolg mit Git!

Ähnliche Artikel:

JavaFX seit Java 8 in Maven


Musste man während Java 7 noch die JavaFX Librarys per Maven dezitiert einbinden, stehen diese mit Java 8 von Haus aus im Classpath zur Verfügung. Einfach das Maven Compiler Plugin entsprechend konfigurieren:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>at.nullpointer</groupId>
	<artifactId>test-java8</artifactId>
	<version>0.0.1-SNAPSHOT</version>	
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.1</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

Zu Java 7 Zeiten war dafür noch ein extra dependency Eintrag notwendig. Wie dieser ausgesehen hat sieht man z.B. im Blog von Hendrik Ebbers.

Ähnliche Artikel:

Java 8 auf der Synology DS214+


Eine schnelle Anleitung wie ich Java 8 auf meiner Synology DS214+ zum Laufen brachte.

  1. Download des JDK 8 in der Linux ARM v6/v7 Hard Float ABI Version.
  2. Entpackt nach /opt
  3. Symlink angelegt:
    ln –s /opt/jdk1.8.0 /opt/java
  4. Zu /etc/profile folgendes hinzufügen:
    JAVA_HOME=/usr/java 
    export JAVA_HOME         
    PATH=/usr/java/bin:$PATH
  5. Neu laden mittels
    source /etc/profile
  6. Viel Spaß mit Java 8 auf der Synology DS214+

Ähnliche Artikel:

Java 8 – Date and Time API


Da etwas in die Jahre gekommen, wurde zu java.util.Date und java.util.Calendar mit dem Release von Java 8 eine Alternative geschaffen. Zuhause im Package java.time versucht die neue Lösung sich an benutzerfreundlicheren Factorymethoden, Formatierungslösungen, Threadsicherheit durch Unveränderlichkeit.

LocalDate und LocalTime

Als für den Menschen lesbare Formate sind java.time.LocalDate und java.time.LocalTime vorgesehen (und natürlich die Kombination: java.time.LocalDateTime). Sie bieten übersichtliche Factory Methoden

LocalDateTime now= LocalDateTime.now( ); // Aktuelles Datum LocalDate.of(2013, Month.DECEMBER, 24);
LocalTime.of(12, 00); LocalTime.parse("10:15:30");

und ihre Werte können mittels Getter erfragt werden und bieten Methoden an um neue Instanzen mit anderen Werten zu generieren.

LocalDate theDate = now.toLocalDate(); Month month = timePoint.getMonth();
//Da immutable, immer neue Instanzen erzeugt
LocalDateTime before = now.withYear(2013);
LocalDateTime soon = new.plusWeeks(1);
LocalDateTime wednesday = now.with(TemporalAdjusters.previousOrSame(ChronoUnit.WEDNESDAY));

Instant

Eine andere Darstellung eines Datums ist das java.time.Instant, das die Zeit als Millisekunden seit 01.01.1970 darstellt.

Abschneiden von Werten

Das Beschneiden von Zeiten auf eine Bestimmte Einheit ist nun ebenfalls Teil der neuen API:

LocalTime truncatedTime = now.truncatedTo(ChronoUnit.SECONDS);

Zeitzonen

Zeitzonen können mittels java.time.ZoneId mit berücksichtigt werden. Die ZoneId entspricht dann entweder dem Zeitzonenkürzel (dem Offset von UTC) oder einem Ort der entsprechenden Zeitzone. Damit kann man sich nun Entsprechungen der oben genannten Zeitklassen mit dem Prefix Zoned erzeugen, die somit eindeutig auf den Gebrauch von Zeitzonen hinweisen. z.B. java.time.ZonedDateTime

ZoneId id = ZoneId.of("Europe/Vienna");
ZonedDateTime zoned = ZonedDateTime.of(dateTime, id);

Noch viel mehr

Außerdem bietet die neue Zeit API verschiedene Kalendersysteme, Zeitspannen (Periods, Durations,…) und Formatierungen mittels DateTimeFormatter.

Werft einen Blick rein! http://docs.oracle.com/javase/tutorial/datetime/TOC.html

Ähnliche Artikel:

Java 8 – Stream API


Java 8 bringt auch Neuerungen im Collections Framework. Es wurde mit dem Ziel erweitert eine einfache Paralellisierung durch Bulk Operations zu ermöglichen. Hierfür wurde von den neuen Möglichkeiten der Interfaces gebrauch gemacht: den default Methoden. Damit wurde zusätzlich sichergestellt, dass die Binär-Kompatiblität zu alten Java Versionen erhalten bleibt. Die so genannten Bulk Operations umfassen Funktionalität, die auf mehrere oder alle Elemente einer Collection angewandt werden können.

Bulk Operations werden dabei mit, ebenfalls in Java 8 eingeführten, Lambda-Ausdrücken oder Methodenreferenzen aufgerufen. Ein Beispiel dafür ist die mit der Stream API eingeführte Filterung

Stream<Foobar> foos = …
foos.filter(Foobar::hasFoo);

Stream API

Streams repräsentieren Sequenzen von Elementen und sind dabei die wesentliche Neuerung im Collections Framework. Ein Stream stellt eine neue Abstraktion im Collections Framwork dar und ist die zentrale Stelle in der die Bulk Operationen gesammelt sind.

Collection-basierte Streams

Dabei ist eine Collection nicht von Haus aus ein Stream sondern muss erst mittels stream() oder parallelStream() erzeugt werden. Anhand der Namen der Erzeugungsmethoden kann man schon erkennen in welche Richtung die Verarbeitung der gestellten Bulk Aufgaben zielt. Bei der Parallelisierung wird der Stream automatisch, mithilfe des in Java 7 eingeführten Fork/Join Frameworks, in kleinere Portionen aufgeteilt und in einem Thread-Pool verarbeitet.

List<String> foos = Arrays.asList(“Foo”, “Bar”, “Foobar”);
foos.parallelStream().filter(Foobar::hasFoo).forEach(System.out::println);

Da einige Stream Methoden wieder eine Instanz von Stream zurückliefern, lassen sich so Verarbeitungsketten bilden. Im Beispiel wird eine vorher gefilterte Liste ausgegeben.

Array-basierte Streams

Diese Konvertierung steht auch für Arrays zur Verfügung und wird mittels der Util Klasse Arrays.stream(array) erzeugt. Aber auch die Stream Klasse selbst bietet diverse Factorymethoden an z.B. Stream.of(“foo”, “bar”,  “foobar”);

Primitive Streams

Neben den selbst aus Objekten generierbaren Streams gibt es auch drei Arten von primitiven Streams: IntStream, LongStream und DoubleStream. Je nach Anwendungsfall kann man sich mit Ausprägungen das Autoboxing ersparen. Dabei bieten IntStream und LongStream besondere Factorymethoden an, die das Erzeugen von Zähl-Sequenzen erleichtern sollen.

// 0 bis 9
IntStream foo = IntStream.range(0,10);
// 0 bis 10
IntStream bar = IntStream.rangeClosed(0,10);

Eine weitere Möglichkeit, eine Sequenz zum Zählen zu bekommen, bietet die Stream API mittels der iterate Methode.

Stream<BigInteger> ints = Stream.iterate(BigInteger.ZERO, i->i.add(BigInteger.ONE));

Zufallszahlen per Stream

Mithilfe von java.util.SplittableRandom kann für jede Art der Primitiven Streams eine Instanz gefüllt mit Zufallszahlen erzeugt werden.

IntStream unlimited = new SplittableRandom().ints();
IntStream limited = new SplittableRandom().ints(3);

 

Weitere Infos gibt es unter anderem unter http://docs.oracle.com/javase/tutorial/collections/streams/index.html

Ähnliche Artikel:

Entwicklung, Fotografie und mehr