Git

Sources:

../../_images/rant.png

References make commits reachable

Snippets

git pull == git fetch && git merge/rebase

git branch -f master HEAD~ == git switch master && git reset --soft HEAD~

git clone -b mybranch --single-branch git://sub.domain.com/repo.git

git pull --ff-only && git push

git push -u origin master

# Fix last commit message
git commit --amend -m "fixed message of last commit"

# Change last commit content
vim ...
git add ...
git commit --amend --no-edit

# Modify specific commit
git rebase --interactive 'bbc643cd^'  # -> `e`

# Clone github wiki (e.g. wiki of https://github.com/swaywm/sway/wiki)
git clone https://github.com/swaywm/sway.wiki

# Accept pull request
curl -sL https://github.com/nodejs/node/pull/37544.patch | git am

# Mirror repo to another server
git clone --bare https://github.com/exampleuser/old-repository.git
cd old-repository.git
git push --mirror https://github.com/exampleuser/new-repository.git

# Delete remote branch
git push origin --delete old-branch

# Add empty commit
# Use cases: Initial commit (easier rebase) and to trigger CI/CD build
git commit --allow-empty -m "Initial commit"
git commit --allow-empty -m "Zero diff"

# Securely backup and restore a git repo
tar -cvf archive.tar .git/ && gpg -c archive.tar
<transfer file>
gpg -d archive.tar.gpg > archive.tar && tar -xvf archive.tar
git reset --hard master

# Cat file contents
git show b3d85ad:./file.txt
git show origin/master:dir/f.txt > g.txt
git show branchA~10:fileA branchB^^:fileB

# Find to which branches the commit belongs
git branch -r --contains <commit>
git branch -a --contains <commit>

# Fix broken link
# src: https://stackoverflow.com/a/69639136
cp -r .git /tmp/.git_bak
git stash clear
git reflog expire --expire-unreachable=now --all
git gc --prune=now

Diff

../../_images/diff.png
# Working Directory vs Stage
git diff
# Working Directory vs Tree
git diff ${tree}
# Stage vs Tree
git diff --cached ${tree:-HEAD}
# Tree vs Tree
git diff ${tree1} ${tree2}

# ... for a file
git diff ... -- <path>

# Compare two current folders
git diff --no-index dir1 dir2
# can use non-git diff: diff -r dir1 dir2

# List files that differ
git diff --name-only origin/master..origin/mybranch

# Compare file/folder F1 from branch B1
# with F2 from branch B2
git diff origin/B1:path/to/F1 origin/B2:path/to/F2

# From common ancestor of A and B, to B commit
git diff A...B
# same as
git diff $(git merge-base A B) B
../../_images/diff-dots.png

Diff Highlighters

Tag

# Create lighter tags (please don't)
git tag v1.4-lw

# Create annotated tags
git tag -a v1.4
git tag -a v1.4 -m "my version 1.4"
git tag -a v1.2 15027957951b64cf874c3557a0f3547bd83b3ff6
# Reassign a tag
git tag -a -f v1.4 15027957951b64cf874c3557a0f3547bd83b3ff6

# List tags
git tag
git tag -l '*-rc*'

# Push tags
git push origin v1.4
git push --tags

# Delete tag
git tag -d v1

# Describe commit relative to a tag
git describe
git describe HEAD~10
# Describe relative to a subcomponent tag (e.g. `subcomponent/v1.0.2`)
git describe --match='subcomponent/*'

Tag push permissions

git tag log and mkdir log creates ambiguety when git log test. Advice: configure Git server to accept tag pushes only from release managers.

Naming

Advice: start tags with v, e.g. v1.0 Shell completion will work with v<Tab>, while 1<Tab> will give you all commits with hash starting with 1.

Use annotated tags

Not lightweight:

  • git describe does not require --tags

  • can be PGP-signed

Checkout

Switch

../../_images/checkout-branch.png ../../_images/checkout-detached.png ../../_images/checkout-b-detached.png
git switch featureX    == git checkout featureX
git switch -d 5bb9e4c  == git checkout 5bb9e4c
git switch -C featureX == git checkout -b featureX
git switch -           == git checkout -
                          git checkout @{-1}

Restore

../../_images/checkout-files.png
git restore [-s|--source <tree>] fileA.txt == git checkout [<tree>] fileA.txt

git restore one.txt two.txt  # Mention multiple files
git restore .                # Discard all local changes
git restore *.rb             # Wildcard option

git restore [-W|--worktree] [-S|--staged]
                  ^
                  \------ default choice

git restore [-p|--patch] fileA.txt

Reset

Reset current HEAD to the specified state

../../_images/reset-soft.png ../../_images/reset-mixed.png ../../_images/reset-hard.png ../../_images/reset-path1.png ../../_images/reset-path3.png ../../_images/reset-checkout.png

Command

HEAD

Index

Workdir

WD Safe?

Commit Level

reset --soft    [commit]

REF

NO

NO

YES

reset [--mixed] [commit]

REF

YES

NO

YES

reset --hard    [commit]

REF

YES

YES

NO

checkout <commit>

HEAD

YES

YES

YES

File Level

reset [commit] <paths>

NO

YES

NO

YES

checkout [commit] <paths>

NO

YES

YES

NO

Stage 1. git reset --soft   Update HEAD
Stage 2. git reset --mixed  Update index
Stage 3. git reset --hard   Update working directory

Revert

# Revert multiple commits at once
git revert --no-commit commit-id-5
git revert --no-commit commit-id-4
git revert --no-commit commit-id-3
git revert --no-commit commit-id-2
git commit -m "The commit message"

git revert OLDER_COMMIT^..NEWER_COMMIT
# e.g. revert last two commits:
git revert --no-commit HEAD~2..HEAD
git revert --no-commit HEAD~2..      # same as above

Rebase and Cherry Pick

Rebase

../../_images/rebase.png ../../_images/rebase-onto.png
# Rebase featureX on master
git rebase master featureX == git switch featureX && git rebase master
git rebase --continue

git rebase [--onto <newparent>] [<oldparent> [<until>]]

# Start rebase from (but not including) commit 169a6
git rebase --onto master 169a6

git rebase --interactive
git rebase --onto F D
git rebase --onto F D my-branch

Before                                    After
A---B---C---F---G (branch)                A---B---C---F---G (branch)
         \                                             \
          D---E---H---I (HEAD my-branch)                E'---H'---I' (HEAD my-branch)
git rebase --onto F D I
git rebase --onto F D HEAD

Before                                    After
A---B---C---F---G (branch)                A---B---C---F---G (branch)
         \                                        |    \
          D---E---H---I (HEAD my-branch)          |     E'---H'---I' (HEAD)
                                                   \
                                                    D---E---H---I (my-branch)
git rebase --onto F D H
git rebase --onto F D HEAD^
git rebase --onto F D HEAD~
git rebase --onto F D HEAD~1

Before                                    After
A---B---C---F---G (branch)                A---B---C---F---G (branch)
         \                                        |    \
          D---E---H---I (HEAD my-branch)          |     E'---H' (HEAD)
                                                   \
                                                    D---E---H---I (my-branch)
git rebase master feature-2
git rebase --update-refs master feature-2

Before                    After (with --update-refs)      After (no --update-refs)
A---B---C (master)       A---B---C (master)             A---B---C (master)
     \                            \                          \   \
      D---E (feature-2)            D'---E' (feature-2)        D   D'---E' (feature-2)
       \                            \                          \
        (feature-1)                  (feature-1)                (feature-1)

Cherry pick

../../_images/cherry-pick.png
git cherry-pick 2c33a
git cherry-pick -x 2c33a  # add "(cherry picked from commit ...)" message

git-revise

A better git rebase -i

# Interactive revise non-published commits in current branch
git revise -i

Merge

git merge master feature == git checkout master && git merge feature
../../_images/git-merge-fast-forward.png ../../_images/git-merge-three-way-merge.png

Stash

../../_images/stash-options.svg ../../_images/stash-patch.svg ../../_images/stash_noop.svg ../../_images/stash_untracked.svg ../../_images/stash_all.svg
# Save
git stash [-u|--include-untracked] [-a|--all]
git stash push -m "add style to our site" == git stash save "add style to our site"
# Save specific files
git stash push -m <message> <path-of-file1> <path-of-file2>
# Save, including untracked files
git stash --include-untracked
# Save, including untracked and ignored
git stash --all

# Apply a stash
git stash apply
# Same, but throws applied stash away then
git stash pop
git stash pop 2 == git stash pop stash@{2}

# Checkout single file
git restore -s 'stash@{0}' -SW sqlite-history.zsh
# Create copy of stashed file under different name
git show stash@{0}:stashed_file.rb > copy_of_stashed_file.rb

# In case of conflict
git restore --staged <conflicting-file>

git stash list
git stash show [-p|--patch] [<n>]

git stash drop
git stash clear

Blame and Bisect

Blame

git blame -L 12,22 products.php
git blame -L 12,22 -C products.php

# Ignore whitespace
# or: git config --local blame.ignoreRevsFile ignorerevs
git blame -w

Bisect

# Start
# from
git bisect start
git bisect bad
# to
git bisect good HEAD~10
# ... check if the state
# and set it:
git bisect [good|bad]

# To initial state
git bisect reset

# Another example:
# git bisect start <bad> <good>
git bisect start linux-next/master v2.6.26-rc8
# find where make fails
git bisect run make kernel/fork.o

Conflicts Resolving

Ours and Theirs

  • us/ours - current HEAD at the moment of conflict (not necessarily HEAD at the moment of writing the command)

  • them/theirs - the other commit

merge (intuitive)

# merge feature into master
git switch master
git merge feature

#   us/ours   = HEAD = master
# them/theirs =        feature

cherry-pick (intuitive)

# apply A to feature
git switch feature
git cherry-pick A

#   us/ours   = HEAD = feature
# them/theirs =        A

rebase (counter-intuitive)

# rebase feature onto latest master
git switch feature
git rebase master
  1. checkouts m2 (ours), cherry-picks f1 (theirs)

  2. checkouts f1' (ours), cherry-picks f2 (theirs)

                                                 f2'  <-- feature'
                                                /
m2  <-- master               master -->  m2--f1'
|                                        |
m1    f2 <-- feature                     m1
|    /                                   |
o--f1                                    o
|                                        |

revert (sort of intuitive)

xxx

Merging example

git switch master
git merge feature
# conflict

# resolve manulally. OR:
git restore --ours   codefile.js  # to select the changes done in master
git restore --theirs codefile.js  # to select the changes done in feature

# continue merge
git add codefile.js
git merge --continue

Rebasing example

git switch feature
git rebase master
# conflict

git restore --ours   codefile.js  # to select the changes done in master
git restore --theirs codefile.js  # to select the changes done in feature

# continue rebase
git add codefile.js
git rebase --continue

Stacked Git

Logs

# Print refs: HEAD/branches/tags (defaults to --decorate=short)
git log --decorate

# One commit is one line + short hashes
git log --oneline
git log --pretty=oneline --abbrev-commit

# Useful snippet
git log --graph --oneline --all
# Third-party alternative
git-foresta --style=10 --all |less -RSX

# Local changes, not yet pushed to remote
git log origin/mybranch..HEAD
git log @{u}..HEAD

# Filtering
# by date
git log --after=2021-07-01 --before=2021-07-25
git log --after=2021-07-01 --before=yesterday
git log --after=2021-07-01 --before=25
git log --since=2021-07-01 --until=25
# by author
git log --author=Lain           # grep-like expression
git log --author='Lain\|Arisu'
# by commit message
git log --grep='bug'
# by file
git log -- foo.py bar.py
# pickaxe: search by added/removed source code
git log -S"Hello, World!"
git log -G"bug|fix"
# by merge (by defaults includes merge commits)
git log --no-merges  # no merge commits
git log --merges     # only merge commmits
# by range
git log ..
git log master..feature  # git log <not here>..<here>

# List files affected by commits
git whatchanged --since="1 week ago" --oneline
../../_images/log_range.svg ../../_images/log-dots.png

Submodule and subtree

Submodule

# Initialize and clone submodules on fresh-cloned repository
git submodule update --init == git submodule init && git submodule update
# or, clone with submodules
git clone --recursive ...
# or
git pull --recurse-submodules

# After moving HEAD (pull/switch),
# the submodule is not updated automatically
git submodule status
git submodule update

# pull submodule from remote
git submodule update --remote

# Add new submodule
git submodule add git://git.mysociety.org/plugin plugin

Change submodule remote:

cd plugin
git remote rm origin
git remote add origin git@github.com:lainiwa/plugin.git
git remote -v

git config branch.master.remote origin
git config branch.master.merge refs/heads/master

git config --list |grep '^submodule'
git config --list |grep '^branch'

Remove submodule:

# Clear `lib/plugin` and entry in the local `.git/config`
git submodule deinit lib/plugin
# Remove filetree at `lib/plugin` and entry in the `.gitmodules`
git rm lib/plugin

# or

# See above
git rm lib/plugin
# Remove gitdir repo and record from local `.git/config`
rm -rf .git/modules/lib/plugin
git config --remove-section submodule.lib/plugin

Subtree

Git UIs

GitWeb

An 8к Perl CGI script.

Configure to bind to localhost, and be served with python:

[instaweb]
    local = true
    httpd = python

Run:

git instaweb         # runs on http://localhost:1234
git instaweb --stop

Tig TUI

Switching views

  • [m]ain

  • [s]tatus

  • [t]ree

  • [y] stash

  • [g]rep

  • [h]elp

Status view

  • [u] Stage/unstage file or chunk

  • [!] Revert file or chunk

  • [C]ommit

  • [M]erge

  • [1] Stage line

  • [[] / []]     Increase/decrease the diff context

Tig

Worktree

git clone https://github.com/cmus/cmus && cd cmus

# Add worktree ../cmus-hotfix, and create branch cmus-hotfix
git worktree add ../cmus-hotfix

# or: add worktree, and create branch hotfix
git worktree add -b hotfix ../cmus-hotfix

# Add worktree for existing branch
git worktree add ../cmus-dev dev

Remote

# Change remote url
git remote set-url origin https://github.com/OWNER/REPOSITORY.git

Notes

# Simple example
git notes add -m 'Acked-by: lainiwa'
git log  # will show Notes after the comment
git notes remove [commit-id]

# Add and append
git notes add -m "message" [commit-id]
git notes append -m "message" [commit-id]

# Use with Gerrit reviewnotes plugin
git fetch origin refs/notes/review:refs/notes/review
git log --notes=review

# Namespaces (default namespace is `commits`)
git notes --ref jenkins add "build pass"
git notes --ref jenkins show HEAD
git log --show-notes=jenkins
git log --show-notes="*"

# Copy notes to between commits
git notes copy commitA commitB

# Viewing
git log --notes=review
git notes show

# Pushing and pulling
git push origin refs/notes/commits
git push origin "refs/notes/*"
git fetch origin refs/notes/commits:refs/notes/commits
git fetch origin "refs/notes/*:refs/notes/*"

Use cases:

  • Code review and tests results

  • Time tracking

  • Linking to external resources

Annex

https://cheatography.com/babobba/cheat-sheets/git-annex/ https://scicomp.aalto.fi/scicomp/git-annex/ https://swan.physics.wsu.edu/forbes/draft/git-annex/ https://oldwiki.scinet.utoronto.ca/images/5/55/Snug-git-annex.pdf https://tylercipriani.com/blog/2015/05/13/git-annex/ https://temofeev.ru/info/articles/organizatsiya-raspredelyennogo-khraneniya-faylov-s-pomoshchyu-git-annex/ https://blog.debiania.in.ua/posts/2013-12-15-advertising-git-annex.html https://superuser.com/questions/564381/moving-two-existing-already-synced-directory-trees-to-git-annex https://anarc.at/hardware/phone/htc-one-s/

Snippets

# Init local repo
git init
git annex init

# Add a file
dd if=/dev/zero of=file_100M bs=1M count=100
git annex add file_100M

# Show remotes-x-files matrix (what where is being stored)
git annex list
# Sync files with remote
git annex sync hetzner --content

# Annex might leave many git objects
git gc && git repack -Ad && git prune

# Ncdu the local repo
ncdu --exclude .git --follow-symlinks

Internals

# List files in `git-annex` brach
git ls-tree -r git-annex |grep -v '/SHA256E-'
# Cat those files' content
git show git-annex:remote.log

Adding remotes

# Add a special remote: WebDav on hetzner storage box
WEBDAV_USERNAME='u123456' \
WEBDAV_PASSWORD='passwordGoesHere' \
git annex initremote hetzner type=webdav \
  url=https://u123456.your-storagebox.de/annex \
  encryption=none

# Add a special remote: WebDav on hetzner storage box (encrypted)
# (30% slower than unencrypted WebDav sync)
WEBDAV_USERNAME='u123456' \
WEBDAV_PASSWORD='passwordGoesHere' \
git annex initremote hetzner-secret type=webdav \
  url=https://u123456.your-storagebox.de/annex-encrypted \
  encryption=hybrid keyid=$GPG_KEY_OR_EMAIL

# Mounting sshfs is too slow
# Use git-annex-remote-rclone instead
# (30% slower than unencrypted WebDav sync)
git annex initremote hetzner-sftp type=external \
    externaltype=rclone target=hetzner-sftp prefix=git-annex \
    encryption=shared rclone_layout=lower

# Add a special remote: S3 on Wasabi (encrypted)
AWS_ACCESS_KEY_ID='123456789ABCDEFGHIJK' \
AWS_SECRET_ACCESS_KEY='123456789abcdefghijklmnopqrstuvwxyz01234' \
git annex initremote wasabi-secret type=S3 \
  host=s3.eu-central-1.wasabisys.com \
  encryption=hybrid keyid=$GPG_KEY_OR_EMAIL

AWS_ACCESS_KEY_ID='123456789ABCDEFGHIJK' \
AWS_SECRET_ACCESS_KEY='123456789abcdefghijklmnopqrstuvwxyz01234' \
git annex initremote wasabi-secret type=S3 \
    host='s3.eu-central-1.wasabisys.com' bucket=lainiwa-annex \
    encryption=hybrid keyid=$GPG_KEY_OR_EMAIL

Cloning Repository

git clone <repo>
git annex init "temporary folder"

WEBDAV_USERNAME='u123456' \
WEBDAV_PASSWORD='passwordGoesHere' \
git annex enableremote hetzner

git annex get -- <file>

Config

# Open git annex config in text editor
git annex vicfg

# Set numcopies and mincopies
git annex numcopies 2
git annex numcopies 1  # excessive: defaults to 1 anyway

Requred and Preferred Content

# Set wasabi remote to prefer to store everything
git annex group wasabi backup
git annex wanted wasabi groupwanted
# same for hetzner remote
git annex group hetzner backup
git annex wanted hetzner groupwanted

# Set `here` to store only some files
git annex wanted . "(include=documents/* or include=imgs/screenshots/*_2023.* or include=imgs/screenshots/*_2024.* or include=imgs/screenshots/*.txt or include=imgs/photos/IMG_2023* or include=imgs/photos/IMG_2024*) and exclude=imgs/photos/*.mp4 and exclude=documents/medical/*/*.tar.xz and exclude=documents/medical/*/*"

# Edit config file in $EDITOR
git annex vicfg

# When set, you can do these
git annex drop --auto
git annex get --auto
# or
git annex sync --content

Attributes

# Require at least two copies of each file
git annex numcopies 2
# and send each file to usbdrive
git annex copy . --to usbdrive

# Redefine numcopies for certain files
echo "*.ogg annex.numcopies=1" >> .gitattributes
echo "*.flac annex.numcopies=3" >> .gitattributes

# Same, but per folder approach
mkdir important_stuff
echo "* annex.numcopies=3" > important_stuff/.gitattributes

Deleting files

git rm file_100M file_1M

# Show locally unused files
git annex unused
# Drop locally unused files
git annex dropunused 1-2

# Same, but for remote
git annex unused --from hetzner
# Will fail unless --force is provided (because numcopies defaults to 1)
git annex dropunused --from hetzner 1-2

Matching and finding files

# Find files that are one one remote but not on the other
git annex find --in=hetzner --and --not --in=wasabi \
          --or --in=wasabi --and --not --in=hetzner

# Find files that are stored locally but not on remote
git annex find --in=here --and --not --in=hetzner

Moving files around

Alternatives

Bundle

PGP & SSH

# Show info on tag
git cat-file -p v5.8-rc7
    #--> object 92ed30...
    #--> type commit
    #--> ...
# Show info on tagged commit
git cat-file -p 92ed30

# Verify tag
git verify-tag    v5.8-rc7
git verify-commit v5.8-rc7

Hooks

Clean

# Clean untracked files (like a `rm *`)
git clean -f|--force
# Recursive clean of untracked files (like a `rm -r *`)
git clean -fd

#
git clean ... [-n|--dry-run]

Refs

Local branches are storead in .git/refs/heads/. Remote branches - in .git/refs/remotes/origin (the remote, as created by git clone, default name is origin).

Example config, as created by git clone (without the --single-branch flag):

$ git config --local --list

remote.origin.url=https://github.com/<git_username>/my_repo.git
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.master.remote=origin
branch.master.merge=refs/heads/master

Reflog

Ignoring

.gitignore

Shared gitignore.

.git/info/exclude

Personal (local) gitignore.

Examples

Create and use a local remote

git init --bare ~/projects/remotes/test.git
mkcd ~/projects/test && git init
git remote add origin ~/projects/remotes/test.git
git push origin master
git clone ~/projects/remotes/test.git test1

Sync a fork

git remote add upstream https://github.com/larkery/zsh-histdb.git
# git remote -v
git fetch upstream
# git branch --all
# <merge/rebase here>

Unfuckup the master branch

# Get the lastest state of origin
git fetch origin
git checkout master
git reset --hard origin/master
# Delete untracked
git clean -fd

Practices

Merge vs Rebase

Commit Messages

Filer-repo

# Remove file(s) from the repo
# without `--invert-paths` it will unuke everything but the `Templates/`
git filter-repo --invert-paths --path Templates/
git push origin --tags

# Remove big files
git filter-repo --strip-blobs-bigger-than 10M
git push origin --force 'refs/heads/*'    # Overwrite all branches
git push origin --force 'refs/tags/*'     # Remove large files from tagged releases
git push origin --force 'refs/replace/*'  # Prevent dead links (created by git filter-repo)

# Change one word in all commits
FILTER_BRANCH_SQUELCH_WARNING=1 \
git filter-branch -f --tree-filter "sed -e 's#shit#flower#g' -i *.txt" 685966d6..HEAD

Git attributes

# Override attribute to unspecified state
# (negative patterns are forbidden)
Foo*    attr1=value1 attr2=value2
*.meta  !attr1

# Ignore all test and documentation with "export-ignore"
# Omit this files when downloading ZIP on github
.gitattributes export-ignore
.gitignore     export-ignore
/docs          export-ignore
/tests         export-ignore

# Set files as either text or binary
# by extension
* text=auto
*.php text
*.png binary
*.jpg binary

# Conserve a CRLF-ending file
tests/newline/CRLF.php text eol=crl

# Set a default for when conflicts appear: default, ours, theirs
# I prefer -diff for mistakes prevention
yarn.lock         merge=ours
package-lock.json merge=ours

# Do not try merge these files
composer.lock          -diff
yarn.lock              -diff
public/build/js/*.js   -diff
public/build/css/*.css -diff
*.map                  -diff
rev-manifest.json      -diff

# Remove compiled assets from github statistics
public/build/css/*.css linguist-vendored
public/build/js/*      linguist-vendored
public/build/font/*    linguist-vendored

Extensions

Git DVC

mkcd tstdvc

git init
dvc init
git status
git commit -m "Initialize DVC"

dvc remote add -d myremote /tmp/dvcstore

Git Bug

git bug user create
git bug add
git bug ls
git bug push
git bug termui
git bug webui

Internals

# A todo inside .git directory
vim .git/todo

# Create and use a draft commit message
vim .git/draft
git commit -eF .git/draft