Skip to content

Instantly share code, notes, and snippets.

@qrichert
Last active March 7, 2024 20:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save qrichert/b01b125a0c30e55da748411d468e88ab to your computer and use it in GitHub Desktop.
Save qrichert/b01b125a0c30e55da748411d468e88ab to your computer and use it in GitHub Desktop.

Git

General

  • which git Git’s location
  • git --version Git’s version

For Ubuntu, add this PPA to get the latest version:

$ sudo add-apt-repository ppa:git-core/ppa # apt update; apt install git

Configuration

After first install, edit ~/.gitconfig file:

[user]
    name = Your Full Name
    email = your@email.address
[core]
    editor = vim
[color]
    branch = auto
    diff = auto
    status = auto
[init]
    defaultBranch = main
[diff]
    colorMoved = default
[pull]
    rebase = true
[alias]
    a = add .
    br = branch
    ci = commit
    eci = commit --allow-empty
    c = commit -a --allow-empty-message --no-edit
    co = checkout
    conf = config --global --edit
    df = diff
    hdf = diff HEAD
    sdf = diff --stat HEAD
    cdf = diff --cached
    wdf = diff --color-words
    chdf = diff --color-words='[^[:space:]]|([[:alnum:]]|UTF_8_GUARD)+'
    fixup = commit -a --fixup
    fx = commit -a --fixup HEAD
    la = log --patch
    lg = log --color --graph --pretty=format:'%C(bold dim white)%h%Creset -%C(bold green)%d%Creset %s %C(bold green)(%ci) %C(bold blue)<%an>%Creset' --abbrev-commit
    ll = log --oneline --graph
    llfp = log --oneline --graph --first-parent
    #lm = log --oneline --decorate=auto --author='Your Full Name'
    p = push
    pf = push --force
    rb = rebase
    rbic = rebase --interactive --rebase-merges --autosquash --committer-date-is-author-date
    rs = restore
    rt = reset
    st = status
    sw = switch
    pick = cherry-pick
    count-lines = "! git log --author=\"$1\" --pretty=tformat: --numstat | awk '{ add += $1; subs += $2; loc += $1 - $2 } END { printf \"added lines: %s, removed lines: %s, total lines: %s\\n\", add, subs, loc }' #"

To change Git's language, add an alias to your ~/.bashrc / ~/.zshrc file:

alias git="LANG=en_US.UTF-8 git"
alias {git,gti}="LANG=en_US.UTF-8 git"  # If you often type "gti" instead of git...

To enable autocompletion on macOS, add this to ~/.zshrc:

autoload -Uz compinit && compinit
Click to show git pull options

$ git pull
warning: Pulling without specifying how to reconcile divergent branches is
discouraged. You can squelch this message by running one of the following
commands sometime before your next pull:

  git config pull.rebase false  # merge (the default strategy)
  git config pull.rebase true   # rebase
  git config pull.ff only       # fast-forward only

You can replace "git config" with "git config --global" to set a default
preference for all repositories. You can also pass --rebase, --no-rebase,
or --ff-only on the command line to override the configured default per
invocation.

Click to show useful Vim settings

Edit ~/.vimrc file.

set number
set mouse=a
set colorcolumn=73
set tabstop=8 softtabstop=0 expandtab shiftwidth=4 smarttab
highlight ColorColumn ctermbg=none
highlight ColorColumn ctermfg=Darkgrey
highlight ColorColumn cterm=underline

Upkeep

  • git init Init new repo
  • git commit --allow-empty -m "Initial commit" Create an empty initial commit (blank repo)
  • git commit --allow-empty-message --no-edit Commit without a commit message
  • git commit --fixup=<commit> Commit and mark for fixup
  • git commit --squash=<commit> Commit and mark for squash
  • git status View current state of modifications
  • git show [<object>] Show information about a commit (or latest if no ID)
  • git add . Add all new files to the repository and stage them for commit
  • git add <file> Add a specific file and stage it for commit
  • git add -N|--intent-to-add <file> Add new file but without staging it
  • git add -u Only add to files which are already staged, leaving others unstaged
  • git commit -m "Commit message" Commit staged files only
  • git commit -am "Commit message" Commit all modifications (except untracked files)
  • git commit --amend Edit last commit message, and commit content if there are staged changes
  • git pull Fetch and merge/rebase modifications from remote
  • git pull --all Pull all local branches
  • git fetch --all Fetch changes froml all remotes
  • git push [<remote>] [<branch>] Send your modifications (commits) to the remote
  • git push --all origin Push everything to origin (including branches and tags)
  • git push --force Push local version to remote ignoring any conflicts (e.g. after a local reset)
  • git reset --soft HEAD~1 Undo last commit (soft, changes are kept)
  • git reset --hard <commit> Go back to repo state at given commit, newer commits will be lost. (See push --force if the commits had been pushed already).
  • git reset <commit> -- <file> Reset single file to its version in a specific commit.
  • git restore --staged <file> Unstage file from staged-for-commit files list
  • git checkout <commit> Explore the state of a given commit
  • git swich [-c] <branch> Switch to a given branch. -c Creates the branch if it doesn't exist
  • git switch -c <branch> --track origin/<branch> Create a local copy of a remote branch
  • git branch [<branch>] --set-upstream-to=origin/<branch> Change remote tracking branch
  • git restore . Restore all files to latest commit state
  • git stash Put changes aside and go back to latest commit state
  • git stash pop Restore stashed changes and removes stash
  • git stash apply Restore stashed changes and keep stash
  • git stash --keep-index|-k Stash only non git-added files
  • git log|lg|l|ll|llg Different types of logs, more or less verbose
  • git log --grep="<string>" Search for a specific string in commit history
  • git log -L 100,150:path/to/file Show log for lines 100 to 150 of file
  • git log -- path/to/file Show log of specific file
  • git log --reverse Show log in reverse order
  • git log -G<search> Search an expression in the log
  • git log --merges Log all merge commits
  • git log --no-merges Log excluding merge commits
  • git grep "<expression>" Grep in files managed by Git
  • git rm --cached file.txt Stop tracking "file.txt" (effective after next commit)
  • git rm -r --cached folder Stop tracking "folder" (effective after next commit)
  • git mv filename newname Move or rename a file (maintaining versioning)
  • git rebase master Rebase current branch to master
  • git rebase master feature Rebase feature branch onto master
  • git rebase [--interactive|-i] [--rebase-merges|-r] <HEAD~X|after-this-commit> Interactive rebase. Without -r option, merge commits will be lost. To work on the X last commits use HEAD~X. To work on commits after a given commit, use a hash (the commit you give as parameter won't be affected, meaning it is the last commit you're OK with as is).
  • git rebase --committer-date-is-author-date <commit or branch> Rebase without changing the committer date (for rewords for instance). Works with --interactive since Git 2.29 (Q4 2020)
  • git reflog Show logs of ref changes (branch switchings, rebasings, merges, resets, etc.)
  • git cherry-pick <commit> Merge a specific commit into current branch, but only the changes introduced by this particular commit (basically like applying the diff of a commit to the current branch).
  • git cherry-pick --no-commit <commit> Cherry pick without committing changes
  • git apply <patch file> Applies a diff output to the current state. As an example, git diff > /home/patch.txt ; git reset . ; git apply /home/patch.txt would save changes, remove changes, and then load them back.
  • git blame -- path/to/file Show which line corresponds to which commit and by whom
  • git blame -L 100,150 -- path/to/file Show blame only for lines 100 to 150
  • git blame -L 100,+50 -- path/to/file Show blame only for 50 lines starting at 100
  • git ls-files List files managed by Git
  • git remote prune origin Remove local references to deleted remote branches (still appearing in git branch -a)
  • git fetch --prune Same as remote prune
  • git push --prune origin Remove remote branches without a local counterpart
  • git config user.email "your@email.com" Set email for current repository only
  • git config --system --edit Edit config globally for the entire machine

It's a good rule of thumb to merge a child branch into its parent to update a parent branch, and to rebase a child branch onto its parent to update a child branch. So, merge to update a parent branch, and rebase to update a child branch.

Never try to undo a merge commit using git revert, only undo using git reset. git revert only cancels the commit by creating a new commit that is the exact opposite. But the merge commit stays unchanged in the history, only now if you want to merge again (child into parent), the diff will be completely off because the child is already merged.

checkout, switch and restore

The general-purpose git checkout has been split into two commands :

  • git switch to handle the branches part
  • git restore to handle the files part

Apply commands chunk by chunk

Most commands like add, restore, reset, stash, etc. can take a --patch or -p parameter that prompts you, chunk of code by chunk of code, whether you want to apply the command to it or not.

Use y (yes), n (no) or s (split chunk) to apply you changes.

Squash a lot of commits

Interactive rebases are great but not very practical if you just want to squash a lot of commits into one.

To do this, you can just do a git reset --soft <commit before those you want to squash>, which will remove the commits but keep the changes. Just re-commit on top of that.

fixup & autosquash

If you don't want to look for commits to fixup and reorganize them by hand during a rebase, there's a way to do it automatically.

It works in two steps:

  • Step 1: Add --fixup=<commit to fixup> when doing fixup commits (-m is useless here)
  • Step 2: Add --autosquash to the interactive rebase

Step 1: First do some work

$ git commit -am "feat: Great new feature"
1026016 - feat: Great new feature

$ git commit -a --fixup=1026016
09e0e09 - fixup! feat: Great new feature

$ git commit -am "feat: Second great feature"
d0eb29b - feat: Second great feature

$ git commit -a --fixup=1026016
649e414 - fixup! feat: Great new feature

Step 2: Rebase

$ git rebase --interactive --autosquash HEAD~4
pick 1026016 feat: Great new feature
fixup 09e0e09 fixup! feat: Great new feature
fixup 649e414 fixup! feat: Great new feature
pick d0eb29b feat: Second great feature

Undo actions with reflogs

Git keeps track of any action thats changes refs (switching branches, rebasing, merging, resetting, committing, etc.).

To access the logs for refs changes, use the command git reflog:

$ git reflog
44c3ec8 HEAD@{0}: ...
25bf8d8 HEAD@{1}: ...
456e911 HEAD@{2}: ...
6ff6781 HEAD@{3}: ...

To undo an action, it works exactly like you'd undo a commit, by using git reset and the hash or HEAD@{x} as reference:

$ git reset --hard 456e911   # Not necessarily a commit hash, can be a state hash
$ git reset --hard HEAD@{2}  # Reset to the state of HEAD two moves ago

Find a bug by dichotomy

Git can help you find which commit introduced a specific bug. You give Git a known "bad" commit, and a knwow "good" commit, then Git will checkout commits by dichotomy. Each time you tell it if the checked ou commit is "good" or "bad", until you are left with the problematic one.

$ git bisect start
$ git bisect bad <bad commit>
$ git bisect good <good commit>

Or the same in one command:

$ git bisect start <bad commit> <good commit>

From here, Git checks out a commit approximatively in the middle between "bad" and "good".

$ git bisect bad  # Tell Git this commit is bad
$ git bisect good  # Tell Git this commit is good

Once done, Git will tell you the problematic commit:

Bisecting: 0 revisions left to test after this (roughly 0 steps)
[23c5102268866666dc6695cc611e652281baf7fd] Commit introducing a bug

Then you can go back to normal:

$ git bisect reset

Search for text in history

$ git grep <regexp> $(git rev-list --all)

Limited to subtree:

$ git grep <regexp> $(git rev-list --all -- lib/util) -- lib/util

Ignore a commit in blame

In git blame you see the last commit that edited a given line.

If you moved your codebase to a new code formatter, for instance, a lot of lines will show useless data.

In this case, you can use the --ignore-rev= option, to ignore the offending commit :

$ git blame -L714,718 modules/qwitus/contract.py
8b811a568b (Black 2023-07-18 11:06:52 +0200 714)         cls._buttons.update(
8b811a568b (Black 2023-07-18 11:06:52 +0200 715)             {
8b811a568b (Black 2023-07-18 11:06:52 +0200 716)                 "button_to_party": {"invisible": Eval("sub_type") == "20"},
8b811a568b (Black 2023-07-18 11:06:52 +0200 717)             }
8b811a568b (Black 2023-07-18 11:06:52 +0200 718)         )
$ git blame --ignore-rev=8b811a568b -L714,718 modules/qwitus/contract.py
27117beb26 modules/openup/contract.py (admreseau 2022-03-21 10:12:05 +0100 714)         cls._buttons.update(
27117beb26 modules/openup/contract.py (admreseau 2022-03-21 10:12:05 +0100 715)             {
27117beb26 modules/openup/contract.py (admreseau 2022-03-21 10:12:05 +0100 716)                 "button_to_party": {"invisible": Eval("sub_type") == "20"},
27117beb26 modules/openup/contract.py (admreseau 2022-03-21 10:12:05 +0100 717)             }
27117beb26 modules/openup/contract.py (admreseau 2022-03-21 10:12:05 +0100 718)         )

You could configure an alias if this is something you have to do on a regular basis.

If there are many commits to ignore, and they need to be updated frequently, you can add them to a file and use the --ignore-revs-file= option instead.

This file is usually called .git-blame-ignore-revs, and can be added once and for all to Git's local configuration :

git config --local blame.ignoreRevsFile .git-blame-ignore-revs

Apply new .gitignore rules on already indexed files

New .gitignore rules don't apply to files already added to Git. You have to manually git rm --cached them.

There is a way to do that quicker for a lot of files:

$ # Make sure working directory is clean before doing that,
$ # commit your changes or reset --hard to HEAD.

$ git rm -r --cached .  # Removed ALL files from Git
$ git add .  # Add everything back (this will use new .gitignore rules)

$ git commit -m "chore: New .gitignore rules"

Backdated commit

$ commit_date="2020-01-23 12:52:27 +0100"
$ export GIT_COMMITTER_DATE=$commit_date
$ export GIT_AUTHOR_DATE=$commit_date

$ git commit -am "Commit Message"

$ unset GIT_COMMITTER_DATE
$ unset GIT_AUTHOR_DATE

Split an old commit into multiple commits

Make sure working directory is clean before doing that, commit your changes or reset --hard to HEAD.

Start an interactive rebase (you can use your favorite rebase strategy):

$ git rebase --interactive <commit to split>~  # Notice the '~'

At the beginning of the line of the commit, pick with edit (or e), and save the file.

You're in rebase mode. Soft cancel the last commit, and unstage the changes:

$ git reset --soft HEAD~
$ git restore --staged .

Commit what you want in however many commits you need (git add -p may help). Then finish the rebase:

$ git rebase --continue

Use a different editor

$ # For one command
$ GIT_EDITOR=gedit git rebase -i HEAD~4

$ # Or for multiple commands
$ export GIT_EDITOR=gedit
$ git commit -a
$ git rebase -i HEAD~4

.gitignore files on local clone only

There is a special .gitignore-like file in the project's .git folder that won't be indexed (and so won't be part of the repository) but that will still by applied to the local clone of the project.

You can find it in <project folder>/.git/info/exclude. It works just like a .gitignore at the root of a project (it is project-wide).

Use this file when you're the only one on a team using certain tools or files.

Merge unrelated projects (or branches)

If you have two (or more) separate projects that started out in different repositories, but now you want them in a single repository, you can.

Basically, you just add the second project as a remote to your current project, then merge with the special flag --allow-unrelated-histories (i.e., histories without a common ancestor).

$ git remote add otherproject
$ git fetch --all otherproject
$ git merge --allow-unrelated-histories otherproject/main
$ git remote remove otherproject

Count lines in a project

$ # Total only
$ git ls-files | xargs cat | wc -l

$ # Per file and total
$ git ls-files | xargs wc -l

Diff

  • git diff View current unstaged modifications
  • git diff --cached View current staged modifications
  • git diff somefile.txt View modifications of a specific file
  • git diff branch1..branch2 Difference between branches at their tip
  • git diff branch1...branch2 Difference between branches at the tip of right branch and common ancestor commit
  • git diff master Difference between current branch and master
  • git diff --name-status Only show file names (works with .. and ... syntax)
  • git diff --color-words Make a diff on the basis of words instead of lines
  • git diff --stat Show diff stats (lines added, removed, etc.)
  • git diff main..dev -- */templates/*.html Diff only files matching a pattern
  • git diff main..dev -- ':(exclude)*/templates/*.html' Diff files excluding those matching a pattern

Apply diff as commit

Sometimes you want to apply the state of a branch/commit to another branch, without doing a merge, rewriting or handling a broken history with potential conflicts to resolve.

This takes a pure data diff between the two, not considering history, and applies the changes to the current branch:

$ git switch <branch>
$ git diff <branch> <branchorcommittocopy> > /tmp/diff.txt
$ git apply /tmp/diff.txt
$ git commit -a

Branches

  • git branch List existing branches
  • git branch newbranch Create new branch called "newbranch"
  • git switch branch Switch to "branch"
  • git switch -c newbranch Create "newbranch" and switch to it
  • git switch --orphan newbranch [&& git rm -rf .] Create and switch to an orphan branch. Envenutally delete all the files and changes in the index (staging area) if you don't have an empty initial commit for example.
  • git branch -d newbranch Delete "newbranch"
  • git branch -D newbranch Force delete "newbranch" even if unmerged commits
  • git push origin --delete newbranch Delete remote branch
  • git merge newbranch Merge "newbranch" into current branch
  • git merge <commit> Merge a specific commit including all the changes leading up to this commit
  • git merge|pull --no-ff branch Merge or pull a branch preventing fast-forwards (always create a merge commit)
  • git merge|pull --ff-only branch Merge or pull a branch imposing a fast-forward (never create a merge commit)
  • git merge --squash <branch> Squash all the commits before merging (only one commit merged)
  • git merge -s ours <branch> Merge and automatically resolve conflicts by keeping target branch (HEAD) changes
  • git merge -s theirs <branch> Merge and automatically resolve conflicts by keeping source branch changes
  • git merge-base <branch1> <branch2> Get hash of last common ancestor commit
  • git branch -v Show branches with their latest commit
  • git branch -r List remote branches (branches the remote knows)

Tags

  • git tag <tagname> 29f478 Give commit n°29f478 the tag "tagname" (or HEAD if no commit given).
  • git tag -a|--annotate <tagname> -m "Tag description" Tag tip of branch with annotated tag
  • git push --tags Push tags to remove (not pushed by default)
  • git tag -d|--delete <tagname> Remove tag "tagname"
  • git push --delete <tagname> Remove tag from remote
  • git tag -l|--list "v1.8.5*" Filter tags
  • git tag -n99 Show full description (technically, 99 lines of text)
  • git tag --sort=-version:refname Sort tags in reverse (lexicographic) order

Create Changelog from tags

If you use annotated tags as Changelog entries, you can perform a simple extraction with most recent first like so:

$ git tag -n99 --sort=-v:refname
v0.2.1          Version 0.2.1
    Fixes:
    - Duis aute irure dolor in reprehenderit in voluptate.
    - Velit esse cillum dolore eu fugiat nulla pariatur.
v0.2            Version 0.2
    Ut enim ad minim veniam, quis nostrud exercitation
    ullamco laboris nisi ut aliquip ex ea commodo consequat.
v0.1            Version 0.1
    Lorem ipsum dolor sit amet, consectetur adipiscing elit,
    sed do eiusmod tempor incididunt ut labore et dolore
    magna aliqua.

To limit to v0.2, you could add -l "v0.2*".

This is then very easy to parse for additional formatting as needed, or just to pipe it to less.

Remotes

  • git clone https://yourgitserver/repo.git Clone remote repository
  • git clone --branch <branch> <remote> Clone a specific branch of remote
  • git clone --depth <depth> Shallow clone with history truncated to specified number of commits
  • git remote -v List remote repositories
  • git remote add origin https://yourgitserver/repo.git Add a remote named "origin"
  • git push -u origin yourbranch Copy master branch to "origin" remote
  • git push -u origin yourbranch Add local branch to "origin" remote
  • git remote set-url origin https://new.url.here Change remote URL
  • git remote remove someremote Remove remote repository (doesn’t delete it from the server, just unlinks it from local one)
  • git remote rename someremote newname Rename a remote
  • git push origin <commit>:yourbranch Push up to a specific commit

Submodules

Submodules enable you to make a large project containing one or more sub-projets. All sub-projects will have their own repo and can be worked on independently. The parent project will more or less just know the repo URL and the hash of the commit it currently needs.

This makes it easy to install the project on new machines automatically without error (just need to clone the parent project, and this will install all submodules/sub-repos automatically at the right commit or tag, everyone will share the same code).

Git sees the submodule as a file. When you change things inside the submodule (files changed, refs changed, etc.), the parent will show it as a unstaged file change in the working area. You can then commit it like any other file, and this will update the "default" state of the submodule for the project.

  • git submodule add https://github.com/<user>/<repo> <folder> Adds submodule (works like git clone)
  • git clone --recurse-submodules <project url> Clone a repo with its submodules
  • git submodule init + git submodule update Equivalent to --recurse-submodules flag on git clone

Forks

Once you've cloned the fork to a local repo, follow these steps to add the original repo as "upstream":

$ git remote add upstream https://originalprojectserver/repo.git  # Step 1: Add original repo as upstream
$ git remote set-url --push upstream DISABLED  # Step 2: Disable push on that repo. "DISABLED" is a fake URL which doesn't exist (doesn't need to be DISABLED)
$ git remote -v  # Step 3: Check if added correctly

Maybe you also need to change the default remote for the main branch, run git config --edit.

With that done, you can update your fork (meaning pull the latest changes from the original repo) like so:

$ git switch <branch to update>  # Step 1: Make sure you're on the right branch
$ git pull --ff-only upstream <branch to update>  # Step 2: Pull changes from upstream (original repo). "--ff-only" means there won't be any merge commits. If it can't do it with a fast-forward it will fail.
$ git push [origin <branch to update>]  # Step 3: Push updates to origin (forked repo)

You should never edit branches directly. Always work on a sub-branch. This way you'll always be able to pull changes from upstream without merge conflicts, and won't need to push back merge commits to solve the issue. It keeps everything clean.

If the upstream was forced pushed to, or its history has been rewritten, you need to hard update your fork like so (be careful though, all you changes to that branch will be lost. That's why you don't work on a branch directly) :

$ git switch <branch to update>
$ git fetch upstream
$ git reset --hard upstream/<branch to update>
$ git push --force origin <branch to update>

When you're done with work on a sub-branch, update your fork (see above) and rebase the sub-branch to the tip of the original branch before merging. If there are merge conflicts solve them in the sub-branch.

$ git switch <sub-branch>
$ git rebase <original branch>

Instead of rebasing, you can also merge the original branch into the sub-branch to updated the sub-branch.

$ git switch <sub-branch>
$ git merge --no-ff <original branch>  # Will create a merge commit for reference

Download a newly created branch from the upstream

$ git fetch upstream  # Get updates
$ git switch -c <same name as new upstream branch>  # It doesn't matter from which branch you create this new one
$ git push -u <new branch>  # Update origin (fork)

Deployment

If you can use SSH key pairs, setup deploy keys from your repository. If not (cheap shared hosting), you can almost do the same using deploy tokens to authentify via HTTPS. This looks like https://<token-name>:<token>@gitlab.com/.

⚠️ Never use your login/password instead of deploy tokens, as they are clear in the URL. And don't use a per-account tokens, only use per-repository tokens with limited read-only access. If you can use SSH key pairs, use them instead of tokens. This if for cheap shared hosting where SSH is not an option.

⚠️ Always have the project root (.git folder location) above the Web root in web projects, so that Git files can’t be accessed through HTTP and remain private.

Also, production repos don’t need to store all the development history, use the --depth 1 option to only download the origin/HEAD state if you want to save bandwidth and/or space .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment