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.