Today I Learned

hashrocket A Hashrocket project

177 posts about #git surprise

Git Log an Individual Function

Today I learned you can print the git log for an individual function with the -L flag - -L :<funcname>:<file>.

So if I have a function named friendlyFunction in app/fileName.ts, I can see it's history with:

% git log -L :friendlyFunction:app/fileName.ts
commit 740ab0c1eeb63e0a6bef06f89397e76407325f58 (HEAD -> main)
Author: Tony Yunker
Date:   Mon Nov 6 17:22:52 2023 -0600

    last commit

diff --git a/app/fileName.ts b/app/fileName.ts
--- a/app/fileName.ts
+++ b/app/fileName.ts
@@ -1,4 +1,4 @@
 export const friendlyFunction = () => {
-  console.log("hi there 👋");
+  console.log("bye now 👋");
   return 2 + 2;
 };

commit 314e725e56871e17c64b811de8e8cdb65badb08e
Author: Tony Yunker
Date:   Mon Nov 6 17:22:31 2023 -0600

    second commit

diff --git a/app/fileName.ts b/app/fileName.ts
--- a/app/fileName.ts
+++ b/app/fileName.ts
@@ -1,4 +1,4 @@
 export const friendlyFunction = () => {
   console.log("hi there 👋");
-  return 1 + 1;
+  return 2 + 2;
 };

commit 61998a2a9dfb64b0ee3aa5c23f783cca517ab15b
Author: Tony Yunker
Date:   Mon Nov 6 17:22:08 2023 -0600

    first commit

diff --git a/app/fileName.ts b/app/fileName.ts
--- /dev/null
+++ b/app/fileName.ts
@@ -0,0 +1,4 @@
+export const friendlyFunction = () => {
+  console.log("hi there 👋");
+  return 1 + 1;
+};

Docs

Push commits with repo creation using Github CLI

When creating a GitHub repo with the CLI tool, you have the opportunity to push up your commits immediately as well using the --push or (-p)

❯ gh repo create OWNER/REPO --push --source .
✓ Created repository OWNER/REPO on GitHub
✓ Added remote git@github.com:OWNER/REPO.git

Using the push flag requires the --source flag to tell it where the repo is locally which is why you see that in the example above.

Create remote with repo creation using GitHub CLI

If you have not partaken, creating a GitHub repo with the CLI tool is pretty cool; however, one thing that always got me was that when you create a new repo it does not create a remote so I'd always have to go back and make an origin.

There is an additional CLI flag that you can pass to do this for you.

gh repo create OWNER/REPO -r origin -s .

So, just for context, the -r (or --remote) flag is for naming the remote. This must also be used with the -s (or --source) flag, which tells the CLI where the repo lives on your local system. In the above example, I referenced the directory I was in using a ..

Create new repository with GitHub CLI

There's a good chance you're using GitHub if you're a developer. A long while ago, GitHub wrapped git commands with a tool they released called hub. This was nice, then, as it helped bridge the gap between Git and GitHub. A few years ago, they released the GitHub CLI, a dedicated command line tool for GitHub... no more wrapping Git commands.

There are many things you can do with this tool, but the one I use the most is setting up a new repository.

With this simple command, you can generate the repo on GitHub.

> gh repo create OWNER/REPO
✓ Created repository OWNER/REPO on GitHub

See Which Files Changed in git

git log is great to see a detailed diff, but what if you only want to see the files that were changed?

There's git whatchanged, but it's essentially deprecated, and git docs recommend using git log instead:

% git whatchanged
commit 9925b8ec8fc8024544a36c71e210a23192a63bf4 (HEAD -> main, origin/main, origin/HEAD)
Author: Tony Yunker <tony.yunker@gmail.com>
Date:   Fri Oct 20 15:07:09 2023 -0500

    ruby - abbreviated assignment operators

:100644 100644 7d41153 7449733 M    README.md
:000000 100644 0000000 5a432d1 A    ruby/rubys_abbreviated_assignment_operators.md

So then we can use git log --name-only to see the names of the files that were changed.

% git log --name-only
commit 9925b8ec8fc8024544a36c71e210a23192a63bf4 (HEAD -> main, origin/main, origin/HEAD)
Author: Tony Yunker <tony.yunker@gmail.com>
Date:   Fri Oct 20 15:07:09 2023 -0500

    ruby - abbreviated assignment operators

README.md
ruby/rubys_abbreviated_assignment_operators.md

But that won't tell us if they were updates or adds or whatnot. For that, we can use git log --name-status

% git log --name-status
commit 9925b8ec8fc8024544a36c71e210a23192a63bf4 (HEAD -> main, origin/main, origin/HEAD)
Author: Tony Yunker <tony.yunker@gmail.com>
Date:   Fri Oct 20 15:07:09 2023 -0500

    ruby - abbreviated assignment operators

M   README.md
A   ruby/rubys_abbreviated_assignment_operators.md

These flags can be used on git show too!

Git checkout to previous branches

In Git you can check out previous branches using @{-N}, where N is how many "branches ago" you want to go back.

For example, say you were in main then checked out branch-1 then checked out branch-2, and finally you checked out branch-3.

If you wanted to checkout the branch you were on "2 branches ago" you would just checkout @{-2}

> git checkout @{-2}
Switched to branch 'branch-1'

If you are using this to just go back to your previous branch @{-1}, there is a nice shortcut:

> git checkout -
Switched to branch 'branch-2'

H/T Matt Polito

Clone a Github Repository Wiki

A Github repository's wiki can be an extremely useful tool with amazing information. However not everyone is best at organizing information and I've had mixed results with Github search.

I knew that each repo wiki was versioned and figured it was git but it turns out you can also get access to that.

Just add .wiki to the end of your repo when cloning.

gh repo clone hashrocket/decent_exposure.wiki

Now you have all of the wiki files locally and can search however you feel.

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