9

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!

fushar
  • 388
  • 2
  • 12
  • Do you mean you can't do a --force push to the upstream? – shengy Aug 19 '15 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. – Nayuki Aug 20 '15 at 00:35
  • @shengy No, I cannot – fushar Aug 20 '15 at 01:51

3 Answers3

8

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.)

Matthew
  • 2,593
  • 22
  • 25
  • 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. – bossi Mar 22 '17 at 02:56
  • 1
    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. – joel boonstra Dec 06 '22 at 23:57
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.

Ajedi32
  • 45,670
  • 22
  • 127
  • 172
  • 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 Aug 20 '15 at 01:48
  • @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. – Ajedi32 Aug 20 '15 at 02:04
  • 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 Aug 20 '15 at 02:12
  • @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. – Ajedi32 Aug 20 '15 at 02:24
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.
Assem
  • 11,574
  • 5
  • 59
  • 97
  • 1
    He already deleted the file and pushed to the upstream, `git reset` at this point is not working for him. – shengy Aug 19 '15 at 17:43