5

My use case is changing my commits in a feature branch before I publish it, e.g., reword commit messages, squash some commits, etc. I do not want to move the commits to a new base.

For this, I usually do something like this:

git rebase -i HEAD~4

where the number "4" is the result of manually counting the commits in my feature branch.

I was wondering if Git has a command like "start interactive rebase for all commits in my feature branch but don't move then to a newer master – just stay where you are". I found the --fork-point option of git rebase and tried this:

git rebase -i --fork-point master

However, this doesn't have any noticeable effect and behaves the same as git rebase -i master.

Instead, this does what I need:

git rebase -i $(git merge-base --fork-point master)

I read the docs of --fork-point in git rebase docs but don't quite understand why it didn't lead to my expected result. Can someone explain it please?

Borek Bernard
  • 50,745
  • 59
  • 165
  • 240

4 Answers4

3

It didn't lead to your expected result because --fork-point has nothing to do with deciding the base for the new commits[1].

So the default is to base the new commits at the upstream (master in this case), and --fork-point doesn't affect that.

(For reference, what --fork-point does is, it uses the reflogs to refine the calculation that "guesses" what commits should be rewritten. This is not always - or, in my experience, even often - very useful.)

Your two options are to use the merge base as the upstream - as you describe - or use the --onto option to explicitly set the new base (in this case, setting it to match the original base).


[1] - remember that even though conceptually you're editing commits, really rebase always writes new commits - except when it does nothing. So when it 'edits' a commit, it really creates new commits that are similar to old commits, but edited.

Mark Adelsberger
  • 42,148
  • 4
  • 35
  • 52
3

I would expect the simpler invocation below to do what you need.

git rebase -i $(git merge-base HEAD master)

The documentation for git merge-base --fork-point shows that the option can highly useful but in the context of a complicated history. Your question does not indicate that you have been doing a lot of history rewriting, so --fork-point may be overkill for your case.

Discussion on fork-point mode

After working on the topic branch created with

git switch -c topic origin/master

the history of remote-tracking branch origin/master may have been rewound and rebuilt, leading to a history of this shape:

                 o---B2
                /
---o---o---B1--o---o---o---B (origin/master)
        \
         B0
          \
           D0---D1---D (topic)

where origin/master used to point at commits B0, B1, B2 and now it points at B, and your topic branch was started on top of it back when origin/master was at B0, and you built three commits, D0, D1, and D, on top of it. Imagine that you now want to rebase the work you did on the topic on top of the updated origin/master.

In such a case, git merge-base origin/master topic would return the parent of B0 in the above picture, but B0^..D is not the range of commits you would want to replay on top of B (it includes B0, which is not what you wrote; it is a commit the other side discarded when it moved its tip from B0 to B1).

git merge-base --fork-point origin/master topic is designed to help in such a case. It takes not only B but also B0, B1, and B2 (i.e. old tips of the remote-tracking branches your repository’s reflog knows about) into account to see on which commit your topic branch was built and finds B0, allowing you to replay only the commits on your topic, excluding the commits the other side later discarded.

The git rebase --fork-point documentation makes the connection between git rebase --fork-point and git merge-base --fork-point.

When --fork-point is active, forkpoint will be used instead of upstream to calculate the set of commits to rebase, where forkpoint is the result of

git merge-base --fork-point <upstream> <branch>

command. (See git-merge-base.) …

Borek Bernard
  • 50,745
  • 59
  • 165
  • 240
Greg Bacon
  • 134,834
  • 32
  • 188
  • 245
  • Thanks, the "Discussion on fork-point mode" section explains it nicely. I didn't appreciate what it does by just reading the `git rebase` docs. – Borek Bernard Sep 07 '19 at 18:10
1

Git 2.24 will introduce a new option, --keep-base:

"git rebase --keep-base " tries to find the original base of the topic being rebased and rebase on top of that same base, which is useful when running the "git rebase -i" (and its limited variant "git rebase -x").

Sounds like it might be the solution to the use case in the OP but I haven't tried yet.

Borek Bernard
  • 50,745
  • 59
  • 165
  • 240
1

I believe these might be what you're looking for:

# uses "graph" to determine topological "fork point"
❯ git rebase --interactive --no-fork-point UPSTREAMREFNAME FEATURE --keep-base
# uses reflog to determine historical "fork point"
❯ git rebase --interactive --fork-point UPSTREAMREFNAME FEATURE --keep-base

They can be simplified to:

[HEAD -> feature] ❯ git rebase -i UPSTREAMREFNAME
[HEAD -> feature] ❯ git rebase -i --fork-point UPSTREAMREFNAME

Elaboration:

  • UPSTREAMREFNAME FEATURE are used to compute the commit range that is to be rebased.

    • if --fork-point is specified, the commit range produced is equivalent to $(git merge-base --fork-point UPSTREAMREFNAME FEATURE)..FEATURE. the "fork point" is determined based on the reflog, i.e along the lifespan of UPSTREAMREFNAME, at which exact hash was the most recent call/command made by FEATURE to branch-off-of/fork-from UPSTREAMREFNAME branch reference.

      • the commit range produced represents: all the created ancestor commits of FEATURE all the way back [in time] to where [in the reflog] the branch was forked from the specified mainline/upstream branch UPSTREAMREFNAME.
    • if --no-fork-point is specified, the commit range produced is equivalent to UPSTREAMREFNAME..FEATURE. the "fork point" is determined based on the topology (arrangement) of the history graph. this means that the "fork point" will be the earliest commit that UPSTREAMREFNAME and FEATURE both share, i.e the earliest point where they intersect in the graph.

      • the commit range produced represents: all commits reachable from FEATURE [i.e walking the history graph], excluding those reachable from UPSTREAMREFNAME.
    • Important: if FEATURE is omitted, it will default to HEAD, which means, if you are checked out [anywhere] on your feature branch, you don't have to specify FEATURE and it will work fine, else you must specify it or you might end up with a result you did not expect.

    • UPSTREAMREFNAME, a.k.a <upstream> in the git-rebase(1) documentation, must be the name of a reference (e.g. branch, tag)

    • FEATURE, a.k.a <branch> or topic in the git-rebase(1) documentation, is technically a "commit-ish" parameter, which means it can be anything (any notation) that resolves to a commit object name (hash id).

  • --keep-base specifies the commit base on top of which the [recreated] commit range will be placed/based.

    • the commit base is computed to be the merge base of UPSTREAMREFNAME and FEATURE, i.e is equivalent to --onto $(git merge-base UPSTREAMREFNAME FEATURE)

References:

  • git-rebase(1)
  • git-merge-base(1): discusses [plumbing] command that can find the merge base of two or more commits, defines and illustrates "fork point", and more.
  • gitrevisions(7): discusses at length regarding "commit-ish" notations.
  • git-rev-parse(1): discusses [plumbing] command that can resolve notations to object names, and more.
8c6b5df0d16ade6c
  • 1,910
  • 15
  • 15