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 add
ed 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:
- Give up on amending, undo the amended commit, and put the changes in a new commit instead
- 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.