5

How would I go about moving a commit up a branch to be as early as possible without any conflicts (without much manual work, eg rebase -i)?

Eg

A-B-C-D-X

should become

A-B-X-C-D

if swapping X with C and D has no conflicts but swapping X with B would result in a conflict.

Thanks.

CoderBrien
  • 663
  • 1
  • 7
  • 22
  • Interesting problem. Even if finding the point in the history was automated and resulted in no conflicts, that would not guarantee that the resulting file would compile correctly/run correctly, etc. – toddsundsted Dec 20 '11 at 18:33
  • shouldn't the tree at the resulting head be identical after the reorder, or am i misunderstanding what a clean commit reorder implies? – CoderBrien Dec 20 '11 at 18:46
  • okay, right... you are thinking about head and I was thinking about the impact of the change at earlier points in the history -- specifically the state of the build at the new location of X (not D) – toddsundsted Dec 20 '11 at 19:09
  • gotcha. it's not crucial to me that the intermediate points compile/etc. – CoderBrien Dec 20 '11 at 19:14
  • there's no command that I am aware of that does exactly this, although bisect is spiritually close... I'm contemplating a solution that uses a patch for X, git bisect, and some elbow grease to find a conflict-free merge point... – toddsundsted Dec 20 '11 at 19:33
  • don't think you can bisect because there may still be a future change on the branch that is in conflict with X. i think you probably have to go back 1 commit at a time. – CoderBrien Dec 20 '11 at 19:39
  • Can I ask why? There might be another solution to accomplish your goal that is easier to implement. – Karl Bielefeldt Dec 20 '11 at 22:44
  • Well, I specifically want to take a group of changes that I've recently made and back propagate them through history to make it look like those changes were there all along. The purpose is to remove specific obsolete elements that should have never been committed. For simple changes like add/remove files, I've already used filter-branch, but I effectively need to patch things along the way to match. – CoderBrien Dec 21 '11 at 02:46

4 Answers4

2

Use git rebase -i X~ where X~ is the revision before X.

Then reorder the lines in the rebase log to you're desired order.

More about interactive rebase.

m0tive
  • 2,796
  • 1
  • 22
  • 36
  • i need to do it in an automated way (not rebase -i) because there might be hundreds of commits on the branch. also, my desired order is not known ahead of time. – CoderBrien Dec 20 '11 at 18:30
  • Unless I misunderstand, this would only show you the X commit in the edit file. – toddsundsted Dec 20 '11 at 18:35
  • @toddsundsted I assumed `X` was the earliest commit, so it should show `X..HEAD`. – m0tive Dec 20 '11 at 18:40
  • @CoderBrien I'm not sure how to automate it, some smaller rebase steps? Also, maybe update the question to specify "without `rebase -i`"? – m0tive Dec 20 '11 at 18:42
2

Here's a demonstration of what I came up with after 15 minutes of hacking. It's not a complete solution to the posed problem, but it should cut down on the work involved.

The goal is to use git bisect to find the earliest conflict-free merge point for a future commit. The solution takes advantage of the binary search capability inherent in git bisect in order to cut down on the steps.

Unfortunately, this does not preclude later commits from conflicting, so an interactive rebase is required to vet the results (but that's the point, anyway).

The one disadvantage/caveat is that you have to reverse the sense of good and bad in your head when you instruct git about whether the step failed or succeeded when testing the patch.

If any of the steps below are unclear, let me know and I'll try to elaborate.

First create the following file in a series of commits. Each commit should add a series of four identical lines (a's, then b's, then c's, then d's).

a
a
a
a
b
b
b
b
c
c
c
c
d
d
d
d

At this point, git log should output something like:

commit 6f2b809863632a86cc0523df3a4bcca22cf5ab17
Author: Todd Sundsted <...>
Date:   Tue Dec 20 22:45:44 2011 -0500

    Added d.

commit 91ba7e6f19db74adb6ce79e7b85ea965788f6b88
Author: Todd Sundsted <...>
Date:   Tue Dec 20 22:44:26 2011 -0500

    Added c.

commit f83beee55d6e060536584852ebb55c5ac3b850b2
Author: Todd Sundsted <...>
Date:   Tue Dec 20 22:44:00 2011 -0500

    Added b.

commit d6d924b0a30a9720f6e01dcc79dc49097832a587
Author: Todd Sundsted <...>
Date:   Tue Dec 20 22:43:38 2011 -0500

    Added a.

commit 74d41121470108642b1a5df087bc837fdf77d31c
Author: Todd Sundsted <...>
Date:   Tue Dec 20 22:43:11 2011 -0500

    Initial commit.

Now edit the file, so that it contains the following, and commit this:

a
a
a
a
b
x
x
b
c
x
x
c
d
d
d
d

The log should now include one more commit:

commit 09f247902a9939cb228b580d39ed2622c3211ca6
Author: Todd Sundsted <...>
Date:   Tue Dec 20 22:46:36 2011 -0500

    Replaced a few lines with x.

Now generate a patch for the X commit.

git diff -p master~ > x.patch

Crank up bisect -- remember to use git bisect good when the patch fails and git bisect bad when the patch succeeds:

$ git bisect start
$ git bisect good 74d41121470108642b1a5df087bc837fdf77d31c
$ git bisect bad master
Bisecting: 2 revisions left to test after this (roughly 1 step)
[f83beee55d6e060536584852ebb55c5ac3b850b2] Added b.
$ patch --dry-run -p1 < x.patch 
patching file file.txt
Hunk #1 FAILED at 3.
1 out of 1 hunk FAILED -- saving rejects to file file.txt.rej
$ git bisect good
Bisecting: 0 revisions left to test after this (roughly 1 step)
[6f2b809863632a86cc0523df3a4bcca22cf5ab17] Added d.
$ patch --dry-run -p1 < x.patch 
patching file file.txt
$ git bisect bad
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[91ba7e6f19db74adb6ce79e7b85ea965788f6b88] Added c.
$ patch --dry-run -p1 < x.patch 
patching file file.txt
Hunk #1 succeeded at 3 with fuzz 2.
$ git bisect bad
91ba7e6f19db74adb6ce79e7b85ea965788f6b88 is the first bad commit
commit 91ba7e6f19db74adb6ce79e7b85ea965788f6b88
Author: Todd Sundsted <...>
Date:   Tue Dec 20 22:44:26 2011 -0500

    Added c.

$ git bisect reset

As expected, the edits in commit X can be moved immediately after commit C. An interactive rebase confirms this:

91e92489 * Added d.
6c082b1f * Replaced a few lines with x.
a60ae2a9 * Added c.
4d5e78f2 * Added b.
7d2ff759 * Added a.
74d41121 * Initial commit.
toddsundsted
  • 6,225
  • 2
  • 21
  • 13
  • thanks for the attempt. i'm working on a solution right now that i think will work. i'm going to write a script that uses `git format-patch` and `git am` in a loop to walk the branch backwards and see where the `git am` fails. hopefully :-). – CoderBrien Dec 20 '11 at 20:53
0

We really need to go commit by commit in order to find the earliest point where the commit in question and all later commits apply. One way to do this efficiently is to iterate backwards through the list of commits. This seems similar to the idea in CoderBrien's answer.

The (very rough) script below finds this point and prints useful information about it that helps decide if you really want to move the commit that far. Compared to CoderBrien's script, it uses higher-level commands like cherry-pick and revert. The script expects a commit at the command line, if not given, the last commit is used.

To actually move the commit, use git rebase -i . While this could be automated, some control over the process seems actually useful here.

Bubbling down a commit can be achieved simply by doing git rebase -i and trying to move the commit to the end. The rebase process will stop at the latest point of failure, or succeed.

For both cases, just because a commit applies doesn't mean it makes sense. Ideally, each commit would also "pass all tests". The script could be enhanced to cater for that.

The most up to date version is at https://github.com/krlmlr/scriptlets/blob/main/home/bin/git-bubble.

#!/bin/sh

set -ex

top_level=$(git rev-parse --show-toplevel)
# TODO: Does this always exist?
git_dir=${top_level}/.git
tmpdir="${git_dir}/bubble-work"
/bin/rm -rf "$tmpdir"
git clone "$top_level" "$tmpdir"

start_commit=$(git rev-parse HEAD)

if [ -n "$1" ]; then
  my_commit=$1
else
  my_commit=$start_commit
fi

git -C $tmpdir reset --hard ${my_commit}^

# https://stackoverflow.com/a/62397081/946850
# FIXME: Better way to find branch point?
for current_commit in $(git log --format="%H" origin/HEAD..${my_commit}^); do
  git -C $tmpdir revert --no-edit $current_commit
  if ! git -C $tmpdir cherry-pick --no-commit $my_commit; then
    # Show info that helps decide if this is really the right place
    # to move that commit to, and to navigate in the subsequent `git rebase -i`
    git --no-pager show $current_commit
    git --no-pager show $my_commit
    git log --oneline -n 1 $current_commit
    git log --oneline -n 1 $my_commit
    exit 1
  fi
  git -C $tmpdir reset --hard HEAD
done

echo "End reached, can be applied onto the main branch"
krlmlr
  • 25,056
  • 14
  • 120
  • 217
0

Well, this pretty much works but needs some cleanup.

After working with it for a while, I've bumped into another problem which I've posted here.

#!/bin/sh -e

# todo: integrate with git
GIT_DIR=./.git/

commitid=$1

if [ "$1" = "" ]; then
   echo usage: $0 commitid
   exit 1
fi

tmpdir="$GIT_DIR/bubble-work"
/bin/rm -rf "$tmpdir"
mkdir "$tmpdir"

# checkout commit with detached head
git checkout -q $commitid^0 || die "could not detach HEAD"

while [ 1 = 1 ]; do

# todo pipe output to avoid temp files
# see git-rebase.sh

patchfile=`git format-patch -k --full-index --src-prefix=a/ --dst-prefix=b/ --no-renames -o "$tmpdir" HEAD~1`
echo patch = $patchfile

git checkout -q HEAD~2
git am --rebasing "$patchfile" || die "git am failed"
/bin/rm -f "$patchfile"

echo looping
done



/bin/rm -rf "$tmpdir"

Community
  • 1
  • 1
CoderBrien
  • 663
  • 1
  • 7
  • 22
  • Did you come up with a cleaner version? It seems like I'm doing similar things in https://stackoverflow.com/a/76856310/946850, but in a slightly different way. – krlmlr Aug 08 '23 at 03:51