As a developer I am collaborating on multiple projects. In many cases it is need to create a fork of the upstream project add add a pull request. The first steps (making a fork) etc are not difficult, and at first you will have an up to date clone of the original website on which you can work and open pull requests to the upstream project. Most of these steps can be down using the de web UI (e.g. when using Github). But while working on a pull request often it is needed to sync your forked clone, for instance to open a new branch. Then the trouble begins. What can happen:
- You started a new branch , but the main branch was not in sync
- You need to rebase and possible there is a conflict
People start rebasing but often pull in unrelated commits causing other issues. In this post I want to share some of methods I have to deal with such cases.
Opening a fresh branch
When you want to open a new pull request you want this to happen correctly. I assume here that you already have set up some develop environment with your cloned git repository, ready to push commits.
To make sure your fork is in sync you need to fetch all upstream updates. We can do that with:
git fetch --all
or if it is just upstream
, we can use
git fetch upstream
which is faster.
The next step is to checkout the target branch you want to create your pull request against. This can be main
, master
or dev
, but sometimes this can be even different. In our example we use main
.
git checkout main
Now we want to reset main
to the same state as upstream\main
, as this is the branch where we want to target the pull request to. We need to make sure there are no uncommitted files as these will be removed!
git reset --hard upstream/main
If we omit the --hard
flag then git will create uncommitted files for all changed between the old and new state. This can be handy if we want to revert commits but not when starting a new branch.
From here we can push the local branch to our fork with:
git push
Now we are at the same level as the target branch and we can open a new branch that is in sync with upstream
. We choose a new branch name that reflects the name of our pull request, in this example I’ll use test-pull-request
as branch name.
git checkout -b test-pull-request
This creates a new local branch, from here you can start coding and add your commits. If you are ready you can publish your branch to your fork (I use origin
as name for the cloned repository). From then every new commit will be pushed to origin
as well. When we publish our new local branch, then we should set the upstream branch too so we can open a pull request later.
git push --set-upstream origin test-pull-request
When all commits are done and we pushed all our commits (git push
), then we are ready to open a pull request.
Open a new pull request
When using Github it is advisable to open a pull request using the web UI or using a plugin in vscode
or other development environment. In most cases there is a template that needs to be filled in. Make sure the correct commits are shown and you have selected the correct target branch!
Rebasing and resolving merging issues
Sometimes we need to rebase. This can happen if the PR is to old and we need to sync with our target branch. Personally I would like to force push the original commits on top of the rebased branch instead of adding a merge commit. Force pushing makes it easier to keep track on the commits in the pull request and avoids additional merging commits. To rebase correctly first we need to fetch the latest updates. We can use:
git fetch upstream
Now we make sure we are checked out to the correct branch.
git checkout test-pull-request
To rebase with our upstream target branch we start a rebase command.
git rebase upstream/dev
If you want to select the commits that should be included (for instance when you want revert some commits) you should consider using the -i
flag to start interactive rebasing.
git rebase -i upstream/dev
For each commit include git will try to rebase, if this fails, then you need to correct, save and stage your files to tell git which changes should be made. After staging you can continue rebasing:
git rebase --continue
Make sure you only make changes to commits that are directly related to the merging conflicts and reflect the wanted changes for that particular commit. Repeat this for the other commits (if any) till the rebase is finished successfully.
If all becomes a mess, then there is a bail out of this by aborting the merge operation.
git rebase --abort
Next step is that we no NOT just pull and push commits now but only force push the rebased commits. This can illogical but what we want is to make sure that we push the exact local situation to our origin
branch.
git push --force
Now our branch should be rebased and in the pull request we should only see the commits of our PR, our the selected commits from the file (when using the -i
option).
Creating new clean commits from previous work
If your have a larger PR with multiple files and you would like to replace or revert existing commits without loosing your work you can use git reset
.
With git log
we can show the commits we have made on top of our branch. To replace these commits we need to rebase to the commit from where we want to start again.
Say we have two commits we want to do over. Show them with git log
.
commit 6090d321e3926ad9c5ffdd026f7c2fb046cdbbf2 (HEAD -> test2)
Author: you <you@example.com>
Date: Wed May 24 14:19:24 2023 +0000
commit 2
commit a0ad22921fb8f5797aeb4e414cf3403b80027a3d
Author: you <you@example.com>
Date: Wed May 24 14:18:03 2023 +0000
commit 1
commit abf08f66a4c7e01955213c228542884951d45a11 (upstream/dev, origin/test2, origin/dev, origin/HEAD, dev)
Author: user <user@users.noreply.github.com>
Date: Wed May 24 01:38:16 2023 -0500
Some commit upstream
Assuming we have no uncommitted files we can rebase to commit abf08f66a4c7e01955213c228542884951d45a11
(upstream/dev, origin/test2, origin/dev, origin/HEAD, dev).
git reset abf08f66a4c7e01955213c228542884951d45a11
Now you will see that the changes of the last 2 commits are unstaged files now. You can now stage the changes for your new commit, or if you want do this in more commits until all changes are staged and committed. Now, to replace the old commits with the new ones we can force push them to our origin
branch.
git push --force
Now your commits have been replaced with the new ones.
Temporary save uncommitted work
Sometimes you might need to change between branches, but you have uncommitted files. If pre-commit is used, it might not be possible to commit. Instead you could stash your changes on the stack. You can get back your changes later by popping them from the stack. This also works if you want to apply uncommitted changes to a different branch.
To stash your uncommitted work:
git stash
To pop back the changes:
git stash pop
There is a lot more you can do with git stash, but I will keep it brief here.
For more git commands you can use the online documentation.