Today I Learned

hashrocket A Hashrocket project

167 posts about #git surprise

Rename Git Remote

If the need arises to change the name of a git remote, in the past, I've normally done one of these two things:

  • delete and recreate the remote
  • manually edit the remote info in .git/config

Turns out there is a git command for this and I completely missed it!

git remote rename OLD_NAME NEW_NAME

As you can probably imagine, this handles the name change as well as the reference change in the config.

Clone a specific git branch

After cloning a repo, git will set your branch to whatever the value of HEAD is in the source repo:

$ ssh git@example.com
$ cat my-repo.git/HEAD
ref: refs/heads/master

If there is a different branch you want to clone, use the --branch flag:

$ git clone --branch my-feature git@example.com/my-repo.git
$ git branch
* my-feature

Git show on merge commit

I always use git show to see the changes for the last commit or git show sha1 to see the changes from a specific commit.

I rarely use git merge commits and recently I was working on a project that had a merge commit. When I did git show sha1 on that merge commit nothing showed up. Then I discovered that you can pass the -m option and it shows all the diff on that merge commit.

git show -m sha1 

Pretty neat!

Rename a local git branch

Ever wanted to rename a local git branch? The git branch -m command is your friend

Want to rename a branch that's not currently checked out?

git branch -m <original_name> <new_name>

Or to rename your current local branch, you can exclude the original name in the git branch args:

git branch -m <new_name>

Add a folder to git with exceptions

Given this folder structure:

app
├── bin
│  └── parse-ansi-codes.rs
├── Cargo.lock
├── Cargo.toml
├── README.md
├── src
│  ├── cursor.rs
│  ├── lib.rs
│  └── style.rs
├── target
│  └── debug
└── test

I can add the entire app directory to git, while ignoring the bin folder:

$ git add . ':!bin'

Github Resolving Git Conflicts

Today I tried out Github to Resolve Git Conflicts of an open PR. I noticed that Github has this gray button Resolve conflicts:

image

Then it drives me to a managing conflicts page with a simple editor where I could manually choose and edit the conflicts:

image

After my editing is done I can Mark as resolved:

image

And finally I can Commit merge:

image

Wait, what? oh no, I know that merging is the easiest way to solve conflicts as you solve all conflicts once, no matter how many commits your branch has, but to be honest I did not like that. I put some small effort to keep my git history clean and I really avoid merge commits as they are not necessary in general. I tend to keep a single history line on my git repos in the main branch, most of the times at least.

I guess that it is what it is, every time we use a tool to "simplify" the work we are giving up a bit of control on how the things are executed, right?

So here's the resolve of Github's solving git conflicts flow:

image

Reversed git log

I use git log -p a lot.

git log shows all the commits and messages in descending order and the -p flag includes the code that changed.

It helps me understanding why some changes happened in the codebase, to search when things got included and more.

Recently I wanted to actually see the history from the beginning and guess what, there's a flag for that:

git log -p --reverse

Alias a Git Branch Name 🏷

I'm working on a long-running Git branch with a long name, and I'd like to have an alias for that long branch name. The best solution I've found so far is to use a Git symbolic ref:

$ git symbolic-ref refs/heads/epic refs/heads/long-epic-branch-name

Once in place, this alias can be used to reference the longer branch name:

$ git checkout epic
Switched to branch 'epic'
$ git branch --show-current
long-epic-branch-name

Resources:

git-symbolic-refs
Answer: Creating aliases for Git branch names

Git + NPM: Resolving Lockfile Conflicts 🤝

Here's a challenging real-world scenario: you're doing a big merge or rebase on a JavaScript project, and you keep getting conflict after conflict in your package-lock.json. These conflicts are tough to resolve, because your package-lock.json is not easy to read, and is, say, 30,000 lines long. What do you do?

When you hit to conflict, on the the conflicting Git SHA, run the following command. Your lockfile will be regenerated, conflict resolved:

$ npm install --package-lock-only

💥

From the resolving lockfile conflicts docs:

image

Give git config more context with `--show-scope`

git config is only as helpful as the options you pass.

In the simplest instance I only get a value:

> git config user.email
dev@example.com

If I pass the --get-regexp flag I get the key and the value for all the instances of that key:

> git config --get-regexp user.email
user.email computer@example.com
user.email dev@example.com

If I pass the --show-scope flag (added in 2.26) I get the scope:

> git config --show-scope --get-regexp user.email
global user.email computer@example.com
local  user.email dev@example.com

If I pass the --show-origin then I also get the file where the key was configured:

> git config --show-scope --show-origin --get-regexp user.email
global file:/home/dev/.gitconfig user.email computer@example.com
local  file:.git/config user.email dev@example.com

The git blog is an incredible way to learn about the new functionality in each git release.

Git Checkout in Patches

Something I've learned pairing with Chris Erin is to checkout code you don't want in patches. Here's the command:

$ git checkout --patch

Like the git add --patch command many developers know, this puts you an interactive workflow where you can decide how to handle each chunk, or patch, of your changes. It works opposite of git add --patch, in that y means check out the patch (abandon the changes), and n means keep the changes.

This is a great flow to go through before trying to commit code; use it to get rid of debugger statements, pseudocode, unwanted autoformatter changes, etc.

Git 101

There are lot of tutorials about Git. One that is built right into Git is the 'everyday help'. Print this help with:

$ git help everyday

The output is a series of must-know commands for a variety of roles in a technical organization: individual developer (standalone), individual developer (participant), integrators, and admins.

Pick a lane and go! 🏁

Run Git Commands From Any Dir

Git has one feature that I find irksome; you have to be in the directory with the Git repository to run commands against it.

...Or do you?

Git provides the -C flag for this scenario. Follow it with a directory name, and Git will execute your command as if it was run from <path>.

$ git -C ~/tilex status
On branch master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   lib/tilex_web/templates/layout/app.html.eex

no changes added to commit (use "git add" and/or "git commit -a")

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.

(not-master)$ 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