In the course of development, there are times you want to look at or retrieve a file deleted sometime in the past – and you probably don’t know when it was deleted. I’ve written a simple git alias that makes it super easy.
|### Git Unremove ##|
|# Adds an alias called "unrm" to retrieve a pile or path deleted at any point in the repositories history.|
|# git unrm [path]|
|# git unrm path/to/file|
|git config –global alias.unrm '!COMMIT=$(git log -1 –pretty=%h — "$1"); git checkout $COMMIT^ — "$1"; echo "File: $1, was restored from commit $COMMIT"; git show -s $COMMIT'|
So how does this work? If you don’t care, then just copy and paste the config line above – if you want to know what’s going on here, read on.
It uses some simple features of the “git log” and “git checkout” commands that everyone is already familiar with. Each can have a file path passed to them after a couple of dashes “–“. First, you need to find out when in the git history the file was removed, for that you can just use “git log” with a “–” and a path.
|# Show specifically only the last commit to affect the path/to/file –|
|# this will be the commit where the file as deleted.|
|git log -1 — path/to/file|
This works just like normal git-log, but narrows the log to show only commits that affect the given path – keep in mind that the path can be a specific file or an entire directory.
What if you want to actually retrieve that file? Well, you can do that with the “git checkout” command by doing basically the same thing. First copy the SHA1 hash of the last commit affecting the file you want to retrieve – this is important because you it will tell git-checkout where to look for the file.
|# Recreate the file at path/to/file in the state in the state it was at|
|# just prior to having been deleted (made up commit hash for example)|
|git checkout 4efa6b0^ — path/to/file|
The commit hash you copied from the “git log” output doesn’t actually contain the file, its just when the file was deleted because that commit is by definition the last commit to affect the file. To get the last state of the file before its removal you need to look to the commit immediately before it, thats what the “^” does, it looks one commit back.
So that a bunch of stuff to remember – although its fairly simple compared to some other more complex tasks in git – but I made the git alias to make it even easier. I think I originally came up with this alias an an answer on StackOverflow, but its been sitting around as a gist on my github account for a while. I’ll probably be sharing more stuff from gist in the future.
Tricks for interacting with a branch, without checking it out.
Browse a directory (like ls):
git show [ref]:[path]
> git show master:your/path/
See contents of a file (command as above):
git show [ref]:[filepath]
> git show master:your/path/file.php
Checkout a specific file or directory from a different branch:
git checkout [ref] -- [path]
> git checkout master -- your/path/file.php
Note: There are other ways to do similar tasks such as `git ls-tree` and they may have more options. However I find these to be more accessible and easy to remember.
I just ran into a little gotcha with regard how fetch handles tags. When it pulls down commits, you will usually see that it also pulls down tags. I was a little confused today when fetch was refusing to pull a tag that was clearly in the repository (“
git ls-remote --tags“) lets you see tags available on the remote).
I kept running the fetch command but the tag wouldn’t get pulled down. The reason has to do with the way fetch works, it only fetches tags that are direct references to a commit that is in a branch being fetched. To get all tags regardless of what commits they reference use the fetch in the following way.
From the git manual (“git help fetch”):
-t, –tags Most of the tags are fetched automatically as branch heads are downloaded, but tags that do not point at objects reachable from the branch heads that are being tracked will not be fetched by this mechanism. This flag lets all tags and their associated objects be downloaded. The default behavior for a remote may be specified with the remote.<name>.tagopt setting. See git-config(1).
This will effectively do the inverse of normal fetch. It will fetch all tags, and bring with is any necessary commits.
Hope this helps some poor confused folks out there a little bit of greif.
Generally speaking Git is fantastic and easy to use – one of the few pain points is where submodules butt heads with branches.
Far from solving all the problems in related to this – I’ve found an
easy way to find out what submodules are in a branch, commit, or tag without having to check that reference out. This method also shows you the commits that each submodule is currently pointing to.
|#Where <ref> should be replaced with a branch name, tag name, or commit hash.|
|git ls-tree -rt <reference> | grep 'commit [0-9A-z]'|
The ls-tree command is fairly simple, its like running the ls unix command but includes git information – like the type of object each item is and the hash reference for that object. That command can also be used on a specific directory also by using <reference>:<path>. The -r flag makes it recursive so that is searches through all the subdirectories in the branch.
The second part of this is a simple grep command, that filters the results by those of type “commit”, which is how ls-tree represents submodules. I’ve included a pattern to match the word commit followed by the SHA1 hash – to avoid getting items that have the word commit as part of a folder or filename.
Hope this helps someone out there – it took me a while to figure out how to do this.