Skip to content

Establishing a semi-automated gitflow/versioning-process with a maven-project

Foreword

Hey, Francisco :-) As promised: here is a description of what I implemented. With you as the primary audience, I decided to write it in English.

The Situation

We have a maven-project with lots of sub-modules. We like to call it "Klotz" (German for chunk)! There is a VCS (namely mercurial) and all the development happens in the branch "default". Every now and then a release-branch is created from the current default-status.
The version in the default-branch is updated once a year, and for the release-branches the versions are changed as follows:
find -name pom.xml -exec sed -i "s/19.0.0-SNAPSHOT/19.2.2.0-SNAPSHOT/g" {} \;

The Issues

  1. The default-branch is unstable from time to time, as every developer commits its work-in-progress to this branch.
  2. Working with branches in mercurial is a pain, especially as branches are global - so feature-branches are rarely used. (The bookmark-extension might be good enough for local work, but its not an adequate replacement)
  3. Bug-fixes are committed directly into the release-branch - without changing its version. So the version that's deployed on production might not be what you expect.

The Target

It is 2019! So it should be possible to do better!
We want:
  • a cleaner branching-model and workflow.
  • creating releases, hot-fixes and updating versions with the press of a button
  • enforce working with pull-requests

The Challenges

Due to the combination of maven and git(flow) several challenges arise.
The first being: having your code in well defined branches will not automatically update your versions in the pom.xml files. So we need a tool for that.
The second is: having different versions/pom.xml in the different branches makes merging quite difficult. So we need tool-support for this as well.
The third challenge is due to the deployment-process we have. The application is deployed based on the maven-artifacts in the company repository, and not the code itself.
So if you want to deploy the changes in a feature-branch, the version in the pom.xml files in this branch must be changed accordingly. But doing so prevents you from creating a simple Pull-Request from this feature-branch to development-branch. So - you guess it - we need tool-support for this case as well.

And speaking of Pull-Requests: of course the developers need to keep in mind the different versions in the branches when creating them. A PR from a branch based on a hotfix-branch against the development-branch for example might not be a good idea.

Implementing a Solution

The Tools...

As mentioned above, we need tool-support, and luckily there is one: gitflow-maven-plugin
The official repo/version still contains some bugs/short-comings. I created a fork with corresponding changes and fixes - hopefully they get merged upstream soon: https://github.com/Iridias/gitflow-maven-plugin/tree/current-status

Whats different:

  • pull dev-branch before pushing to avoid rejection due to remote-changes
  • support the option to not merge the release-branch into dev-branch
  • support processAllModules of versions-maven-plugin (important for child-modules without parent specified)
  • support separate additional merge-options on hotfix-finish and release-finish, for merging into production-branch and dev-branch respectively

With this plugin at hand, we're prepared to face the first requirement.

The "release with a press of a button"-requirement can be solved with JenkinsCI. If you're not using Jenkins, your alternative should provide similar capabilities for configuring freestyle-jobs. If not: why are you using it anyway?

To enforce the usage of pull-requests, we make use of the branching-restrictions of Bitbucket - because we already have Bitbucket available. But other alternatives, e.g. gitlab, rhodecode or azure devops have similar features - at least in the paid versions.

Now that we have our tools together...

Lets start!

Migration

First we need to setup the git-repository. To migrate the mercurial-repository to git, we use https://github.com/frej/fast-export
Then we need a development-branch: git branch development
And finally we add the Bitbucket repository as remote and push everything:
git remote add origin ssh://user@yourgitrepo/project.git
git push -u origin --all

Permissions

Now that the project is in the new git-repository, we can configure the branch-permissions: ..as well as the merge-checks: As you can see, we need an account for an ci-user. Because someone has to have the right to merge the changes into the corresponding branches, when using the gitflow-maven-plugin.
And maybe you, as admin, want to have such permissions too - in order to fix things that might go wrong.

Jenkins Jobs

For the next step, we need several Jenkins-Jobs.
To verify the application itself, we need jobs for building Pull-Request and the various branches:
  • Build-PR
  • Build-Develop
  • Build-Master
  • Build-Hotfix
  • Build-Feature
  • Build-Release
Next we need jobs to invoke the gitflow-maven-plugin:
  • Version-Create-Feature-Branch
  • Version-Finish-Feature-Branch
  • Version-Create-Hotfix-Branch
  • Version-Finish-Hotfix-Branch
  • Version-Create-Release-Branch
  • Version-Finish-Release-Branch
For the "Build-PR" Job, you'll need to use the bitbucket-pullrequest-builder-plugin (https://github.com/nishio-dens/bitbucket-pullrequest-builder-plugin) if you're using Bitbucket-Server!
This is due to a bug in Bitbucket-Server, that won't send/trigger a web-hook when a pull-request is updated: https://jira.atlassian.com/browse/BSERV-10279

For the following Jobs, you must use wildcards in the branch-specifier of the jenkins-git-plugin:
  • Build-Hotfix
  • Build-Feature
  • Build-Release
That's due to the fact, that the corresponding branches always have the same prefix, but the actual name differs and can't be known beforehand.
It's not a big deal, as it can be specified like follows: */hotfix-*
But the important thing is, that you must not delete the workspace after the build (Post-Build action)! You can, on the other hand, delete it before build! That's because Jenkins can't check for changes in the matching branches without the project being checked out in the workspace!

Deploy the maven artifacts

As we're using Sonatype Nexus Repository for our maven repository, I decided to also introduce nexus-staging-maven-plugin. It replaces the default deploy-maven-plugin and makes the process more robust, as it ensures, that all artifacts are deployed or none.
So if something horrible happens during the upload-process, you won't end up with an inconsistent state. The downside is: to have that feature also for release-artifacts (and not just snapshots) you'll need the paid professional version of Nexus Repository.
Use -DskipStaging=true as parameter for mvn deploy in the job that creates your release-artifacts - otherwise the build will break.
But, well, it's an improvement anyway, so it might be worth considering it.

The Jenkins-Jobs in detail:

Version-Create-Feature-Branch

Parameter:

Type: Text
Name: FeatureName

Shell Build-Step:

echo "Checking Feature-Name...";
if [[ ! "${FeatureName}" =~ ^[a-zA-Z0-9_\-]*$ ]]; then 
   echo "Invalid Feature-Name specified! Aborting!";
   exit 1;
fi

echo "Fetching repository...";
git clone ssh://USER@yourgitrepo:9999/prj/repo-name.git
cd repo-name

# gitflow:feature-start checks for existing branch in refs/heads/ instead of refs/remotes/origin
git branch -r | grep -v '\->' | grep -v 'master' | while read remote; do git branch --track "${remote#origin/}" "$remote"; done
git pull --all
git checkout staging

echo "Starting feature-branch...";
mvn -U -B gitflow:feature-start -DversionProcessAllModules=true -DinstallProject=false -DskipFeatureVersion=false -DallowSnapshots=true -DpushRemote=true -DfeatureName="${FeatureName}"
We're doing the git clone in the shell-script, because we have to do some additional git operations. If we'd use Jenkins' git-plugin, this would lead to an error like:
fatal: could not read Username for 'https://yourgitrepo'

As stated in the comment, the loop to create local tracking-branches is necessary, due to how the gitflow-plugin works. This applies to the following jobs as well - but I won't mention it again.

Version-Finish-Feature-Branch

Parameter:

Type: Extensible Choice
Name: FeatureName
Groovy Script:
def p = "/path/to/fetch-feature-branches.sh".execute()
p.waitFor()
File versions = new File("/path/to/git-feature-branch-names.txt")
return versions.readLines()
fetch-feature-branches.sh:
#!/bin/bash

cd /path/to/copy/of/your/repo-name
git pull --rebase --all
git remote prune origin
# dfb == deployable feature branch
git branch -r | grep "dfb-" | sed -e 's/.*dfb-//gi' > /path/to/git-feature-branch-names.txt

Shell Build-Step:

echo "Checking Feature-Name...";
if [[ ! "${FeatureName}" =~ ^[a-zA-Z0-9_\-]*$ || -z "${FeatureName}" ]]; then 
   echo "Invalid Feature-Name specified! Aborting!";
   exit 1;
fi

echo "Fetching repository...";
git clone ssh://USER@yourgitrepo:9999/prj/repo-name.git
cd repo-name
git branch -r | grep -v '\->' | grep -v 'master' | while read remote; do git branch --track "${remote#origin/}" "$remote"; done
git pull --all

echo "Finish feature-branch...";
mvn -U -B gitflow:feature-finish -DversionProcessAllModules=true -DinstallProject=false -DadditionalMergeOptions="-Xours" -DskipTestProject=true -DskipFeatureVersion=false -DallowSnapshots=true -DpushRemote=true -DfeatureName="${FeatureName}"

FIXME: merge-options need to be adapted according to Version-Finish-Hotfix-Branch

The Extensible Choice plugin and the script to fetch the existing feature-branches is not required - but its definitely more convenient for your users!
They can simply select the right feature-branch instead of determining the name them-self and struggle with typos - and trust me: they're always struggling with typos!

Version-Create-Hotfix-Branch

Shell Build-Step:

echo "Fetching repository...";
git clone ssh://USER@yourgitrepo:9999/prj/repo-name.git
cd repo-name
git branch -r | grep -v '\->' | grep -v 'master' | while read remote; do git branch --track "${remote#origin/}" "$remote"; done
git pull --all

echo "check for existing hotfix-branch..."
amount=`git branch -r |grep -i "hotfix-" | wc -l`
if [ $amount -gt 0 ]; then
   echo "Found $amount existing hotfix-branche(s)! Aborting...";
   exit 2;
fi

echo "creating hotfix branch..."
mvn -B gitflow:hotfix-start -DversionProcessAllModules=true -DuseSnapshotInHotfix=true
git push -u origin --all
Exit code to set build unstable: 2

Maybe you want to also use the Slack Notification Plugin.
There shall be only one hotfix-branch at a time - so no parameters needed.

Version-Finish-Hotfix-Branch

Parameter:

Type: Select
Name: MergeStrategy
Values:
AUTO
HOTFIX
STAGING
MANUAL

Shell Build-Step:

echo "Fetching repository...";
git clone ssh://USER@yourgitrepo:9999/prj/repo-name.git
cd repo-name
git branch -r | grep -v '\->' | grep -v 'master' | while read remote; do git branch --track "${remote#origin/}" "$remote"; done
git pull --all

echo "check for existing hotfix-branch..."
amount=`git branch -r |grep -i "hotfix-" | wc -l`
if [ $amount -eq 0 ]; then
   echo "No existing hotfix-branch found! Aborting...";
   exit 2;
fi

hotfixBranchVersion=`git branch -r |grep -i "hotfix-" | sed -e 's/.*hotfix-//gi'`

skipMergeDevBranch="false"
keepHotfixBranch="false"
devBranchMergeOptions="";

if [ "${MergeStrategy}" == "HOTFIX" ]; then
	devBranchMergeOptions="-Xtheirs";
elif [ "${MergeStrategy}" == "STAGING" ]; then
	devBranchMergeOptions="-Xours";
elif [ "${MergeStrategy}" == "MANUAL" ]; then
	skipMergeDevBranch="true";
	keepHotfixBranch="true";
fi

echo "releasing hotfix branch..."
mvn -U -B gitflow:hotfix-finish -DversionProcessAllModules=true -DskipTestProject=true -DallowSnapshots=true -DuseSnapshotInHotfix=true -DproductionBranchMergeOptions="-Xtheirs" -DdevBranchMergeOptions="${devBranchMergeOptions}" -DkeepBranch="${keepHotfixBranch}" -DskipMergeDevBranch="${skipMergeDevBranch}" -DhotfixVersion="${hotfixBranchVersion}"
Exit code to set build unstable: 2

Now things get a little tricky.
This operation has to merge changes into two branches: the master and the development-branch. Merging into the master is easy! There is just no way any conflicting changes could have gotten into the master-branch! So its save to enforce to always take changes from the hotfix-branch!

With the development-branch on the other hand, its not so easy!
The longer the hotfix-branch is open, the more likely it is, that there will be merge-conflicts - and we have to handle them!

The default-value of the MergeStrategy-parameter is AUTO. With this, the gitflow-plugin tries to merge the branches without any special treatment.
If that fails, then its the turn of the developers to decide how to solve the merge-conflicts - after all, they've produced them in the first place.
To make things easier, they have 3 options:
  • ignore the conflicting changes in the development-branch and just merge the changes from the hotfix-branch
  • vice-versa: ignore the conflicting changes in the hotfix-branch and just keep the changes in the development-branch
  • do not merge into development-branch at all and keep the hotfix-branch!
In the first two cases, you can just re-run the Job, with the MergeStrategy-parameter set to the corresponding option, and thats it.
In the last case ..well, you also re-run the Job, with the MergeStrategy-parameter set to the corresponding option - but one (or more) of the developers have to take care of merging the changes into the development-branch and deleting the hotfix-branch afterwards!
That's a little bit more work, as the developers have to respect the different versions in the pom.xml files (in the jenkins-job, the gitflow-maven-plugin will take care of this). But as mentioned before: they're responsible for the merge-conflicts they've introduced.

Version-Create-Release-Branch

Parameter:

Type: Extensible Choice
Name: ReleaseVersion
Groovy Script:
def p = "/path/to/fetch-git-version.sh".execute()
p.waitFor()
File versions = new File("/path/to/git-current-version.txt")
return versions.readLines()
fetch-git-version.sh:
#!/bin/bash

credentials=`cat ~/.bitbucket_user`
curl -u "$credentials" "https://yourgitrepo/projects/PRJ/repos/repo-name/raw/pom.xml?at=refs%2Fheads%2Fmaster" > /tmp/repo-pom.xml
grep -m 1 "" /tmp/repo-pom.xml | sed -e 's/.*version>\(.*\)<\/version>/\1/gi' > /path/to/git-current-version.txt

Parameter:

Type: Text
Name: DevelopmentVersion

Shell Build-Step:

echo "Checking target release-version...";
if [[ ! "${ReleaseVersion}" =~ ^[0-9]{2}\.[0-9]{1,2}\.[0-9](\.[0-9]+)?$ ]]; then 
   echo "Invalid Release-Version specified! Aborting!";
   exit 1;
fi

echo "Enforcing SNAPSHOT for dev-version...";
if [[ ! "${DevelopmentVersion}" =~ ^.*-SNAPSHOT$ ]]; then 
	DevelopmentVersion="${DevelopmentVersion}-SNAPSHOT"
fi

echo "Checking target dev-version...";
if [[ ! "${DevelopmentVersion}" =~ ^[0-9]{2}\.[0-9]{1,2}\.[0-9](\.[0-9]+)?-SNAPSHOT$ ]]; then 
   echo "Invalid Development-Version specified! Aborting!";
   exit 1;
fi

echo "Fetching repository...";
git clone ssh://USER@yourgitrepo:9999/prj/repo-name.git
cd repo-name
git branch -r | grep -v '\->' | grep -v 'master' | while read remote; do git branch --track "${remote#origin/}" "$remote"; done
git pull --all

echo "Starting release-branch...";
mvn -B gitflow:release-start -DversionProcessAllModules=true -DpushRemote=true -DuseSnapshotInRelease=true -DallowSnapshots=true -DcommitDevelopmentVersionAtStart=true -DreleaseVersion="${ReleaseVersion}" -DdevelopmentVersion="${DevelopmentVersion}" -DversionDigitToIncrement=2
In our company the colleagues want high flexibility regarding the version-numbers.
But to make things a bit easier and give them some support, we use the extensible choice plugin, to show the current version.
I'm sure, you can do the same thing completely in groovy, without a separate shell-script - but I found this solution shorter, more easy and especially more quick to implement!

In the build-step, the important part is, to check and enforce SNAPSHOT-version for development-branch! You can not rely on you users to get this right! Really!
For the release-branch the gitflow-plugin will take care of this (via parameter -DuseSnapshotInRelease=true)

Version-Finish-Release-Branch

Parameter:

Type: Select
Name: MergeStrategy
Values:
AUTO
HOTFIX
STAGING
MANUAL

Shell Build-Step:

echo "Fetching repository...";
git clone ssh://USER@yourgitrepo:9999/prj/repo-name.git
cd repo-name
git branch -r | grep -v '\->' | grep -v 'master' | while read remote; do git branch --track "${remote#origin/}" "$remote"; done
git pull --all

echo "check for existing release-branch..."
amount=`git branch -r |grep -i "hcf-" | wc -l`
if [ $amount -eq 0 ]; then
   echo "No existing release-branch found! Aborting...";
   exit 2;
fi

skipMergeDevBranch="false"
keepReleaseBranch="false"
devBranchMergeOptions="";

if [ "${MergeStrategy}" == "HOTFIX" ]; then
	devBranchMergeOptions="-Xtheirs";
elif [ "${MergeStrategy}" == "STAGING" ]; then
	devBranchMergeOptions="-Xours";
elif [ "${MergeStrategy}" == "MANUAL" ]; then
	skipMergeDevBranch="true";
	keepReleaseBranch="true";
fi

echo "Finish release-branch...";
mvn -B gitflow:release-finish -DversionProcessAllModules=true -DpushRemote=true -DuseSnapshotInRelease=true -DskipTestProject=true -DallowSnapshots=true -DcommitDevelopmentVersionAtStart=true -DproductionBranchMergeOptions="-Xtheirs" -DdevBranchMergeOptions="${devBranchMergeOptions}" -DkeepBranch="${keepReleaseBranch}" -DskipMergeDevBranch="${skipMergeDevBranch}" -DversionDigitToIncrement=2

Much like the "Version-Finish-Hotfix-Branch" - except that it'll check for existing release-branch and use the corresponding gitflow-plugin goal.
The concerns regarding merge-conflicts apply to this job as well.

Conclusion

With this Tools and Jenkins-Jobs at hand, you can leave release-management to a non-technical person in most cases. And even for the developers, things get a lot easier and more convenient.
And more important: your repository and releases will be in a consistent and reproducible state!

Any comments and improvements welcome!

Föderalismus der Streaming-Anbieter

Nachdem vor kurzem schon die Ministerpräsidenten der Bundesländer das Hohelied auf den Föderalismus gesungen haben, und dafür zu Recht kritisiert wurden, machen es die großen Contentproduzenten jetzt nach. Konzerne wie Disney verabschieden sich von Netflix und machen ihre eigene Abo-Platform auf. Der geneigte Zuschauer/Konsument muss also in Zukunft immer mehr Abos abschließen um seine Lieblingsfilme und Serien sehen zu können. Das dass kein guter Trend ist, muss sicher nicht noch extra betont werden. Aber erschreckend daran ist nicht nur die allgemeine Tendenz, sondern auch die Reaktionen darauf. Die beschränken sich nämlich weitestgehend auf “Dann saug’ ich halt Illegal. Dann merken die schon was sie davon haben!” Und während die Analyse, warum diese Tendenz schlecht ist, richtig ausfällt sind Schlussfolgerungen wie die eben genannte nicht nur kurzsichtig sondern gehen grundlegend in die verkehrte Richtung.

Abgesehen davon, dass es überaus unwahrscheinlich ist, dass die Anbieter “merken, was sie davon haben” – wäre es nicht viel sinnvoller, wenn es Leute gäbe, die Gesetze erlassen könnten, die die Anbieter zur Interoperabilität zwingen würden? Nennen wir diese Leute spaßeshalber “Politiker”. Diese könnten ein Gesetz erlassen, dass alle Anbieter verpflichtet, eine offene API anzubieten, über die man ihren Content kaufen oder leasen kann, und mit allen anderen Anbietern, die das wünschen, entsprechende Verträge/Peerings einzugehen. Und bei der Gelegenheit kann man gleich noch eine äquivalente Verpflichtung für die Socialmedia-Anbieter mit rein schreiben!

Alle FDP-Wähler und Teile der CDU (dazu zählt auch die SPD) werden jetzt schreien: “So eine Regulierung geht zu weit! Das schadet unserer Wirtschaft!” Halt das Standardargument derer, die erst den Mund aufmachen bevor sie denken. Sonst würden sie ja vielleicht merken, dass alle großen Socialmedia- und Contentanbieter in den USA sitzen und “unsere Wirtschaft” davon eigentlich nur profitieren könnte! Und für alle anderen, die ebenfalls immer noch der Meinung sind, dass das nicht geht, machen wir jetzt ein kleines Gedankenexperiment!

Wir stellen uns vor, dass es im Internet kein Peering zwischen den einzelnen Netzen und ISPs gäbe. Dann müsste ein Telekomkunde auch noch einen Vertrag mit Vodafone machen, wenn er Inhalte von Servern in deren Netz abrufen will. Und wenn der Betreiber dieser Server plötzlich zu Unitymedia wechselt, braucht der gebeutelte Telekomkunde auch noch einen Vertrag (mithin also ein Abo) mit Unitymedia!
Klingt absurd? Wäre es wohl auch. So absurd, dass die Netzbetreiber – in den meisten Fällen – gar nicht erst dazu gezwungen werden müssen, Peerings einzugehen.

Wenn also die Contentanbieter dazu gezwungen werden müssen – warum nicht? Ist ja nicht so, als gäbe es ein Naturgesetz, dass diese Anbieter machen dürfen, was sie wollen. Viele Leute scheinen sich schon so daran gewöhnt zu haben, in einer Demokratie zu leben, die bestimmte Strukturen aufweist, dass ihnen nicht (mehr) bewusst ist, dass Demokratie sich nicht darauf beschränkt alle vier Jahre zwei Kreuze auf einem Wahlzettel zu machen!
Demokratie meint “Herrschaft des Staatsvolkes”! Und wenn das Staatsvolk sich nicht länger von den Socialmedia- und Contentanbietern gängeln und ausbeuten lassen will, dann ist es nicht nur legitim, sondern vielmehr angebracht – ja, erforderlich – das zu fordern und letztlich in Gesetzesform zu gießen!

Und was den Faible unserer Ministerpräsidenten für Föderalismus angeht, so sollten sich diese mal klar machen, dass das Staatsvolk aus mehr besteht als nur den Einwohnern ihres jeweiligen Bundeslandes. Und das “Konkurrenzfähigkeit” beim Thema Bildung – und darum geht es denen augenscheinlich nämlich – nicht nur absurd ist, sondern zum Schaden für alle Bürger.
Der Föderalismus – nicht nur im Bildungssektor – wurde von den Alliierten (wieder) eingeführt, als Lehre aus der Nazizeit. Es sollte damit eine “zu Missbrauch verleitende Machtkonzentration” verhindert werden. Nun ist aber diese Gefahr heute nicht mehr gegeben. Unter anderem, weil der (Rechts)populismus heute dank der Digitalisierung, mehr Möglichkeiten hat, als ein Bildungssystem unter Kontrolle des Bundes jemals bieten könnte. Wir haben also keinen Vorteil mehr vom Bildungsföderalismus, dafür aber jede Menge Nachteile!
Aber dazu wollte ich eigentlich gar nicht so viel schreiben. Denn das hat Richard David Precht schon getan, der ein ganzes Buch zu dem Thema heraus gebracht hat: “Anna, die Schule und der liebe Gott

Also: statt illegal zu saugen, beschwert euch mal lieber bei eurem Abgeordneten darüber! Die meisten von denen sind durchaus offen für Argumente und bei weitem nicht so abgehoben und ignorant wie die Spitzen der etablierten Parteien!

Atlassian ist schlechter als sein Ruf!

Neulich haben wir das Budget für einen selbst gehosteten BitBucket Server bekommen.
Nun war mir schon bewusst, dass BitBucket nicht gleich BitBucket Server ist. Genau genommen, sind BitBucket (Cloud) und BitBucket Server zwei verschiedene Produkte, die außer dem Namen und dem Einsatzzweck nicht viel gemeinsam haben.

Aber wie beschissen die Situation ist, war mir nicht klar. Zu behaupten, dass Atlassian die Server-Variante von BitBucket stiefmütterlich behandelt, wäre noch untertrieben.

Das fängt damit an, dass Änderungen an Pull-Requests keine Webhooks triggern können. Das ist dummerweise nicht sofort ersichtlich, das es durchaus eine Checkbox dafür gibt.
Erst bei genauerem Hinsehen fällt auf, dass das nur für Änderungen an “Titel, Beschreibung oder Zielbranch” eines Pull-Request gilt. Da fragt man sich doch, was sich häufiger bei einem Pull-Request ändert – ein weiterer Commit zum Nachbessern, oder Titel und Beschreibung.

Das ist zwar schon beschissen genug, aber man denkt sich halt: OK, dann muss ich das Ding halt minütlich mit Requests gegen die REST-API bombardieren …bis man sich mit den Einzelheiten der REST-API beschäftigt.
Denn auch diese ist fundamental anders, als die API der Cloud-Version und lässt ungefähr die Hälfte der Features vermissen!
Angaben über den Status eines Pull-Request? Pustekuchen! Von den Feldern die tatsächlich vom Server zurück kommen, ist auch nur die Hälfte dokumentiert.
Und auch an anderer Stelle ist die Doku für den Arsch! Das fällt spätestens dann auf, wenn man versucht die Kommentare eines Commit oder Pull-Request zu holen. Das geht nämlich nicht!
Kommentare können nur pro File (eines Commit/PR) abgerufen werden. Nur steht das nicht in der Doku. Auch nicht, dass der dazu gehörige Parameter “path” verpflichtent ist. Als Beschreibung steht da nur “the path to stream comments for a given path” – was zum Geier soll mir das sagen?
Auch das Anlegen eines Kommentar ist nicht weniger bekackt. Dafür braucht es nämlich zusätztlich zur Commit/PR-Id und dem Pfad noch einen “fromHash” und “toHash” .

Und nur um zu demonstrieren, dass sich dieses Verhalten nicht auf BitBucket (Server) beschränkt: In JIRA kann man für ein Projekt den default assignee nur auf den Projekt-Lead oder unassigned setzen. Es gibt seit Jahren ein Ticket, um auch andere User zu erlauben – bislang ohne Erfolg.

Und um noch mal auf BitBucket zurück zu kommen: wenn man eine Lizenz für BitBucket Server kauft, bekommt man ein Jahr Support – was Updates mit einschließt. Aber nach dem Jahr gibt es nicht nur keine Feature-Updates mehr. Es gibt gar keine Updates mehr – also auch keine Sicherheitsupdates!
Dafür dass das Zeug verdammt viel Geld kostet, ist das nicht nur ziemlich armselig sondern einfach nur dreist!

return null;

In seinem Buch “Clean Code” empfiehlt Robert C. Martin, nicht explizit “null” zurück zu geben. Weil es Arbeit erzeugt und den Code mit null-checks unleserlich macht.
Ich bin aber der Meinung, dass man das nicht verallgemeinern kann und sollte. An folgenden Beispiel will ich das einmal deutlich machen.

These:
Wenn wir einen DAO haben, mit einer Methode “findItemById(id)“ dann soll “null” zurück geben werden, wenn kein Item mit dieser Id existiert!

Begründung:
Zum einen bekommen wir mit dem Wert “null” genau das zurück, was wir erwarten würden. Denn wir müssen davon ausgehen, dass in dem Repository oder der DB die wir da abfragen kein Eintrag mit dieser Id existiert.
Es wäre idiotisch zu erwartet, dass die DB einen Eintrag für jede Id hat, die wir uns ausdenken können – es zwingt uns schließlich niemand, der Methode nur Ids zu übergeben, für die ein Eintrag existiert!
Die Methode in dem DAO gibt uns also genau das zurück, was in der DB steht: kein Eintrag! Und “null” ist IMO die beste Repräsentation von “kein Eintrag”.

Ich habe schon Implementationen gesehen, wo im Fall von “kein Eintrag” eine UnknownEntryException geschmissen wird. Aber das ist ja nicht richtig.
Eine Exception – also eine Ausnahme – impliziert ja, dass ein Fehler aufgetreten ist, der so nicht zu erwarten war. Aber das stimmt ja nicht. Wie bereits erläutert mussten wir ja zwingend davon ausgehen, dass so ein Fall eintreten könnte. Und es ist auch kein Fehler! Einen Eintrag mit einer beliebigen Id in einer DB nicht zu finden ist kein Fehler sondern normal.

Uncle Bob empfiehlt in seinem Buch “special case”-Objects zu verwenden. Aber in dem Fall würde das ja nur noch mehr Verwirrung stiften. Denn es wäre noch schwerer zu erkennen ob er den Eintrag in der DB gefunden hat, und dieser einfach nur leer ist, oder ob der Eintrag nicht existiert. (Special-Case Objekte sind nur dann sinnvoll, wenn ich nicht explizit diese Unterscheidung machen muss, weil sie ggfs. nicht relevant ist)

Prüfen muss ich es in dieser Konstellation nämlich in jedem Fall!
Das heißt, egal was ich mache um das Zurückgeben von “null” zu vermeiden – die Arbeit, den Fall “kein Eintrag gefunden” zu checken, kann ich mir dadurch nicht ersparen!
Wenn ich es doch machen würde, hieße das: ich frage explizit etwas an, schere mich aber einen Dreck um das Ergebnis – das wäre hochgradig Verantwortungslos. (Zumindest in den allermeisten Fällen)

Da wir nun also festgestellt haben, dass wir eine Überprüfung machen müssen, schauen wir doch mal, wie diese am Einfachsten realisiert werden kann (KISS-Prinzip).

Fall 1 – return null

ResultObject result = dao.findItemById(42);
if(result == null) {
    return;
}
Ich denke dazu brauch ich nicht viel schreiben: kurz, auf den Punkt, sofort verständlich.

Fall 2 – throw Exception

ResultObject result;
try {
    result = dao.findItemById(42);
} catch(UnknownEntryException e) {
    return;
}
Doppelt so viele Zeilen wie in Fall 1.
Es macht den Code außerdem unleserlicher und kann zu Verwirrung führen, da nicht auf den ersten Blick erkenntlich ist, ob hier tatsächlich ein Fehlerfall behandelt wird.
Dass es keine Option ist, das try-catch-statement einfach weg zu lassen, muss ich hoffentlich nicht extra erwähnen (das hab ich oben schon genug getan).

Fall 3 – “Special case object 1”

ResultObject result = dao.findItemById(42);
if(result.getId() == 0) {
    return;
}
Wie ein Special-Case-Object gestalltet wird, ist natürlich nicht fest vorgegeben. Hier also der Fall, dass es einfach ein normales ResultObject mit leeren Feldern ist.
Und wir sehen direkt die Probleme die damit einher gehen! Zum einen ist nicht klar, ob “0” nicht eventuell eine gültige Id sein könnte.
In diesem Fall setzen wir auch voraus, dass die Id, mit der wir anfragen auch Teil des Ergebnis ist. Wenn sie das nicht wäre, könnten wir nicht mehr unterscheiden, ob der Eintrag gefunden wurde und einfach nur leer ist oder ob er nicht gefunden wurde.
Das nächste Problem damit, erläutere ich gleich in Fall 4.

Fall 4 – “Special case object 2”

ResultObject result = dao.findItemById(42);
if(result.isNoEntry()) {
    return;
}
In diesem Fall haben wir dass Problem, dass wir unser Ergebnis-Objekt um eine Methode oder ein Feld erweitern müssen, um diesen Fall explizit abzufragen.
(bzw. wird es sich wohl eher um eine DataStructure handeln, aber in Java gibt es da keine so explizite Unterscheidung, also nennen wir es der Einfachheit halber ein Objekt)
Das führt direkt zu einem weiteren Problem: wie benennt man so etwas? “isEmpty()“ wäre z.B. schlecht, weil jemand der den Code ließt, davon ausgehen könnte, es handelt sich um ein Objekt dass von Collection erbt.
Aber auch bei “isNoEntry()“ stellt sich die Frage, ob es sich dabei um einen Wert handelt, der so im Eintrag in der DB steht.
Und was das schon in Fall 3 angeteaserte Problem angeht: bei diesem Code wird die verwendete IDE vermutlich anmeckern, dass “result” null sein könnte. Aber selbst wenn nicht: normalerweise arbeitet man ja nicht alleine. Und sobald ein Kollege den Code anfasst, wird ihm (oder ihr) wahrscheinlich auffallen, dass dort ein null-check fehlt! Und dann sieht der Code ganz schnell so aus:

ResultObject result = dao.findItemById(42);
if(result == null || result.isNoEntry()) {
    return;
}
Was fällt uns auf? Genau: der Code sieht so aus wie in Fall 1 – nur mit einem zusätzlichen Aufruf von “isNoEntry”!
Wäre es nicht einfacher, diesen zusätzlichen Aufruf einfach weg zu lassen? Dann könnten wir auch darauf verzichten, das ResultObject zu erweitern.

Schlussfolgerung:
Wenn wir in einem Repository ein bestimmtes Item abfragen, dann ist es sinnvoll “null” zurück zu geben, wenn dieses Item nicht existiert!

Zusätzliche Anmerkung:
Und dass mir jetzt keiner mit dem Vorschlag kommt, doch stattdessen eine Liste mit nur einem Eintrag zurück zu geben!
Nicht nur, dass wir dem Aufrufer (also dem Nutzer des DAO) damit zusätzliche und unnötige Arbeit aufhalsen, weil dieser auch noch den unwahrscheinlichen Fall abfragen muss, falls mehr als ein Item in der Liste ist.
Es ist unnötige Ressourcenverschwendung, dass Item noch mal in eine Liste zu packen, die sonst keinen Zweck erfüllt.
Außerdem wird das API des DAO dadurch kontra-intuitiv und umständlicher zu benutzen (um es mal vorsichtig zu formulieren).

Nachtrag:
Eine zusätzliche Methode im DAOexistsItemWithId(id)“ die ein boolean zurück gibt, halte ich im Hinblick auf Performance ebenfalls für untauglich. Die Datenbank ist ohnehin schon das Bottleneck der gesammten Applikation – da muss man nicht noch eine zweite Abfrage absetzen, wenn eine auch reichen würde.

Maven in Eclipse

Wie beschissen das m2e-Plugin in Eclipse ist, sollte ja hinlänglich bekannt sein.

Die letzte halbwegs funktionierende Version von m2eclipse war 0.12.1 – bevor es von Sonatype zu Eclipse gewandert ist und dort bis zur Unbenutzbarkeit verhunzt wurde. Die repositories/update-sites für die alten Versionen gibt es natürlich schon lange nicht mehr. Das Plugin selber funktioniert an und für sich noch mit aktuellen Eclipse-Versionen – sofern das Eclipse-eigene m2e-plugin nicht installiert ist.

Ich war so frei, die nötigen Daten zusammen zu stellen:
m2eclipse-0.12.1.tar.gz

Das Plugin selbst, benötigt noch einige andere Standard-Plugins (wie zum Beispiel die Web- und XML-Tools). Zum “installieren” müssen dann einfach nur noch die Verzeichnisse “features” und “plugins” in das Eclipse Install-dir kopiert werden.

Das ist zwar alles nicht das Gelbe vom Ei, sollte aber um einiges besser funktionieren, als das aktuelle m2e-Plugin, dem immer irgendwelche Module fehlen!