Cheat Sheet - git

.gitignore

Overwrite remote branch with local branch

git push --force overwrites a remote branch with your local branch.

git push --force-with-lease is a safer option that will not overwrite any work on the remote branch if more commits were added to the remote branch (by another team-member or coworker or what have you). It ensures you do not overwrite someone elses work by force pushing.

You can create an alias to make it easier to use --force-with-lease by default:

git config --global alias.pushfl "push --force-with-lease"

Or you can use a wrapper script to condition you to relearn your muscle memory:

Add to your ~/.bash_profile, ~/.bashrc or ~/.zshrc.

git() {
  if [[ $@ == 'push -f'* || $@ == 'push --force'* ]]; then
    echo "Hey stupid, use --force-with-lease instead of --force"
    exit 1
  else
    command git "$@"
  fi
}

Overwrite local branch with remote branch

Replaces your current branch with its remote tracking branch:

git reset --hard @{u}

Undo a commit (git reset)

git reset does know five “modes”: soft, mixed, hard, merge and keep. I will start with the first three, since these are the modes you’ll usually encounter. After that you’ll find a nice little a bonus, so stay tuned.

soft

When using git reset --soft HEAD~1 you will remove the last commit from the current branch, but the file changes will stay in your working tree. Also the changes will stay on your index, so following with a git commit will create a commit with the exact same changes as the commit you “removed” before.

mixed

This is the default mode and quite similar to soft. When “removing” a commit with git reset HEAD~1 you will still keep the changes in your working tree but not on the index; so if you want to “redo” the commit, you will have to add the changes (git add) before committing.

hard

When using git reset --hard HEAD~1 you will lose all uncommitted changes in addition to the changes introduced in the last commit. The changes won’t stay in your working tree so doing a git status command will tell you that you don’t have any changes in your repository.

Tread carefully with this one. If you accidentally remove uncommitted changes which were never tracked by git (speak: committed or at least added to the index), you have no way of getting them back using git.

keep (Bonus)

git reset --keep HEAD~1 is an interesting and useful one. It only resets the files which are different between the current HEAD and the given commit. It aborts the reset if any of these files has uncommitted changes. It basically acts as a safer version of hard.

This mode is particularly useful when you have a bunch of changes and want to switch to a different branch without losing these changes - for example when you started to work on the wrong branch.

Note When doing git reset to remove a commit the commit isn’t really lost, there just is no reference pointing to it or any of it’s children. You can still recover a commit which was “deleted” with git reset by finding it’s SHA-1 key, for example with a command such as git reflog.

Reuse commit message after reset

After a git reset, this one-liner can do it:

git commit --reuse-message=ORIG_HEAD

# or
git commit --reuse-message=HEAD@{1}

# or even shorter:
git commit -C HEAD@{1}

To edit on reuse:

git commit --reedit-message=HEAD@{1}

To change the author:

git commit --reset-author

Cleanup

How can I undo every change made to my directory after the last commit, including deleting added files, resetting modified files, and adding back deleted files?

  1. You can undo changes to tracked files with:

    git reset HEAD --hard
    
  2. You can remove untracked files with:

    git clean -f
    
  3. You can remove untracked files and directories with:

    git clean -fd
    

    but you can’t undo changes to untracked files.

  4. You can remove ignored and untracked files and directories

    git clean -fdx
    

    but you can’t undo change to ignored files.

You can also set clean.requireForce to false:

git config --global --add clean.requireForce false

to avoid using -f (--force) when you use git clean.

List staged files

git --no-pager diff --name-only --cached

Remove a directory tree from staging area

If you unintentionally added a directory that should have been ignored, run:

git rm --cached -r directory-name

to recursively remove the tree rooted at directory-name from the index.

List repo root dir

"$(git rev-parse --show-toplevel)"

Filter log

git log --author="username" --pretty=format:"%h - %an, %ar : %s"

%an - author name %ae - author email %cn - committer name

You don’t need to use the whole name:

git log --author=John
git log --author="John Doe"

Add --all if you intend to search all branches and not just the current commit’s ancestors in your repo.

You can also easily match on multiple authors as regex is the underlying mechanism for this filter:

git log --author="\(John\)\|\(Bob\)"

In order to exclude commits by a particular author or set of authors using regular expressions as noted in this question, you can use a negative lookahead in combination with the --perl-regexp switch:

git log --author='^(?!John|Bob).*$' --perl-regexp

Show all files modified by a user:

git log --no-merges --author="John" --name-only --pretty=format:"" | sort -u

Show all commits to a file:

The --follow works for a particular file and accounts for renames.

git log --follow -- <filename|dirname>

Difference to other solutions given

Note that other solutions include git log path (without the --follow). That approach is handy if you want to track e.g. changes in a directory, but stumbles when files were renamed (thus use --follow filename).

Edit author of specific commits

Interactive rebase off of a point earlier in the history than the commit you need to modify (git rebase -i <earliercommit>). In the list of commits being rebased, change the text from pick to edit next to the hash of the one you want to modify. Then when git prompts you to change the commit, use this:

git commit --amend --author="Author Name <email@address.com>" --no-edit

For example, if your commit history is A-B-C-D-E-F with F as HEAD, and you want to change the author of C and D, then you would…

  1. Specify git rebase -i B (here is an example of what you will see after executing the git rebase -i B command)
    • if you need to edit A, use git rebase -i --root
  2. Change the lines for both C and D from pick to edit
  3. Exit the editor (for vim, this would be pressing Esc and then typing :wq).
  4. Once the rebase started, it would first pause at C
  5. You would git commit --amend --author="Author Name <email@address.com>"
  6. Then git rebase --continue
  7. It would pause again at D
  8. Then you would git commit --amend --author="Author Name <email@address.com>" again
  9. git rebase --continue
  10. The rebase would complete.
  11. Use git push -f to update your origin with the updated commits.

Hooks

Hooks can either be configured locally on a per repository basis ($GIT_DIR/hooks/*) or globally for all git repositories (git config core.hooksPath/*). To use this hook:

  1. Copy it to .git/hooks/post-merge
  2. Ensure it’s executable with chmod +x .git/hooks/post-merge

A list of available hooks can be found here: https://git-scm.com/docs/githooks

post-merge

#!/usr/bin/env python
import sys
import subprocess

diff_requirements = 'git diff ORIG_HEAD HEAD --exit-code -- requirements.txt'

exit_code = subprocess.call(diff_requirements.split())
if exit_code == 1:
    print 'The requirements file has changed! Remember to install new dependencies.'
else:
    print 'No new dependencies.'

List git aliases

$ git config --get-regexp alias
# alias.st status

In this case git st is the same as git status

Show only commits that touch specific lines

When you run git log, you are listing all commits in reverse-chronological order for the current branch. There are ways of filtering the commits that get output from git-log. As of Git 1.8.4, git-log output can be filtered by commits that touch a range of line numbers.

This is done with the -L flag.

For instance, if I want to see all commits that touched the 13th line of my README.md file, then I can do this:

$ git log -L13,13:README.md

I can alter the command to show commits that touched a range of lines like so:

$ git log -L19,45:README.md

I used the -L flag recently to find when a dependency was added to mypackage.json file even though the most recent changes to that line were version bumps.

Currently, as of Git 2.25 (Q1 2020), the line-log functionality (git log -L) only supports displaying patch output (-p | --patch, its default behavior) and suppressing it (-s | --no-patch).

In short this means:

  • that -L implies -p,
  • that patch output can be suppressed using -s,
  • and that all other diff formats are not allowed.

Work with a Gist locally

I like Gists, those fun-size Git repositories I fill with coding demos, scripts, and WIP markdown files. Today I learned you can edit these files locally and push them to a remote, just like any Git repo.

Grab your Gist URL:

https://gist.github.com/yourhandle/5c61ee3fe0083

Alter it slightly, and clone:

$ git clone git@gist.github.com:5c61ee3fe0083.git
$ cd 5c61ee3fe0083/

Make changes, commit, and push away. Your commits will show up under the /revisions tab of your Gist.

Show shortlog of committers and their commits

You can use git shortlog to get a quick summary of committers and their commits on a project.

$ git shortlog

Andrew Vogel (10):
      Initial commit
      Add local zshrc, vimrc, and tmux.conf
      Add README
      Remove unneeded stuffs
      Fix Markdown links
      Add installation info and image to README
      Update local zsh config
      Add local vim plugins
      Add lightline to vimrc
      Update README

Show list of most recently committed branches

The standard way to list your branches is with the git branch command. If you use branches extensively for feature work and bug fixes, you may find yourself overwhelmed by the list of branches trying to visually parse through them for the one that you had worked on recently.

With the git for-each-ref command, we can produce a better list of branches.

$ git for-each-ref --sort=-committerdate --count=10 --format='%(refname:short)' refs/heads/

The command itself will iterate over all of the repository’s refs and print them out as a list. The --sort=-committerdate option will ensure that list is sorted by refs mostly recently committed to. The --count=10 option limits the list output to 10 refs. The format flag cleans up the output a bit, only showing the shortname of the ref. Lastly, the refs/heads/ argument ensures that only local refs are included in the output, thus ignoring remote refs.

The result is a list of local branches ordered by recency which generally corresponds to relevance.

See man git-for-each-ref for more details.

Include some stats in your git log

A simple git log command is going to give you a concise set of information for each commit. Usually it is enough info. When it’s not, git log can provide additional information with the right flags. To include overall and per-file stats on the number of insertions and deletions, use the --stat flag.

$ git log --stat
commit 66e67741a1cd6857a4467d1453c9f17ef5849f20
Author: jbranchaud <jbranchaud@gmail.com>
Date:   Mon Nov 13 21:24:41 2017 -0600

    Add Focus The URL Bar as an internet til

 README.md                     |  3 ++-
 internet/focus-the-url-bar.md | 10 ++++++++++
 2 files changed, 12 insertions(+), 1 deletion(-)

commit 9241e3919ef1e4f68b71a1491d368ae6361084aa
Author: jbranchaud <jbranchaud@gmail.com>
Date:   Sat Nov 11 11:41:40 2017 -0600

    Add Freeze An Object, Sorta as a javascript til

 README.md                            |  3 ++-
 javascript/freeze-an-object-sorta.md | 44 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 46 insertions(+), 1 deletion(-)

...

Automatically track remote branch with git

As of git 2.37.0, this is now possible with git configuration.

Run to update your configuration:

git config --global --add --bool push.autoSetupRemote true

Then git push will automatically setup the remote branch.

Clean up dangling branches

This deletes local branches that are no longer present in the remote:

git fetch -p && for branch in `LC_ALL=C git branch -vv | grep ': gone]' | awk '{print $1}'`; do git branch -D $branch; done

Sources: