Get and stay out of trouble
Git is a powerful yet complex version control system. Even for contributors experienced at using version control, it can be confusing. The good news is that nearly all Git actions add information to the Git database, rather than removing it. As such, it’s hard to make Git perform actions that you can’t undo. However, Git can’t undo what it doesn’t know about, so it’s a good practice to frequently commit your changes and frequently push your commits to your remote repository.
Undo a merge commit
A merge commit is a special type of commit that has two parent commits. It’s created by Git when you merge one branch into another and the last commit on your current branch is not a direct ancestor of the branch you are trying to merge in. This happens quite often in a busy project like Zulip where there are many contributors because upstream/zulip will have new commits while you’re working on a feature or bugfix. In order for Git to merge your changes and the changes that have occurred on zulip/upstream since you first started your work, it must perform a three-way merge and create a merge commit.
Merge commits aren’t bad, however, Zulip doesn’t use them. Instead Zulip uses a forked-repo, rebase-oriented workflow.
A merge commit is usually created when you’ve run git pull
or git merge
.
You’ll know you’re creating a merge commit if you’re prompted for a commit
message and the default is something like this:
Merge branch 'main' of https://github.com/zulip/zulip
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
And the first entry for git log
will show something like:
commit e5f8211a565a5a5448b93e98ed56415255546f94
Merge: 13bea0e e0c10ed
Author: Christie Koehler <ck@christi3k.net>
Date: Mon Oct 10 13:25:51 2016 -0700
Merge branch 'main' of https://github.com/zulip/zulip
Some graphical Git clients may also create merge commits.
To undo a merge commit, first run git reflog
to identify the commit you want
to roll back to:
$ git reflog
e5f8211 HEAD@{0}: pull upstream main: Merge made by the 'recursive' strategy.
13bea0e HEAD@{1}: commit: test commit for docs.
Reflog output will be long. The most recent Git refs will be listed at the top.
In the example above e5f8211 HEAD@{0}:
is the merge commit made automatically
by git pull
and 13bea0e HEAD@{1}:
is the last commit I made before running
git pull
, the commit that I want to rollback to.
Once you’d identified the ref you want to revert to, you can do so with git reset:
$ git reset --hard 13bea0e
HEAD is now at 13bea0e test commit for docs.
Important
git reset --hard <commit>
will discard all changes in your
working directory and index since the commit you’re resetting to with
<commit>
. This is the main way you can lose work in Git. If you need
to keep any changes that are in your working directory or that you have
committed, use git reset --merge <commit>
instead.
You can also use the relative reflog HEAD@{1}
instead of the commit hash,
just keep in mind that this changes as you run Git commands.
Now when you look at the output of git reflog
, you should see that the tip of your branch points to your
last commit 13bea0e
before the merge:
$ git reflog
13bea0e HEAD@{2}: reset: moving to HEAD@{1}
e5f8211 HEAD@{3}: pull upstream main: Merge made by the 'recursive' strategy.
13bea0e HEAD@{4}: commit: test commit for docs.
And the first entry git log
shows is this:
commit 13bea0e40197b1670e927a9eb05aaf50df9e8277
Author: Christie Koehler <ck@christi3k.net>
Date: Mon Oct 10 13:25:38 2016 -0700
test commit for docs.
Restore a lost commit
We’ve mentioned you can use git reset --hard
to rollback to a previous
commit. What if you run git reset --hard
and then realize you actually need
one or more of the commits you just discarded? No problem, you can restore them
with git cherry-pick
(docs).
For example, let’s say you just committed “some work” and your git log
looks
like this:
* 67aea58 (HEAD -> main) some work
* 13bea0e test commit for docs.
You then mistakenly run git reset --hard 13bea0e
:
$ git reset --hard 13bea0e
HEAD is now at 13bea0e test commit for docs.
$ git log
* 13bea0e (HEAD -> main) test commit for docs.
And then realize you actually needed to keep commit 67aea58. First, use
git reflog
to confirm that commit you want to restore and then run
git cherry-pick <commit>
:
$ git reflog
13bea0e HEAD@{0}: reset: moving to 13bea0e
67aea58 HEAD@{1}: commit: some work
$ git cherry-pick 67aea58
[main 67aea58] some work
Date: Thu Oct 13 11:51:19 2016 -0700
1 file changed, 1 insertion(+)
create mode 100644 test4.txt
Recover from a git rebase
failure
One situation in which git rebase
will fail and require you to intervene is
when your change, which Git will try to re-apply on top of new commits from
which ever branch you are rebasing on top of, is to code that has been changed
by those new commits.
For example, while I’m working on a file, another contributor makes a change to
that file, submits a pull request and has their code merged into main
.
Usually this is not a problem, but in this case the other contributor made a
change to a part of the file I also want to change. When I try to bring my
branch up to date with git fetch
and then git rebase upstream/main
, I see
the following:
First, rewinding head to replay your work on top of it...
Applying: test change for docs
Using index info to reconstruct a base tree...
M README.md
Falling back to patching base and 3-way merge...
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
error: Failed to merge in the changes.
Patch failed at 0001 test change for docs
The copy of the patch that failed is found in: .git/rebase-apply/patch
When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
This message tells me that Git was not able to apply my changes to README.md after bringing in the new commits from upstream/main.
Running git status
also gives me some information:
rebase in progress; onto 5ae56e6
You are currently rebasing branch 'docs-test' on '5ae56e6'.
(fix conflicts and then run "git rebase --continue")
(use "git rebase --skip" to skip this patch)
(use "git rebase --abort" to check out the original branch)
Unmerged paths:
(use "git reset HEAD <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: README.md
no changes added to commit (use "git add" and/or "git commit -a")
To fix, open all the files with conflicts in your editor and decide which edits
should be applied. Git uses standard conflict-resolution (<<<<<<<
, =======
,
and >>>>>>>
) markers to indicate where in files there are conflicts.
Tip: You can see recent changes made to a file by running the following commands:
git fetch upstream
git log -p upstream/main -- /path/to/file
You can use this to compare the changes that you have made to a file with the ones in upstream, helping you avoid undoing changes from a previous commit when you are rebasing.
Once you’ve done that, save the file(s), stage them with git add
and then
continue the rebase with git rebase --continue
:
$ git add README.md
$ git rebase --continue
Applying: test change for docs
For help resolving merge conflicts, see basic merge conflicts, advanced merging, and/or GitHub’s help on how to resolve a merge conflict.
Working from multiple computers
Working from multiple computers with Zulip and Git is fine, but you’ll need to pay attention and do a bit of work to ensure all of your work is readily available.
Recall that most Git operations are local. When you commit your changes with
git commit
they are safely stored in your local Git database only. That is,
until you push the commits to GitHub, they are only available on the computer
where you committed them.
So, before you stop working for the day, or before you switch computers, push
all of your commits to GitHub with git push
:
$ git push origin <branchname>
When you first start working on a new computer, you’ll clone the Zulip repository and connect it to Zulip upstream. A clone retrieves all current commits, including the ones you pushed to GitHub from your other computer.
But if you’re switching to another computer on which you have already cloned
Zulip, you need to update your local Git database with new refs from your
GitHub fork. You do this with git fetch
:
$ git fetch <username>
Ideally you should do this before you have made any commits on the same branch
on the second computer. Then you can git merge
on whichever branch you need
to update:
$ git checkout <my-branch>
Switched to branch '<my-branch>'
$ git merge origin/main
If you have already made commits on the second computer that you need to
keep, you’ll need to use git log FETCH_HEAD
to identify that hashes of the
commits you want to keep and then git cherry-pick <commit>
those commits into
whichever branch you need to update.