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
TL;DR
Everything so far is good. Just run
git merge --ff-only editing(or even justgit merge editing) now. But you may want to do something aboutperformance_limit_routesandperformance_testing.Long
I think your only error here is a slight mistake in your mental model for
git rebase. Whatgit rebaseis 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 rebasedoes, 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:
git checkout editinggit rebase -i mainmeans:
List out all commits that
git log main..editingwould show.Switch to the commit at the top of main (
010141chere).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
editingselect the last-copied commit.You're left with a bunch of commits (14, if I counted right) that are "ahead of"
mainin branchediting. Checking outmainputs you on commit010141cvia the namemain, so the output from yourgit logmakes perfect sense here.To cause these new-and-improved commits to become part of branch
main, you now need only run:The
--ff-onlyoption 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 runsgit 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 logto show everything, and it did: that includesperformance_limit_routesandperformance_testingand alsorefs/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
6be7f1aand010141cand 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 ofREADME.mdin 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 logoutput. They store the commit message, of which the first line is the "subject line", which you also see in yourgit logoutput: e.g.,Add agency fields(from commit6be7f1afor 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 yourgit stashresult.2Let'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
Hfor "hash"—we can draw the commits like this:Here
His a commit such as24b9ac9 (editing) Only show single selected stop. It has this arrow (pointer) sticking out of it, pointing to the next commit back, which we'll callG(or more literally1ccc747 Scroll to stop with button in stop_time). Let's drawGin now:Of course,
Ghas an arrow pointing to another, still-earlier commitF(or68ce7c4 Scroll item into view when clicked):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:Note that we can have more than one name, and/or more than one name pointing to any one particular commit:
Here the names
mainandorigin/mainpoint to a commit I'm callingBfor drawing purposes. CommitCpoints back toB;Hleads back toCeventually, which leads back toB, 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:
Git's
git log --all --decorate --oneline --graphis drawing these same kinds of graphs, but doing them vertically with one commit per line: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 logwill start from. Thengit logwill 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 stashcommand works by making commits. These commits are on no branch, which is supposed to hide them from you, but as yourgit logoutput shows, they're not very well hidden.Running
git stashmakes 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 usegit stashto deal with the stash commits correctly.This is one of several reasons I try to discourage users from using
git stashvery 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 will start with the commit found using the name
editingand 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 nameediting. That includes commits like those found by starting fromperformance_limit_routes. That's because this is one of those branch-y situations, like:If we start at
Land work backwards, we don't seeI-J; if we start atJand work backwards, we don't seeK-L. Git'sgit logcan, of course, be told start with all the name—that's what--allmeans—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:
we could run:
and then we'd see *only commits
I-J. The two-dot..syntax here means stop when you get to commits found from the namemain.This can get a little confusing, because:
would do the same thing:
git logstops when it reaches a commit that can be found, not just "the commit to the left of the two dots". Starting atLand working backwards, we goL,K,H,G, etc., so we still stop atH.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..Ysyntax. (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 rebaseuses this same trick to find the commits to copy.Your original set of commits to copy were these:
(I can tell because of the
origin/editinglabel here, which indicates that you had rungit push origin editingor similar at some point, plus the subject lines that match up.)The
git rebase -icommand—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. Sogit rebaseuses 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 rebaseis so clever (perhaps too clever for its, or your, own good sometimes, but definitely clever).You are supposed to (and did) run:
first. This gives Git its end point for which commits to copy.
Then, you run:
The name
mainhere givesgit rebaseboth 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 ismain..editing(ormain..HEADsinceHEADmeans 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 bymain(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-pickby default: that's what "pick" means in the interactive rebase command-sheet. Replacing apickwithsquashmodifies 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:
We run
git rebase mainorgit rebase -i mainnow, and Git lists out commitsC-D-E-F-G-Hto copy. Then Git checks out commitIand starts copying, with onecherry-pick(or cherry-pick-and-squash or whatever) at a time:where the tick marks indicate copies, and
CDis a new commit that's the squashed result of copyingCandDbut only making one commit, for instance.Now that all of this is done, Git simply yanks the name
editingoff commitHand makes it point to new copyGH:By switching to the name
editing, Git re-attachesHEAD, 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 namemainpoint to commitGH; usinggit checkoutandgit merge(with or without--ff-only) do this, giving you:Potential problems
Note that
origin/editingandperformancehere still point to un-rebased commits. If commitJisn't needed for anything, you can just delete it. CommitJis still there, but nothing finds it any more.Your
origin/editingis your Git's memory of brancheditingas seen in a repository you callorigin. This means you'll need to force-update the nameeditingin that repository, or maybe just delete it entirely. Then you can have your Git update or delete yourorigin/editing:or:
(the
--pruneoption togit fetchmeans: do agit fetchas 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 therefs/stashin yourgit logoutput. 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, becausegit logonly 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 avoidgit stash: just make a new branch and commit, instead.)