5

Imagine we use Gitflow where we have split a release branch from develop that will eventually be merged into both main and develop. On release there are only quality improvements. Most of these require a deployment to an integration stage, so their version in several pom.xml (multi-module) and package.json is updated and tagged on the release branch.

On develop there is regular (unstable) feature development for future releases, and the version has been set accordingly. Occasionally the improvements from release are merged back into develop. There we get merge conflicts, marked with an X in the following illustration.

main     ----------------------o----
                              /
release        o---o-----o-o-o
              /     \     \   \
develop  ----o---o---x--o--x-o-x----
                           ^
               we are here |

Example:

  • On release the version number is 1.0.0-SNAPSHOT.
  • On develop the version number is 1.1.0-SNAPSHOT after branching off.
  • New features go into develop, the version number stays constant there.
  • The version in release is incremented (and tagged) occasionally to 1.0.1, 1.0.2, 1.0.3 and so on.
  • Now of course there is a conflict when I want to merge version 1.0.x into 1.1.0 while the common ancestor is 1.0.0.
    • (We fully understand what happens there, don't need explanation for that.)
$ git checkout develop
$ git merge --no-commit --no-ff release

Auto-merging pom.xml
CONFLICT (content): Merge conflict in pom.xml
...
Auto-merging client/package.json
CONFLICT (content): Merge conflict in client/package.json
Automatic merge failed; fix conflicts and then commit the result.

We are looking for ideas to handle this situation. Some research shows that this is not an uncommon problem, so I found several suggestions. Mostly it is said to just manually resolve the conflicts. But I am still eager to find a way that can be automated in a script. Maybe there is some Git magic to help? Maybe we started badly in the first place?

The following discussion brings a better image to it, where we are at "Only bugfixes!":

Approach 0 -- Do not do this?

Since our team started doing this kind of version increments and tagging, I've been unsure if this is a good practice. But it basically works, we defined the details of the workflows together, and our customer and partners and test team require us to deliver release candidates as if it were actual releases. When a version x.y.z has been tested successfully, it goes to productive environment unchanged and then release is merged into main. But the problem stays: As soon as a hotfix is made in main and should be backported to develop, we are going to get version conflicts again.

Approach 1 -- Cherry Picking?

Sorry, I won't do this. Too often I read that cherry picks are evil. And they would be against Gitflow.

Approach 2 -- Accept to resolve manually?

That is what we do now. The process is not automated. Every time the version number in release is changed, the following merge gets a conflict that has to be manually resolved. We accept it but are unhappy with that.

Approach 3 -- Do not merge that often?

I guess that would be bad practice. We want to have the quality improvements merged to all our branches.

Approach 4 -- Use merge option --ours or similar?

The problem is that automated "resolution" of merge conflicts is file-based, from what all I could find out, not line- or block-based. We need to keep the version number from develop, but other changes in those files pom.xml or package.json might be on either side and not to be overridden blindly, so these kind of conflicts we want to see and resolve manually. I am open to any suggestions in this direction though!

Approach 5 -- Move version number to separate file?

This way we would reduce the conflicts to one single location where it can be trivially resolved using --ours. While it seems to be possible with newer Maven versions, I am not aware of an approch for package.json to refer to an externally defined version number. Has anyone made good experience with that and would recommend to go further this way?

Approach 6 -- Prepare and reset version in develop?

I saw such behavior by jgitflow-maven-plugin which is not maintained for over 6 years now. We could make a commit in develop, writing the release version into the files, then merge, and change the version back to the original one.

I dislike that there would be additional commits that have nothing to do with actual development, and I see no possiblity to clean up the Git history.

So this would be an interesting follow-up question: I know I can rebase/squash D into C, but I don't see how I can rebase/squash A or B into C. Does anyone else?

-----B---------
      \
---A---C---D---

Approach 7 -- Prepare version in release?

Similar to the previous approach, we could make a commit in release, write the target version, then merge to develop without conflict. We would then not need a revert commit in release but could just move the branch pointer back with git reset --hard HEAD^ and/or simply not push it, so this preparation commit would be located "between" the two branches.

-----B-----------
      \
       B'
        \
---A-----C---D---

The following article describes a similar thing using an intermediate branch (to fulfill the requirement for a pull request), but it is several manual steps that don't solve my challenge.

Approach 8 -- Prepare version without commit?

My favorite solution would be to just write the target version in local develop without commit, then merge release onto that... but git merge does not allow this. I do not see any switch to override this behavior and ignore unmerged

error: Your local changes to the following files would be overwritten by merge:
        client/package.json
        ...
        pom.xml
Please commit your changes or stash them before you merge.
Aborting

Searching the web tells me to stash the local changes, but that is not an option of course.

Approach 9 -- Write program to resolve conflicts?

I play with the idea that these conflicts are well-structured and can even be fully predicted, so it should be possible to write a small shell script to grep/sed the conflicts in order to automatically resolve and commit the merge. But I hesitate to put large efforts here and hope for enlightenment by other people!

General Grievance
  • 4,555
  • 31
  • 31
  • 45
Lynax
  • 117
  • 1
  • 11
  • 1
    I wonder if it wouldn’t be possible to rely on [Rerere](https://git-scm.com/book/en/v2/Git-Tools-Rerere) to do this: you would write the version conflict resolution in the rr-cache before merging, and then git won’t cause a conflict for that. Or alternatively… I just came across [this](https://github.com/ralfth/pom-merge-driver) (maybe with [this caveat](https://stackoverflow.com/q/33412003/525036)?) – Didier L Jul 06 '22 at 15:22
  • Thanks for those ideas! I did a few tests with Rerere, but it seems to work on very specific data only, no wildcards, no patterns, so it would be useful only if you re-apply the exact same merges many times. The second idea goes towards writing a custom merge strategy as far as I understand. That looks promising indeed, but I am reluctant to take that effort for now. Also I'd prefer a solution that works on arbitrary file formats, not POMs only. – Lynax Jul 08 '22 at 16:30
  • I am not fully done with this topic yet. I tend to use approach 7 "intermediate commit" maybe with some nice scripting, or plan B is to write custom conflict resolvers using heuristics maybe. – Lynax Jul 08 '22 at 16:33
  • For Rerere, I thought maybe it would be possible to train it by triggering a conflict on 2 branches whose only difference would be the version, using `git merge -X ours` or something like that (not sure `-X ours` trains Rerere). For the custom merge strategy, note that the link I gave already provides a solution for `pom.xml`. Most projects only store versions in 1 or 2 different file formats (e.g. a `package.json`), so if you only need a second implementation, that seems acceptable. – Didier L Jul 08 '22 at 22:50

2 Answers2

2

Note: "Best Practice" for something like this is difficult to define, since everyone's situation is likely different.

That being said, one of our projects has a similar situation as yours: we use Git Flow, and our develop branch build numbers are always different than the release branch build numbers. Our potentially ideal solution has not been implemented, but would likely be similar to your suggested Approach 8, where we would inject the version into the build pipeline without it being hard-coded in a commit (i.e. don't even modify the version file at all). The downside of this though is you can't know what version is represented by a specific commit based on code alone. But you could tag the commit with a specific version, which is probably what we would do if we implemented that. We could also bake the commit ID along with version info into the artifact meta data for easy lookup.

The solution we currently use is a combination of Approaches 4, 5, and 7. We separate version files (your Approach 5), and every time we create a release (or hotfix) branch, the first commit only changes the version file to the upcoming release version (your Approach 7). We make sure that release always has the tip of main in it, so that anytime we deploy release to production we can cleanly merge release to main. (Note we still use --no-ff as suggested by Git Flow but the point is we could fast-forward if we wanted to.)

Now, after you complete the release branch into main, Git Flow suggests merging release back to develop, but we find merging main back to develop slightly more efficient so that the tip of main is also on develop, but occasionally we also merge release back into develop before deployment if important bug fixes appear on release. Either way, both of those merges back to develop will always have conflicts with the version files on develop, and we use your Approach 4 to automate choosing the develop version of those files. This enables the merge back to be fully automated, however, sometimes there are still other conflicts that have to be resolved manually, just as a course of normal development happening on develop and release simultaneously. But at least it's usually clean.

Note that a side effect of our approach is that our versions files are always different on develop and main, and that's fine with us.

TTT
  • 22,611
  • 8
  • 63
  • 69
  • Upvoted for sure! Sorry for the late reply. You precisely describe the situation. Though your input was not able to solve the problem, it still gives helpful insights, thanks a lot! No 4 "--ours" is quite no-go for us unfortunately. For No 5 I still didn't find a solution for NPM package.json. I slowly work on using No 7 or 9 perhaps. Btw I fully agree on your comment regarding "--no-ff". – Lynax Jul 08 '22 at 16:50
  • 1
    @Lynax regarding #4, we don't actually use automated conflict resolution there (-X ours). We have our version files completed separated so that the version is the *only* thing that ever changes in that file. (Any other meta data is in another file.) So we just completely skip release version changes on `develop` all together. The automation is basically, `git merge release-branch --no-commit` followed by reset and restore all the version files to their develop version, then commit to complete the merge. – TTT Jul 08 '22 at 17:49
0

What about using an external tool to manage the version? We use GitVersion for this. Now I am not sure if there is a smarter way, but a brute-force one is to have something like this <version>${env.GitVersion_SemVer}</version> in your pom.xml, where env.GitVersion_SemVer is an output from GitVersion.

  • I'm afraid I don't really understand what that tool technically does. Does it write the version into my files during build-time? Or does it provide variables only? How can I make use of this with NPM package.json? – Lynax Jul 08 '22 at 16:40
  • The tool sets values to variables, that can be accessed later. This is how I use it in the release pipeline https://github.com/mermaid-js/mermaid-cli/blob/master/.github/workflows/release-publish.yml#L46 and https://github.com/mermaid-js/mermaid-cli/blob/master/.github/workflows/release-publish.yml#L103 – Mindaugas Laganeckas Aug 16 '22 at 13:09