4

if i have two commits with a distance of many commits between them, with many files committed in both of them, how is it best to move a hunk from one to the other, for example:

in commit 100: i have many files changes and many changes in a file "aa.txt" and also this one: - aaaa - bbbb + cccc

10 commits later i have another commit changing a lot of files and also "aa.txt": +dddd

I want to move the line change -aaaa from the former to the later.

Is there any CLI/UI tool that helps you to easily do it? (i obviously have no problem with history rewriting)

Vitali Zaidman
  • 899
  • 1
  • 9
  • 24

2 Answers2

6

It's possible that an interactive rebase would work. But a lot of factors could complicate this. So you've described this much:

x -- x -- A -- B -- C -- D -- x -- x <--(master)

Some line was changed in A, and therefore also is different in B, C, D, etc. But you want a new history where it isn't changed until D. So you could say

git rebase --interactive A^ master

(where A is the SHA ID for the commit that previously had the change, and note the ^ that goes at the end of said ID). In the text editor that comes up, you'll see a "todo" list. The first line says to pick commit A; change that to edit. Then find the line for D (which in this example is 3 commits later, but in your stated example might be 10 commits later). Mark it, as well, for edit.

The rebase will start, but after tentatively re-applying A it will pause so you can edit the commit.

Now, if reversing the change in question isn't something you can easily do by hand / from memory, then you can unstage the changes to that file

git reset HEAD^ -- aa.txt

And then stage them again in "patch mode".

git add --patch -- aa.txt

You'll be prompted how to handle each change hunk. If the change you want to remove appears in the same hunk as other changes, you can answer the prompt with e and edit the change hunk (and then replace the - that's before the line you no longer want to delete with a ).

Now get your (staged) edits into the commit

git commit --amend

The change you reverted from the index is still in the work tree; get it out of the way, and tell rebase to get back to work.

git stash 
git rebase --continue

As rebase continues working toward commit D, intervening commits could get conflicts (if they edited aa.txt too close to the change you reverted). These conflicts should be easy to resolve. (The HEAD side of the conflict will include the line you no longer deleted; other than that line, you likely want the "other" side of the conflict.)

Commit D might come up as a conflict, too. If so your job is easy: just resolve that one by keeping the "other" side of the conflict (including removal of the line in question, since this is where you finally want to do this). Then when separately prompted to edit D (because you marked it edit in the TODO list) you can just immediately tell rebase to --continue.

If D doesn't conflict, no big deal. You'll be prompted to edit it. Pop the stash you created earlier to re-apply the change you deferred from the A commit; add; commit --amend

Now the problems with this approach: If there are multiple refs that can reach A, this only updates the one (master in the above example). So for example

x -- A -- B -- C -- D -- x <--(master)
           \
            x -- x <--(branch)

In this case, branch still sees the old history. You end up with

x -- A' -- B' -- C' -- D' -- x <--(master)
 \
  A -- B -- x -- x <--(branch)

You could address this by doing something like

rebase --onto B' B branch

This will get old fast if there are a lot of branches to deal with.

Another problem would be if the rebase will encounter merge commits. To some extent you can use --preserve-merges to mitigate this, but it will still cause problems (possibly silently corrupting history) if the merges are "evil" (i.e. could be resolved automatically using the default merge strategy, but were edited in some way). In that case, the closest thing you could do would be to rebase in segments and reproduce the merges carefully at each step of the way.

Mark Adelsberger
  • 42,148
  • 4
  • 35
  • 52
  • what i end up doint is indeed `git rebase {COMMIT-HASH}^ -i --preserve-merges` marking both commits with `edit` and just copying the changes i wanted by hand. I still wonder if there is a UI tool that can do it easier. – Vitali Zaidman Jul 12 '17 at 16:21
  • This is extremely helpful and I wish this Q/A had more exposure so I would have found it more easily. I was using rebase for something similar but didn't understand why I was having a merge conflict. Turns out I didn't remove the hunk from the commit that originally introduced it. Thanks! +1 – okovko Sep 08 '18 at 12:06
  • @Mark Adelsberger, you should update your answer to talk about git rebase --rebase-merges -i which solves the issues you outlined in your answer. Here is another SO Q/A that talks about --rebase-merges https://stackoverflow.com/questions/15915430/what-exactly-does-gits-rebase-preserve-merges-do-and-why – okovko Sep 08 '18 at 12:52
  • 1
    @okovko - according to the docs, `--rebase-merges` does *not* solve the issues mentioned above. It solves some issues with interactive rebasing, but it still can't deal with conflict resolution and it still wants to apply the default merge result if doing so doesn't conflict (without regard to whether the merge was "evil") – Mark Adelsberger Sep 09 '18 at 22:19
  • Good to know the limitations of the improvements of the new --rebase-merges feature, thanks (: – okovko Sep 10 '18 at 15:10
2

Export your changes with the git format-patch

than edit the desired patch and apply it using the apply or am

$ git format-patch HEAD~10
$ git am ...

If you want to create a single file you can use the --stdout and print it to a file:

$ git format-patch master --stdout > changes.patch
CodeWizard
  • 128,036
  • 21
  • 144
  • 167