Home > other >  Git interactive rebase HEAD not point at last commit
Git interactive rebase HEAD not point at last commit

Time:07-19

I just ran an interactive rebase using the following commands:

git checkout editing
git rebase -i main
# do interactive rebasing, specificically squashing
git checkout main

At this point I expected all the (squashed) commits to be copied/replayed onto main, and for HEAD to be pointing at what was the last commit to editing, now in main. But it seems HEAD is still pointing to the previously last commit to main. Note: editing was based off of the second to last commit to main, 010141c (Update notes) was not in editing.

I'm not confident with git and I'm not sure if I've done this wrong or if I just need to get HEAD to point to the most recent commit somehow.

This is the output of git log --graph --oneline --all --decorate:

* 24b9ac9 (editing) Only show single selected stop
* 1ccc747 Scroll to stop with button in stop_time
* 68ce7c4 Scroll item into view when clicked
* 0f76a85 Add list of stops
* 50fbec3 Add stop_time fields
* 6be7f1a Add agency fields
* 6c5227a Add trip fields
* f71a471 Add dropdowns
* 5d1ae61 Fix formatting issue
* d7d00c7 Add Newtype radiogroup
* 6f0ed7a Start adding updatable fields to routes
* 160c0f8 Fix text fields not updating
* 175b763 Add new trip using data method
* 29d32da Delete and then restore trips
* 010141c (HEAD -> main, origin/main, origin/HEAD) Update notes
| * f1f127d (origin/performance_limit_routes, performance_limit_routes) Limit routes
| | *   535f628 (refs/stash) WIP on performance_testing: a06ab13 Improve performance by not impl Lens and Data for MyStopTime
| | |\  
| | | * 5aea13d index on performance_testing: a06ab13 Improve performance by not impl Lens and Data for MyStopTime
| | |/  
| | * a06ab13 (origin/performance_testing, performance_testing) Improve performance by not impl Lens and Data for MyStopTime
| |/  
| * d9d9f89 Fix performance by reducing number of stop_times
| * 9c12ca4 Backup
| * 2c23d01 (origin/editing) Only show single selected stop
| * 3313e88 Scroll to stop with button in stop_time
| * c1a329f Scroll item into view when clicked
| * 712eb18 Add list of stops
| * d3333e6 Add stop_time fields
| * 116ef9f Add agency fields
| * 9eb0f43 Add trip fields
| * 8d1453e Add dropdowns
| * 3f4a0ca Fix formatting issue
| * 61613d4 Add Newtype radiogroup
| * 8ca937b Start adding updatable fields to routes
| * 02fca9b Backup
| * f2d756a Fix text fields not updating
| * cf6907e Backup
| * f9c4115 Add new trip using data method
| * 6ab663e WIP
| * caac18e WIP
| * 7a70e4a Delete and then restore trips
| * 2457455 WIP
|/  
* ff0cef7 Replace list expander checkbox with custom widget

CodePudding user response:

TL;DR

Everything so far is good. Just run git merge --ff-only editing (or even just git merge editing) now. But you may want to do something about performance_limit_routes and performance_testing.

Long

I think your only error here is a slight mistake in your mental model for git rebase. What git rebase is about can be put fairly simply:

  • You have some collection of commits. You like most of the things about most of these commits, or some of the things about some of these commits. But there's at least one thing you don't like about the collection of commits.

  • It's literally impossible to change any Git commit once you have made it. But: when we talk about the commits on branch B (for some B), what we mean is: the commits that Git finds by using the name B to locate one specific commit, then working backwards from commit to commit. (I'll say more about this in a bit, but remember that each branch name simply records the raw hash ID of a single commit.)

This means that if we were to copy the original commits to some series of new-and-improved commits, then have Git force the name B to point to the last copy, instead of the last original, why then, anyone who isn't paying attention to the raw hash IDs of commits will see the new copies, instead of the originals.

So this is what git rebase does, in a nutshell. It takes some series of original commits, where you mostly like them but dislike something about them, and copies them to new and improved commits. Then it makes the branch name we are using to find the commits find the new and improved commits, instead of the old (and lousy?) ones.

As such, your sequence of these two commands:

  1. git checkout editing
  2. git rebase -i main

means:

  • List out all commits that git log main..editing would show.

  • Switch to the commit at the top of main (010141c here).

  • Copy each commit, and/or squash some, as directed by the "pick" commands as you updated them with the interactive-rebase command sheet.

  • Make the branch name editing select the last-copied commit.

You're left with a bunch of commits (14, if I counted right) that are "ahead of" main in branch editing. Checking out main puts you on commit 010141c via the name main, so the output from your git log makes perfect sense here.

To cause these new-and-improved commits to become part of branch main, you now need only run:

git merge --ff-only editing

The --ff-only option isn't strictly necessary here, I just like to use it myself. (I use it so often that I made an alias, git mff, that runs git merge --ff-only. My goal here is to prevent myself from making mistakes via typos or whatever.)

The "duplicates" you see are there because you have told git log to show everything, and it did: that includes performance_limit_routes and performance_testing and also refs/stash. These refer to the old commits. The original 19 (if I counted right, again) commits still exist, and you and Git can still find them.

Is this a problem? Maybe, or maybe not. If not, you don't need to do anything. If so, you need to do something. That something might be as simple as "delete these branches so nobody will see them any more" (and drop your stash with git stash drop). Whether you want to keep these commits, and if so, whether you want to copy (or rebase) any of them, is up to you. See the "gory details" below.

The gory details about how this all works

To use Git, we need to know—sort of deep in our bones, as it were, without even thinking about it—that Git is all about commits, and that each commit is numbered (with a random-looking hash ID like 6be7f1a and 010141c and so on, except these are abbreviated—the full ones are much longer) and stores both a snapshot of all files, and some metadata.

The snapshot-of-all-files is pretty simple conceptually: it's like a tar or WinRAR or zip archive. It is, however, stored in a special Git-only format, with the file contents being compressed and (importantly) de-duplicated. This means that only the first commit you make actually has to store all the files: unless the second commit replaces every file wholesale, the second commit re-uses some of the first commit's files, and those are literally shared, via the de-duplication trick. You might have 1000 commits, but only three actual versions of README.md, for instance, in which case there are only three versions of README.md in the repository, shared across all the commits.

The metadata, which are stored per-commit (i.e., never shared1), store things like the name and email address of the person who made the commit. This is the stuff you see in a longer git log output. They store the commit message, of which the first line is the "subject line", which you also see in your git log output: e.g., Add agency fields (from commit 6be7f1a for instance).

But—and this is crucial for Git's own operation—each commit's metadata also stores a list of previous commit hash IDs. Most commits hold just one hash ID in this list. A merge commit, such as 535f628 (refs/stash) WIP on performance_testing: a06ab13 Improve performance by not impl Lens and Data for MyStopTime, has more than one hash ID in the list: this merge commit is your git stash result.2

Let's ignore the merge commit and concentrate on the ordinary commits instead. Each such commit stores the hash ID of one previous commit, which we (and Git) call the parent of the commit. We also say that the commit points to its parent. As a result, if we use uppercase letters to stand in for real commits—e.g., use H for "hash"—we can draw the commits like this:

            <-H

Here H is a commit such as 24b9ac9 (editing) Only show single selected stop. It has this arrow (pointer) sticking out of it, pointing to the next commit back, which we'll call G (or more literally 1ccc747 Scroll to stop with button in stop_time). Let's draw G in now:

        <-G <-H

Of course, G has an arrow pointing to another, still-earlier commit F (or 68ce7c4 Scroll item into view when clicked):

... <-F <-G <-H

This goes on forever, or rather, until we get to the very first commit in your repository, which has an empty list of parents because there's no earlier commit.

The branch name, in this case editing, simply points to the last commit in the chain, like this:

...--F--G--H   <-- editing

Note that we can have more than one name, and/or more than one name pointing to any one particular commit:

...--B   <-- main (HEAD), origin/main
      \
       C--...--H   <-- editing

Here the names main and origin/main point to a commit I'm calling B for drawing purposes. Commit C points back to B; H leads back to C eventually, which leads back to B, which keeps going back from there, and so on.

We get branch-like stuff when we have something that, drawn this way, looks like this:

          I--J   <-- branch1
         /
...--G--H
         \
          K--L   <-- branch2

Git's git log --all --decorate --oneline --graph is drawing these same kinds of graphs, but doing them vertically with one commit per line:

* L (branch2) subject for commit L
* K  subject for commit K
| * J (branch1) subject for commit J
| * I subject for commit I
|/
* H subject for commit H
* G subject for commit G

and so on. The parenthesized name(s) show you which names point to the commit on this line. The | and other lines serve as connectors to skip past other lines. The asterisks * mean "this is a commit".

Other Git graph drawing software exists, and it all does something slightly different. See Pretty Git branch graphs for many ways to view the graph. The graph is very important because it's how Git finds commits. We can give Git a raw hash ID (or abbreviated one), or a name that points to a hash ID. That's where git log will start from. Then git log will use the parent hash ID stored in each commit to find the previous commit—or, for a merge commit, use all the parents to find all the previous commits.


1The metadata are never shared because every commit is unique. Git sticks a date-and-time stamp in here to help out, plus it's extremely rare for two commits to be identical in everything except the date-and-time-stamp. The metadata also include the hash ID of the parent commits, as described above, plus the hash ID of the tree that holds the snapshot. The tree need not be unique, but the parent hash will be unique.

There's one oddball case here: if you have two branch names that point to the same commit, and you use the computer itself to do rapid-fire git commits of the same update (or lack of update) to both branches all at the same exact second, you'll get a single shared new commit and both branch names will point to this same single new commit. So "never shared" is a slight overstatement, but the only sharing case is difficult to hit and just results in two branch names that previously shared an existing commit both also sharing the new commit.

(I actually had this happen to me once when I was writing and running a script, and I was surprised until I reasoned it out.)

2The git stash command works by making commits. These commits are on no branch, which is supposed to hide them from you, but as your git log output shows, they're not very well hidden.

Running git stash makes at least two commits, with one of the two or three commits it makes having the form of a merge commit. However, the substance in that merge commit isn't like a regular Git merge at all, and feeding stash commits to regular Git commands may result in shock, horror, and/or tears: unless you are a Git mechanic, you must use git stash to deal with the stash commits correctly.

This is one of several reasons I try to discourage users from using git stash very much: the high-voltage inner workings are not sufficiently off-limits from causal users, who may get zapped.


Sometimes we want some commits, not all commits

If you run:

git log editing

Git will start with the commit found using the name editing and working backwards. Git will keep going forever, or rather, until it runs out of commits (reaches the very first one). This won't show commits that aren't found by starting from the name editing. That includes commits like those found by starting from performance_limit_routes. That's because this is one of those branch-y situations, like:

          I--J   <-- sort-of-like-editing
         /
...--G--H
         \
          K--L   <-- sort-of-like-performance_limit_routes

If we start at L and work backwards, we don't see I-J; if we start at J and work backwards, we don't see K-L. Git's git log can, of course, be told start with all the name—that's what --all means—and then we see all the commits, but a lot of the time, that's not what we want, and we just use one name and see all of the commits findable by that name.

But sometimes we don't want every commit. We want some chosen subset of all commits. If we have:

          I--J   <-- sort-of-like-editing
         /
...--G--H   <-- main
         \
          K--L   <-- sort-of-like-performance_limit_routes

we could run:

git log main..sort-of-like-editing

and then we'd see *only commits I-J. The two-dot .. syntax here means stop when you get to commits found from the name main.

This can get a little confusing, because:

git log <hash-of-L>..sort-of-like-editing

would do the same thing: git log stops when it reaches a commit that can be found, not just "the commit to the left of the two dots". Starting at L and working backwards, we go L, K, H, G, etc., so we still stop at H.

A trick that I find works for me and many others is this: To understand the two-dot syntax, start by drawing the graph. Then, pick the commit to the left of the two dots. Paint it red. Follow its parent, backwards, and paint that one red, and keep going with the red paint until you run entirely out of commits. Then, pick the commit to the right of the two dots. If it's not painted at all, paint it green, and then follow it backwards to its parent and paint that one green; keep going until you either run out of commits, or hit red-painted ones. Stop immediately at any red-painted ones.

When you're done with this "temporarily paint the commits colors" process, the green ones are the ones selected by the X..Y syntax. (We drop all the color before the next Git operation, which may or may not use the two-dot syntax.)

Rebase wants to copy "some commits"

The reason for the above exercise—you should do it on a few graphs; draw them on paper, or whiteboard, or whatever, and color them and so on, until you have a feel for how this works—is that git rebase uses this same trick to find the commits to copy.

Your original set of commits to copy were these:

| * 2c23d01 (origin/editing) Only show single selected stop
| * 3313e88 Scroll to stop with button in stop_time
| * c1a329f Scroll item into view when clicked
| * 712eb18 Add list of stops
| * d3333e6 Add stop_time fields
| * 116ef9f Add agency fields
| * 9eb0f43 Add trip fields
| * 8d1453e Add dropdowns
| * 3f4a0ca Fix formatting issue
| * 61613d4 Add Newtype radiogroup
| * 8ca937b Start adding updatable fields to routes
| * 02fca9b Backup
| * f2d756a Fix text fields not updating
| * cf6907e Backup
| * f9c4115 Add new trip using data method
| * 6ab663e WIP
| * caac18e WIP
| * 7a70e4a Delete and then restore trips
| * 2457455 WIP

(I can tell because of the origin/editing label here, which indicates that you had run git push origin editing or similar at some point, plus the subject lines that match up.)

The git rebase -i command—which is the "interactive" version of rebase, which lets you fiddle with the way the copying works—needs to know which commits to copy. That, in Git, practically cries out for the two-dot syntax. So git rebase uses this two-dot syntax for you internally: you don't even have to type the two dots, it just uses them.

The rebase operation needs to know two things though. First, it needs to know: Which commits should I copy, and which ones shouldn't I copy? But then it also needs to know: Where should I put the copies? This is where git rebase is so clever (perhaps too clever for its, or your, own good sometimes, but definitely clever).

You are supposed to (and did) run:

git checkout editing

first. This gives Git its end point for which commits to copy.

Then, you run:

git rebase [options] main

The name main here gives git rebase both of the two things it needs to know: where to put the copies, and what not to copy.

The "what not to copy" part is main, so what to copy is main..editing (or main..HEAD since HEAD means the current branch or current commit, depending on which question Git wants to ask internally). The where to put the copies is just after the commit named by main (010141c).

Git actually does the copying using "detached HEAD" mode, which we won't go into in any detail here. It makes each new commit using git cherry-pick by default: that's what "pick" means in the interactive rebase command-sheet. Replacing a pick with squash modifies the way the cherry-picking is done so that you only get one final commit for however many commits you're copying; the final commit's snapshot is the one made by combining each of the copied commits.

The result of all this copying is something we can draw like this. Let me make a slightly more realistic drawing to start with:

          I   <-- main
         /
...--A--B
         \
          C--D--E--F--G--H   <-- editing (HEAD) [before rebase], origin/editing

We run git rebase main or git rebase -i main now, and Git lists out commits C-D-E-F-G-H to copy. Then Git checks out commit I and starts copying, with one cherry-pick (or cherry-pick-and-squash or whatever) at a time:

            CD-E'-F'-GH   <-- HEAD
           /
          I   <-- main
         /
...--A--B
         \
          C--D--E--F--G--H   <-- editing, origin/editing
                          \
                           J   <-- performance

where the tick marks indicate copies, and CD is a new commit that's the squashed result of copying C and D but only making one commit, for instance.

Now that all of this is done, Git simply yanks the name editing off commit H and makes it point to new copy GH:

            CD-E'-F'-GH   <-- editing (HEAD)
           /
          I   <-- main
         /
...--A--B
         \
          C--D--E--F--G--H   <-- origin/editing
                          \
                           J   <-- performance

By switching to the name editing, Git re-attaches HEAD, so that you're in the normal everyday Git usage mode again, rather than in the middle of a rebase, and now the rebase is complete.

If you now "fast-forward" main, Git will make the name main point to commit GH; using git checkout and git merge (with or without --ff-only) do this, giving you:

            CD-E'-F'-GH   <-- editing, main (HEAD)
           /
          I
         /
...--A--B
         \
          C--D--E--F--G--H   <-- origin/editing
                          \
                           J   <-- performance

Potential problems

Note that origin/editing and performance here still point to un-rebased commits. If commit J isn't needed for anything, you can just delete it. Commit J is still there, but nothing finds it any more.

Your origin/editing is your Git's memory of branch editing as seen in a repository you call origin. This means you'll need to force-update the name editing in that repository, or maybe just delete it entirely. Then you can have your Git update or delete your origin/editing:

git push -f origin editing   # force-update their editing
git fetch                    # update my origin/editing

or:

git push --delete origin editing  # delete their editing
git fetch --prune                 # delete my origin/editing

(the --prune option to git fetch means: do a git fetch as usual, but if some branch(es) on their end are gone, delete my altered copies of their branch names).

Meanwhile, you still have at least one stash you made with git stash. That's the refs/stash in your git log output. If this stash is still useful, you can apply it and then drop it; if not, you can just drop it. (Remember to check whether you have any other stashes as well, because git log only sees the "top" stash@{0} stash. Keeping stashes around for long periods is generally a bad idea: it's very hard to know where they should go, a month or two after you made them. This is another reason to avoid git stash: just make a new branch and commit, instead.)

  • Related