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.

8
  • 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. Commented Dec 20, 2011 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? Commented Dec 20, 2011 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) Commented Dec 20, 2011 at 19:09
  • gotcha. it's not crucial to me that the intermediate points compile/etc. Commented Dec 20, 2011 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... Commented Dec 20, 2011 at 19:33

4 Answers 4

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.

Sign up to request clarification or add additional context in comments.

4 Comments

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.
Unless I misunderstand, this would only show you the X commit in the edit file.
@toddsundsted I assumed X was the earliest commit, so it should show X..HEAD.
@CoderBrien I'm not sure how to automate it, some smaller rebase steps? Also, maybe update the question to specify "without rebase -i"?
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. 

1 Comment

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 :-).
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" 

1 Comment

Did you come up with a cleaner version? It seems like I'm doing similar things in stackoverflow.com/a/76856310/946850, but in a slightly different way.
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" 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.