Home

Awesome

My opinionated tips about Git

Show branches

You can show your branches with git branch -l. But if there are many branches, the output has a drawback. It is not sorted by date.

git branch --sort=-committerdate this lists the branches with the most recent branches on top.

You can change the default sorting in you config like that:

git config --global branch.sort -committerdate

I have an alias in my .bashrc: alias gbs="git branch --sort=-committerdate | fzf --header Checkout | tr '*' ' ' | xargs git switch

Update: I switched from the Bash shell to the Fish shell. This gives me a better autocomplete. When I type git switch TABTAB, then I get the latest branches. In Bash I get unfortunately just a long list of branches sorted by the alphabet. I think I won't use above alias any more.

checkout --> switch+restore

In the past git checkout was used for different use-cases.

I think it is time to use the new commands:

git switch to switch to a different branch

git restore to restore files

I avoid git checkout.

Switch branches

Often I want to switch between two branches. This is handy:

git switch -

This switches to the previous branch. And to get back ... again git switch -.

Like cd - in the bash shell.

History for selection

I do 95% of my git actions on the command-line. But "history for selection" is super cool. It is a feature of IntelliJ-based IDEs. You can select a region in the code and then you can have a look at the history of this region.

On the command-line you can use git blame some-file

git stash

git stash is like a backpack.

Example: You started to code. Then you realize (before you commit) that you work on the main branch. But you want to work in a feature-branch before first. Then you can git stash your uncommitted changes. Then you switch or create the branch you want to work on. After that you git stash pop and take your changes out of your backpack.

git diff of pull-request

Imagine you work on a branch which is a pull-request.

You want to see all changes of your pull-request.

git diff main

Above command might show you a lot of changes which happend on the main branch since you created the branch. You don't want to see those changes.

What was changed on your branch since the branch was created?

git diff origin/main...

Unfortunately this does not show your local changes, which are not committed yet.

To see them, too:

git diff $(git merge-base main HEAD)

Create a backup of a branch

# Create a new branch
git switch -c foobar-backup

# Switch back from "foobar-backup" to the previous branch
git switch -

You could use tagging for this, too. But I prefer above solution.

Don't be afraid to to delete your local branch

Imagine you did some strange things on your local branch "foo", and all you want is to get back to the "origin/foo" branch.

That's easy:

# create a backup of your current local branch
> git switch -c backup-of-foo

# delete the local copy of the branch.
> git branch -D foo

# switch to "origin/foo"
> git switch foo

Find removed code

You are looking for a variable/method/class name which was in the code once, but which is no longer in the current code.

Which commit removed or renamed it?

git log -G my_name

Attention: git log -G=foo will search for =foo (and I guess that is not what you wanted).

Find string in all branches

If you know a co-worker introduced a variable/method/class, but it is not in your code, and git log -G my_name does not help, then you can use git log --all -G my_name. This will search in all branches.

Find branch which contains a commit

You found a commit (maybe via git log -G ...) and now you want to know which branches contain this commit:

git branch --contains 684d9cc74d2

Hyperlink from git commit hash to preview

If I do git log -Smysearchterm in the vscode terminal, then I see a list of commits.

Now I would like to see a preview of these commits.

It is very easy, I was just not aware of that at the beginning.

Example:

commit 6ae936342d2c3c30fba47eec5a543ce6c53d0ebb
Author: foobar <foobar@example.com>
Date:   Wed Feb 14 01:54:04 2024 +0530

The commit hash "6ae936..." is a hyperlink.

You just need to click on it, and you can inspect the details of the commit.

I don't care much for the git tree

Many developers like to investigate the git tree.

I almost never do this.

If you avoid long running git branches, then it is even less important.

The native GUI gitk --all gives you a graphical overview. Don't ask me why the --all parameter is not the default. Without it, you won't see other branches.

rebase vs merge

I don't care much. In the past there have been endless discussion about this.

Avoid long running branches and then it matters even less.

git bisect

"git bisect" is a great tool in conjunction with unit tests. It is easy to find the commit, which introduced an error. Unfortunately, it is not a one-liner for now. You can use it like this:

user@host> git bisect start HEAD HEAD~10 


user@host> git bisect run py.test -k test_something
 ...
c8bed9b56861ea626833637e11a216555d7e7414 is the first bad commit
Author: ...

But if your pull-requests get tested before they get merged (Continous-Integration), then you hardly need "git bisect".

git bisect for lazy people

This walks the git history down from the current commit to the older commits.

Copy and adapt for your needs.

#!/bin/bash

BRANCH=your_branch

set -euxo pipefail
log_report() {
    echo "Error on line $1"
} 
trap 'log_report $LINENO' ERR


git switch $BRANCH
git log --oneline | cut -d' ' -f1 | while read hash; 
    do
    echo
    echo    $hash;
    git switch -d $hash
    DO_SOMETHING
    if YOUR_COMMAND; then
        echo "this is good (the commit above this introduced the bug): $hash"
        break
    fi
    git restore .
    sleep 1
done
git switch $BRANCH

You need to adapt these parts:

Merge several commits into one commit

Sometimes you want to merge small commits into one bigger commit. For example if you worked on a branch which was not merged to main yet.

But be careful. This re-writes the git history. This means other people developing on this branch will get trouble if you do this.

But if this branch is your Merge-Request (aka Pull-Request), and you know nobody else uses this branch, then it is fine to do so:

git rebase -i HEAD~N

N is the number of commits you want to work on. If you are working on a branch which was branched of "main", and you want to rebase all your changes: git rebase -i $(git merge-base main @)

Interactive rebase asks you for every commit what you want to do.

More about rewriting the git history: Git Book: Rewriting History

But you can't push the branch with the re-written history. You need to use:

git push --force-with-lease

But overall: Don't do this to often. This is not very productive (compared to writing new code, fixing old bugs or writing more detailed tests)

Squash all commits into a single commit

Pull-Requests in Kubernetes should be squashed. See PR Guidelines.

git rebase -i HEAD~N works fine, except you merged the main branch into your branch after creating the branch. Then your branch will contain merge commits, and the normal procedure won't work.

You can use git reset --soft, and then create a new commit which contains all the changes between "main" and "your-pr-branch".

git switch main
git pull

git switch your-pr-branch

# create a backup, just in case something goes wrong
git switch -c your-pr-branch-backup

# switch back to your-pr-branch
git switch -

# If you want to copy commit messages from your branch,
# then copy them now. After the following command, you need
# to look into your backup branch.
git reset --soft $(git merge-base main HEAD)
git commit
git push --force-with-lease

Change a git branch "inplace"

Imagine you developed your changes on a branch called "feature-foo". This branch was created from branch "feature-base".

Requirements change, and now you need to merge your changes into the main branch, but not the changes from branch "feature-base".

You could create a new branch, but since the central git-UI (github/gitlab) already references "feature-foo", you want to change the branch "inplace".

This creates the patches in a directory:

git switch feature-foo
git format-patch feature-base -o ~/tmp/foo-patches
git reset --hard origin/main
patch -p0 < ~/tmp/foo-patches/000... (files one by one)

Apply difference between two branches on a third branch

The above tip Change a git branch "inplace" uses external patches.

This can be used to Apply difference between two branches on a third branch

Restore a single file

Imagine you are working on a feature branch. But you want to restore one file to the original version of the main branch.

git restore -s main path/to/file

s like "source branch"

Source: https://stackoverflow.com/a/59756673/633961

List all files

Git directories often contain a lot of auto-created files. For example files created by tests.

If you want to use grep on all files which get tracked by git, you can use this:

git ls-files | grep -vP 'exclude1|exclude2' | xargs -r -d'\n' grep -nP '...'

In detail:

You can give ls-files a glob expression. This matches the whole filename (including the parent directories).

Imagine you have a directory containing many git repos, and there are files in REPO/foo/test_project_bar/settings.py, you can use grep like this:

for repo in *; do (cd "$repo"; git ls-files '*test_project*settings.py' |xargs -r -d'\n' grep RECAP ); done

Don't forget the * before "test_project".

If you add a comment at the end, you can easily find this command in your shell history (for example via ctrl-r (backward search)):

(cd ~/projects/; for repo in * ; do (
     cd "$repo"; git ls-files '*.my-extension' |
     xargs -r -d'\n' grep -P 'my-term' ) ;
 done) # grep over all repos

Once you executed this once, you can easily get back to this line by ctrl-r (search backwards in history) and then type “over all”

Architecture: Keep Backend and Frontend in one git Repo

If you split your code into two repos. One for the backend code, one for the frontend code, then you will make your life harder. The problem is that often a frontend change needs a corresponding change in the backend. Syncing the deployment of both changes is usualy easier if you have one repo.

BTW, many big companies use a gigantic monorepo for all their code. Wikipedia Monorepo

Autocompletion

If you configured auto-completion, then you can easy switch a branch if you know the first characters of the branch name:

git switch foo[TAB] 
 --->        foobar

Show current branch (for loop)

show the current branch name: git rev-parse --abbrev-ref HEAD

Example: you are in a directory containing many git repos. You want to know which one is not on the "main" branch:

for repo in *; do (cd "$repo"; echo $repo $(git rev-parse --abbrev-ref HEAD) ); done| grep -v main

Empty commit

Most web-GUIs of CI-systems have a "retry" button. But sometimes this does not work, or you don't want to leave your context.

git commit --allow-empty -m "Trigger CI"

side by side diff

Imagine you want to see an old commit side-by-side.

You could do git show 8d73caed, but this would not be side-by-side.

git difftool 8d73caed~1 8d73caed

~1 means "commit before 8d73caed"

--> launches meld, if installed, or your prefered diff-tool. See git-difftool

Resolve, take theirs

You merged a branch into your branch, and now you have conflicts. You want to discard your change, and take their changes:

git restore --theirs path/to/file

After resolving conflict: git diff HEAD~1

After resolving a conflict by hand, git diff HEAD~1 shows the file compared to the previous version. Somehow git diff shows something else.

git log over many git repos

You have a directory called "all-repos". This contains many git-repos. Now you want to use git log -G FooBar over all git repos. You only want to search for commits which where done during the last 8 months and you want to sort the result by the timestamp of the commit.

A bit ugly, but works:

for repo in *; do (cd "$repo"; git log -G FooBar --all --pretty="%ad %h in $repo by %an, %s" --date=iso --since=$(date -d "8 months ago" --iso)) ; done | sort -r| head

Think outside the box

Your local git repo is just a simple directory. Sometimes it is easier to just use cp -a my-repo my-repo2 to create a copy of your git repo.

Now you can checkout branch1 in one git repo, and branch2 in the second git repo.

Especialy if you are new to git, and unsure what will happen. Then relax and create a copy of your git repo before you execute command which make you feel uncomfortable.

Merge-tool (Meld)

I like Meld, which is a visual diff and merge tool.

Imagine I changed several parts in a file. Now a realize that some parts are good, and should stay. And some parts should get removed again.

I am on a feature-branch which was created from "main".

# Create a copy of the file
cp my-dir/my-file.xyz ~/tmp/

# restore the file to the original version
git restore -s main my-dir/my-file.xyz

meld my-dir/my-file.xyz ~/tmp/my-file.xyz

Now Meld opens and I can easily choose which parts I want to take into my branch, and which parts I don't need.


Image you created a PR containing several changes. Now you decide that you want to create three PRs and not one.

First, create a backup of your branch:

git switch -c my-backup; git switch -

Create the branch for the first feature, create a copy of your whole directory, and switch the copy to main:

git switch -c feature-1
cd ..
cp -a myrepo myrepo-main
cd myrepo-main
git switch main
git pull

Now launch meld:

cd ..
meld myrepo-main myrepo

Now you can easily remove all changes from belonging to feature-2 and feature-3.

I tried that with vscode, but somehow meld is more usable for that. You can copy files between both directories with the context menu.

You can accept (click on arrow) or reject (shift-click) single changes.

show change of merge commit

This shows no changes for merge commits:

git show <commit-hash>

Use:

git show -m <commit-hash>

The output of above command has several parts. For each parent commit one part.

Git pager

I use delta which shows git diff colorful, so that you can easily spot small changes in long lines.

revert a merge commit

You want to revert this merge commit:

commit 67181091ac5069fc78cc2e79cc5641ee43516eee (HEAD -> main, origin/main, origin/HEAD)
Merge: 90d4ee9c 14f8548e
Author: Some One <someone@example.com>

    Merge branch 'super-feature' into 'main'

A merge commit has two parents: 90d4ee9c and 14f8548e. You need to tell git which parent you want to choose.

If you want to keep 90d4ee9c you use -m 1. If you want to keep 14f8548e, you use -m 2.

This following line will keep 14f8548e and drop 90d4ee9c.

git revert 67181091 -m 2

Related Stackoverflow Answer

cherry-pick -n

git cherry-pick ... creates a new commit automatically. Sometimes you don't want only some changes of the original commit.

You can use the option -n to only get the changes. Now you can modify the changes and commit manually.

parent branch

Unfortunately it is not straight forward to find the branch name of the parent branch.

Example:

You created "feature-1" by branching off "main".

Then you create "feature-2" by branching off "feature-1" (because the second feature depends on a change which was done in feature-1).

Then for some weeks different things are more urgent, and now you are unsure if you branched off main or from an other branch.

I stored this in my local script directory

#!/bin/bash
# parent-branch.sh
git show-branch -a 2>/dev/null \
| grep '\*' \
| grep -v `git rev-parse --abbrev-ref HEAD` \
| head -n1 \
| perl -ple 's/\[[A-Za-z]+-\d+\][^\]]+$//; s/^.*\[([^~^\]]+).*$/$1/'

Source: https://stackoverflow.com/a/74314172/633961

delete merged branches

After some months there are too many old branches. Time to clean up.

This deletes all branches which are completely merged. This only deletes local branches.

❯ git branch --merged | grep -Pv '^\s*(\*|master|main|staging)' | xargs -r git branch -d

Unfortunately there will be several branches left which are not merged yet. No script can decide if they can be deleted or not.

Use branch -rd to delete the remote branch, too.

tig: text based GUI for git

tig is a text based GUI for git.

Much better than git log on the command line.

ripgrep: recursive grep which respects .gitignore

ripgrep: recursive grep which respects .gitignore

Handy, if there are huge directories in you git-repo which you usualy want to skip.

Undelete a branch

Imagine you accidentally deleted a branch:

❯ git branch -D foo-branch 
Deleted branch foo-branch (was d885d38).

Oh my god! What have I done?

Relax, you can easily create the branch again.

❯ git switch -d d885d38
❯ git switch -c foo-branch

Feature branch, only one commit

For some git repos exists a policy that your feature branch should contain only one commit before the branch can get merged.

Nevertheless I want to commit several times.

I could do git rebase -i HEAD~N before the PR gets merged, but an alternative is this:

I use --amend to alter the previous commit. This needs a force-push, because it rewrites the history.

git commit --amend . && git push --force-with-lease

How to Use Multiple Git Configs on One Computer

Image you up to now had only a personal Github account.

Now you want to have two (on one computer): one for your personal stuff and one for work related stuff.

Create two gitconfig files:

cd $HOME
cp .gitconfig .gitconfig-personal
mv .gitconfig .gitconfig-work
# change email address to your address for work related mails

vi .gitconfig-work

vi .gitconfig

[includeIf "gitdir:~/personal/"]
  path = ~/.gitconfig-personal
[includeIf "gitdir:~/work/"]
  path = ~/.gitconfig-work

Source: How to Use Multiple Git Configs on One Computer

Pick some lines from an other branch with git difftool

Imagine you want to take some changes of a different branch into your code.

If you care about the lines of code, not the commits, then you can use the following way to get the changed lines into your code.

Switch to your branch (the branch which should get updated).

I like the GUI tool meld as difftool.

git difftool -t meld other-branch -- your-file.txt

This will open meld and you can take some lines to your local version.

pre-commit.com

I use pre-commit.com.

For example I use this to avoid committing, if there are untracked files:

# See https://pre-commit.com/hooks.html for more hooks
repos:
  - repo: local
    hooks:
      - id: no-untracked-files-in-git
        name: no-untracked-files-in-git
        language: system
        entry: "bash -c 'files=$(git ls-files --exclude-standard --others); echo $files; test -z \"$files\"'"

Related: https://stackoverflow.com/a/75543767/633961

git submodules

I like to update submodules automatically:

git config --global submodule.recurse true

Don't ask me why this is no the default.

git subrepo (Alternative to submodule)

If you want to include code of a third-party into your git repo, you can "vendor" it via git subrepo.

Personal Notes per git Repo

I want to have personal notes per git repo which are not part of the git repo.

I use this pattern:

First I create a global git ignore:

git config --global core.excludesfile ~/.gitignore

Then I create ~/.gitignore, and add me:

me

If I want to save personal notes or scripts I create a symlink from the git repo a file in ~/docs:

mkdir ~/doc/COMPANY/some-git-repo
cd ~/COMPANY/some-git-repo
ln -s ~/doc/COMPANY/some-git-repo me

Now I can edit notes easily:

code me/foo.txt

But don't be careful. Don't increase the "bus factor" by building a single-person "information silo".

Never commit a .envrc file

I use direnv to manage environment variables. The tool direnv uses .envrc files to set environment variables.

In never want the .envrc file to be part of a git repo, because it usualy contains credentials (for example GITHUB_TOKEN).

To prevent accidental commits of .envrc files in all your Git repositories, you can set up a global .gitignore file like above, and add .envrc to the file.

Github: Tab width: 4

If you use tabs for indentation (for example in Golang), then you might want to change the default tab width from 8 to 4: https://github.com/settings/appearance

Which line ignores a file?

You have a file foo/bar.baz which gets somehow ignored by a line in a .gitignore.

But you are unsure which line is responsible for ignoring this file.

You can use git check-ignore -v:

❯ git check-ignore -v foo/bar.baz
.gitignore:23:foo/*.baz foo/bar.baz

Related