10

Suppose I have a file a.txt. One day, I deleted it, committed, and pushed.

The next day, I wanted like to revert the last commit, bringing back a.txt. I tried using git revert, but when I did git blame, all lines are showing the revert commit hash. The original blame history is lost.

Can I recover the file and preserve the file history, i.e., as if the file has not been deleted before? Note that I must not change the history as the commit has been pushed.

Thanks!

3
  • Do you mean you can't do a --force push to the upstream? Commented Aug 19, 2015 at 17:09
  • 2
    Git doesn't track file history; it only tracks the history of the entire root directory. So reconstructing file history is a problem when requesting to view the history, not when reverting the file. Commented Aug 20, 2015 at 0:35
  • @shengy No, I cannot Commented Aug 20, 2015 at 1:51

3 Answers 3

11

You CAN do this! Here's how:

  1. Start a new branch from the commit preceding the delete that you want to undo.
  2. Merge the offending change with git merge <sha> -s ours.
  3. If the commit had changes besides the deletion that you want to keep:
    1. Reapply the changes to your working copy with git diff <sha>^..<sha> | git apply.
    2. Discard the deletions (many techniques are available; git checkout -p may work well for you).
  4. Merge this branch back into the main branch (e.g. master).

This produces a history with two branches; one in which the file was deleted, and one in which it was never deleted. As a result, git is able to track the file history without resorting to heroics such as -C -C -C. (In fact, even with -C -C -C, the file isn't "restored", because what git sees is that a new file was created as a copy of a previously existing file. With this technique, you are reintroducing the same file to the repository.)

Sign up to request clarification or add additional context in comments.

2 Comments

Works like a treat and I learned something now, thanks @Matthew! My case was fairly complex and I needed to read a bit into git checkout -p but even on a partially offending commit with mixed changes this approach ended up working exactly as I needed it to.
This is an excellent answer, and did exactly what I needed. On the off chance it helps someone in the future, if you happen to have diff.noprefix set to true in your git config, you will need to temporarily unset that for the git diff [...] | git apply command to work. This will ensure that git diff outputs src and dest prefixes so that the patch can find the correct files when applying.
2

Run git blame with the -C option specified three times:

git blame -C -C -C

This causes git blame to look for content copied from files in previous commits.

From the documentation for git blame:

-C|<num>|

In addition to -M, detect lines moved or copied from other files that were modified in the same commit. This is useful when you reorganize your program and move code around across files. When this option is given twice, the command additionally looks for copies from other files in the commit that creates the file. When this option is given three times, the command additionally looks for copies from other files in any commit.

<num> is optional but it is the lower bound on the number of alphanumeric characters that Git must detect as moving/copying between files for it to associate those lines with the parent commit. And the default value is 40. If there are more than one -C options given, the <num> argument of the last -C will take effect.

4 Comments

Are you sure this is working? I tried something like git init echo "test" > a.txt" git add a.txt git commit -m "Commit 1" echo "foobar" >> a.txt git add a.txt git commit -m "Commit 2" git rm a.txt git commit -m "Commit 3" git revert HEAD git blame -C -C -C a.txt and both lines show the revert commit...
@fushar I'm pretty sure you need more than one word for git to register that you moved something. The docs say 40 characters is the minimum. I've edited the quote in my answer to be more complete.
I just wanted to show that your solution does not work, in the simplest example. Actually it does not work on my real project either (my deleted file content is much larger than 40 of course). For your edit -- git blame -C1 -C1 -C1 a.txt unfortunately also does not work for the a.txt example.
@fushar Huh, you're right. I can't seem to get it to work. The documentation seems to be saying that it should work though, so maybe it's a bug. Either that or I'm completely misunderstanding how it's supposed to work.
0

You can do it by using git reset instead of git revert. git reset drops the new commit and checkout a previous commit. This is not recommended if you pushed already to upstream.

NAME
       git-reset - Reset current HEAD to the specified state

SYNOPSIS
       git reset [-q] [<tree-ish>] [--] <paths>...
       git reset (--patch | -p) [<tree-ish>] [--] [<paths>...]
       git reset [--soft | --mixed | --hard | --merge | --keep] [-q] [<commit>]

DESCRIPTION
       In the first and second form, copy entries from <tree-ish> to the index. In the third form, set the
       current branch head (HEAD) to <commit>, optionally modifying index and working tree to match. The
       <tree-ish>/<commit> defaults to HEAD in all forms.

Since you did already push:

  • If you have no active collaborators that pulled that day , use git reset and force the push git push -f.

1 Comment

He already deleted the file and pushed to the upstream, git reset at this point is not working for him.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.