Today I Learned

A Hashrocket project

113 posts about #git

Clean untracked files in Git

Given I am a developer
And I am working on a new branch in an existing project
And during one of my commits I introduced a few files/folders
And those files/folders are untracked (in .gitignore)
And those files/folders are automatically generated (e.g. node_modules/ webpack_bundle.js)
When I switch back to the main branch
Then I see those files
And I don’t want to…

If you find yourself in the above situation, you may want to clean your untracked files. Git provides a command for that: git clean.

This command comes with a way to see which files/folders are going to be deleted (DRY RUN):

git clean -n

You may notice that the above command does not show any untracked directories. To add directories to that list use the -d switch:

git clean -dn

Alternatively you may choose to only remove files/dirs that are in .gitignore with the -X option:

git clean -X -dn

If you are ready to take action use the -f switch and remove the -n switch:

git clean -fd

Filter Your Git Diffs

Sometimes reading a git diff can be a big task. Imagine working through a big file cleanup, removing and modifing hundreds of files, and one of those modifications had an undesireable side effect. How can we filter the noise to find the problem?

git diff has a --diff-filter flag for this purpose. The specific command I used today was:

$ git diff --diff-filter=M HEAD~5 > changes.txt

This showed only modified files over the previous five commits, excluding thousands of deleted references. By directing the output to a file and visually scanning, I quickly found the problem— a forced redirect to HTTPS on the development server.

See git diff --help for more info.

:Gbrowse!

One of my favorite developer-to-developer communication tools is :Gbrowse, provided by vim-fugitive. This command opens your current Vim buffer, or a line or range, in the hosting provider specified by your remote. It’s irreplaceable for quickly sharing code worthy of discussion.

Take this command to the next level with :Gbrowse! which puts the URL your browser would open into your paste buffer. From there, paste away into your project management tool or chat client, getting everybody on the same (web)page.

Commit Message Templates

We set something up on a recent project that I really liked.

In the midst of writing a bunch of tests, I realized I was starting every commit message with Test, followed by a description of the feature. Test login, Test that bots are redirected, etc. Typing the same word over and over is redundant, and it adds up quickly.

We paused and set up a commit template for the project.

Here’s the file we made (~/.git_commit_template.txt):

Test 

And the command in the project directory:

$ git config commit.template "~/.git_commit_template.txt"

Our next commit message started with Test, so we just used CTRL-A to jump to the end of the sentence and start filling it in.

This does more than just save keystrokes. It reminds us that we have a format we want to stick to, and helps keep our commit message more uniform.

This could also be set globally, if you have a format you prefer to use on every project.

git difftool

You’ve likely used git diff to see changes between your commits and your working tree, etc, but you can also use git difftool to open up the diff in your editor. In vim, this will open the original file in one pane, and the diff in another.

May require some config.

Checking Commit Ancestry

I have two commit shas and I want to know if the first is an ancestor of the second. Put another way, is this first commit somewhere in the history of this other commit.

Git’s merge-base command combined with the --is-ancestor flag makes answering this question easy. Furthermore, because it is a plumbing command, it can be used in a script or sequence of commands as a switch based on the answer.

Here is an example of this command in action:

$ git merge-base --is-ancestor head~ head && echo 'yes, it is'
yes, it is
$ git merge-base --is-ancestor head~ head~~ && echo 'yes, it is'

In the first command, head~ is clearly an ancestor of head, so the echo command is triggered. In the second, head~ is not an ancestor of head~~ so the return status of 1 short-circuits the rest of the command. Hence, no echo.

See man git-merge-base for more details.

source

List Different Commits Between Two Branches

There are times when I want to get a sense of the difference between two branches. I don’t want to look at the actual diff though, I just want to see what commits are on one and the other.

I can do just this by using the git-log command with a couple flags, most importantly the --cherry-pick flag.

To compare the feature branch against the master branch, I can run a command like the following:

$ git log --left-right --graph --cherry-pick --oneline feature...branch

This lists commits with the first line of their messages. It also includes either a < or > arrow at the front of each commit indicating whether the commit is on the left (feature) or right (master) branch, respectively.

Note: you can use more than branches in a command like this. Any two references will work. You can just use two SHAs for instance.

source

You Can Git Push to an SSH Alias

When we are using Git via ssh, we can take full advandage of our ssh conveniences.

So, lets say we have some ssh coniguration in our .ssh/config

Host til-production
  Hostname 11.22.333.44
  User ubuntu

Now we ssh into our server and create a bare repository.

ssh til-production
mkdir ~/til
cd ~/til
git init --bare

Then back on our local machine, instead of adding a remote with:

git remote add production ubuntu@11.22.333.44:til

we can simply:

git remote add production til-production:til

It’s a little bit less to think about.

h/t @vidalekechukwu

Clean your Working Tree, Interactively

git clean is a great command. It’s the best tool in the box for wiping out untracked junk files from your working tree.

This command has a noteworthy optional flag, -i, for interactive mode. This gives you the option to clean everything, select by number, ask each, and more. I see it as the reverse of git add -p— we get granular control over what stays and what goes.

More context with git diff

So if you want to see more (or less) context when running a git diff command you can use -U<number_of_lines>.

Here it is a regular git diff (3 lines as default):

vnegrisolo@home:~/my-app(master)% git diff
index modified: app/jobs/github/sync_organization_job.rb
@ sync_organization_job.rb:24 @ module Github
      organization.login       = data['login']
      organization.description = data['description']
      organization.avatar_url  = data['avatar_url']
-      organization.user        = @user
+      organization.user        = user

      organization.save
      recursive_sync(organization) if @recursive

Now just showing the changed lines:

vnegrisolo@home:~/my-app(master)% git diff -U0
index modified: app/jobs/github/sync_organization_job.rb
@ sync_organization_job.rb:27 @ module Github
-      organization.user        = @user
+      organization.user        = user

Finally with 5 lines of context instead of 3:

vnegrisolo@home:~/my-app(master)% git diff -U5
index modified: app/jobs/github/sync_organization_job.rb
@ sync_organization_job.rb:22 @ module Github
      organization = Organization.find_or_initialize_by(github_id: data['id'])

      organization.login       = data['login']
      organization.description = data['description']
      organization.avatar_url  = data['avatar_url']
-      organization.user        = @user
+      organization.user        = user

      organization.save
      recursive_sync(organization) if @recursive
      organization
    end

Diffing With Patience

The default diff algorithm used by Git is pretty good, but it can get mislead by larger, complex changesets. The result is a noisier, misaligned diff output.

If you’d like a diff that is generally a bit cleaner and can afford a little slow down (you probably can), you can instead use the patience algorithm which is described as such:

Patience Diff, instead, focuses its energy on the low-frequency high-content lines which serve as markers or signatures of important content in the text. It is still an LCS-based diff at its core, but with an important difference, as it only considers the longest common subsequence of the signature lines:

Find all lines which occur exactly once on both sides, then do longest common subsequence on those lines, matching them up.

You can set this as the default algorithm by adding the following lines to your ~/.gitconfig file:

[diff]
    algorithm = patience

or it can be set from the command line with:

$ git config --global diff.algorithm patience

source

h/t Josh Davey

Tag a Commit

I ❤️ Git tags. But tagging is a manual process, and sometimes I forget to do it.

My old solution was to find the commit I deployed, check it out, tag it, and push the tags to the internet. There is a better way!

You can tag any commit from anywhere in the tree:

$ git tag v1.0.0 a795622
$ git push --tags

This will tag commit a795622 and push it to Github. A sample workflow, if your deploy was one commit in the past, would be:

$ git tag v1.0.0 HEAD~1

Execute a shell command on every commit in rebase

Git rebase allows you to execute a command on any commit in a rebase:

git rebase -i --exec "git reset-authors"

The result will look something like this:

pick xxxyyy Do stuff to things
exec git reset-authors
pick yyyzzz Do things to stuff
exec git reset-authors

It is very useful for resetting authors on multiple commits in case you forgot to set the current pair.

P.S. if you are curious about what git reset-authors does check out our .gitconfig in Hashrocket’s dotfiles:

[alias]
reset-authors = commit --amend --reset-author -CHEAD

https://github.com/hashrocket/dotmatrix/blob/master/.gitconfig#L18

Open a Pull Request from the Command Line

Do you open source? If so, then check out hub. It enhances various Git commands, making OSS contribution and project management a lot easier.

Today I learned how to open a pull request from the command line. This is the base command:

$ git pull-request

This opens an editor where you can add more information, and see your branch and your target branch. Change the target branch with the -b flag.

Once installed, get more info here:

man hub

How long have I been working on this Project?

Today I was thinking back on my time on a project, and wanted to see how long I’d been assigned to it. We’ve been on this project for a while, so traversing the Git log would require precision.

My solution:

$ git log --author="\(jwworth\)" --reverse

We use go-pear to set Git authorship for pairs; as a result my username ‘jwworth’ could be in several locations in the text of the author email. Luckily, the --author flag matches regular expressions.

Global git hooks

There are no global git hooks.

But you can create global hook templates and for every project you can use them.

Create a folder for your global templates:

mkdir -p ~/.git-templates/hooks

Configure git to use this templates folder:

git config --global init.templatedir '~/.git-templates'

Create an executable hook, for example `~/.git-templates/hooks/pre-commit bash #!/bin/sh echo "This happens before the commit" For every project you want to use these templates restart git project running: bash git init These will create symbol links like: bash lrwxr-xr-x pre-commit -> /Users/vnegrisolo/.git-templates/hooks/pre-commit

Save harddrive space by using git shallow clones

Sometime you clone starter projects, libraries, or plugins to help you get started on a new project or POC.

Perhaps you are using git clone to get your project on a remote server.

When you clone a project what you also get is all the previous states of that repository at each commit. This can add up fast and take a good chunk of your hard drive space as well as take long to download and decompress.

To solve this you can clone a shallow copy of the repository by using the depth switch on the git clone command, and providing it with the value of 1.

git clone --depth 1 https://github.com/dkarter/bullets.vim

Fixing a past commit

To add some code changes to a past commit you can commit with the fixup option and then rebase it.

# add the code changes you want to add to the past commit
git add .

# get the git commit you want to add the changes and copy its reference
git log

# commit the changes with the options `fixup`
git commit --fixup <COMMIT_REF>

# rebase the commits to apply the fixup from one commit before the starter commit
git rebase -i --autosquash <COMMIT_REF>~1

This will open your editor (vim) with the rebase plan, so you can apply it.

Show All Commits For A File Beyond Renaming

By including -- <filename> with a git log command, we can list all the commits for a file. The following is an example of such a command with some formatting and file names.

> git log --name-only --pretty=format:%H -- README.md
4e57c5d46637286731dc7fbb1e16330f1f3b2b7c
README.md

56955ff027f02b57212476e142a97ce2b7e60efe
README.md

5abdc5106529dd246450b381f621fa1b05808830
README.md

What we may not realize, though, is that we are missing out on a commit in this file’s history. At one point, this file was renamed. The command above wasn’t able to capture that.

Using the --follow flag with a file name, we can list all commits for a file beyond renaming.

> git log --name-only --pretty=format:%H --follow README.md
4e57c5d46637286731dc7fbb1e16330f1f3b2b7c
README.md

56955ff027f02b57212476e142a97ce2b7e60efe
README.md

5abdc5106529dd246450b381f621fa1b05808830
README.md

ea885f458b0d525f673623f2440de9556954c0c9
README.rdoc

This command roped in a commit from when README.md used to be called README.rdoc. If you want to know about the full history of a file, this is the way to go.

source

Delete Remote Git Tags

Tagging releases with Git is a good idea. In case your tags get off track, here is how you delete a Git tag locally and on a remote:

$ git tag -d abc
$ git push origin :refs/tags/abc
To git@github.com:hashrocket/hr-til
 - [deleted]         abc

It gets trickier if you’re using Semantic Versioning, which includes dots in the tag name. The above won’t work for v16.0.0. This will:

$ git tag -d v16.0.0
$ git push origin :v16.0.0
To git@github.com:hashrocket/hr-til
 - [deleted]         v16.0.0

Git Log With Authors

In my never-ending quest to better summarize my work at the end of the day using computers, I discovered today the Git --author flag. It works like this:

$ glg --since=midnight --author=dev+jwworth+mikechau@hashrocket.com
* 4ba91a8 (HEAD, origin/checkout, checkout) Add guard for manual entry of employee discount
* 3a4e4c9 Seed a coupon and code and auto-apply in preview
* cb1adee Add discount
...

The alias glg is discussed here.

I use this when multiple developers or teams are committing throughout the day to the same repository, to disambiguate our work from others. Ready to paste into your billing software of choice.

Show The diffstat Summary Of A Commit

Use the --stat flag when running git show on a commit to see the diffstat summary of that commit. For instance, this is what I get for a recent commit to delve:

$ git show 8a1f36a1ce --stat
commit 8a1f36a1ce71d708d1d82afbc2191de9aefba021
Author: Derek Parker <derek.parker@coreos.com>
Date:   Wed Jan 27 23:47:04 2016 -0800

    dlv: Flag to print stacktrace on trace subcommand

 cmd/dlv/main.go     | 45 ++++++++++-----------------------------------
 terminal/command.go |  7 +++++--
 2 files changed, 15 insertions(+), 37 deletions(-)

The following is a description of the diffstat program

This program reads the output of diff and displays a histogram of the insertions, deletions, and modifications per-file.

See man git-show and man diffstat for more details.

Checkout a Pull Request

Today I learned how to checkout and inspect a Git pull request locally.

Open the .git/config file in the root of the project directory. Locate the [remote "origin"] section, and add this line:

fetch = +refs/pull/*/head:refs/remotes/origin/pr/*

Fetch the changes, and you should receive the pull request references:

From github.com:hashrocket/hr-til
 * [new ref]         refs/pull/1/head -> origin/pr/1
 * [new ref]         refs/pull/10/head -> origin/pr/10
 * [new ref]         refs/pull/11/head -> origin/pr/11
...

From here, you can checkout any pull request with git checkout pr/1, git checkout pr/10, etc.

Source: https://gist.github.com/piscisaureus/3342247

Reference A Commit Via Commit Msg Pattern Matching

Generally when referencing a commit, you’ll use the SHA or a portion of the SHA. For example with git-show:

$ git show cd6a63d014
...

There are many ways to reference commits though. One way is via regex pattern matching on the commit message. For instance, if you recently had a commit with a typo and you had included typo in the commit message, then you could reference that commit like so:

$ git show :/typo
Author: Josh Branchaud
Date: Mon Dec 21 15:50:20 2015 -0600

    Fix a typo in the documentation
...

By using :/ followed by some text, git will attempt to find the most recent commit whose commit message matches the text. As I alluded to, regex can be used in the text.

See $ man gitrevisions for more details and other ways to reference commits.

Source

Resetting A Reset

Sometimes we run commands like git reset --hard HEAD~ when we shouldn’t have. We wish we could undo what we’ve done, but the commit we’ve reset is gone forever. Or is it?

When bad things happen, git-reflog can often lend a hand. Using git-reflog, we can find our way back to were we’ve been; to better times.

$ git reflog
00f77eb HEAD@{0}: reset: moving to HEAD~
9b2fb39 HEAD@{1}: commit: Add this set of important changes
...

We can see that HEAD@{1} references a time and place before we destroyed our last commit. Let’s fix things by resetting to that.

$ git reset HEAD@{1}

Our lost commit is found.

Unfortunately, we cannot undo all the bad in the world. Any changes to tracked files will be irreparably lost.

source

Move The Latest Commit To A New Branch

I sometimes find myself making a commit against the master branch that I intended to make on a new branch. To get this commit on a new branch, one possible approach is to do a reset, checkout a new branch, and then re-commit it. There is a better way.

$ git checkout -b my-new-branch
$ git checkout - 
$ git reset --hard HEAD~

This makes better use of branches and avoids the need to redo a commit that has already been made.

Note: The example was against the master branch, but can work for any branch.

Grep Over Commit Messages

The git log command supports a --grep flag that allows you to do a text search (using grep, obviously) over the commit messages for that repository. For the git user that writes descriptive commit messages, this can come in quite handy. In particular, this can be put to use in an environment where the standard process involves including ticket and bug numbers in the commit message. For example, finding bug #123 can be accomplished with:

$ git log --grep="#123"

See man git-log for more details.

How to remove newly git ignored files

While developing a Cordova app I realized I was checking in files that are generated at build time. For example, the www folder is copied into each platform (ios, android). I was not sure what I could safely ignore until I looked at ember-cli-cordova’s gitignore.

So I changed my .gitignore to look something like:

...
# android
platforms/android/assets/www
platforms/android/bin
platforms/android/gen
platforms/android/local.properties
platforms/android/ant-build
platforms/android/ant-gen
platforms/android/CordovaLib/ant-build
platforms/android/CordovaLib/ant-gen
platforms/android/CordovaLib/bin
platforms/android/CordovaLib/gen
platforms/android/CordovaLib/local.properties
platforms/android/.gradle
platforms/android/res/xml/config.xml
...

But these folders are already checked in so now I have to git rm each of them.

Note: Don’t forget to use the --cached or you will actually delete the files

$ git rm -r --cached platforms/android/assets/www
$ git rm -r --cached platforms/android/bin
... and so on

This is super tedious and I could easily miss a file or directory. There has to be a better way!

$ git rm -r --cached . && git add .

Git already knows how to ignore the files in .gitignore so just untrack all files in the project and add them back.

The staged changes should now be the new lines in .gitignore and the deleted files that accompany the changes.

The Alpha Commit

I like to read commit logs.

Today I wanted to see the first commit on a project. Here’s what I used:

$ git rev-list --max-parents=0 HEAD

Translation:

Show me the commits that led to HEAD in reverse chronological order; then limit those results to the commits with no parent.

Here’s a small modification, to show the entire commit rather than the SHA alone:

$ git show $(git rev-list --max-parents=0 HEAD)

Untrack A File Without Deleting It

Generally when I invoke git rm <filename>, I do so with the intention of removing a file from the project entirely. git-rm does exactly that, removing the file both from the index and from the working tree.

If you want to untrack a file (remove it from the index), but still have it available locally (in the working tree), then you are going to want to use the --cached flag.

$ git rm --cached <filename>

If you do this, you may also consider adding that file to the .gitignore file.

source

Clean Up Old Remote Tracking References

After working on a Git-versioned project for a while, you may find that there are a bunch of references to remote branches in your local repository. You know those branches definitely don’t exist on the remote server and you’ve removed the local branches, but you still have references to them lying around. You can reconcile this discrepancy with one command:

$ git fetch origin --prune

This will prune all those non-existent remote tracking references which is sure to clean up your git log (git log --graph).

source

Git Log Since

At the end of each day, I try to record what I did, to jog my memory during the next morning’s standup. This helps:

git log --since="24 hours ago"

I SSH into my work machine and run this in my project’s root directory. Combined with an alias from the Hashrocket Dotmatrix, glg (git log --graph --oneline --decorate --color --all), I get a terse summary of the day’s work, ready to be pasted into your note-taking or project management tool of choice:

$ glg --since="24 hours ago"
* 7191b92 (HEAD, origin/master, origin/HEAD, master) Good changes
* 3f4d61e More good changes
* ecd9dcd Even more
...

Drop the --all if you only care about your current branch (and --decorate --color, they don’t add value when only one branch is being examined).

glg --since=midnight is also useful.

Caching Credentials

When public key authentication isn’t an option, you may find yourself typing your password over and over when pushing to and pulling from a remote git repository. This can get tedious. You can get around it by configuring git to cache your credentials. Add the following lines to the .git/config file of the particular project.

[credential]
    helper = cache --timeout=300

This will tell git to cache your credentials for 5 minutes. Use a much larger number of seconds (e.g. 604800) to cache for longer.

Alternatively, you can execute the command from the command line like so:

$ git config credential.helper 'cache --timeout=300'

source

Remove whitespace changes, then `git add -p`

git diff -w --no-color | git apply --cached --ignore-whitespace && git checkout -- . && git reset && git add -p

git diff -w --no-color creates a diff

git apply --cached --ignore-whitespace applies the diff ignoring whitepace, and indexes it

git checkout — . removes the unindexed “whitespace” changes

git reset resets the index to just he non-whitespace cahnges

git add -p adds the non-whitespace changes in patch mode

We did this with it alias gwap=“git diff -w --no-color | git apply --cached --ignore-whitespace && git checkout -- . && git reset && git add -p”

Poke around in your `git stash`

For some reason it’s never occurred to me to see if there’s more to git stash than just pop until now. Turns out you can git stash list to see a big list of all your stashes across all branches:

stash@{0}: WIP on email-unsubscribe: ce66d0f Add missing "EXISTS" to data view
stash@{1}: WIP on master: 484855f Add name to email 'from' address
stash@{2}: WIP on nav_settings: cd3cc88 User revokes pending invitation
stash@{3}: WIP on new_home_integration: d6345a3 Style payment form Stripe errors

You can then git stash apply some stash other than the top one, if you’re into that, no matter what branch you’re on. Just use the name, e.g. git stash apply stash@{2}.

Same goes for git stash show stash@{2}, plus you can add -v if you’d like the line-by-line view.

[Addendum: you can also use git show stash@{2}, which is equivalent to git stash show stash@{2} -v. h/t @joshbranchaud]

Determine The Hash Id For A Blob

Git’s hash-object command can be used to determine what hash id will be used by git when creating a blob in its internal file system.

$ echo 'Hello, world!' > hola
$ git hash-object hola
af5626b4a114abcb82d63db7c8082c3c4756e51b

When a commit happens, git will generate this digest (hash id) based on the contents of the file. The name and location of the file don’t matter, just the contents. This is the magic of git. Anytime git needs to store a blob, it can quickly match against the hash id in order to avoid storing duplicate blobs.

Try it on your own machine, you don’t even need to initialize a git repository to use git hash-object.

source

Rebase all commits to whats been pushed

When git rebasing interatively, git rebase -i you need to specify a commit to rebase interactively back to, like:

git rebase -i HEAD^^

And generally I specify the commit to rebase to with as many carrots after HEAD as commits I needed. But generally you are rebasing commits that aren’t yet pushed to master, so in that case just rebase back to ‘origin/master’.

git rebase -i origin/master

Which Branches Have This Commit?

Imagine you have found an important commit in a complex git tree. Now you’d like to know which branches have this commit. Are the commit’s changes on the branch we just deployed to staging?

--contains to the rescue:

$ git branch --contains <SHA>
* master
* staging
* feature/killer-feature

These three branches contain commit <SHA>.