0

I went back some number of commits:

> git reset --hard e7aec0fea8ed66e17c277298eebecae58ff044aa
> git status

On branch some-branch
Your branch is behind 'origin/some-branch' by 51 commits, and can be fast-forwarded.

I would like to merge with a newer commit, but not the newest one. Conceptually something like this:

> git ???
> git status

On branch some-branch
Your branch is behind 'origin/some-branch' by 50 commits, and can be fast-forwarded.

Ideally it would allow for selecting the number of steps:

Your branch is behind 'origin/some-branch' by 50 commits, and can be fast-forwarded.

> git ??? --steps 2
> git status

On branch some-branch
Your branch is behind 'origin/some-branch' by 48 commits, and can be fast-forwarded.

Is that possible in git?

Why not bisect?

It might look like I want git bisect, but it'll be hard to decide when to git bisect good and when to git bisect bad. I want to start from a certain known point and progress towards origin commit-by-commit.

Why not this thread ("git fast-forward one commit")?

The solution from this thread allows to ff to a specified commit instead of master, but you have to manually specify the commit hash. I'm looking for a way that doesn't require the hash.

Rafał G.
  • 1,529
  • 22
  • 35

2 Answers2

2

In the case that HEAD is behind origin/some-branch by N commits and can be fast-forwarded, if the commits between HEAD and origin/some-branch are linear, you can use git merge origin/some-branch~(N-1) to "git fast-forward one commit". For example, git merge origin/some-branch~49 results in behind origin/some-branch by 49 commits. You can also use git reset origin/some-branch~49 --hard to reach the goal.

But in many cases, the commit history is not linear. You still have to manually select which commit to merge or reset to. origin/some-branch~49 could be a valid commit, but not the one you want. And in these cases, git reset --hard somecommit is better, because git merge may introduce new merge commits.

The linear case, dev is behind master by 4 commits:

* 8a79cfc master
* d21f40f 
* f11b7fc 
* 93a4590 
o 864a80d HEAD -> dev

To git fast-forward one commit to 93a4590, git merge master~3 or git reset master~3 --hard.

The parallel case, dev is behind master by 6 commits:

*   9dd6d4e master
|\  
| * 5363dc7 
* | 8a79cfc 
|/  
* d21f40f 
* f11b7fc 
* 93a4590 
o 864a80d HEAD -> dev

master~5 is 864a80d where dev is now, so git merge master~5 won't "git fast-forward 1 commit".

ElpieKay
  • 27,194
  • 6
  • 32
  • 53
  • To avoid accidentally creating merge commits with `git merge`, the `--ff-only` option can be used. – mkrieger1 Nov 25 '20 at 13:45
  • Thanks, that'll work. The history will be linear in most cases, and if it's not, I guess I can deal with some parts of it manually (by using a commit hash) and then go back to numbers once there's no ambiguity again. – Rafał G. Nov 26 '20 at 21:19
1

I've updated the answer to the question you linked-to, to note that git mff (git merge --ff-only) doesn't require a hash ID. But your particular goal—to move to a given commit that does not have a specific name on it—does require a hash ID or a relative expression. As ElpieKay notes, any relative expression must take the commit graph's "shape" into account.

I like to draw my commit graphs horizontally rather than vertically. Git draws them vertically, with newer commits towards the top, resulting in the kind of git log --decorate --oneline --graph output ElpieKay showed. I draw them with newer commits towards the right to emphasize the parallel nature of a branch-and-merge within a single branch:

          I--J
         /    \
...--G--H      M--N   <-- branch
         \    /
          K--L

Suppose you have commit G checked out, as a detached HEAD. You might like to move "forward" one commit. That's pretty straightforward: one commit closer to commit N, at the tip of branch branch, is commit H. So if you step forward one commit, that's where you wind up.

But which commit is one step forward from H? Is it commit I, or commit K? Or is it, perhaps, both commits, I and K?

(Spoiler: it's both.)

This means your goal, of moving forward 1 or N commits, may be impossible. If there is a branch-and-merge bubble here, you must pick one "side" to travel. You can, of course, go back and try the other side as well, but you need to know that you've hit this case.

If you don't have this problem, you can still have a related problem. Suppose we have this graph:

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

If you are on a detached HEAD at commit G, the next commit forward is commit H, but after that, the next commit forward requires an in the direction of clause: are we trying to move towards branch1, i.e., commit J? Or are we trying to move towards branch2, i.e., commit L?

In this particular case, git rev-list --ancestry-path can come to our rescue. We can automate the idea of moving forward one commit in some direction, because:

git rev-list --topo-order --ancestry-path HEAD..branch1

will list out, in Git's natural (backwards) order, the commits "between" the current commit (HEAD) and branch1 that are:

  • descendants of HEAD (not includingHEAD itself), and
  • ancestors of branch1 (including the commit identified by the name branch1)

This constraint—descendants of HEAD and ancestors of branch1—is precisely what --ancestry-path means.

In our example case means that we'll list out commits H-I-J, but in the other order. So the last commit hash ID listed out is the one we want.1 We can therefore make an alias that will allow us to move our detached HEAD or current branch name, whichever we're on, one step forward in the direction of some given branch name, with:

target=$(git rev-list --topo-order --ancestry-path $endpoint | tail -n 1)
if [ "$target" = "" ]; then
    echo "we have reached the endpoint $endpoint"
else
    git merge --ff-only $target
fi

You can wrap this up into a small script that lets you move forward one commit at a time. The work needed to make it advance n steps, n > 1, is left as an exercise.


1It's temping to add --reverse and -n 1 to this, but alas, that doesn't work. You can use --reverse and head -n 1 instead of using tail -n 1, but I dislike the potential broken-pipe error this can cause.

The --topo-order option is mainly paranoia and probably not required, but I haven't proved that to myself, so I have left it in the answer here as a bit of magic.

torek
  • 448,244
  • 59
  • 642
  • 775
  • Thanks for elaborating on the graph shapes. I'll be able to use the ~ operator in most cases, but understanding the graphs better is helpful as well. – Rafał G. Nov 26 '20 at 21:23