Today I Learned

A Hashrocket project

147 posts about #git

Git Diff Matches Paths 📁

Today I learned that the git-diff command accepts a <path> argument. I used this knowledge to construct a command that runs only the RSpec test files on my branch that differ from master.

(other-branch)$ git diff master --name-only spec | xargs rspec

This is handy on a big test suite; when you can’t run every test, make sure you run the ones that you have changed.

See man git-diff for more info.

Auth with gh on remote machine

gh is a new github cli in the alpha phase right now hosted at https://github.com/cli/cli. It’s auth right now is through the browser which doesn’t work great on a remote machine.

When you try to do something with gh that should connect to github you’ll get a bunch of errors and this:

$ gh pr create
Please open the following URL manually:
https://github.com/login/oauth/authorize?client_id=XXXXXXXXXXXXXXXXXX&redirect_uri=http%3A%2F%2Flocalhost%3A34869%2Fcallback&scope=repo&state=XXXXXXXXXXXXXXXXXXXX

This is running a server in the background that is waiting for a request, don’t kill the process until the authentication is complete.

You can copy that url to your browser, authenticate with github and then you are redirected to a localhost url where the server is hosted

Copy the localhost url that you were redirected to, open a new window and then curl the localhost url.

$ curl http://localhost:34869/callback&scope=repo&state=XXXXXXXXXXXXXXXXXXXX

Now you should be authenticated and the gh process should continue.

Git Interactive Single Key 👇

Today I became familiar with the Git configuration interactive.singleKey. In the Hashrocket dotfiles this is enabled by default, and I’m not sure how common it is out in the wild. To quote the docs:

In interactive commands, allow the user to provide one-letter input with a single key (i.e., without hitting enter).

This allows git add --patch to be very powerful by enabling a quick vote up or down on a patch with a single keystroke.

Make sure you have the Perl module Term::ReadKey installed; it is a dependency.

See git history of a renamed file

If you rename a file, git won’t show history of the previous name:

$ git log --pretty=oneline things/text.txt
8567d... Move file into things directory

However, the --follow flag will allow you to see the history of commits beyond the rename:

$ git log --follow --pretty=oneline things/text.txt
8567d... Move file into things directory
1458a... Fix something
0aac5... Add something

Docs:

--follow
  Continue listing the history of a file beyond renames (works only for a single file).

Most recent branches

You can get a list of all the branches in git using the very programmatically named for-each-ref command. This command by itself isn’t very useful and needs quite a bit of tailoring for it to produce the information you want.

Just by itself it returns all refs

git for-each-ref
# returns all the references

This shows all refs (branches, tags, stashes, remote branches). To just get branches pass an argument that corresponds to the .git directory that contain branch refs.

git for-each-ref ref/heads

To reduce the number of refs it produces use --count

git for-each-ref --count=5
# returns just 5 refs

This outputs a line that looks like this:

c1973f1ae3e707668b500b0f6171db0a7c464877 commit refs/heads/feature/some-feature

Which is a bit noisy. To reduce the noise we can use --format.

git for-each-ref --format="$(refname)"
# line is just: refs/heads/feature/some-feature

Which can be shortened with :short

git for-each-ref --format="$(refname:short)"
#just: feature/some-feature

To get most recent branches we’ll need to sort. The - in front of the field name reverses the sort.

git for-each-ref --sort=-committerdate

All together:

git for-each-ref \
--format="%(refname:short)" \
--count=5 \
--sort=-committerdate \
refs/heads/

How to only stage deleted files

If I delete a bunch of files and just want to stage the deleted ones, I can use xargs to add them:

$ git ls-files --deleted | xargs git add

Example output:

$ git status

  deleted: lib/to/deleted_1.txt
  deleted: lib/to/deleted_2.txt
  modified lib/that/changed_1.txt
  deleted: lib/to/deleted_3.txt
  deleted: lib/to/deleted_4.txt
  modified lib/that/changed_2.txt
  deleted: lib/to/deleted_5.txt
  modified lib/that/changed_3.txt
$ git ls-files --deleted | xargs git add
Changes to be committed:

  deleted: lib/to/deleted_1.txt
  deleted: lib/to/deleted_2.txt
  deleted: lib/to/deleted_3.txt
  deleted: lib/to/deleted_4.txt
  deleted: lib/to/deleted_5.txt

Changes not staged for commit:

  modified lib/that/changed_1.txt
  modified lib/that/changed_2.txt
  modified lib/that/changed_3.txt

What does that git alias do? 🕵️‍♂️

I have some git aliases in my dotfiles and sometimes I use an alias for too long that I actually forget what it does under the hood.

I can open my ~/.gitconfig file in nvim (which I have an alias for) and search for the line that introduced the alias, but when pairing sometimes it’s easier and faster to just use git help to get the definition of an alias:

git help doff
'doff' is aliased to 'reset HEAD^'

Sort Git Branches by Latest Commit

Some of my repos have a lot of branches, and a command I discovered this week sorts them by the most recent committer date:

$ git branch --sort=-committerdate
* new-branch
  not-so-new-branch
  super-old-branch

The - reverses the order, putting the most-recently updated branch at the top, rather than the bottom, of my output. That’s more intuitive to me, but it certainly works in reverse.

Useful Git Diff Filenames

If you type git config --global diff.noprefix true into your terminal, it will set the value like so in your ~/.gitconfig file:

[diff]
    noprefix = true

The upshot is that git diffs will no longer prefix files with a/ and b/, such that you can copy a useful path when you double-click to select it in the terminal.

--- a/file/that/changed.awesome
+++ b/file/that/changed.awesome

becomes

--- file/that/changed.awesome
+++ file/that/changed.awesome

Credit to @brandur for the tip. Here’s the source tweet

Find that change you know you made!

Sometimes you do awesome work…

Sometimes you even remember that awesome work when at a later point it is not there but everything in your being knows that you wrote it already.

Hopefully this will help:

git log --all --source -- path/to/my/file

This super useful command will show you changes on a particular file (that you totally know you changed) across branches; because sometimes our brains work so hard that we forget to get something merged and those important changes just sit there waiting to be useful again on another branch.

Set Git Tracking Branch on `push`

You hate this error, right?

$ git push
There is no tracking information for the current branch.

I especially hate git’s recommendation at this stage:

$ git branch --set-upstream-to=origin/<branch> my-branch

You can check for tracking information in your config file with:

$ git config -l | grep my-branch
# returns exit code 1 (nothing)

Yep, no tracking info. The first time you push you should use the -u flag.

# assuming you are on my-branch
$ git push -u origin HEAD

No do you have tracking info?

# returns the tracking information stored in config!
$ git config -l | grep my-branch
branch.my-branch.remote=origin
branch.my-branch.merge=refs/heads/my-branch
branch.my-branch.rebase=true

Did you forget to set up tracking on the first push? Don’t worry, this actually works anytime you push.

$ git push
There is no tracking information for the current branch.

$ git push -u origin HEAD
Branch 'my-branch' set up to track remote branch 'my-branch' from 'origin' by rebasing.

This is so more ergonomic than git’s recommendation.

Get Back To Those Merge Conflicts

You’ve probably experienced this:

Decision A
<<<<<<< HEAD
Decision H
Decision I
=======
Decision F
Decision G
>>>>>>> branch a
Decision E

And you wind up making some iffy decisions:

Decision A
Decision I
Decision G
Decision E

The tests don’t pass, you’re not confident in the choices you’ve made, but this is the third commit in a rebase and you don’t want to start over.

It’s easy to get back to a place where all your merge conflicts exist with:

git checkout --merge file_name
# or
git checkout -m file_name

Now you can re-evaluate your choices and make better decisions

Decision A
<<<<<<< HEAD
Decision H
Decision I
=======
Decision F
Decision G
>>>>>>> branch a
Decision E

H/T Brian Dunn

GitHub PR Team Reviews

Pull request openers on GitHub can request a review from a GitHub user, or a team that has at least read-access to the repository. In the screenshot below, deploying the ‘Ninjas’ to review this PR is effectively the same as requesting a review from each ninja on the team. If they have notifications for such things, they’ll be notified, and they’ll see a banner on the PR’s show page requesting their input on the change.

image

This is a handy way to request a lot of reviews at once.

About Pull Request Reviews

Switch branches in git with... `git switch`

It’s experimental. It’s intuitive. It’s in the newest version of git, version 2.23.0. It is:

git switch my-branch

And, with every git command there are many ways to use the command with many flags:

You might want to create a branch and switch to it:

git switch -c new-branch

You might want to switch to a new version of a local branch that tracks a remote branch:

git switch -t origin/remote-branch

You can throw away unstaged changes with switching by using -f.

git switch -f other-branch

I feel that if I were learning git from scratch today, this would be much easier to learn, there’s just so much going on with checkout.

Delete remote branches with confirmation

Branches on the git server can sometimes get out of control. Here’s a sane way to clean up those remote branches that offers a nice confirmation before deletion, so that you don’t delete something you don’t want to delete.

git branch -a | grep remotes | awk '{gsub(/remotes\/origin\//, ""); print;}' | xargs -I % -p git push origin :%

The -p flag of xargs provides the confirmation.

Worktrees in Git

Ever want to have multiple branches of the same repository checked out at the same time? One option is to clone the repository multiple times. But Git worktrees allow multiple working trees to be attached to the same repository. This can be simpler to use and save storage space.

From inside an existing repository run the following to create a new worktree:

git worktree add /path/to/new/checkout other-branch

other-branch is now checked out at /path/to/new/checkout. You can work in that directory just as you can the original.

To remove the worktree when you are done:

git worktree remove /path/to/new/checkout

GitHub Insert a Suggestion ☝

Today I learned that GitHub has allows you to insert a one-line suggestion while conducting a pull request review. Click on the button circled in red, and you’ll get a suggestion fenced code block, as shown.

image

Inside the code block is the current code; replace it with your proposed change. GitHub will present your suggestion in a nice diff format.

When the pull requester views your suggestion, they can accept the change with one click. Efficient!

Multiple-lines are not yet supported, according to this GitHub blog post it is a frequently requested feature.

h/t Jed and Raelyn

Git Log Relative Committer Date 📝

Git log format strings are endlessly interesting to me. There are so many different ways to display the data.

Today I learned a new one, %cr. This formats your commit from the committer date, relative to now. After pulling from the remote, use it like this to see how long ago the latest commmit was committed:

$ git log -1 --format=%cr
8 hours ago

I use this command when surveying a Github organization, or an old computer with lots of dusty projects laying around.

Push git branch to another machine

Whenever I’m working in the same git repo on multiple machines, and I need to work on a branch on both machines I usually push the branch to a shared remote for the sole purpose of pulling it down on the other machine. This third machine can be avoided by push directly between machines:

$ myproject(feature-branch): git remote add machine2 ssh://machine2:/Users/dillon/dev/myproject
$ myproject(feature-branch): git push machine2 feature-branch

Over on machine2:

$ myproject(master): git branch
* master
  feature-branch       <= 😯

Reduce Depth of an Existing Git Repo ⚓️

git pull --depth works on an existing repo to set the depth of references retrieved. To quote the --depth docs:

Limit fetching to the specified number of commits from the tip of each remote branch history.

So, what happens when you run this? Here’s me experimenting with the Elixir repo:

$ git clone https://github.com/elixir-lang/elixir.git
$ cd elixir
$ git log --oneline | wc -l
   15670 # 15670 entries!
$ git pull --depth=1
# ...pulling the shallow repo
$ git log --oneline | wc -l
   1 # 15670 to 1... woah!
$ git log
# ...output from just one commit

Is .git/ a lot smaller now? Not yet, because there are many dangling references. This Stack Overflow answer shows you how to cleanup that directory. After setting a depth of one and following its instructions, I reduced the Elixir repo’s .git/ size by 90%.

Check out man git-pull for more information.

Give a commit a new parent

Given I have these logical commits on two branches:

normalbranch: A - B - C - D

funkybranch: Z - Y - X
git co normalbranch
git rebase --onto X C

Then the logical commits of normalbranch will now be:

normalbranch: Z - Y - X - C - D

We’ve given commit C the new parent of X. C’s hash will change and D’s hash will change.

I use this when I build on top of a branch that I’ve already submitted for a PR. That branch will get merged into the new root branch, and then I’ll need the new parent on the root branch for the commits that I’m currently working on.

Git Interactive Rebase The First Commit

Everytime I want to do an interactive rebase I pass the number of commits back that I want using head~number:

> git rebase -i head~3

Recently I created a repo that had only 2 commits and I got an error when I tried to do a rebase the same way:

> git rebase -i head~2
fatal: Needed a single revision
invalid upstream 'head~2'

To avoid that error, you can use a --root option to rebase the first commit:

> git rebase -i --root

Pulling In Changes During An Interactive Rebase

My standard workflow when doing feature development is to checkout a feature branch and commit changes as I go. When the feature is finished, I clean up the commit history with an interactive rebase and then integrate those changes with master.

I initiate the interactive rebase like this (while on the feature branch):

$ git rebase -i master

This allows me to squash, fixup, and delete commits that I’ve made since checking out this branch from master.

It is important to note that an another thing will happen seemingly behind the scenes. Any commits on master since the feature branch was checked out will be applied to the feature branch before the effects of the interactive rebase are applied.

If you want to strictly do an interactive rebase of the commits on the feature branch ignoring what is on master, then reference the commit you checked out from — put another way, reference the commit before the first commit on this branch.

$ git rebase -i <sha-of-first-commit-on-this-branch>~

The tilde (~) will go back one commit from the specified commit sha.

See man git-rebase for more details.

Reset Hub Credentials

On a shared computer, multiple users logged into Hub can lead to confusing Github activity, such as a PR opened from the command line with your name on it that you didn’t open.

These credentials are stored in ~/.config/hub. To reset your Hub credentials, delete this file and run a Hub command, and you will get an opportunity to reautheniticate.

Show Only Commits That Touch Specific Lines

When you run git log, you are listing all commits in reverse-chronological order for the current branch. There are ways of filtering the commits that get output from git-log. As of Git 1.8.4, git-log output can be filtered by commits that touch a range of line numbers.

This is done with the -L flag.

For instance, if I want to see all commits that touched the 13th line of my README.md file, then I can do this:

$ git log -L13,13:README.md

I can alter the command to show commits that touched a range of lines like so:

$ git log -L19,45:README.md

I used the -L flag recently to find when a dependency was added to my package.json file even though the most recent changes to that line were version bumps.

source

Show Changes In The Compose Commit Message View

When you execute git commit, you’ll be dropped into your default editor and prompted to compose a commit message. By default you’ll only see the status and names of the files involved in the commit.

To also see the line-by-line-changes in this view, you’ll want to commit in verbose mode.

$ git commit --verbose

You can also set verbose mode as the default by updating your ~/.gitconfig file.

[commit]
    verbose = true

source

Find The Date That A File Was Added To The Repo

The git log command has a bunch of flags that you can use to filter commits and format their output.

We can get git log to only show the date for a commit in the short format with the following flags:

$ git log --pretty=format:"%ad" --date=short

We can also get git log to filter commits to just those that have files being added:

$ git log --diff-filter=A

Like many git commands, we can restrict the output to those that match a path or file.

$ git log -- README.md

If we put all of these together, then we have a one-line command for getting the date a specific file was added to the repository:

$ git log --pretty=format:"%ad" --date=short --diff-filter=A -- README.md
2015-02-06

See man git-log for more details.

Show better image diff on git

Today I learned how to improve my git diff with images. For context check this TIL first.

I gave a step further on my git config to allow a better diff of my images using imagemagick compare features.

git config --global diff.image.textconv 'imgcat'
git config --global diff.image.command 'imgdiff'

So textconv will be used by git show commands and it’s using iTerm2 imgcat.

And command will be used by git diff command and it uses my new shell script imgdiff added to my PATH:

#!/bin/bash

if [[ -f "$1" ]] && [[ -f "$2" ]]; then
  compare "$2" "$1" png:- | montage -geometry +4+4 "$2" - "$1" png:- | imgcat
else
  if [[ -f "$1" ]]; then
    echo "+ Image Added"
    imgcat "$1"
  else
    echo "- Image Removed"
    imgcat "$2"
  fi
fi

exit 0

With that I can have a diff like that:

git diff

So previous image to the left, new to the right and imagemagick comparison in the middle.

Show images on git diff

Today I learned how to show binary files, more specifically images, using git diff or git show CLI. For that I am using iTerm2 imgcat.

In order to get there I had to configure git to allow a custom diff command to specific file types.

So I used git attributes to do that:

echo "*.gif diff=image" >> ~/.gitattributes
echo "*.jpg diff=image" >> ~/.gitattributes
echo "*.png diff=image" >> ~/.gitattributes
git config --global core.attributesfile '~/.gitattributes'

Then I had to change the git diff text converter to use iterm2 imgcat:

git config --global diff.image.textconv 'imgcat'

Check this out:

git-diff-images

Notes:

  • imgcat does not work with new versions of tmux
  • git pagers like less or more won’t work either, so you can run git --no-pager diff or you can pipe with cat like:
git diff | cat

Check also Open images in vim

Delete remote git branch - the declarative way

Cleaning up after yourself is important, and not just in real life. Good Git Hygiene™ goes a long way.

One of the methods I like to clean up is deleting unused feature branches. I do that both locally and on the remote source control server (github/gitlab etc).

As is common with Git there are many ways to feed a cat. Some people use this:

git push origin :name-of-branch

I prefer the more declerative way, especially for potentially destructive operations such as deleting a remote branch:

git push origin --delete name-of-branch

Either way, keeping your remote branches trim makes for a happier development team!

Add an Empty Directory to Git

Have you ever seen a directory containing a single .gitkeep? Today I learned the history of that file.

Git won’t let us add an empty directory, but sometimes there’s a good reason to want to do that. For instance, I’m building a single-page app that requireds a src/data/ directory, even when there’s no data. Instead of each developer on the project making this directory by hand, I’d like to check it into version control.

There are two competing strategies to achieve this: adding a .gitkeep to the directory, or adding a .gitignore. I prefer .gitkeep because the name tells you what it does and it’s not conventionally used for another purpose.

$ touch src/data/.gitkeep

Stack Overflow

Git Push Force but with lease

If you need to run a git push --force to push a fixup or an amended commit you can try the --force-with-lease tag first for safety. It will protect you to overwrite a commit made by other dev.

alias gplease='git push --force-with-lease'

Check Git push documentation

--force-with-lease will protect all remote refs that are going to be updated by requiring their current value to be the same as the remote-tracking branch we have for them.

Open Bitbucket PR's from the Command Line

I’m a fan of Git command line tools. The more familiar I become with Git hosting platform’s website, the more I want to control it with a CLI.

When you push a branch to Bitbucket, part of the response is a pull request URL. That URL is a create page for a pull request against the default branch.

I’ve taken this idea a step further and created a script for my project that visits this page:

#!/bin/sh

BRANCH=`git rev-parse --abbrev-ref HEAD`
open "http://bitbucket.org/<organization>/<project>/pull-requests/new?source=${BRANCH}&t=1#diff"

This script reads my current branch name and visits the Bitbucket ‘create PR’ page in a diff view.

Work with a Gist locally

I like Gists, those fun-size Git repositories I fill with coding demos, scripts, and WIP markdown files. Today I learned you can edit these files locally and push them to a remote, just like any Git repo.

Grab your Gist URL:

https://gist.github.com/yourhandle/__5bac61ee3fe0083__

Alter it slightly, and clone:

$ git clone git@gist.github.com:5c61ee3fe0083.git
$ cd 5c61ee3fe0083/

Make changes, commit, and push away. Your commits will show up under the /revisions tab of your Gist.

Show What Is In A Git Stash

Usually when I want to inspect anything in git, I’ll use git show with a specific ref. This can even be done with stash refs.

$ git stash list
stash@{0}: WIP on ...
stash@{1}: Some commit on ...

$ git show stash@{0}
# ...

The git-stash command has a built-in way of showing stashes that will save you from having to type out the somewhat awkward stash@{n} ref.

$ git stash show 1

This will show you the stash@{1} ref. You can also omit a number which will show you the latest stash (stash@{0}).

See man git-stash for more details.

Git: Stash Everything

Running the git stash command will take all the changes to tracked files in the working directory and stash them away. You can tack on the --include-untracked flag to make sure that untracked files are also included in the stash.

You can take this a step further with the --all flag. This will stash everything included files that you’ve told git to ignore.

You probably don’t want to do this. I ran this command and realized after the command hung for about 10 seconds that I had just stashed the node_modules directory.

See man git-stash for more details.

Show Git Changes For Files That Match A Pattern

The git show command allows you to view the changes associated with a reference, such as a commit sha (e.g. git show 86748aacf14e).

Consider a commit that has changed a bunch of JS files as well as two CSS files. If we run git show abcd1234, we will see all of the changes to each file which can result in quite a bit of noise. What if we only want to view the changes to the CSS files?

We can instruct the command to only show changes to files that match a pattern by tacking that pattern on to the end.

$ git show abcd1234 *.css

Alternatively, we could scope the output of the command to the files that live in a certain directory.

$ git show abcd1235 src/css

Rename a file in git with different casing

On MacOS git doesn’t handle file name casing changes very well.

If I have a committed file Something.txt I can mv it and git doesn’t recognize the change:

> mv Something.txt something.txt
> git status
On branch master
nothing to commit, working tree clean

Git will recognize the change if you perform the move with git mv.

> git mv Something.txt something.txt
> git status

renamed: Something.txt -> something.txt

There is a configuration regarding this:

git config core.ignorecase false

This is set to false by default. Setting this to true may provide the behaviour you want.

On Linux, this is not an issue. The filesystem recognizes files with different casing as different files and git likewise.

Traversing Git Conflict Markers

Today I solved several nagging inefficiencies in my Vim setup. One was memorizing a mapping for traversing Git conflict markers.

If you’ve ever had a merge conflict and opened the unresolved file, you’ll see these markers:

<<<<<<<
console.log('keep this code?')
=======
console.log('...or this?')
>>>>>>>

Deciding what to keep can be a process, and Vim-Unimpaired makes it easier by providing mappings to jump between the markers— ]n for the next marker, [n for the previous marker. Use these to traverse the diff and learn about what might be gained or lost during resolution.

Dropping Commits With Git Rebase

I’ve been warned enough times about the potential dangers of git reset --hard ... that I always second guess myself as I type it out. Is it git reset --hard HEAD or git reset --hard HEAD~?

If the working directory and index are clean, then there is another way to remove commits. A way that gives me more confidence about what exactly is being removed.

Doing an interactive rebase gives you a number of options. One of those options is d (which stands for drop).

$ git rebase -i master

This pulls up an interactive rebase with all commits going back to what is on master — great for when working from a feature branch.

pick 71ed173 Add Create A Stream From An Array as a reasonml til
pick 80ac8d3 Add some clarity by distinguishing var names
d 4f06c32 Add Data Structures With Self-Referential Types as a reasonml til
d 01a0e75 Fix the name of this file

Adding d next to the commits you want to get rid of and saving will drop those commits. The great part is that there is zero ambiguity about which ones are being dropped.

h/t Jake Worth

git merge --squash

Today I learned a new Git command that’s really useful. git merge --squash takes all the changes from one branch and stages them on top of another branch, ready to be summarized.

Here’s a sample workflow:

$ git checkout -b feature-branch

# Make changes across multiple commits
$ echo 1 > 1.txt
$ git add 1.txt
$ git commit -m 'Add first textfile'
$ echo 2 > 2.txt
$ git add 2.txt
$ git commit -m 'Add second textfile'

# Stage all changes on master
$ git checkout master
$ git merge --squash feature-branch
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   1.txt
        new file:   2.txt

# Summarize
$ git commit -m 'Add files 1 and 2'

This is a fast way to boil down a lot of WIP commits from a feature branch into a single commit on master.