Backporting a Merge Request
Introduction
When a merge request is merged into main, it may need to be backported to maintenance branches. As each merge request consists of multiple commits, backporting it to another branch boils down to using git cherry-pick
:
-
Create a new branch to be merged into the release branch. For example, if you are working on a GitLab issue number 9999 for version 9.16, you could create
9999-short-description- v9_16
. -
Cherry-pick each commit from the original merge request:
git cherry-pick -x <commit>
-
Some commits won't apply cleanly, fix the commits manually.
-
Creating a merge commit linking the previous HEAD of the target branch with the last cherry-picked commit, preferably retaining the commit log message from the original merge commit (e.g. to leave the merge request identifier intact for future reference).
There are two scripts that can make this process more convenient:
-
git-replay-merge.sh
is a scripted version of the previous workflow. The script can be found in the repository. If it works for you, you can run:sh util/git-replay-merge.sh <merge_commit_id> <target_remote> <target_branch>
So for the example above you might run:
sh util/git-replay-merge.sh <merge_commit_id> origin 9999-short-description-v9_16
-
GitLab also has a "Cherry-pick Merge Request" button. If commits apply cleanly this is perhaps the most intuitive workflow.
Backporting a merge request to a subscription release
Backporting to a subscription release is slightly different. Basically, we squash the merge request into single commit and then merge it into the subscription release branch. The reason for squashing is that it becomes quite a mess after a while when continuously rebasing stuff on top of the regular release branch.
git-replay-merge.sh
to the rescue
Overview
A script called git-replay-merge.sh
was written to make backporting merge requests more convenient by automating the process as much as possible.
How it works
The script can be thought of as a wrapper for git cherry-pick
and git merge
which cowardly interrupts its actions as soon as things go south. In short, the script:
- Detects the range of commits that needs to be cherry-picked.
- Initiates the cherry-picking process on a throwaway local branch, called the replay branch below.
- If conflicts arise while cherry-picking, you need to fix them yourself and let the script know when you are done.
- Once cherry-picking succeeds, the replay branch is merged into a local branch with the same name as the target branch. Original merge commit log message with a modified subject line is used for the merge commit in the target branch.
- The replay branch is removed.
Summing up, given:
- the ID of the merge commit in the "original" branch (i.e. the branch the merge request was merged into),
- the name of the git remote to ask about the target branch,
- the name of the target branch,
the script prepares a local branch with the relevant merge request "replayed" for pushing.
The script only prepares a branch for pushing; it is not pushed automatically.
If at any point in the process the user decides the script should not continue its work, --abort
can be used to restore the working copy to the state it was in before the script was called.
Installation
To use the script:
- copy
util/git-replay-merge.sh
somewhere into your$PATH
, - rename the script to
git-replay-merge
, - make sure the script is executable.
Running git replay-merge
(without the first hyphen) should then reward you with a usage message:
$ git replay-merge
Usage:
git replay-merge <merge_commit_id> <target_remote> <target_branch>
git replay-merge --continue
git replay-merge --abort
Example: the hard case (with conflicts)
Let's take a deep dive first and imagine !22 (merged) needs to be backported to v9_12.
Initial situation
Git setup:
$ git remote -v
gitlab git@gitlab.isc.org:isc-projects/bind9.git (fetch)
gitlab git@gitlab.isc.org:isc-projects/bind9.git (push)
...
Original branch (excerpt):
$ git log --no-decorate --graph --oneline gitlab/master
...
* 3abc7bf264 Merge branch 'fix-loadpending-handling' into 'master'
|\
| * 801dfe8f5d Add CHANGES entry
| * f5079bb877 Do not recheck DNS_ZONEFLG_LOADPENDING in zone_asyncload()
| * b9e9361c7b Asynchronous zone load events have no way of getting canceled
| * 29b7efdd9f Only clear DNS_ZONEFLG_LOADPENDING in zone_asyncload() if zone loading is completed immediately
| * 0e4fba2ced Lock zone before checking whether its asynchronous load is already pending
* | 883a9485e9 [master] copyrights
|/
* 3548061d03 Merge branch 'gitlab-ci-keep-artifacts' into 'master'
...
Target branch:
$ git log --no-decorate --max-count=1 --oneline gitlab/v9_12
44d995992a Merge branch 'fix-cpp-check-errors' into 'v9_12'
git replay-merge
session log
$ git replay-merge 3abc7bf264 gitlab v9_12
Attempting to replay 883a9485e95916a686e56d81fff5130ac3102953..801dfe8f5d69ccb1252fbc57e2feeed34a9a438c on top of gitlab/v9_12 in fix-loadpending-handling-v9_12...
Switched to a new branch 'fix-loadpending-handling-v9_12'
[fix-loadpending-handling-v9_12 6b5b0dcb94] Lock zone before checking whether its asynchronous load is already pending
Date: Thu Feb 15 20:31:49 2018 +0100
1 file changed, 5 insertions(+), 2 deletions(-)
[fix-loadpending-handling-v9_12 8c9de139df] Only clear DNS_ZONEFLG_LOADPENDING in zone_asyncload() if zone loading is completed immediately
Date: Thu Feb 15 20:31:51 2018 +0100
1 file changed, 4 insertions(+), 2 deletions(-)
[fix-loadpending-handling-v9_12 eb8de80fcd] Asynchronous zone load events have no way of getting canceled
Date: Thu Feb 15 20:31:53 2018 +0100
1 file changed, 1 insertion(+), 6 deletions(-)
[fix-loadpending-handling-v9_12 87ac04b893] Do not recheck DNS_ZONEFLG_LOADPENDING in zone_asyncload()
Date: Thu Feb 15 20:31:54 2018 +0100
1 file changed, 11 deletions(-)
error: could not apply 801dfe8f5d... Add CHANGES entry
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'
Replay interrupted. Conflicts need to be fixed manually.
When done, run "git replay-merge --continue".
Use "git replay-merge --abort" to abort the replay.
Oops, there is a conflict in CHANGES
:
$ git status
On branch fix-loadpending-handling-v9_12
Your branch is ahead of 'gitlab/v9_12' by 4 commits.
(use "git push" to publish your local commits)
You are currently cherry-picking commit 801dfe8f5d.
(fix conflicts and run "git cherry-pick --continue")
(use "git cherry-pick --abort" to cancel the cherry-pick operation)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: CHANGES
no changes added to commit (use "git add" and/or "git commit -a")
Let's fix the conflict using an editor of choice. Once that is done, cherry-picking can continue:
$ $VISUAL CHANGES
$ git add CHANGES
$ git replay-merge --continue
[fix-loadpending-handling-v9_12 b7a6e602e9] Add CHANGES entry
Date: Thu Feb 15 20:31:55 2018 +0100
1 file changed, 6 insertions(+)
Attempting to merge fix-loadpending-handling-v9_12 into v9_12...
Switched to a new branch 'v9_12'
Merge made by the 'recursive' strategy.
CHANGES | 6 ++++++
lib/dns/zone.c | 29 +++++++++--------------------
2 files changed, 15 insertions(+), 20 deletions(-)
Replayed fix-loadpending-handling onto v9_12.
To push the replay, use:
git push gitlab v9_12:v9_12
That's it, the replay is complete:
$ git log --graph --oneline v9_12
* 6801ed2c4f (v9_12) Merge branch 'fix-loadpending-handling-v9_12' into 'v9_12'
|\
| * b7a6e602e9 Add CHANGES entry
| * 87ac04b893 Do not recheck DNS_ZONEFLG_LOADPENDING in zone_asyncload()
| * eb8de80fcd Asynchronous zone load events have no way of getting canceled
| * 8c9de139df Only clear DNS_ZONEFLG_LOADPENDING in zone_asyncload() if zone loading is completed immediately
| * 6b5b0dcb94 Lock zone before checking whether its asynchronous load is already pending
|/
* 44d995992a (gitlab/v9_12) Merge branch 'fix-cpp-check-errors' into 'v9_12'
...
The branch is now ready to be pushed using the command suggested by git replay-merge
.
Example: the easy case (no conflicts)
With the pessimistic scenario sorted out, let's now try backporting !21 (merged) to v9_12.
Initial situation
Git setup:
$ git remote -v
gitlab git@gitlab.isc.org:isc-projects/bind9.git (fetch)
gitlab git@gitlab.isc.org:isc-projects/bind9.git (push)
...
Original branch (excerpt):
$ git log --no-decorate --graph --oneline gitlab/master
...
* 54823ea037 Merge branch 'fix-dnstap-output-file-rolling' into 'master'
|\
| * 448eb98797 (gitlab/mr/21) Add CHANGES entry
| * 02063cbae2 Make dns_dt_send() call dns_dt_reopen() asynchronously
| * 8e3c16175a Make dns_dt_reopen() request task-exclusive mode on its own
| * f199a5a9ae Add dns_dt_create2()
|/
* 522e5dd9bc 4893. [bug] Address various issues reported by cppcheck. [GL #51]
...
Target branch:
$ git log --no-decorate --max-count=1 --oneline gitlab/v9_12
fba6c2e982 Merge branch 'fix-loadpending-handling-v9_12' into v9_12
git replay-merge
session log
$ git replay-merge 54823ea037 gitlab v9_12
Attempting to replay 522e5dd9bc6df64bc2388e4056ff293377ef678c..448eb987970144461b264fe2f238c850a50090e4 on top of gitlab/v9_12 in fix-dnstap-output-file-rolling-v9_12...
Switched to a new branch 'fix-dnstap-output-file-rolling-v9_12'
[fix-dnstap-output-file-rolling-v9_12 4871445b5c] Add dns_dt_create2()
Date: Mon Feb 5 21:19:44 2018 +0100
3 files changed, 25 insertions(+), 2 deletions(-)
[fix-dnstap-output-file-rolling-v9_12 9cac22573b] Make dns_dt_reopen() request task-exclusive mode on its own
Date: Mon Feb 5 21:22:53 2018 +0100
3 files changed, 15 insertions(+), 6 deletions(-)
[fix-dnstap-output-file-rolling-v9_12 b4ace595e1] Make dns_dt_send() call dns_dt_reopen() asynchronously
Date: Mon Feb 5 21:04:39 2018 +0100
1 file changed, 93 insertions(+), 6 deletions(-)
[fix-dnstap-output-file-rolling-v9_12 6529f4d5b5] Add CHANGES entry
Date: Fri Feb 16 09:38:48 2018 +0100
1 file changed, 3 insertions(+)
Attempting to merge fix-dnstap-output-file-rolling-v9_12 into v9_12...
Switched to a new branch 'v9_12'
Merge made by the 'recursive' strategy.
CHANGES | 3 ++
bin/named/server.c | 8 ++---
lib/dns/dnstap.c | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
lib/dns/include/dns/dnstap.h | 17 ++++++++--
lib/dns/win32/libdns.def.in | 1 +
5 files changed, 136 insertions(+), 14 deletions(-)
Replayed fix-dnstap-output-file-rolling onto v9_12.
To push the replay, use:
git push gitlab v9_12:v9_12
That's it, the branch is all set for pushing.