Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

Kris Hicks

Posts By

LABS
Rewinding git pull

If you're using a rebase strategy for the first time you may run git pull in a situation where Git practically tells you to do it, but you don't actually want to do it. The situation is described below, and the method to unwind it follows. I've added pictures of the branches between hashes to make it clear what the state of the world is at any given point prior to or after running a particular command, which I hope makes it easier to follow.

First, you update master, as it's out of date:

#############################
A--B--C--D origin/master
A--B--C master
#############################

(master) $ git pull
Updating C..D
Fast-forward
...snip...

#############################
A--B--C--D origin/master
A--B--C--D master
#############################

You create and checkout a topic branch, topicA:

(master) $ git checkout -b topicA
Switched to a new branch 'topicA'

#############################
A--B--C--D origin/master
A--B--C--D master

           topicA
#############################

You make some changes, commit, and push it to origin.

(topicA) $ git commit -am "Changed README"

#############################
A--B--C--D origin/master
A--B--C--D master

           E topicA
#############################

(topicA) $ git push -u origin
(topicA) Counting objects: 28, done.
...snip...
(topicA)  * [new branch]      topicA -> topicA
(topicA) Branch topicA set up to track remote branch topicA from origin.

#############################
A--B--C--D origin/master
A--B--C--D master

           E topicA
A--B--C--D--E origin/topicA
#############################

At this point you have your local topicA, and also origin has a copy of topicA:

(topicA) $ git branch -a
* topicA
remotes/origin/topicA

You continue doing work, making more commits on your branch. Some time has passed since you branched from master, and another commit, F, has been pushed to origin/master.

(topicA) $ git fetch # to update your local repository's knowledge of the remote

#############################
A--B--C--D--F origin/master
A--B--C--D master

           E--G--H topicA
A--B--C--D--E origin/topicA
#############################

At this point you want to rebase on top of origin/master to get the updates (in this case, F).

$ git rebase origin/master
First, rewinding head to replay your work on top of it...
...snip...

#############################
A--B--C--D--F origin/master

              E'--G'--H' topicA
A--B--C--D master
A--B--C--D--E origin/topicA
#############################

And then you want to push your updated topicA, with your new commits and the new commit from origin/master up to origin:

The following assumes you have git config.push default upstream set. This configuration parameter limits the branch that git will attempt to push. I highly recommend you set that value as the default is to push all branches, which you probably do not want.

(topicA) $ git push
To git@github.com:intentmedia/code.git
! [rejected]        topicA -> topicA (non-fast-forward)
error: failed to push some refs to 'git@github.com:krishicks/krishicks.git'
To prevent you from losing history, non-fast-forward updates were rejected
Merge the remote changes (e.g. 'git pull') before pushing again.  See the
'Note about fast-forwards' section of 'git push --help' for details.

This is where trouble sets in.

First, Git tells you the push was rejected because you would have lost history. Then comes the troublesome line: "Merge the remote changes (e.g. 'git pull') before pushing again."

If you were working on master and made some commits, and someone else made some commits on master as well and pushed before you did, you would end up with the same situation as above. The push would be rejected because you don't have the commits that the other person made. Thus, if your push were to succeed their work would be lost on origin/master. This is what Git is referring to when it says "To prevent you from losing history.." The history that would be lost would be the work the other person did.

In the situation we've been building up, making commits and rebasing topicA, you're the only person committing to the branch and the history you would "lose" is the set of pre-rebase commits that you pushed to the remote originally (in this case, E). You've overwritten E with E' during the rebase, and Git doesn't want you to lose the E that's on the remote.

What you should do in this situation is git push -f to force-push the branch. You know you're going to lose E that's on the remote, but that's fine because you have E' on your local topicA. You intend to replace whatever is on the remote with whatever you have locally.

The problem is that it says to do a git pull, which will pull the remote E into your local, which you don't want. That will give you a merge commit, I:

A--B--C--D--F origin/master

              E'--G'--H' topicA

                        I
A--B--C--D master      /
A--B--C--D------------E origin/topicA

If you did a git log at this point you'd see that the merge commit I brought in E, which has the same message but a different SHA than your rebased E'.

(topicA) $ git log --graph --oneline
* I Merge branch 'origin/topicA' into topicA
|
| * E
* | H'
* | G'
* | E'
|/
* D
* C
* B
* A

So, how do you fix this? You can hard reset back to H', which gets rid of the merge commit, but only if you didn't already make more commits after the faulty git pull.

(topicA) $ git reset --hard H' # Only if you didn't make any more commits!

If you have made commits after, with your log looking like this:

(topicA) $ git log --graph --oneline
* K
* J
* I Merge branch 'origin/topicA' into topicA
|
| * E
* | H'
* | G'
* | E'
|/
* D
* C
* B
* A

You need to use the two-argument form of git rebase --onto:

(topicA) $ git rebase --onto H' J~1

That will get rid of the merge commit, leaving you with:

(topicA) $ git log --graph --oneline
* K
* J
* H'
* G'
* E'
* D
* C
* B
* A

And now you can git push -f. The docs on git-push (or git push --help) do give you a better explanation than the message when the push is rejected, in the section NOTE ABOUT FAST-FORWARDS.

A simple rule about git pull is to not ever use it unless you're on master and have made no commits that put you ahead of origin/master, which you can easily tell with:

(master) $ git fetch
(master) $ git log @{u}..

or

(master) $ git fetch
(master) $ git status
# On branch master
nothing to commit (working directory clean)

If the result from git log @{u} is empty or you don't get "# Your branch is ahead of 'origin/master' by commit(s)" message after git status, you're OK to git pull. In no other case do you need to, or should you, git pull.

I generally recommend always working on a topic branch and keeping master clean to avoid accidentally running git push -f on master, and to enforce the idea that after you fetch, you're rebasing on top of origin/master directly instead of doing git pull --rebase while on master, which hides the fact that you're rebasing on top of origin/master.

LABS
quick tips

The following are things I found very helpful, which you also may find make your day-to-day usage of Git more enjoyable.

Go HEADless

In many (and perhaps all) cases HEAD is implied when no ref is given, such as the following equivalent statements:

$ git log origin/master..HEAD
$ git log origin/master..

Upstream reference

Before you start referring to the upstream, you'll want to do a git fetch to update your refs/heads:

$ git fetch

You only need to do this once in a while, or whenever you know the upstream to have changed.

You can refer to the upstream of any branchname or otherwise symbolic-ref by appending @{u} to the ref, such as:

$ git log master@{u}

If you just want the upstream of the current branch, whatever it may be, you can replace master from above with HEAD:

$ git log HEAD@{u}

Upstream difference

Oftentimes I'll want to see what commits are on upstream that I don't have. I usually will do this via:

$ git log @{u}..

Which, when on master, is equivalent to:

$ git log origin/master..HEAD

You can see the reverse difference, or the commits are on the remote that you don't have, by reversing the range:

$ git log ..@{u}

Which, again on master, is equivalent to:

$ git log HEAD..origin/master

To see the full list of formats recognizable, see git revisions --help

If you want to see both of the above at the same time, you can use git log with the --left-right parameter. I've also included --format='' and --oneline (which must be passed in that order), which may be optional depending on your format configuration, but for me are not:

$ git log HEAD@{u}...HEAD --left-right --format='' --oneline

Which can be shortened to:

$ git log @{u}... --left-right --format='' --oneline

Note: The above example uses three dots, not two, to show only the commits which are reachable as ancestors of one branch but not the other.

You can replace HEAD in the above with any symbolic-ref or branchname to use as a reference instead. The output (with the format given previously) will look like:

< e86bd83 D
< 6ea2155 C
> eb2de55 B
> 74829bd A

Commits beginning with "<" are only on the left side of the triple-dot (in this case, the upstream), commits beginning with ">" are only on the right side (in this case, the current branch). A..D are the commit messages.

Is it merged?

To find out if a particular branch has been merged into origin/master, you could dig through the log on master and search for it:

$ git log origin/master

Or, if you knew who would have merged it, you could limit the above with that:

$ git log origin/master --author Kris

I wanted a better way, and I found it. I wanted to see if a particular branch had been merged to master without changing branches or having to dig through a log looking for the merge of the branch.

The situation that prompted this was that I had a topic branch, topicB, which depended on another topic branch, topicA. I wanted to see if topicA had been merged into master to find out if I could rebase onto master to get up to date, or if I should continue rebasing on top of the branch.

$ git fetch
$ git branch --contains topicB@{u} -r

This gives you a list of all the remote branches that can reach (as an ancestor) the ref given. origin/master will be right at the top if the branch has been merged into it.

Many thanks to John Wiegley, Scott Chacon and Mark Longair for the sources of the tips.

LABS
rewinding git commit –amend

It may come to pass that you will amend HEAD with changes that were meant to go on a commit earlier than HEAD via git commit --amend. In this case, you'll want to rewind that operation you just did, and apply it to the correct commit. With simple changes you may find git reset -p handy. For times when the changes are far too large and intertwined to the commit, you will want to refer to git reflog for help.

The reflog records when the tip of a branch is updated. The tip is updated any time you create a new commit, amend a commit, reset a commit, switch branches, etc. Basically, any time HEAD changes, you will get a reflog entry. The reflog therefore is a great tool for understanding how the repository came to be in a particular state.

git reflog -2 will give you the last two operations that Git performed. In this case, it will look something like:

8751261 HEAD@{0}: commit (amend): Something something something commit message
9d3a192 HEAD@{1}: reset: moving to HEAD~1

git commit --amend is kind of shorthand for (given changes have been made, and are either in the index or in the working directory):

git stash
git reset HEAD~1
git stash pop
git add .
git commit

Or, in English:

  • Save the changes that you want to apply to the HEAD commit off in the stash
  • Remove the HEAD commit and put its contents in the index
  • Apply the stashed changes to the working directory, adding them to the changes from the commit that was reset
  • Make a new commit.

Thus, the last two operations in the reflog are reset and commit.

So, what can we do with this? Well, 9d3a192 was HEAD before the amend (specifically, before the reset) happened. 8751261 was the commit that resulted at the end of the amend process. git diff 8751261..9d3a192 will show you what changes were applied as part of the amend.

From here, you can use git apply to apply the difference from before the amend to after the amend to your working tree via git diff:

git diff 8751261..9d3a192 | git apply -
  • Note: The hyphen in git apply - causes git apply to take stdin as input.
  • Extra Note: The arguments here are given in reverse order, with the later commit happening first to show the reverse of the amend. It's the same as doing git diff 9d3a192..8751261 -R, which reverses the diff output. Additionally, the -R argument may be applied to git apply instead of git diff to achieve the same effect.

Now we can do another amend to put the commit back to where it was before we did the previous amend:

git commit -a --amend -CHEAD

And then, by reversing the order of the SHAs to git diff, get the changes we want to apply to the correct commit back:

git diff 9d3a192..8751261 | git apply -

And commit as necessary, this time using --fixup to indicate the correct commit (in this example, 1234567):

git commit -a --fixup 1234567

Then you can rebase at a later time (or now) to do the 'amend' you had originally intended:

git rebase --i --autosquash 1234567~1

So don't fret when you do an accidental amend. It's just a couple commands away from being unwound and applied to the correct commit.

LABS
git config rerere.enabled true

There have been times where I performed a rebase and had to resolve conflicts as part of the rebase, and then decided to abort the rebase for one reason or another. Without rerere the next time I went to perform the rebase I'd end up having to resolve at least some of the same conflicts I had previously, which is annoying.

This is where rerere comes in. It stands for reuse recorded resolution.

What rerere does is save the resolution of a conflict so that it can be re-applied later if it sees the same conflict again. When Git sees the conflict which it already has a resolution recorded for, it will apply the resolution automatically for you, and give you the opportunity to accept the resolution as applied, or change it.

Turning it on can be done two ways: set it as a configuration parameter using git config rerere.enabled true, or use it only when you think you might need it with git rerere both before and after the resolution of a conflict.

A more verbose explanation of rerere exists in Scott Chacon's Pro Git.

LABS
exec

(Update #1 below)

Say you're going to do an interactive rebase where you're going to be squashing commits or reordering them. During this process you may want Git to execute a command after applying certain items of the todo list. An example of this would be when you want to run rake or similar to ensure a newly-squashed commit is still green.

You can do this by adding a task to the todo list, exec, followed by the command you'd like Git to run at that point in the rebase. If the command you specify should return a non-zero exit code, Git will pause the rebase and allow you to sort it out, in the same way that it pauses when a conflict arises while applying the todo list during any other rebase.

Here's an example of the above situation, where two commits are going to be squashed, and I want Git to run rake after it does the squash.

Pre-edits, this would look like:

pick dad8d12 Commit #1
pick f613ac1 Commit #2
pick 58822ee Commit #3

Post-edits, this would look like:

pick dad8d12 Commit #1
f f613ac1 Commit #2
x rake
pick 58822ee Commit #3

What happens here is Git will fixup Commit #2 into Commit #1, creating a new commit, then run rake. If rake returns a zero exit code, Git applies Commit #3 and completes the rebase. If rake had returned a non-zero exit code, Git would have paused the rebase operation at that point, allowing any necessary changes to be made to the HEAD commit, which is the squashed #1/#2.

I typically do this separate from doing an initial rebase, where I rebased and made a change to Commit #1 and had to resolve conflicts throughout the rest of the commits. This way I can keep my head straight while doing the rebase, then fix anything I missed as a second operation.

Update #1: As of Git 1.7.12 you can pass -x <cmd> to git rebase -i to have Git run the exec command after every commit in the resulting history: git rebase -i <treeish> -x <cmd>.

LABS
git rebase –onto

"Hold on to your butts.." --Samuel L. Jackson as Ray Arnold in 'Jurassic Park'

Part One:

The two-argument form of git rebase --onto

Say there's a commit C made on master that made a change to a configuration parameter that, it turns out, wasn't actually necessary, so that commit needs to go. For the purposes of this demonstration, commits D and E don't rely upon the changes made in C. (If D or E did rely on C, you'd end up with a conflict to resolve, which you'd be able to do at that point.)

A--B--C--D--E master

One way to get rid of the offending commit would be to do an interactive rebase, deleting the line that has commit C on it:

git rebase -i C~1
delete the line containing commit C
save and close the editor

A quicker way is to use the two-argument git rebase --onto, as going interactive just to delete a commit (or commits) is a little overkill, and considerably slower to do.

git rebase --onto takes a new base commit (which the manpage for git-rebase calls newbase) and an old base commit (oldbase) to use for the rebase operation.

So, what we want to do is tell Git to make commit B the newbase of commit D, making C go away. This looks like:

git rebase --onto B C

But usually I like to talk about the commits I care about rather than the ones I don't (in this case, I care about B and D, but not C), so instead of the previous command I use a backreference from D:

git rebase --onto B D~1

This means that everything in the range from B (non-inclusive) to D~1 (inclusive) will be removed.

git rebase --onto allows you to, in a non-interactive way, change the base of a commit, or rebase it. If you think about the commits as each having a base, or parent commit, you can see how you might be able to change the base of any commit to be another commit. In doing so, you remove everything that used to be in between the oldbase and the newbase.

It's also good to know that it works exactly the same way as if you were to have done an interactive rebase and deleted the commit. Should a conflict arise while performing the rebase, Git will still pause and allow you to resolve the conflict before continuing.

I've also used the two-argument form when fixing a mistake: I had a branch from master, topicA, with some commits that I wanted to change via interactive rebase:

A--B master

     C--D--E topicA

But when I rebased, I went back too far, and rewrote a commit that I had gotten from master:

A--B' master

     C'--D'--E' topicA

(Note that there was no change to the master branch here, just to topicA.)

What did I do to fix this situation? Well, I can't fix this via interactive rebase. I can, however, fix it via git rebase --onto:

git rebase --onto master C'~1

Or, in other words: Replace the oldbase C'~1 with the newbase, master (which is HEAD of master, or B).

It's a handy undo mechanism.

If you forget to give the two-argument form of --onto its second argument, such as:

git rebase --onto master

..it will be the same as doing:

git rebase --hard master

You probably don't want this.

Why is this? The second argument (the oldbase) is required if you want a range of commits to be applied on top of master. Without it, you haven't supplied a range of commits to be applied on top of master, so HEAD of the branch gets reset to the HEAD of master.

What that means is any commits you have on your branch will be removed from the branch, and the branch will resemble master at that point. These commits are still in Git until garbage collection happens, accessible via the reflog (git reflog).

Part Two:

The three-argument form of git rebase --onto

Say there is a branch 'topicA' that diverges from master at some point:

A--B--C--D--E master

     F--G--H topicA

Let's also say that someone else has branched from topicA to create topicB, and added more commits:

A--B--C--D--E master

     F--G--H topicA

             I--J--K--L--M topicB

This is an example of a real-world case I came across, where topicA had only a couple very large commits that were hard to digest and could have been split into many smaller commits. topicB was created as a continuation of the work done on topicA.

I checked out my own local copy of topicA, and through much interactive rebasing and prodigious use of git add -e, I was able to split topicA into smaller commits, making topicC:

A--B--C--D--E master
   |
   | F--G--H topicA
   |
   |         I--J--K--L--M topicB
   |
   N--O--P--Q--R--S--T--U--V--W topicC

I talked with the person that made topicA and we agreed that my branch topicC should take the place of topicA. But what to do about the work that was done on topicB?

The operation that we wanted to do is: make topicC the new base of topicB, cutting it at the point topicB diverged from topicA, which looks like:

A--B--C--D--E master
   |
   | F--G--H topicA
   |
   |         I--J--K--L--M topicB

     N--O--P--Q--R--S--T--U--V--W--I'--J'--K'--L'--M' topicC

The five commits from topicB (I through M), get played on top of topicC, starting from where topicB diverged from topicA, to create I', J', K', L', and M'.

The command to do this is:

git rebase --onto topicC topicA topicB

Where topicC is the newbase, topicA is the oldbase, and topicB is the reference for what HEAD of topicC will become.

LABS
git add -e

git add -e is like git add -p, except instead of adding things at the hunk level, you edit the entire patch at once. Or, in other words, whereas git add -p will show you each hunk for every file and ask what you want to do for each of them, git add -e will show you the entire patch and allow you to edit it at will. I used this trick to recently split apart one massive commit into 28 smaller, digestible ones.

Say you've replaced a line containing "baz" with one containing "bar". When you git add -e, you'll be presented with a diff like so:

foo
- baz
+ bar

From here you can decide, actually, you don't want to delete baz, you just want to add bar. And you want to add it above baz.

From this spot you can just change the minus to a space, making that line context for the diff. Then you can move the line that adds bar above baz, with this result:

foo
+ bar
baz

After saving and closing the editor you'll be able to look at the result of your work in the index with git diff --cached:

foo
+ bar
baz

And the line you made into context remains in your working directory, which you can see with git diff:

foo
bar
-baz

But what if you end up modifying the diff in a way that makes it a patch that doesn't apply? Git's got you covered there.

Let's say that when you moved bar above baz, after removing the minus from baz, you added an extra line on accident, making the patch invalid:

foo

+ bar
baz

When you save and close the editor Git will tell you of the problem:

error: patch failed: <some_filename>:1
error: file.txt: patch does not apply
fatal: Could not apply '.git/ADD_EDIT.patch'

In which case you'll be able to attempt the add -e again, as Git will not have made any changes to the working directory or index at this point.

In some cases Git will attempt to apply the patch and give you the option of retrying the add, re-opening the editor with the modified .git/ADD_EDIT.patch if you choose to retry. If you don't choose to retry, Git will delete .git/ADD_EDIT.patch.

In addition to editing the patch wholesale via git add -e, you can also choose during git add -p to edit a particular hunk manually by choosing 'e' to edit it instead of simply adding it via 'a'. You can also add a file glob to the end of add -e as you would any other command to limit the size of the patch you're about to edit.

LABS
git reset -p

I've been using git reset -p a lot recently and I think it makes sense to clarify what it is that it does because when I first started using it I found it a little confusing.

I sometimes realize that an earlier commit contains some change that I don't want, so I want to remove it from the commit. This also works when not rebasing, so for simplicity I'll use an example where the commit to be modified is already HEAD. Previously I would have done:

git reset HEAD~1
git add -p
git commit -C <treeish>
git checkout .

Or, in English:

  • take off the HEAD commit, adding it to the working directory
  • add back the parts you want to keep
  • make a new commit using the message from what used to be the HEAD commit
  • throw away the changes you didn't want

With git reset -p this process is a little different. Here's what it looks like:

git reset -p HEAD~1
git commit --amend -C HEAD
git checkout .

Again, in English:

  • apply to the index the negations of certain parts of the HEAD commit
  • amend the HEAD commit with the negations
  • throw away the changes you don't want

How does this work?

In the second example you added -p to the reset command. This will reset only parts of the original commit, leaving it intact otherwise. That's worth stating a different way: When you reset -p, the original commit remains unchanged. The only changes are made to your working directory and index.

The trick is to know what you're doing when you're saying "y" to a hunk git presents to you for resetting. Say you added a line to the commit originally:

foo
+ bar
baz

But you want to get rid of it. When you git reset -p, git will ask you:

foo
- bar
baz
Apply this hunk to index [y,n,q,a,d,/,e,?]?

If you say 'y', Git will apply that hunk to the index. What you also get, however, is the original hunk (that added "bar") in your working directory.

To summarize, your working directory will have:

foo
+ bar
baz

While the index has:

foo
- bar
baz

While the commit (unchanged, remember) has:

foo
+ bar
baz

You now have a chance to tell git what you want to do, without having done anything to the original commit yet. In the example above, you wanted to get rid of the addition of the "bar" line. To do that, you want to take what's in the index (the negation of the addition of "bar") and apply it to the commit, making it go away:

git commit --amend -C HEAD

Then you still have in your working directory the adding of "bar", which in this case you just want to get rid of, so:

git checkout .

I like using reset -p because it makes it really easy to make small changes to a commit, removing something I added or putting back something I deleted.

reset -p allows you to more easily get a grip on the changes you've made and the ones you're about to make. It also makes much better use of Git, in that you can do even more interesting operations when in the resulting state, which I won't go into now as to avoid information overload.

And there you have it.