This page is part of a static HTML representation of TriTarget.org at https://tritarget.org

Multiple Dependent Pull Requests in Git

Sukima16th October 2023 at 10:15am

How do I manage multiple dependent Pull Requests in the least chaotic way possible?

Often my work is multi faceted and requires several incremental steps to complete. In efforts to keep the scope of work — and the resultant Pull Requests — to a reasonable size I would want to split the work up. Unfortunately, this introduces a maintenance nightmare as each subsequent branch will quickly diverge as I address feedback on each pull request individually — often catastrophically so.

How can I manage multiple dependent pull requests in a less chaotic way while addressing feedback and maintaining possibly frequent rebasing on the main branch?

This is a complex topic but there is hope. The latest Git versions have several helper options to manage this better. You will need a fairly new version of Git. I’ve tested this with git version 2.41.0.28.

Add the following config options:

git config --global rebase.autosquash true
git config --global rebase.updateRefs true
git config --global rebase.rebaseMerges true

In this example I will build up a PR set. The first PR will be featureA.

* 3d477ad (HEAD -> featureA) A2
* f0751af A1
* 1304eb2 (main) M1

Make your usual Pull Request and base it off main. While that is waiting I'll move on to featureB.

* eb9af14 (HEAD -> featureB) B1
* 3d477ad (HEAD -> featureA) A2
* f0751af A1
* 1304eb2 (main) M1

But while working on featureB I realized I need to fix a bug in another module. The fix is isolated on its own and deserves its own pull request but featureB also depends on the fix. To make it easy I will work on it here and rebase/cherry-pick the changes to their own branch. This will temporarily look like:

* 2fcb9e3 (HEAD -> featureB) B2
* 947f7a7 fix1
* eb9af14 B1
* 3d477ad (featureA) A2
* f0751af A1
* 1304eb2 (main) M1

Then I'll run the following:

# Make a new branch off main
git checkout -b my-fix main # merge base
# Pull in the temp changes from featureB
git cherry-pick 947f7a7
# ... Create pull request ...
git checkout featureB

We will rebase just featureB'on its own. git rebase -i featureA and edit the todo list to remove the no longer needed fix commit(s) and merge the fix branch into this one. Change it from:

label onto

reset onto
pick eb9af14 B1
pick 947f7a7 fix1
pick 2fcb9e3 B2

# Rebase 3d477ad..2fcb9e3 onto 3d477ad (5 commands)

To this:

label onto

reset onto
merge my-fix
pick eb9af14 B1
pick 2fcb9e3 B2

# Rebase 3d477ad..2fcb9e3 onto 3d477ad (5 commands)
* 6843025 (HEAD -> featureB) B2
* 139e024 B1
*   b30d28a Merge branch 'my-fix'
|\
| * e4544ef (my-fix) fix1
* | 3d477ad (featureA) A2
* | f0751af A1
|/
* 1304eb2 (main) M1

Now I'm ready for another pull request. When creating a pull request for featureB make sure to set the base branch in your Pull Request to featureA not main. This is important.

For Bitbucket when you merge a previous PR all subsequent PRs get a chance to be rebased on the new merge. There is a checkbox in the modal dialog for this, make sure you check it.

While working on to featureC we get some feed back on featureA. To keep things easy we address the feed back in featureC but we make the commits as fixup commits so that when we rebase the set Git will apply them in the right place. To do this easily when you commit add the --fixup argument with the SHA of the commit these changes will fix-up.

git commit --fixup f0751af
* 389ccd3 (HEAD -> featureC) fixup! A1
* 76ff438 C1
*   282d2f0 (featureB) Merge branch 'my-fix' into featureB
|\
| * ed6674a (my-fix) fix2
| * d48e7e3 fix1
* | c49ef70 B2
* | fbe9abb B1
* | 9a06f61 (featureA) A2
* | 59358d3 A1
|/
* 08971e5 M1

Time to get our changes up-to-date with main. Do your usual checkout main and pull.

git checkout main
git pull
git checkout -
* 6a9e9e7 (main) M2
| * b7111e3 (HEAD -> featureC) fixup! A1
| * 84f29fe C1
| * 6843025 (featureB) B2
| * 139e024 B1
| *   b30d28a Merge branch 'my-fix'
| |\
| | * e4544ef (my-fix) fix1
| |/
|/|
| * 3d477ad (featureA) A2
| * f0751af A1
|/
* 1304eb2 M1

Then we interactively rebase the entire chain from (in this case) featureC: git rebase -i main which will produce the following todo in your editor:

label onto

# Branch my-fix
reset onto
pick e4544ef fix1
update-ref refs/heads/my-fix

label my-fix

reset onto
pick f0751af A1
fixup b7111e3 fixup! A1
pick 3d477ad A2
update-ref refs/heads/featureA

merge -C b30d28a my-fix # Merge branch 'my-fix'
pick 139e024 B1
pick 6843025 B2
update-ref refs/heads/featureB

pick 84f29fe C1

# Rebase 6a9e9e7..b7111e3 onto 6a9e9e7 (15 commands)

It is recreating the branching structure as well as preserving the merges we’ve done. You may also notice that the fixup was moved to the correct location. Save this off and allow Git to perform all the steps. You’ll get an output that looks like this.

Successfully rebased and updated refs/heads/featureC.
Updated the following refs with --update-refs:
        refs/heads/featureA
        refs/heads/featureB
        refs/heads/my-fix

Some things to note is that your current branch (featureC) is not in the list as a rebase presumes the current branch is the focus of the rebase unlike the others which were side-effects of the rebase. I find it helpful to copy the current branch name and this output for the next step.

These branches are setup locally but we need to push them out to our cloud service for CI and review use. There is no easy way to do this through git but you can write a command to loop over each and push them up. I typically use an editor to do this step. In default Bash you can press CTRL-X+CTRL-E or for Vi bindings ESC+v.

for branch in featureA featureB my-fix featureC; do
  git checkout $branch && git push --force-with-lease
done
* 1be7b9c (HEAD -> featureC) C1
* 783c35b (featureB) B2
* 3120777 B1
*   a9685fe Merge branch 'my-fix'
|\
| * 345b14e (my-fix) fix1
* | 739ca89 (featureA) A2
* | 66f72bf A1
|/
* 6a9e9e7 (main) M2
* 1304eb2 M1

Now my-fix was merged in our cloud service and you did a pull for main. You will need to rebase everything on that. You can git branch -d my-fix since that is no longer needed locally.

*   5c24663 (main) Merge branch 'my-fix'
|\
| | * 1be7b9c (HEAD -> featureC) C1
| | * 783c35b (featureB) B2
| | * 3120777 B1
| | *   a9685fe Merge branch 'my-fix'
| | |\
| | |/
| |/|
| * | 345b14e (my-fix) fix1
|/ /
| * 739ca89 (featureA) A2
| * 66f72bf A1
|/
* 6a9e9e7 M2
* 1304eb2 M1

You may need to update the todo list in this case to remove the merge of my-fix. Also remove the first reset line.

pick 66f72bf A1
pick 739ca89 A2
update-ref refs/heads/featureA

pick 3120777 B1
pick 783c35b B2
update-ref refs/heads/featureB

pick 1be7b9c C1

# Rebase 2e5faef..1be7b9c onto 2e5faef (10 commands)

And again update your cloud service (without the my-fix branch).

for branch in featureA featureB featureC; do
  git checkout $branch && git push --force-with-lease
done
* bfafaef (HEAD -> featureC) C1
* 726034d (featureB) B2
* 4e4d6fb B1
* 4bbc781 (featureA) A2
* 2d55b55 A1
*   2e5faef (main) Merge branch 'my-fix'
|\
| * 345b14e (my-fix) fix1
|/
* 6a9e9e7 M2
* 1304eb2 M1

Finally, featureA is ready to merge. Do so via your cloud service. It should update the subsequent pull requests for you. But once you have, perform another git pull and you can again rebase to update everything.

*   a63253c (main) Merge branch 'featureA'
|\
| | * bfafaef (HEAD -> featureC) C1
| | * 726034d (featureB) B2
| | * 4e4d6fb B1
| |/
| * 4bbc781 (featureA) A2
| * 2d55b55 A1
|/
*   2e5faef Merge branch 'my-fix'
|\
| * 345b14e (my-fix) fix1
|/
* 6a9e9e7 M2
* 1304eb2 M1
git checkout main
git pull
git branch -d featureA
git co -
git rebase -i main
label onto

reset onto
pick 4e4d6fb B1
pick 726034d B2
update-ref refs/heads/featureB

pick bfafaef C1

# Rebase a63253c..bfafaef onto a63253c (6 commands)
* cb76d36 (HEAD -> featureC) C1
* f4ee99c (featureB) B2
* 2d608ce B1
*   a63253c (main) Merge branch 'featureA'
|\
| * 4bbc781 (featureA) A2
| * 2d55b55 A1
|/
*   2e5faef Merge branch 'my-fix'
|\
| * 345b14e (my-fix) fix1
|/
* 6a9e9e7 M2
* 1304eb2 M1

Continue till all pull requests in the chain have been exhausted. I know it is complicated but this is the best way I've figured out how to manage such a complex situation. Not sure how the tech industry arrived at this place but here we are.

Discuss this article