git rebase -i foo/dev gives noop

5.6k views Asked by At

When I try to get into the interactive rebase mode as explained here, all I'm getting is noop which is weird but I cannot figure out why.

I am inside a temp branch in my machine and when I try to view the difference between, I get this:

Tugberk@TUGBERKPC /D/Dropbox/AppsPublic/gulp-aspnet-k (temp-a)
$ git log --pretty=oneline  temp-a..ajryan/dev
f0ef4ee40fde4de5360515fd556de9c3ed40431c update readme with new optionsfcba1ae3306ae041a1d82647e4cf65a0c96fe2f7 do not loop for build, restore8bedd238660908f06d698815ef12fce786d716ed fix command concat
e2e3a0d00e3950911c00c0e6e51a671f5f2ac2d3 bump version
d65923ff91eb9d6b9782bc2bf5ab606d19733203 add quiet option
91a1faff0a3aa1ca552df2151190a3ecd87cfc6f allow caller to loop all comma962c55854457314324d81457c42913532875cc85 trailing whitespace, tabs > sp

However, when I run the following command, I'm getting noop:

Tugberk@TUGBERKPC /D/Dropbox/AppsPublic/gulp-aspnet-k (temp-a)
$ git rebase -i ajryan/dev

Any idea why and what am I doing wrong?

1

There are 1 answers

4
torek On

Your git log command is showing commits that are on ajryan/dev but not on temp-a. Your rebase command is told to rebase commits that are on temp-a but not on ajryan/dev, but presumably there are no such commits. Pictorially:

... - o - o - o                   <-- HEAD=temp-a
                \
                  o - o - o - *   <-- ajryan/dev

Therefore, there are no commits to pick up and rebase onto the commit I have marked with a star *. So rebase -i generates a single no-op command.

If you execute that no-op, the rebase operation should conclude by moving temp-a to point the last commit added on after *, which should make temp-a point to commit *. (If that doesn't happen, it's a small bug in rebase.) If you delete the no-op, though, the rebase operation will be aborted, and temp-a will not be moved.


Note that if you did have different items on temp-a you could rebase them, or rebase the other branch. Here are two more illustrations, starting with almost the same base, but with one commit that's on temp-a that is not on ajryan/dev:

... - o - o - o - @               <-- HEAD=temp-a
                \
                  o - o - o - *   <-- ajryan/dev

In this case, you can either put the @ commit on top of the other commits:

... - o - o - o
                \                     (illustration 1)
                  o - o - o - * - @

or at the bottom, below the other commits:

... - o - o - o
                \                     (illustration 2)
                  @ - o - o - o - *

Here I have left out the labels (temp-a and ajryan/dev). This is on purpose: in git, branch labels are easy to move around (and are actually expected to move as long as it's in a "forward" fashion); all of the actual branch-iness is stored in the commits themselves. What git rebase really does is copy old commits to new ones, so that you can change their parent-age and/or contents.

In Illustration #1, we've re-parented commit @: it now has commit * as its parent. All the other commits are unchanged, and hence only commit @ has to be copied.

In Illustration #2, however, we've re-parented the entire chain of commits that were originally only on ajryan/dev. Commit @ itself is unchanged, but we had to copy all four of the original o - o - o - * commits: we copy the first o to set its parent to @. This new commit has a new and different SHA-1 ID, so we copy the second o to use the new commit's ID as its parent. This second copy forces the third copy, which then forces the last copy of the * commit. See the next section as to what can be bad about that.


In git, to join two different chains of commits, you would normally use git merge:

          A - B
        /       \
... - o           M   <-- HEAD=branch
        \       /
          C - D

Here merge commit M joins the two chains A - B and C - D that fork off from o. If all of A through D exist, and you create new commit M, nothing happens to A through D: they are still the same as before.

For simple development, though, it's usually nicer to "rebase" work: to make C and D sit "on top" of A - B. But to do that you must copy the commits, because a commit in git is permanent and unchanging. You copy C to a new, slightly different C', and copy D to a new, slightly different D'. The main thing that's different about C' is that its parent is B, not o, and the main thing that's different about D' is that its parent is C', not C:

          A - B - C' - D'   <-- HEAD=branch
        /
... - o
        \
          C - D            [abandoned]

Once the rebase copy is done, you simply abandon the old C and D commits and start pretending C' and D' were the original commits:

          A - B - C - D   <-- HEAD=branch
        /
... - o

Now we no longer need the little kink in the graph since we've forgotten all about the original C and D, and history just looks like this:

... - o - A - B - C - D   <-- HEAD=branch

The dirty secret (not so secret, not that dirty) is that the original C and D are still in there, they're just invisible (to you). The part that makes it truly dirty happens if someone else has the original two C and D commits. Git commit IDs are cryptographic checksums of their contents, including the parent SHA-1 IDs, so the original C and D are still there and are different from the new C and D, and anyone else.

This matters when you pull commits from someone else: if you rebase their commits onto your work, you change the IDs. You make new copies and you might forget they had the originals, but they don't know about your new copies yet and they have not forgotten their original IDs.

If you later present them with rebased copies of their commits, they need to do more work to pick up your copies of their commits and maybe move any additional work they've done.

In short, unless you've made arrangements with them (whoever "they" may be) in advance, a rebase like that shown in Illustration #2 is usually not a good idea.


What about "fast forwards"? Well, let's take a look at Illustration #1 again. Let's assume the o - o - o - @ chain came from someone else (a "pull request"), and that you add commit * on top of it by rebasing your work onto theirs. Let's re-draw the graph too, adding one more kink, and put the ajryan/dev label back:

... - o - o - o     [your original '@' commit, before rebasing, was here]
                \
                  o - o - o - *       <-- ajryan/dev
                                \
                                  @   <-- HEAD=(something, maybe "dev")

Now let's further assume that they, whoever they are, have the o - o - o - * sequence on their dev branch and have not done any additional work, so that their stuff ends at the commit marked *. If you make this commit-sequence available to them, they can pick it up and add commit @ to their dev branch, by simply sliding their dev label down-and-right to point to the @ label. Now let's also erase our ajryan/dev label, and straighten out the kinks in these drawings, and use the label dev. Here's the result:

... - o - o - o - o - o - o - * - @   <-- HEAD=dev

This is what they will have, and what you will have, once you're both in sync. It's a very simple looking history, even if it's a slight deviation from the actual development process (you wrote commit @ without first seeing their chain that ended in *). But this is the kind of "rebase" that makes following the development work a lot easier, and is in general what people want to do.

Sliding a label "rightward" (including down-and-right, or up-and-right, in drawings like these) is a "fast forward" operation. If you run git merge with --ff-only, it will only do the "merge" if it is really this kind of non-merge fast-forward.


Bottom line: I think you wanted to rebase your @ commit onto the ajryan/dev work, but you didn't have an @ commit to rebase. That's why rebase gave you a noop. In this case, a simple non-merge fast-forward git merge suffices to make your label match their label.


It's also worth thinking about this: when you do git log X..Y you're asking git to draw the graph like I did here:

... - o - o - o       <-- X
        \
          o - o - o   <-- Y

The X..Y notion essentially says: starting with this graph, take out a highlighter and highlight the commit that X points to, and all commits left-and/or-up-or-down from there. (That leaves the three os pointed-to by Y un-marked.) Then, find all commits pointed-to by Y that are not already highlighted, and log those.

If you reverse the X and Y, as in git log Y..X, you ask git to "run the highlighter" on commits marked by Y, which leaves only the two os only on X, and then log all commits pointed-to by X that are not already highlighted. So here you get the two os on X, and none of the shared ones.