How do you fix a bad merge, and replay your good commits onto a fixed merge?

后端 未结 12 909
时光取名叫无心
时光取名叫无心 2020-11-21 10:25

I accidentally committed an unwanted file (filename.orig while resolving a merge) to my repository several commits ago, without me noticing it until now. I want

相关标签:
12条回答
  • 2020-11-21 10:36

    The simplest way I found was suggested by leontalbot (as a comment), which is a post published by Anoopjohn. I think its worth its own space as an answer:

    (I converted it to a bash script)

    #!/bin/bash
    if [[ $1 == "" ]]; then
        echo "Usage: $0 FILE_OR_DIR [remote]";
        echo "FILE_OR_DIR: the file or directory you want to remove from history"
        echo "if 'remote' argument is set, it will also push to remote repository."
        exit;
    fi
    FOLDERNAME_OR_FILENAME=$1;
    
    #The important part starts here: ------------------------
    
    git filter-branch -f --index-filter "git rm -rf --cached --ignore-unmatch $FOLDERNAME_OR_FILENAME" -- --all
    rm -rf .git/refs/original/
    git reflog expire --expire=now --all
    git gc --prune=now
    git gc --aggressive --prune=now
    
    if [[ $2 == "remote" ]]; then
        git push --all --force
    fi
    echo "Done."
    

    All credits goes to Annopjohn, and to leontalbot for pointing it out.

    NOTE

    Be aware that the script doesn't include validations, so be sure you don't make mistakes and that you have a backup in case something goes wrong. It worked for me, but it may not work in your situation. USE IT WITH CAUTION (follow the link if you want to know what is going on).

    0 讨论(0)
  • 2020-11-21 10:46

    Just to add that to Charles Bailey's solution, I just used a git rebase -i to remove unwanted files from an earlier commit and it worked like a charm. The steps:

    # Pick your commit with 'e'
    $ git rebase -i
    
    # Perform as many removes as necessary
    $ git rm project/code/file.txt
    
    # amend the commit
    $ git commit --amend
    
    # continue with rebase
    $ git rebase --continue
    
    0 讨论(0)
  • 2020-11-21 10:47

    This is what git filter-branch was designed for.

    0 讨论(0)
  • 2020-11-21 10:48

    This is the best way:
    http://github.com/guides/completely-remove-a-file-from-all-revisions

    Just be sure to backup the copies of the files first.

    EDIT

    The edit by Neon got unfortunately rejected during review.
    See Neons post below, it might contain useful information!


    E.g. to remove all *.gz files accidentally committed into git repository:

    $ du -sh .git ==> e.g. 100M
    $ git filter-branch --index-filter 'git rm --cached --ignore-unmatch *.gz' HEAD
    $ git push origin master --force
    $ rm -rf .git/refs/original/
    $ git reflog expire --expire=now --all
    $ git gc --prune=now
    $ git gc --aggressive --prune=now
    

    That still didn't work for me? (I am currently at git version 1.7.6.1)

    $ du -sh .git ==> e.g. 100M
    

    Not sure why, since I only had ONE master branch. Anyways, I finally got my git repo truely cleaned up by pushing into a new empty and bare git repository, e.g.

    $ git init --bare /path/to/newcleanrepo.git
    $ git push /path/to/newcleanrepo.git master
    $ du -sh /path/to/newcleanrepo.git ==> e.g. 5M 
    

    (yes!)

    Then I clone that to a new directory and moved over it's .git folder into this one. e.g.

    $ mv .git ../large_dot_git
    $ git clone /path/to/newcleanrepo.git ../tmpdir
    $ mv ../tmpdir/.git .
    $ du -sh .git ==> e.g. 5M 
    

    (yeah! finally cleaned up!)

    After verifying that all is well, then you can delete the ../large_dot_git and ../tmpdir directories (maybe in a couple weeks or month from now, just in case...)

    0 讨论(0)
  • 2020-11-21 10:50
    You should probably clone your repository first.
    
    Remove your file from all branches history:
    git filter-branch --tree-filter 'rm -f filename.orig' -- --all
    
    Remove your file just from the current branch:
    git filter-branch --tree-filter 'rm -f filename.orig' -- --HEAD    
    
    Lastly you should run to remove empty commits:
    git filter-branch -f --prune-empty -- --all
    
    0 讨论(0)
  • 2020-11-21 10:52

    Intro: You Have 5 Solutions Available

    The original poster states:

    I accidentally committed an unwanted file...to my repository several commits ago...I want to completely delete the file from the repository history.

    Is it possible to rewrite the change history such that filename.orig was never added to the repository in the first place?

    There are many different ways to remove the history of a file completely from git:

    1. Amending commits.
    2. Hard resets (possibly plus a rebase).
    3. Non-interactive rebase.
    4. Interactive rebases.
    5. Filtering branches.

    In the case of the original poster, amending the commit isn't really an option by itself, since he made several additional commits afterwards, but for the sake of completeness, I will also explain how to do it, for anyone else who justs wants to amend their previous commit.

    Note that all of these solutions involve altering/re-writing history/commits in one way another, so anyone with old copies of the commits will have to do extra work to re-sync their history with the new history.


    Solution 1: Amending Commits

    If you accidentally made a change (such as adding a file) in your previous commit, and you don't want the history of that change to exist anymore, then you can simply amend the previous commit to remove the file from it:

    git rm <file>
    git commit --amend --no-edit
    

    Solution 2: Hard Reset (Possibly Plus a Rebase)

    Like solution #1, if you just want to get rid of your previous commit, then you also have the option of simply doing a hard reset to its parent:

    git reset --hard HEAD^
    

    That command will hard-reset your branch to the previous 1st parent commit.

    However, if, like the original poster, you've made several commits after the commit you want to undo the change to, you can still use hard resets to modify it, but doing so also involves using a rebase. Here are the steps that you can use to amend a commit further back in history:

    # Create a new branch at the commit you want to amend
    git checkout -b temp <commit>
    
    # Amend the commit
    git rm <file>
    git commit --amend --no-edit
    
    # Rebase your previous branch onto this new commit, starting from the old-commit
    git rebase --preserve-merges --onto temp <old-commit> master
    
    # Verify your changes
    git diff master@{1}
    

    Solution 3: Non-interactive Rebase

    This will work if you just want to remove a commit from history entirely:

    # Create a new branch at the parent-commit of the commit that you want to remove
    git branch temp <parent-commit>
    
    # Rebase onto the parent-commit, starting from the commit-to-remove
    git rebase --preserve-merges --onto temp <commit-to-remove> master
    
    # Or use `-p` insteda of the longer `--preserve-merges`
    git rebase -p --onto temp <commit-to-remove> master
    
    # Verify your changes
    git diff master@{1}
    

    Solution 4: Interactive Rebases

    This solution will allow you to accomplish the same things as solutions #2 and #3, i.e. modify or remove commits further back in history than your immediately previous commit, so which solution you choose to use is sort of up to you. Interactive rebases are not well-suited to rebasing hundreds of commits, for performance reasons, so I would use non-interactive rebases or the filter branch solution (see below) in those sort of situations.

    To begin the interactive rebase, use the following:

    git rebase --interactive <commit-to-amend-or-remove>~
    
    # Or `-i` instead of the longer `--interactive`
    git rebase -i <commit-to-amend-or-remove>~
    

    This will cause git to rewind the commit history back to the parent of the commit that you want to modify or remove. It will then present you a list of the rewound commits in reverse order in whatever editor git is set to use (this is Vim by default):

    pick 00ddaac Add symlinks for executables
    pick 03fa071 Set `push.default` to `simple`
    pick 7668f34 Modify Bash config to use Homebrew recommended PATH
    pick 475593a Add global .gitignore file for OS X
    pick 1b7f496 Add alias for Dr Java to Bash config (OS X)
    

    The commit that you want to modify or remove will be at the top of this list. To remove it, simply delete its line in the list. Otherwise, replace "pick" with "edit" on the 1st line, like so:

    edit 00ddaac Add symlinks for executables
    pick 03fa071 Set `push.default` to `simple`
    

    Next, enter git rebase --continue. If you chose to remove the commit entirely, then that it all you need to do (other than verification, see final step for this solution). If, on the other hand, you wanted to modify the commit, then git will reapply the commit and then pause the rebase.

    Stopped at 00ddaacab0a85d9989217dd9fe9e1b317ed069ac... Add symlinks
    You can amend the commit now, with
    
            git commit --amend
    
    Once you are satisfied with your changes, run
    
            git rebase --continue
    

    At this point, you can remove the file and amend the commit, then continue the rebase:

    git rm <file>
    git commit --amend --no-edit
    git rebase --continue
    

    That's it. As a final step, whether you modified the commit or removed it completely, it's always a good idea to verify that no other unexpected changes were made to your branch by diffing it with its state before the rebase:

    git diff master@{1}
    

    Solution 5: Filtering Branches

    Finally, this solution is best if you want to completely wipe out all traces of a file's existence from history, and none of the other solutions are quite up to the task.

    git filter-branch --index-filter \
    'git rm --cached --ignore-unmatch <file>'
    

    That will remove <file> from all commits, starting from the root commit. If instead you just want to rewrite the commit range HEAD~5..HEAD, then you can pass that as an additional argument to filter-branch, as pointed out in this answer:

    git filter-branch --index-filter \
    'git rm --cached --ignore-unmatch <file>' HEAD~5..HEAD
    

    Again, after the filter-branch is complete, it's usually a good idea to verify that there are no other unexpected changes by diffing your branch with its previous state before the filtering operation:

    git diff master@{1}
    

    Filter-Branch Alternative: BFG Repo Cleaner

    I've heard that the BFG Repo Cleaner tool runs faster than git filter-branch, so you might want to check that out as an option too. It's even mentioned officially in the filter-branch documentation as a viable alternative:

    git-filter-branch allows you to make complex shell-scripted rewrites of your Git history, but you probably don’t need this flexibility if you’re simply removing unwanted data like large files or passwords. For those operations you may want to consider The BFG Repo-Cleaner, a JVM-based alternative to git-filter-branch, typically at least 10-50x faster for those use-cases, and with quite different characteristics:

    • Any particular version of a file is cleaned exactly once. The BFG, unlike git-filter-branch, does not give you the opportunity to handle a file differently based on where or when it was committed within your history. This constraint gives the core performance benefit of The BFG, and is well-suited to the task of cleansing bad data - you don’t care where the bad data is, you just want it gone.

    • By default The BFG takes full advantage of multi-core machines, cleansing commit file-trees in parallel. git-filter-branch cleans commits sequentially (ie in a single-threaded manner), though it is possible to write filters that include their own parallellism, in the scripts executed against each commit.

    • The command options are much more restrictive than git-filter branch, and dedicated just to the tasks of removing unwanted data- e.g: --strip-blobs-bigger-than 1M.

    Additional Resources

    1. Pro Git § 6.4 Git Tools - Rewriting History.
    2. git-filter-branch(1) Manual Page.
    3. git-commit(1) Manual Page.
    4. git-reset(1) Manual Page.
    5. git-rebase(1) Manual Page.
    6. The BFG Repo Cleaner (see also this answer from the creator himself).
    0 讨论(0)
提交回复
热议问题