Search code examples
gitgit-rebasegit-rewrite-history

rewrite history to undo all changes to a file on current branch in git


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.


Solution

  • 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 replaces the entire contents of file(s) with something unique (perhaps even nothing). The point here is to make sure all new changes you wish to ignore will definitely conflict with this change.
    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