Home > front end >  rewrite history to undo all changes to a file on current branch in git
rewrite history to undo all changes to a file on current branch in git

Time:06-13

Suppose I have a feature branch that modifies half a dozen files. Most of the commits on that branch involve file.py. I eventually realize that there's a potentially better way to implement this feature without touching file.py at all. Is there a way to tweak all the commits on my branch to not touch that file? I feel that there should be some kind of trick with interactive rebase that does this easily, but I'm not sure what trick that would be.

Of course, I could simply revert the changes as a new commit, but seems a pity to clutter up history for posterity.

CodePudding user response:

Do an interactive rebase all the way back to the commit before the "start" of the branch (that is, to the most recent reachable commit where this file was still the way you want it). Let's call that commit abcde:

git rebase -i abcde

The Todo list will appear, listing all the commits after that "good" commit.

pick 11111
pick 22222
pick 33333
...

In the Todo list, mark every commit edit.

edit 11111
edit 22222
edit 33333
...

Save and close the editor. The rebase will begin. When each stage in the rebase stops and waits for your instructions, restore the state of the file in question from before the branch:

git restore --source abcde --worktree --staged -- <pathToFile>
git rebase --continue

Your editor will ask you whether you want to change the commit message; you don't, so just close the editor, and on we go to the next commit.

When you get to the very end of the process, you will have carried an unchanged file all the way thru the branch.


It is possible to automate everything I just said by using git filter-repo, but you asked for a trick with interactive rebase, so that's what I described.

Also, note that if you happen know that the file in question was not altered in a particular commit, you can leave that commit set to pick in the Todo list and thus save some steps. There are ways to find that out. But my answer is deliberately simple and ignores that issue.


I always like to prove my answer by an illustration. Here's the opening situation:

* f2fa796 (HEAD -> main) three
* dc90263 two
* 4861739 one

Each commit has a file a.txt, and it changes that file from the previous commit, as I shall now show:

% git show HEAD:a.txt
aaa
% git show HEAD~1:a.txt
aa
% git show HEAD~2:a.txt
a

Very well, let's begin our interactive rebase:

% git rebase -i 4861739

In the editor, I mark both commits as edit, and close the editor. The rebase begins:

Stopped at dc90263...  two
You can amend the commit now, with

  git commit --amend 

Once you are satisfied with your changes, run

  git rebase --continue

So I say:

% git restore --source 4861739 --staged --worktree -- a.txt
% git rebase --continue

We proceed to the next commit. There is a merge conflict in this file, but that doesn't matter to me:

% git restore --source 4861739 --staged --worktree -- a.txt
% git rebase --continue   

And Git says:

Successfully rebased and updated refs/heads/main.

So now let's inspect the situation again:

% git show HEAD:a.txt
a 
% git show HEAD~1:a.txt                                    
a
% git show HEAD~2:a.txt                                    
a

That is exactly the desired result.

CodePudding user response:

This can be automated by using automatic conflict resolution during the rebase. The algorithm could work like this:

  1. Create a branch at the commit containing the last changes for the file(s) in question.
  2. Create a new commit on that branch that changes the file(s) in some way. Note the modification can be pretty much anything as long as it is an edit (not a delete).
  3. Rebase your main branch onto (--onto) the new temp branch, and use merge strategy "ours" (-Xours).
  4. Now remove the temporary commit from your branch, using interactive rebase or via another rebase --onto as demonstrated below.

Here's a bash script that demonstrates the algorithm in action:

#!/bin/bash -v
git init

git branch -m main # name branch main in case that isn't your default

echo asdf > asdf; echo stuff > file.py; git add .; git commit -m "Create commit 1"
echo line2 >> asdf; echo line2 >> file.py; git add .; git commit -m "Create commit 2"
echo line3 >> asdf; echo line3 >> file.py; git add .; git commit -m "Create commit 3"
echo line4 >> asdf; echo line4 >> file.py; git add .; git commit -m "Create commit 4"

git branch main-rebase # make a copy of main for the rebase

# make a branch off of commit 1 (3 commits ago)
git switch -c temp-branch @~3

# make a temp commit that modifies file.py
echo testing >> file.py; git add .; git commit -m "wip: change file.py"

# rebase using "ours" strategy to remove all changes to file.py after commit 1
git rebase main-rebase~3 main-rebase --onto temp-branch -Xours

# remove the temporary wip commit from the branch
git rebase main-rebase~3 main-rebase --onto main-rebase~4

# remove the temp branch
git branch -D temp-branch

# show the full log
echo "Oneline graph log of all branches:"
git log --all --graph --oneline

echo "Show history of file.py on main"
git log main --oneline -- file.py

echo "Show history of file.py on main-rebase"
git log main-rebase --oneline -- file.py
  • Related