Git
Git is a distributed version control system. The sections below are a working reference; the everyday commands come first, followed by setup and less-common scenarios.
Everyday commands
git status
git switch BRANCH_NAME # modern replacement for `git checkout BRANCH_NAME`
git switch -c BRANCH_NAME # create and switch in one step
git log --oneline --graph --decorate -20
git diff # unstaged changes
git diff --staged # what a commit would contain right now
Sync a fork with its upstream
Add the upstream remote once, then repeat the fetch/merge whenever the fork falls behind.
# One-time setup
git remote add upstream UPSTREAM_URL
git remote -v
# Every time you need to catch up
git fetch upstream --tags
git switch main
git merge upstream/main
git push origin main
Two things to watch for:
--tags(plural) is the correct flag;--tagis not agit fetchoption and the command fails.- The upstream default branch is not always
main. Older repositories still usemaster, in which case mergeupstream/master. Confirm withgit remote show upstreamand read theHEAD branchline.
Prefer git rebase upstream/main over merge when the fork has local commits
that have not been pushed anywhere yet — it keeps the history linear. Never
rebase commits that others have already pulled.
Branch from a tag
Useful when a release needs a patch but main has already moved on.
git fetch --tags
git tag # list tags; `git tag -l "v1.*"` to filter
git switch -c BRANCH_NAME TAG_NAME # branch based on the tag
git push -u origin BRANCH_NAME # -u records the upstream for later `git push`
Delete a branch
git branch -d BRANCH_NAME # refuses if the branch is not merged
git branch -D BRANCH_NAME # force delete, discards unmerged commits
git push origin -d BRANCH_NAME # delete the remote branch
git fetch --prune # drop remote-tracking refs that no longer exist
git branch -d only checks whether the branch is merged into the branch you are
currently on, so switch to main first or the check gives a false negative.
Undo
git restore FILE # discard unstaged changes to a file
git restore --staged FILE # unstage but keep the edit
git commit --amend # rewrite the last commit (only if unpushed)
git revert COMMIT # new commit that undoes COMMIT; safe on shared branches
git reset --hard COMMIT # destructive: moves the branch and discards changes
git reflog lists where HEAD has pointed recently and is the way back from a
bad reset or rebase. Entries expire after 90 days by default.
Stash work in progress
git stash push -m "MESSAGE" # add -u to include untracked files
git stash list
git stash pop # apply the newest entry and drop it
git stash apply stash@{1} # apply an older entry and keep it
Setup and config
Enable ssh-agent if your private key has a passphrase.
Reusable variables for the commands below:
USER_EMAIL="[email protected]"
USER_NAME="User Name"
SIGNING_KEY="0x7EF6B94D09F6AAAA"
BRANCH_NAME="next-version"
GIT_LOG_MESSAGE="New commit."
GIT_REPO_URL="https://github.com/user/repo.git"
Scope: --system (root), --global (user), --local (repo).
# Identity and signing key (local scope)
git config --local user.email $USER_EMAIL
git config --local user.name $USER_NAME
git config --local user.signingkey $SIGNING_KEY
# Credential storage
git config --local credential.helper store # save the password
git config --local --unset credential.helper # clear it
# Ignore file-mode (chmod) changes
git config --global core.fileMode false
# Show config
git config --local --list
Merge strategies
# Octopus merge: several branches on top of the current one
git merge fixes enhancements
# Keep the current branch's content, discarding the other side
git merge -s ours obsolete
git merge --continue
Tags
# -a annotated, -s GPG-signed (default key), -u GPG-signed with a specific key
git tag -u $SIGNING_KEY -s v1.0 -m $GIT_LOG_MESSAGE
git push origin --tags # push tags to the remote
git tag -l # list tags
git checkout TAG_NAME # check out a tag
git describe --tags # the current tag
Convert a lightweight tag to an annotated one by force-recreating it, then force-push (required since Git 1.8.2):
git tag -a -f <tagname> <tagname>
git push --force origin <tagname>
Commit message conventions
Prefixes: fix, add, change, refactor, remove, revert, merge,
update, hotfix, disable, upgrade.
Rewrite history
git commit --amend -m "New message" # last commit only, if unpushed
git rebase -i COMMIT_HASH # interactive rebase from a commit
git rebase -i --root # include the very first commit
Interactive-rebase actions:
pick— keep the commit.reword— keep the commit, edit its message.edit— stop at the commit to amend it (including files).squash— merge into the previous commit, keeping both messages.fixup— merge into the previous commit, discarding this message.exec— run a shell command.drop— discard the commit.
Change the author across a range of commits:
git rebase -i HEAD~12
# change `pick` to `edit` on each commit, then, for each:
git commit --amend --author="Rojar Smith <[email protected]>"
git rebase --continue
Repository statistics
Contribution and volume figures pulled out of the history. Everything here is read-only, so it is safe to run on any clone.
These pipelines use POSIX tools (sort, uniq, awk, wc). On Windows, run
them from Git Bash — they will not work in cmd.exe or PowerShell.
Lines changed per author
Walks the unique author names, then sums the --numstat columns for each one.
git log --format='%aN' | sort -u | while read name; do echo -en "$name\t"; git log --author="$name" --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 }' -; done
--numstat prints added removed path per file, --pretty=tformat: suppresses
the commit headers so only those rows remain, and awk accumulates the two
columns. The third figure is added minus removed — a net total, not the number
of lines that author owns today.
Lines changed across the repository
The same sum without the per-author loop.
git log --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 }' -
Top committers
git log --pretty='%aN' | sort | uniq -c | sort -k1 -n -r | head -n 5
Git has this built in, and it is both shorter and faster:
git shortlog -sn HEAD # commits per author, descending
git shortlog -sne HEAD # include the email address
git shortlog -sn --no-merges HEAD # ignore merge commits
Number of authors
git log --pretty='%aN' | sort -u | wc -l
# Built-in equivalent
git shortlog -sn HEAD | wc -l
Number of commits
git log --oneline | wc -l
# Faster: counts revisions without formatting or piping
git rev-list --count HEAD
git rev-list --count --no-merges HEAD
git rev-list --count HEAD --since="2025-01-01"
Narrowing the range
Raw totals over a whole history are rarely the interesting number. Restrict by date, and exclude generated or vendored files so they do not dominate:
# One quarter only. %d rather than %s so an empty range prints 0, not blank.
git log --since="2025-01-01" --until="2025-04-01" --pretty=tformat: --numstat | awk '{ add += $1; subs += $2 } END { printf "added: %d, removed: %d\n", add, subs }' -
# Skip lock files and generated output
git log --pretty=tformat: --numstat -- . ':(exclude)package-lock.json' ':(exclude)**/*.g.dart'
Reading the numbers
Every figure above has a caveat, and all of them matter before quoting one at someone:
- Net lines are not lines owned. A developer who refactors 500 lines into 200
scores negatively. To see who owns the code as it stands now, use
git blame— for examplegit blame --line-porcelain FILE | grep "^author "— not the log. - Binary files count as zero.
--numstatprints-instead of a number for them, andawkevaluates-as0. This repository has such rows, so the totals silently exclude every image and PDF. - Merge commits contribute nothing.
git logshows no diff for a merge unless asked, so merge-heavy histories under-report. --authoris a substring match. "Chris" also matches "Christopher", which double-counts. It matches against the wholeName <email>string, so--author="^Rojar Smith$"finds nothing — the email follows the name. Anchor the end on the opening bracket instead:
git log --author="^Rojar Smith <" --oneline
- Empty output means no commits matched, not zero lines. With
printf "%s"an unsetawkvariable prints as an empty string; a date range that selects nothing therefore printsadded lines: , removed lines: ,rather than zeros. Use%dif a script has to parse the result. - Duplicate identities split the totals. The same person committing from two
machines with different
user.emailvalues appears twice.%aNandgit shortlogboth honour.mailmap, so map the aliases once at the repository root:
Rojar Smith <[email protected]> <[email protected]>
git shortlogreads stdin when it has no revision argument. In a script, a cron job, or CI, plaingit shortlog -snproduces empty output rather than an error. Always pass a revision, such asHEAD.- Lines of code is a poor measure of contribution. It rewards verbosity and penalises deletion, review, and design. These commands are useful for sizing a codebase and spotting where churn is concentrated; they are not a performance review.
Adapted from Git: from beginner to professional.
Scenarios
Initialize over existing files
git init
git add -A -f # -f: add even gitignored files
git commit -m "Initial commit"
git remote add origin $GIT_REPO_URL
git push -f origin main
Discard all local changes
git reset --hard
git clean -fdx
Revert a single file to an earlier version
git log
git checkout <commit> path/to/file
Pull from an upstream repository
git remote add upstream https://github.com/eclipse/hawkbit.git
git remote -v
git pull upstream master --tags
error: pathspec 'XXX' did not match any files
git add -A -f
Remote host identification has changed
ssh-keygen -f "/root/.ssh/known_hosts" -R "github.com"
Multiple SSH keys
# Generate a deploy key
ssh-keygen -t rsa -C 'root@vm5' -f ~/.ssh/id_rsa_github
~/.ssh/config:
Host github-rs-company
HostName github.com
IdentityFile ~/.ssh/id_rsa_rs_company
git clone git@github-rs-company:rs-company/company-project.git
Remove the last commit
git reset --hard HEAD^
git push origin -f
Compare commits between tags
git log --pretty=oneline tagA...tagB # symmetric difference (three dots)
git log --pretty=oneline tagA..tagB # reachable from tagB but not tagA (two dots)
git log --pretty=oneline ^tagA tagB # equivalent to the two-dot form
Apply a patch or diff
# The file must be Unix (LF), UTF-8, with a trailing newline
git diff > modify.diff
git apply --check path/to/xxx.patch
git apply path/to/xxx.patch
Cherry-pick commits onto an upstream branch
git fetch --all
git checkout -b new-branch-name upstream/master
git cherry-pick commit-hash1
git cherry-pick commit-hash2
git push -u origin new-branch-name
Clone with username and password
git pull https://user:[email protected]/name/repo.git master
Change the remote URL
git remote set-url origin https://github.com/USERNAME/OTHERREPOSITORY.git
Move recent commits to a new branch
git branch my-branch
git reset --hard HEAD~3 # or: git reset --keep HEAD~3
git checkout my-branch
Sparse checkout
mkdir folder && cd folder
git init
git remote add origin [email protected]:rojarsmith/bitdove-hawkbit.git
git config core.sparseCheckout true
echo "docs/*" >> .git/info/sparse-checkout
git pull origin bitdove