Today I Learned

ins8s

55 posts about #git

Configuring The Pager

When you run Git commands that produce a bunch of output, Git will use a pager to present the content starting at the beginning, rather than spitting it all out to the screen at once. This will almost always come in handy for commands like git-diff and git-log.

By default, Git uses less as its pager.

You can also choose to configure it to something else by updating your git config file or by running the following command:

$ git config --global core.pager 'more'

If you’d like to turn the pager off altogether, set it to a blank string.

$ git config --global core.pager ''

source

Show The Good And The Bad With Git Bisect

The git bisect command is a powerful tool for tracking down a past commit where something verifiable about the code changed — whether it be visual or functional. After using git bisect to traverse back and forth through your commit history, you may be wondering where things stand.

The git bisect log command will show you each commit that has been inspected and whether you’ve marked it as good or bad.

These records can be handy for double checking your work if you’re worried that you made a mistake along the way.

Show List Of Most Recently Committed Branches

The standard way to list your branches is with the git branch command. If you use branches extensively for feature work and bug fixes, you may find yourself overwhelmed by the list of branches trying to visually parse through them for the one that you had worked on recently.

With the git for-each-ref command, we can produce a better list of branches.

$ git for-each-ref --sort=-committerdate --count=10 --format='%(refname:short)' refs/heads/

The command itself will iterate over all of the repository’s refs and print them out as a list. The --sort=-committerdate option will ensure that list is sorted by refs mostly recently committed to. The --count=10 option limits the list output to 10 refs. The format flag cleans up the output a bit, only showing the shortname of the ref. Lastly, the refs/heads/ argument ensures that only local refs are included in the output, thus ignoring remote refs.

The result is a list of local branches ordered by recency which generally corresponds to relevance.

See man git-for-each-ref for more details.

source

Include Some Stats In Your Git Log

A simple git log command is going to give you a concise set of information for each commit. Usually it is enough info. When it’s not, git log can provide additional information with the right flags. To include overall and per-file stats on the number of insertions and deletions, use the --stat flag.

$ git log --stat
commit 66e67741a1cd6857a4467d1453c9f17ef5849f20
Author: c0mpiler <c0mpiler@ins8s.com>
Date:   Mon Nov 13 21:24:41 2017 -0600

    Add Focus The URL Bar as an internet til

 README.md                     |  3 ++-
 internet/focus-the-url-bar.md | 10 ++++++++++
 2 files changed, 12 insertions(+), 1 deletion(-)

commit 9241e3919ef1e4f68b71a1491d368ae6361084aa
Author: c0mpiler <c0mpiler@ins8s.com>
Date:   Sat Nov 11 11:41:40 2017 -0600

    Add Freeze An Object, Sorta as a javascript til

 README.md                            |  3 ++-
 javascript/freeze-an-object-sorta.md | 44 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 46 insertions(+), 1 deletion(-)

...

See man git-log for more details.

Show File Diffs When Viewing Git Log

Include the -p flag with the git log command to include the diffs along with the rest of the information for each commit. Here is an example of running this against this repository.

$ git log -p
commit b9809a329acd8150b2474168f8faaf008f376e35
Author: c0mpiler <c0mpiler@ins8s.com>
Date:   Wed Oct 11 07:27:53 2017 -0500

    Add Inline Style Attributes Should Be Camel Cased as a react til

diff --git a/README.md b/README.md
index c982f8e..6ee7d32 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ smart people at [Hashrocket](http://hashrocket.com/).
 For a steady stream of TILs from a variety of rocketeers, checkout
 [til.hashrocket.com](https://til.hashrocket.com/).

-_574 TILs and counting..._
+_575 TILs and counting..._

 ---

See man git-log for more details.

Rebase Commits With An Arbitrary Command

Interactive rebasing is a powerful way to manage and tend to the history of a git repository. Rewording and squashing commits are fairly common actions. But what if you need to run some arbitrary command against a series of recent commits?

This is where the --exec flag comes in to play.

$ git rebase -i HEAD~3 --exec "git commit --amend --reset-authors -CHEAD"

This generates an interactive rebase file that you can review and save when ready.

pick ea4a215 Add Globally Install A Package With Yarn as a javascript til
exec git commit --amend --reset-author -CHEAD
pick a4f4143 Add Initialize A New JavaScript Project With Yarn as a javascript til
exec git commit --amend --reset-author -CHEAD
pick 2f00aeb Add Default And Named Exports From The Same Module as a javascript til
exec git commit --amend --reset-author -CHEAD

As you can see, the specified command is prepared for execution for each commit involved in the rebase.

h/t Patricia Arbona

Rename A Remote

If you just added a remote (git remote add ...) and messed up the name or just need to rename some existing remote, you can do so with the rename command.

First, let’s see the remotes we have:

$ git remote -v
origin  https://github.com/c0mpiler/til.git (fetch)
origin  https://github.com/c0mpiler/til.git (push)

To then rename origin to destination, for example, we can issue the following command:

$ git remote rename origin destination

See man git-remote for more details.

Cherry Pick A Range Of Commits

Git’s cherry-pick command allows you to specify a range of commits to be cherry picked onto the current branch. This can be done with the A..B style syntax — where A is the older end of the range.

Consider a scenario with the following chain of commits: A - B - C - D.

$ git cherry-pick B..D

This will cherry pick commits C and D onto HEAD. This is because the lower-bound is exclusive. If you’d like to include B as well. Try the following:

$ git cherry-pick B^..D

See man git-cherry-pick for more details.

Update The URL Of A Remote

I just changed the name of a Github repository. One of the implications of this is that the remote URL that my local git repository has on record is now out of date. I need to update it.

If I use git-remote with the -v flag. I can see what remotes I currently have.

$ git remote -v
origin  git@github.com:c0mpiler/pokemon.git (fetch)
origin  git@github.com:c0mpiler/pokemon.git (push)

Now, to update the URL for that remote, I can use git remote set-url specifying the name of the remote and the updated URL.

$ git remote set-url origin git@github.com:c0mpiler/pokemon_deluxe.git

If I check again, I can see it has been updated accordingly.

$ git remote -v
origin  git@github.com:c0mpiler/pokemon_deluxe.git (fetch)
origin  git@github.com:c0mpiler/pokemon_deluxe.git (push)

Grep For A Pattern On Another Branch

Git has a built-in grep command that works essentially the same as the standard grep command that unix users are used to. The benefit of git-grep is that it is tightly integrated with Git. You can search for occurrences of a pattern on another branch. For example, if you have a feature branch, my-feature, on which you’d like to search for occurrences of user.last_name, then your command would look like this:

$ git grep 'user\.last_name' my-feature

If there are matching results, they follow this format:

my-feature:app/views/users/show.html.erb:  <%= user.last_name %>
...

This formatting is handy because you can easily copy the branch and file directive for use with git-show.

See man git-grep for more details.

Viewing A File On Another Branch

Sometimes you want to view a file on another branch (without switching branches). That is, you want to view the version of that file as it exists on that branch. git show can help. If your branch is named my_feature and the file you want to see is app/models/users.rb, then your command should look like this:

$ git show my_feature:app/models/users.rb

You can even tab-complete the filename as you type it out.

See man git-show for more details.

source

What Changed?

If you want to know what has changed at each commit in your Git history, then just ask git whatchanged.

$ git whatchanged

commit ddc929c03f5d629af6e725b690f1a4d2804bc2e5
Author: c0mpiler <c0mpiler@ins8s.com>
Date:   Sun Feb 12 14:04:12 2017 -0600

    Add the source to the latest til

:100644 100644 f6e7638... 2b192e1... M  elixir/compute-md5-digest-of-a-string.md

commit 65ecb9f01876bb1a7c2530c0df888f45f5a11cbb
Author: c0mpiler <c0mpiler@ins8s.com>
Date:   Sat Feb 11 18:34:25 2017 -0600

    Add Compute md5 Digest Of A String as an Elixir til

:100644 100644 5af3ca2... 7e4794f... M  README.md
:000000 100644 0000000... f6e7638... A  elixir/compute-md5-digest-of-a-string.md

...

This is an old command that is mostly equivalent to git-log. In fact, the man page for git-whatchanged says:

New users are encouraged to use git-log(1) instead.

The difference is that git-whatchanged shows you the changed files in their raw format which can be useful if you know what you are looking for.

See man git-whatchanged for more details.

List All Files Changed Between Two Branches

The git-diff command can help with finding all files that have changed between two branches. For instance, if you are at the HEAD of your current feature branch and you’d like to see the list of files that have changed since being in sync with the master branch, you’d formulate a command like the following:

$ git diff --name-only master

The --name-only flag is important because it cuts out the rest of the non-essential output.

You’ll want to list the older branch first and the current branch second as a way of showing what has changed from the original branch to get to the current branch.

Though the example shows the use of branches any git reference can be used to see what has changed between two commits.

See man git-diff for more details.

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

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

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

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.

Dry Runs in Git

There are a few commands in git that allow you to do a dry run. That is, git will tell you the effects of executing a command without actually executing that command.

For instance, if you are clearing out untracked files, you can double check what files are going to be deleted with the dry run flag, like so:

$ git clean -fd --dry-run
Would remove tmp.txt
Would remove stuff/

Similarly, if you want to check in which files a commit is going to be incorporated, you can:

$ git commit --dry-run --short
M  README.md
A  new_file.rb

Try running git commit --dry-run (that is, without the --short flag). Look familiar? That is the same output you are getting from git status.

Interactively Unstage Changes

I often use git add --patch to interactively stage changes for a commit. Git takes me through changes to tracked files piece by piece to check if I want to stage them. This same interactive staging of files can be used in reverse when removing changes from the index. Just add the --patch flag.

You can use it for a single file

$ git reset --patch README.md

or you can let it operate on the entire repository

$ git reset --patch

This is useful when you’ve staged part of a file for a commit and then realize that some of those changes shouldn’t be committed.

Find The Initial Commit

By definition, the initial commit in a repository has no parents. You can exploit that fact and use rev-list to find the initial commit; a commit with no parents.

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

The rev-list command lists all commits in reverse chronological order. By restricting them to those with at most 0 parents, you are only going to get root commits. Generally, a repository will only have a single root commit, but it is possible for there to be more than one.

See man git-rev-list for more details.

source

Reference A Commit Via Commit Message 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: c0mpiler Dev
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.

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

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

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

Whitespace Warnings

You can configure git to warn you about whitespace issues in a file when you diff it. This is handled by the core.whitespace configuration. Add the following to your .gitconfig file:

[core]
      whitespace = warn

By default, git will warn you of trailing whitespace at the end of a line as well as blank lines at the end of a file.

Renaming A Branch

The -m flag can be used with git branch to move/rename an existing branch. If you are already on the branch that you want to rename, all you need to do is provide the new branch name.

$ git branch -m <new-branch-name>

If you want to rename a branch other than the one you are currently on, you must specify both the existing (old) branch name and the new branch name.

$ git branch -m <old-branch-name> <new-branch-name>

h/t Dillon Hafer

Excluding Files Locally

Excluding (read: ignoring) files that should not be tracked is generally done by listing such files in a tracked .gitignore file. Though it doesn’t make sense to list all kinds of excluded files here. For files that you’d like to exclude that are temporary or are specific to your local environment, there is another option. These files can be added to the .git/info/exclude file as a way of ignoring them locally.

Add specific files or patterns as needed to that file and then save it. Relevant files will no longer show up as untracked files when you git status.

h/t Dillon Hafer

Last Commit A File Appeared In

In my project, I have a README.md file that I haven’t modified in a while. I’d like to take a look at the last commit that modified it. The git log command can be used here with few arguments to narrow it down.

$ git log -1 -- README.md
commit 6da76838549a43aa578604f8d0eee7f6dbf44168
Author: c0mpiler <c0mpiler@ins8s.com>
Date:   Sun May 17 12:08:02 2015 -0500

    Add some documentation on configuring basic auth.

This same command will even work for files that have been deleted if you know the path and name of the file in question. For instance, I used to have an ideas.md file and I’d like to find the commit that removed it.

$ git log -1 -- docs/ideas.md
commit 0bb1d80ea8123dd12c305394e61ae27bdb706434
Author: c0mpiler <c0mpiler@ins8s.com>
Date:   Sat May 16 14:53:57 2015 -0500

    Remove the ideas doc, it isn't needed anymore.

Stashing Only Unstaged Changes

If you have both staged and unstaged changes in your project, you can perform a stash on just the unstaged ones by using the -k flag. The staged changes will be left intact ready for a commit.

$ git status
On branch master
...
Changes to be committed:

    modified:   README.md

Changes not staged for commit:

    modified:   app/models/user.rb

$ git stash -k
Saved working directory and index state ...

$ git status
On branch master
...
Changes to be committed:

    modified:   README.md

h/t Chris Erin

List Filenames Without The Diffs

The git show command will list all changes for a given reference including the diffs. With diffs included, this can get rather verbose at times. If you just want to see the list of files involved (excluding the diffs), you can use the --name-only flag. For instance,

$ git show HEAD --name-only
commit c563bafb511bb984c4466763db7e8937e7c3a509
Author: c0mpiler <c0mpiler@ins8s.com>
Date:   Sat May 16 20:56:07 2015 -0500

    This is my sweet commit message

app/models/user.rb
README.md
spec/factories/user.rb

Accessing A Lost Commit

If you have lost track of a recent commit (perhaps you did a reset), you can generally still get it back. Run git reflog and look through the output to see if you can find that commit. Note the sha value associated with that commit. Let’s say it is 39e85b2. You can peruse the details of that commit with git show 39e85b2.

From there, the utility belt that is git is at your disposal. For example, you can cherry-pick the commit or do a rebase.

Single Key Presses in Interactive Mode

When staging changes in interactive mode (git add -p), you have a number of options associated with single keys. y is yes, n is no, a is this and all remaining, and so on.

By default, you have to press enter after making your selection. However, it can be configured for single key presses. Add the following to your git configuration file to enable single key presses for interactive mode:

[interactive]
    singlekey = true

source

List Untracked Files

Git’s ls-files command is a plumbing command that allows you to list different sets of files based on the state of the working directory. So, if you want to list all untracked files, you can do:

$ git ls-files --others

This includes files ignored by the .gitignore file. If you want to exclude the ignored files and only see untracked and unignored files, then you can do:

$ git ls-files --exclude-standard --others

There are some other flags worth checking at at git help ls-files.

source

Clean Out All Local Branches

Sometimes a project can get to a point where there are so many local branches that deleting them one by one is too tedious. This one-liner can help:

$ git branch --merged master | grep -v master | xargs git branch -d

This won’t delete branches that are unmerged which saves you from doing something stupid, but can be annoying if you know what you are doing. If you are sure you want to wipe everything, just use -D like so:

$ git branch --merged master | grep -v master | xargs git branch -D

source

Checkout Old Version Of A File

When you want to return to a past version of a file, you can reset to a past commit. When you don’t want to abandon a bunch of other changes, this isn’t going to cut it. Another option is to just checkout the particular file as it was at the time of a past commit.

If the sha of that past commit is 72f2675 and the file’s name is some_file.rb, then just use checkout like so:

$ git checkout 72f2675 some_file.rb

Delete All Untracked Files

Git provides a command explicitly intended for cleaning up (read: removing) untracked files from a local copy of a repository.

git-clean - Remove untracked files from the working tree

Git does want you to be explicit though and requires you to use the -f flag to force it (unless otherwise configured).

Git also gives you fine-grained control with this command by defaulting to only deleting untracked files in the current directory. If you want directories of untracked files to be removed as well, you’ll need the -d flag.

So if you have a local repository full of untracked files you’d like to get rid of, just:

$ git clean -f -d

or just:

$ git clean -fd

source