Notes on Using Git

Anyone who works with code will probably need to use git at some point to share their code with other people. It’s a very powerful and flexible tool that can handle almost any situation that arises when developing a large software project, but unfortunately it is saddled with an obtuse and unintuitive command-line interface.

I’ve created this page to record the correct command sequence for several semi-common tasks that are difficult to figure out, even if you already know the basic git workflow of adding, committing, pushing, and pulling. This will be a useful reference for me, and I hope it will also be helpful for anyone else struggling to remember the right sequence of incantations to make git do something.

Showing Staged Changes

After you have used git add to add some files to the “staged changes,” you might want to double-check what you’re about to commit before you commit it. Unfortunately, git diff won’t show you your local changes once you have added them to the staging area:

~/project$ git add main.c
~/project$ git diff main.c
~/project$

If you want to see the changes you’ve added, you have to use the --staged flag:

~/project$ git add main.c
~/project$ git diff --staged main.c
diff --git a/main.c b/main.c
index c638853..19106c3 100644
--- a/main.c
+++ b/main.c
...diff output here...

Un-Adding a File

Let’s say you git added a bunch of files, and then realized you didn’t really want to include one of them in this commit - those changes aren’t ready to commit yet. How do you undo a git add without discarding your local changes?

~/project$ git reset HEAD <file>

Sometimes “reset” means “un-stage,” but other times it means “delete commit” or “discard changes,” so you need to be careful when using it. Make sure to specify a file name as the second argument if the meaning you want is “un-stage this change.”

Adding a Change After Commit

Sometimes you create a commit, but then before pushing it realize that you forgot to include something. You can add a change to your most recent commit with the --amend flag:

~/project$ git add foo.c
~/project$ git commit --amend

This will re-open your text editor to edit the commit message you just wrote for this commit. If you don’t actually need to change the commit message, you can do

~/project$ git commit --amend --no-edit

If you have already pushed the commit, you can still use --amend, but it will change the hash of your commit (even if you just changed the commit message, and didn’t add any new files), so your local branch will “diverge” from the remote repository and you’ll get an error when you try to push. I’ll discuss this problem next.

Amending a Commit After Pushing

If you’ve already pushed your most recent commit to the remote repository, git commit --amend will create problems because it rewrites the local commit (even if all you changed was the commit message), thus changing its hash. This means when you try to push the amended commit, you’ll get the error message “error: failed to push some refs…Updates were rejected because the remote contains work that you do not have locally”. The “work you do not have locally” is actually the pre-amended version of your most recent commit, but git can no longer tell that they’re the same commit.

There are two ways to get out of this:

  1. Give up on amending, undo the amended commit, and put the changes in a new commit instead
  2. Forcibly push the updated commit to the remote, then forcibly pull it down to other repositories that already pulled it.

If you choose to go the latter route, you should first do this on the local repository where you amended the commit:

~/project$ git push -f

Let’s assume you have another local repository (on another computer) that has already pulled from the same remote. After the forced push, if you try to do a “git pull” on this repository, you will be prompted to create a merge commit, even though what you are “merging” is just the pre-amend and post-amend versions of the same commit. Instead, you should make sure your workspace is clean (stash local changes if necessary), then do this:

~/project$ git fetch
~/project$ git reset --hard @{u}

This will overwrite the local repository with the current head of the remote (@{u} means “the upstream branch”), which contains the amended commit.

Undoing the Last Commit

If you created a commit, but haven’t yet pushed it, you can undo the commit without losing your local changes. (If you take option 1 above, “Give up on amending,” this is what you’ll need to do). This command will discard the current commit but leave all your local files in their current state (they will show up as “modified” in git status):

~/project$ git reset HEAD^

If you want to undo just the “commit action” but leave all of your changes staged and ready to commit, you can do this:

~/project$ git reset --soft HEAD^

Why does a caret symbol mean “the previous commit”? Who knows? The important part is that “HEAD^” means the commit immediately before the one you just created, and you want to reset the committed state of the repository to equal that commit.

Aborting an Unnecessary Merge

Even if you’re using the recommended workflow of making each change on a branch (other than master) to avoid conflicts, there will still be times when more than one person is working on the same branch. In this situation, you may be prompted to create an unnecessary merge due to git’s policy of not fetching updates to the upstream branch until you ask it to pull or push. What usually happens is that you make some changes, are ready to commit them, and see something like this in git status:

~/project$ git status
On branch new-feature
Your branch is up to date with 'origin/new-feature'

Changes to be committed:

    modified: foo.c
    modified: foo.h
    new file: thing.h
    new file: thing.c

So you create a commit, write a commit message, and then push it, but you get an error message like this:

~/project$ git push
 ! [rejected]        new_feature -> new_feature (fetch first)
    error: failed to push some refs to https://github.com/myname/myproject.git
    hint: Updates were rejected because the remote contains work that you do
    hint: not have locally. This is usually caused by another repository pushing
    hint: to the same ref. You may want to first integrate the remote changes
    hint: (e.g., 'git pull ...') before pushing again.

This means your coworker has recently pushed a commit to the same branch, but your local git repository didn’t know about it. Unfortunately, if you follow git’s advice and run git pull, you will then be thrown into an editor window to compose a commit message for a merge commit. This merge doesn’t make a lot of sense because it essentially represents merging the “new_feature” branch with itself, and it pollutes your commit history with messages like “Merge to synchronize with Bob.” Most of the time the merge is also entirely avoidable, since you were actively writing new changes until a few minutes ago, and it wouldn’t have been too hard for you to integrate your coworker’s changes with your own before finalizing the commit.

To get out of this situation and merge your changes with your coworker’s more cleanly (without a merge commit), you should first delete the merge commit message in the editor window to ensure the merge commit aborts. Then, discard the partially-merged changes and stash your own work before pulling, like this:

~/project$ git reset --hard
~/project$ git reset HEAD^
~/project$ git stash
~/project$ git pull
~/project$ git stash pop

This time, the git pull will do a fast-forward without creating a merge commit, and after you pop your work from the stash, it can be added to a new commit that will appear linearly after your coworker’s. If there were any genuine conflicts between your coworker’s commit and your local work, you’ll see them when you run git stash pop, but you can decide how to resolve them (and re-test your code if necessary) before creating any commits.