NAME
git-tricks
- A collection of tricks/tips for using git efficiently
SYNOPSIS
git push . origin/master:master
, git commit --fixup
, and many more!
DESCRIPTION
This is a random list of nice git features/tricks that I have found to be very useful but which a lot people don't seem to know about.
I will probably update this from time to time, so come back again if this is interesting to you :)
OPTIONS
--delete-your-main/master-branch
--diff-against-the-upstream-version-of-a-branch
--display-a-graph-of-the-history
--don't-ignore-all-files-with-the-same-name
--don't-pull-the-gh-pages-branch
--edit-previous-commits-easily
--ignore-a-file-without-adding-it-to-gitignore
--list-lines-per-author
--summarize-changes-of-the-day
--never-attempt-to-merge-during-a-pull
--pull-a-branch-without-checking-it-out
--write-commit-messages-like-a-pro
DELETE YOUR MAIN/MASTER BRANCH
This is probably the most weird sounding advice I've ever given... But it is really useful!
If you're mostly working on feature-branches and only seldomly checking out the
main
/master
branch, your local branch ref for it will most likely lag behind
its remote counterpart (e.g. origin/main
) quite often. This can lead to
annoying mistakes like for example accidentally rebasing against master
instead of origin/master
or checking out the branch, expecting it will be
up-to-date.
By just deleting the local branch ref with git branch -d master
you can stop
all of this from happening. With the branch deleted, a git rebase master
will
fail, not accidentally passing; or a git switch master
will recreate it at the
most recent commit from upstream (which is known locally from e.g. git fetch
).
DIFF AGAINST THE UPSTREAM VERSION OF A BRANCH
Suppose you're working on a feature in a branch. You've pushed it to a project and opened a PR/MR for it. Now, after code-review, you went back to it, did some changes, maybe updated some commits to reflect the requested changes to your code.
Before sending it off, it is nice to take another look over the changes since
the version which is currently online. The easiest way to do this is of course
git diff origin/branch-name
. But there is actually a shorter one, which
doesn't even need you to look up the branch name! It looks like this:
git diff @{u}
The @{u}
denotes the upstream ref for the current branch as explained in more
details in gitrevisions(7)
.
DISPLAY A GRAPH OF THE HISTORY
Sometimes it's useful to see the history not in the linearized form which git log
shows by default, but to see all branching and merging that happened.
Sure, tools like tig
and gitk
can do this,
but actually we don't need to go that far: I found a Stack Overflow
answer with a super nice alias which does exactly this. Here it
is (slightly modified):
[alias]
lg = !"git lg-specific --all"
lgs = !"git lg-specific"
lg-specific = log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(bold yellow)%d%C(reset)'
git lg
will now show a graph with all branches in your repository while git lgs
will only show the graph of ancestors of the current branch. Here is an
example:
* 558002a0f223 - (12 days ago) Merge https://source.denx.de/u-boot/custodians/u-boot-riscv - Tom Rini (HEAD -> master) |\ | * c0ffc12a7016 - (3 weeks ago) riscv: Enable SPI flash env for SiFive Unmatched. - Thomas Skibo | * 6a863894ad53 - (3 weeks ago) riscv: Support booting SiFive Unmatched from SPI. - Thomas Skibo | * ffb78a7c71d5 - (4 weeks ago) doc: board: Update Microchip MPFS Icicle Kit doc - Padmarao Begari | * 5c007d24b9ae - (4 weeks ago) riscv: Update Microchip MPFS Icicle Kit support - Padmarao Begari | * 06142d6874ca - (4 weeks ago) riscv: dts: Split Microchip device tree - Padmarao Begari | * 0dc0d1e09441 - (4 weeks ago) i2c: Add Microchip PolarFire SoC I2C driver - Padmarao Begari | * 0d914ad10da3 - (4 weeks ago) net: macb: Remove Microchip compatible string - Padmarao Begari | * 666da85dc989 - (6 weeks ago) board: ae350: Support autoboot from RAM - Leo Yu-Chi Liang * | 5b9ee0168529 - (13 days ago) Merge branch 'master' of https://source.denx.de/u-boot/custodians/u-boot-net - Tom Rini |\ \ | |/ |/| | * 3fbd17aadf79 - (5 weeks ago) net: dwc_eth_qos: Enable clock in probe - Marek Vasut | * 877703372271 - (5 weeks ago) net: eth-phy: Handle gpio_request_by_name() return value - Marek Vasut |/ * 4a14bfffd42f - (2 weeks ago) Merge https://source.denx.de/u-boot/custodians/u-boot-marvell - Tom Rini
DON'T IGNORE ALL FILES WITH THE SAME NAME
Say you have the following line in your .gitignore
:
target/
This will ignore ./target
as intended, but it would also ignore
./src/foo/target
as well which might be unwanted. This can be fixed by
prefixing the pattern in .gitignore
with a /
:
/target/
Now, git will match this pattern from the root of the working tree so only
./target
will actually get ignored.
In general I've started to just add the leading /
to almost all entries in
.gitignore
, just in case a file with a matching name ever shows up in the
wrong place - this way it will be visible in git status
and I can investigate
what happened.
The gitignore(5)
manpage explains all the detailed semantics of
the format in .gitignore
and friends.
DON'T PULL THE gh-pages
BRANCH
On GitHub you can host a website for a project by pushing the webcontent to
a branch named gh-pages
. This is a very convenient feature, especially
because you can setup CI to automatically build documentation and push it to
this branch.
There is a downside to this, though: When you clone a project like this and
there is quite a lot of content on the gh-pages
branch, this will slow down
every git pull
for no good reason. After all, almost nobody will ever look at
the gh-pages
branch locally.
Luckily you can make git not pull a certain branch/ref! Just run the following commands in your local checkout:
# Instruct git to never pull gh-pages again
git config --local --add remote.origin.fetch ^refs/heads/gh-pages
# Remove all leftovers of the remote branch in the local repository
git branch -rd origin/gh-pages
EDIT PREVIOUS COMMITS EASILY
This was a gamechanger for me when I first learned about it. I am a strong
advocate of keeping a project's commit history "clean", in the sense that each
commit should be a sensible and standalone change. To create such clean
changesets, one often needs to edit a previously created commit. If it is the
last one, this can be done with git commit --amend
but for anything before it,
one has to resort to git rebase --interactive
and then marking said commit as
edit
. This works but is cumbersome because you're constantly jumping around
in the history and there also isn't any way to track the "changes to changes"
you made.
Enter git commit --fixup
and git commit --squash
: When you notice you need
to do some more changes which really belong in a certain past commit, you stage
the new changes like normal and then do a
git commit --fixup="sha1 of past commit to amend"
this will create a new commit with the message prefixed by fixup!
. Once you
think you've got everything right, you can fold these fixup commits into their
referenced commit by doing
git rebase --interactive --autosquash
This was a huge improvement to my workflow: I can now linearly work on my changeset, making fixup commits as a go along and once I am done, I can more or less automatically fold all those "development commits" into a clean set of commits which each do one logical change.
LIST LINES PER AUTHOR
If you are curious how many lines of code in the current version of your codebase can be attributed to each author, this shell-pipeline does the trick:
git ls-files -z | xargs -0n1 git blame -w --line-porcelain | sed -n 's/^author //p' | sort | uniq -c | sort -nr
SUMMARIZE CHANGES OF THE DAY
To look back on the work of the last day you can use this command to summarize what has happened in a repository since yesterday:
git diff --shortstat @{yesterday}
IGNORE A FILE WITHOUT ADDING IT TO GITIGNORE
Sometimes while hacking on a project you create files in a repository's working
tree which aren't meant to be commited nor will anyone else ever have similarly
named files around. They show up in git status
which is annoying but adding
them to .gitignore
is not an option because nobody else cares about these
filenames.
Instead, just add them to the .git/info/exclude
file! It has the same syntax
as .gitignore
but is not part of the working tree so it just applies to this
one local checkout of a repository.
NEVER ATTEMPT TO MERGE DURING A PULL
You're working on a branch, did some commits and in the meantime someone else
pushed their own commits to the same branch upstream. Now you git pull
and
for some odd reason the default behavior of git is to try and create a merge
commit.
To me, this is the worst it can do. In most situations, you didn't expect this to happen so you didn't intent to get this ugly merge into your history. I would say that in most situations, taking a step back and then rebasing is probably the much saner choice.
We can prevent git from automatically attempting the merge and instead just failing with noise by setting the following configuration:
git config --global pull.ff only
PULL A BRANCH WITHOUT CHECKING IT OUT
Maybe it's just me but for some reason I use this quite often: Say you're on
a branch doing work and you want to pull your local master
up to its latest
remote commit. You could go and check the branch out, then git pull
and then
switch back but that's of course cumbersome and also doesn't work when you have
unstaged changes in the working directory.
Instead, I learnt of the following neat little trick:
# First update all remote refs
git fetch
# Now update the local master to its remote counterpart
git push . origin/master:master
This works because using .
in the place of the remote for git push
is
interpreted as the current directory. So it is pushing origin/master
to the
master
branch in the git repository of the current directory.
WRITE COMMIT MESSAGES LIKE A PRO
For some reason, most people are taught to commit with
git commit -m "commit message goes here"
This sadly perpetuates the bad practice of only writing single-line commit messages. I won't go into details here, but it is a very good idea to include a longer description of why a changes was made and what is changed, at least for any non-trivial changesets. The Describe your changes section from the kernel patch submission document has some nice tips.
If you leave out the -m "message"
part of a git commit
command, git will
instead open your favorite editor (selected via the $EDITOR
environment
variable) to let you compose a commit message.
Even better, you can instruct git to attach the diff that you're about to commit to this file so you can see it while writing your commit message. To do this, enable the following configuration:
git config --global commit.verbose true
If you're using vim
, it even has proper syntax highlighting for the diff (with
the gitcommit
filetype) :)
Even better, again for vim
/neovim
users, you can automatically open the
diff in a split so you can see it side-by-side to the window where you're
composing the commit message. I found a gist by
@aroben for this which I later heavily modified.
With all this in place, writing a commit message looks like this for me:
The modified version of @aroben's gist which I am currently using looks like this (beware ugly vim hacks):
function! CommitPencil() abort
setlocal comments=:#,n:>,f:[
setlocal textwidth=0
call CustomPencil({
\ 'wrap': 'hard',
\ 'textwidth': 72,
\ })
setlocal nonumber norelativenumber
setlocal nofoldenable
endfunction
" originally from https://gist.github.com/aroben/d54d002269d9c39f0d5c89d910f7307e
" heavily modified for my personal needs
"
" TODO: make this work with fugitive
function! OpenCommitMessageDiff()
" if the window is too small, don't do anything
if &columns < 170
return
endif
" if we already added a split, ignore
if exists('b:did_commit_split')
return
endif
let b:did_commit_split = 1
" save the contents of the z register
let l:old_z = getreg('z')
let l:old_z_type = getregtype('z')
" save window id of the current window
let l:winid_commitmsg = win_getid()
try
call cursor(1, 0)
let diff_start = search('^diff --git')
if diff_start == 0
" there's no diff in the commit message; generate our own.
let @z = system('git diff --cached -M -C')
else
" yank diff from the bottom of the commit message into the z register
silent execute ':.,$yank z'
call cursor(1, 0)
endif
" paste into a new buffer
setlocal nosplitright
vnew
silent normal! V"zP
finally
" restore the z register
call setreg('z', l:old_z, l:old_z_type)
endtry
" configure the buffer
setlocal filetype=diff noswapfile nomodified readonly
silent file [Changes\ to\ be\ committed]
setlocal nofoldenable
let g:git_winid_diff = win_getid()
" get back to the commit message
call win_gotoid(l:winid_commitmsg)
autocmd QuitPre <buffer> execute win_id2win(g:git_winid_diff)."wincmd c"
endfunction
augroup filetype_git
autocmd!
autocmd VimEnter COMMIT_EDITMSG call OpenCommitMessageDiff()
autocmd BufRead EDIT_DESCRIPTION setfiletype gitcommit
autocmd FileType git,gitsendemail,*commit*,*COMMIT* call CommitPencil()
augroup END
SEE ALSO
I really recommend reading through some of the git manpages like
git-commit(1)
, git-add(1)
,
git-rebase(1)
, gitrevisions(7)
, or
git-config(1)
. There are a lot of really useful gems hidden
in there, some commandline options/config settings which you might find super
useful. Also, catching up on the release notes whenever a new
git version is released often helps to learn some cool new features :)