Using git

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.