Search code examples
gitrebase

Git "rebase" but ignoring files in new .gitignore


Is there a way to replay commits from a source branch into a destination branch but taking into consideration changes made in .gitignore in the destination branch?

Here is the scenario:

Say I branch out of master and start committing files, including .bin files that should've been in .gitignore but weren't. Is there any way I can go to master, commit "*.bin" to .gitignore and then fix my topic branch when rebasing it (or some other automatic operation)? By fixing I mean remove the changesets of any .bin file, which are now ignored. This means 1) if a commit has a change on a.txt and foo.bin it should commit only a.txt and 2) if a commit only has a change on foo.bin it should be dropped altogether

The goal is to easily cleanup multiple mistakes only caught at Pull Request time.

Regular git rebase didn't work. the mistakenly committed file remains committed even if in the (new) linear history of the repo, the gitignore pattern was there before the bad commit


Solution

  • Is there a way to replay commits from a source branch into a destination branch but taking into consideration changes made in .gitignore in the destination branch?

    Yes, it's possible, however, it would take some scripting work. (My example script in the loop below might even work for you as is.) Basically you would simulate the rebase but with a few steps in-between each commit. The algorithm would look something like this:

    1. Obtain the list of commits to re-write and store them in an array or list.
    2. Reset your source branch to the target. (The .gitignore file should now be in place.)
    3. For each commit in the list: cherry-pick the commit with the --no-commit flag so you don't finish the commit, then reset to unstage the changes, then add them back which heeds the .gitignore instructions.

    Here's a working example bash script (save it as a file and run it in an empty directory to test):

    #!/bin/bash -v
    
    git init
    
    git branch -m main # rename to main in case that's not your default branch name
    
    echo asdf > asdf.txt && git add . && git commit -m "Add asdf.txt"
    
    git branch test-branch
    
    echo "*.log" > .gitignore && git add . && git commit -m "Add .gitignore"
    
    git switch test-branch
    
    echo abc > abc.txt
    echo def > def.log
    git add . && git commit -m "Add abc.txt and def.log"
    
    echo ghi > ghi.txt
    echo hij > hij.log
    git add . && git commit -m "Add ghi.txt and hij.log"
    
    git log --all --graph --name-only
    
    # Get all the commit IDs that would be rebased if we rebased test-branch onto main,
    # and store them into an array in reverse order
    declare -a commitIDs=(`git log main..test-branch --pretty=format:"%h" --reverse`)
    
    # reset test-branch to main
    git reset --hard main
    
    # loop through the commitIDs and cherry-pick each one without committing
    for i in "${commitIDs[@]}"
    do
       echo "About to cherry-pick commit $i"
       git cherry-pick $i --no-commit # this leaves the commit staged
       git reset # unstage so we can commit with the .gitignore
       git add . # this doesn't add ignored files
       git commit --reuse-message=$i
    done
    
    git log --all --graph --name-only
    

    When finished, look at the output of the two git log statements. The first one has 2 commits each with a .txt and .log file in it. The second one uses the .gitignore file to remove all the .log files from those commits.

    Note I used the previous commit messages when re-writing since that's usually what you would want. But I purposefully named my commits in such a way to highlight scenarios where you wouldn't want to do this, for example when the file name you are ignoring is specified in the commit message.