💾 Archived View for dmerej.info › blog › 0049-how-i-use-git.gmi captured on 2022-04-29 at 11:28:51. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2022-04-28)
-=-=-=-=-=-=-
2017, Jul 22 - Dimitri Merejkowsky License: CC By 4.0
Welcome!
This article will be a guided tour of how I use git.
We'll talk about configuration of git itself, the aliases and scripts I've written, and the other tools I work with.
Since this article is quite long, here's a table of contents:
Everything can be found in my dotfiles repo[1].
1: https://github.com/dmerejkowsky/dotfiles/blob/master/configs/git/config
Feel free to read, but please don't use it directly: the code was written to be used by only one person: me. Unless you are my hidden tween brother, it's very likely to be *not* suited for you.
I prefer having my git configuration files in `~/.config/git/config` rather than in `~/.gitconfig`.
Several reasons:
With this out of the way, let's dive in!
I like to keep a separate list of file patterns I always want to ignore, without to touch the `.gitignore` of all the projects I contribute to:
# in ~/.config/git/config [core] excludesfile = ~/.config/git/excludes
# in ~/.config/git/excludes # Vim
`rerere` stands for *replay recorded merge resolution*.
You have to explicitly enable it:
[rerere] enabled = true
Let's see it in action:
$ git fetch origin $ git rebase oirigin/master First, rewinding head to replay your work on top of it... Applying: new feature ... CONFLICT (content): Merge conflict in bar.txt Recorded preimage for 'bar.txt' # <---- rerere error: Failed to merge in the changes. Patch failed at 0001 new feature $ git mergetool # fix conflicts $ git rebase --continue Applying: new feature Recorded resolution for 'bar.txt'. # <--- rerere
Thus, assuming both `master` and `new-feature` continue to change and we need to re-run `git rebase`:
$ git rebase origin/master bar.txt | 1 + 1 file changed, 1 insertion(+) First, rewinding head to replay your work on top of it... Applying: new feature .... Falling back to patching base and 3-way merge... Auto-merging bar.txt CONFLICT (content): Merge conflict in bar.txt Resolved 'bar.txt' using previous resolution. # <--- rerere
Note that in this case , you will get a message saying *No files need merging* if you try to run `mergetool` as usual. Instead, use `git add`:
$ git add bar.txt $ git diff --staged -- bar.txt # Check that the changes still make sense $ git rebase --continue
If you don't like having to type `git add` explicitly, you can tell git to do it for you:
[rerere] autoUpdate = true
By default, `pull` in nothing more than `git fetch` followed by `git merge`.
I usually prefer rebases over merges, so I configured `git pull` to always perform a rebase:
[pull] rebase = true
If I *really* need to merge, I'll run:
$ git fetch origin $ git merge origin/master
By default, git will show you a summary of what changed (a *diffstat*) in many cases, like a fast-forward merge:
$ # on master, behind origin/master $ git merge Updating 24878f5..5be8c2e Fast-forward bar.txt | 1 + 1 file changed, 1 insertion(+)
But it will *not* do that if you are not fast-forward and rebase a different branch, unless you have:
[rebase] stat = true
The reason may be that computing the *diffstat* is expensive (at least more expensive than the actual merge in many cases), but personally I don't mind the cost in time.
Usually this information allow me to be aware of the potential conflicts.
Let's assume you have a list of 3 commits, the last one being a fix of the first:
# edit foo.py $ git commit -m "Foo: add bar() method" # write some code in bar.py $ git commit -m "Bar: add baz() method using Foo" # realize a crash, patch foo.py again
Now, you want the third commit to be squashed with the first one. This will make code review easier and a cleaner history.
There are two ways to fix this:
First, you can make a new commit and run `git rebase --interactive`:
$ git commit -m "Fix Foo.bar() crash when called without arguments" $ git rebase -i master pick bbace84 Foo: add bar() method pick 5499c6d Bar: add baz() method using Foo pick 33c32e1 Fix Foo.bar() crash # Edit file to have: pick bbace84 Foo: add bar() method fixup 33c32e1 Fix Foo.bar() crash pick 5499c6d Bar: add baz() method using Foo # quit and save Successfully rebased and updated refs/heads/foobar.
Or (and this is quicker), make sure your last commit starts with `fixup!`, followed by a prefix of the message of the commit you want to fix.
Then set:
[rebase] autosquash = true
Afterwards, when you'll run `git rebase -i`, the line `fixup` will magically appear:
$ git comit -m 'fixup! Foo: add bar' $ git rebase -i master # check the "rebase-todo" file is correct # done!
[merge] tool = kdif3
I use `KDiff3` mainly because I'm too used to it to change. Some facts:
Thus, when KDiff3 fails, I often just edit the un-merged file directly in Neovim and deal with the conflicts markers (`<<<<<<`, `=======`, `>>>>>>`) manually .
Note: `git` automatically writes a `.orig` file during merge resolution for backup purposes.
You can turn off this feature with:
[merge] keepBackup = false
It took me a while to figure this out, but KDiff3 *also* writes a `.orig` file when it's done, so you have to untick a box in `KDiff3` settings window for the `.orig` file to really be gone.
I have a *lot* of aliases. Some of them are quite common:
ci = commit co = checkout
Since I never managed to remember how many *m* there are in *amend*, I have:
mend = commit --amend
You'll find dozen of people trying to get a colorful and useful `git log`.
Here's my take on it:
lg = log --color --graph --pretty=format:'%Cgreen%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit lgs = log --graph --pretty=format:'%Cgreen%h%Creset - %s %C(yellow)%d' --abbrev-commit
The trick is to grok the `pretty format` string. You'll find all the relevant information in the git documentation.
Sometimes you want to cherry-pick a bug fix from the development branch to the release branch. In that case, adding the `-x` option causes the original sha1 not to be lost:
$ git cherry-pick -x develop $ git log Critical bug fix (cherry picked from commit af123ed)
To make sure I don't forget the `-x`, I created dedicated aliases:
ck = cherry-pick ca = cherry-pick --abort cx = cherry-pick -x
When you run `git rebase -i`, git will prompt you to run `git rebase --skip`, or `--continue`. And sometimes you'll want to abort. I made it so that in any case, I only have two letters to type:
ri = rebase -i rc = rebase --continue rs = rebase --skip ra = rebase --abort
(Note that the alias that runs `--abort` ends with the letter `a`, like the alias for `cherry-pick --abort`)
Usually, I'm either at work and there's a central repository at `origin`, or I'm contributing to an open source project and I have a `upstream` origin.
So this means I need two different aliases:
ro = rebase -i origin/master ru = rebase -i upstream/master
`go` is `reset --hard`. It's just that almost *never* use `git reset` without this option, so this is more handy:
go = reset --hard
`@{u}` is a special syntax that means "the remote ref of the tracked by the current branch'. So if you are on `master`, this usually is `origin/master`
Since `@{u}` is hard to type, I have a few aliases, all ending with `u`:
gou = reset --hard @{u} logu = log @{u} diffu = diff @{u}
For the same reason, I often need to compare with `origin/master`, so this time the aliases end with `o`:
logo = log origin/master diffo = diff origin/master
Git allows to insert small pieces of bash code instead of just a replacement string when you start the right value with a bang:
[alias] prune-merged = !git branch --merged | grep dm/ | grep -v "\\*" | xargs -n1 git branch -d
At work we all prefix our dev branches with our initials, but I often forget to delete them when I'm done.
This alias looks for all the local branches that are fully merged and start with `dm/` and deletes them.
Note how we use `xargs -n1` to by-pass the fact that `git branch -d` only takes one argument.
By default, when you run `git foo`, git will look for an executable anywhere in `$PATH` named `git-foo` and run it.
(If you are wondering, the built-in commands like `fetch` or `push` are in `/usr/lib/git-core/`)
You can combine this with aliases to get nice names for the helper scripts you write, while still having to type fewer letters.
For instance, I have:
[alias] fp = fpush
And then in I have a script named `bin/git-fpush`:
#!/bin/bash set -e function main() { if [[ -z "$1" ]] ; then echo "Missing file name" return 1 fi if [[ ! -f "$1" ]] ; then echo "$1 is not a regular file" return 1 fi git add $1 git commit -m "Update $1" git push } main "$@"
This means that when I type `git fp foo`, `git` will expand the alias to `git fpush`, and then run the `git-fpush` script.
I use the script mostly in private repositories, when all I want is to commit just a file, without bothering with a real message.
In the same vein, I have an other script that does everything it can to push all the changes in just one command:
#!/bin/bash set -e function main() { if [[ -z "$1" ]] ; then echo "Missing commit message" return 1 fi git commit --all --message "$1" git push } main "$@"
[alias] cp = commit-and-push
Lastly I have a helper script to rebase the last 'n' commits, because, as you could have guessed, I spent a lot of time rebasing stuff.
So `git rebase -i HEAD~5` becomes `git r 5`.
Same idea, a bash script:
#!/bin/bash set -e function main() { if [[ -z "$1" ]] ; then echo "Usage git r <number of commits>" return 1 fi git rebase --interactive "HEAD~$1" } main "$@"
and an alias:
[alias] r = rebase-n-commits
I use this command a lot. Here's the implementation:
function gcd() { topdir=$(git rev-parse --show-toplevel) if [[ $? -ne 0 ]]; then return 1 fi cd "${topdir}/${1}" }
Assuming I'm the root of the repository is `foo` and I'm in `foo/src`, `gcd` will send me to `foo`, and `gcd include` to `foo/include`.
Note the call to `git rev-parse` which allows you to not try and duplicate the logic used by git to find the top level directory (hint: it's harder than you think)
I do most of my git commands from a shell, but I sometimes need a graphical interface (And use a mouse)
I use gitk when I want to:
Behind the scenes, the `-S` option of `git log` is used.
By the way, `gitk` understands all the options `git log` does. So you can use: `gitk -- src/foo` to only show the commits in a given subdirectory for instance.
I use git-gui when I know I left things in the files I do not want to be part of the next commit: debug logs, comments, ...
I like the fact that you can select big hunks or small lines in a very intuitive way.
I also use the 'revert changes made to this file' (`ctrl-j` by default) feature a lot, because I'm looking directly at the changes that would be lost, so I feel more confident about not overwriting something important.
Apart from that, I've added a few configuration options to have more actions available in the top menu:
[guitool "pull-rebase"] cmd = git pull --rebase [guitool "clean"] cmd = git clean -fd confirm = true [guitool "reset"] cmd = git reset --hard confirm = true
Note how I have `confirm = true` for the "dangerous" operations, so that `git-gui` will display a pop-up for confirmation beforehand.
The last one is to activate spell checking for English when writing the commit message:
[gui] spellingdictionary = en_US
I have a similar configuration for Neovim:
augroup spell autocmd! autocmd filetype gitcommit :setlocal spell spelllang=en augroup end
The last piece of the puzzle is the interaction with Neovim.
I use Tim Pope's vim-fugitive[3] for this.
3: https://github.com/tpope/vim-fugitive
It has *tons* of features.
I use it to display a current branch in my status line:
set statusline=%{VimBuddy()}\ [%n]\ %<%f\ %{fugitive#statusline()}%h%m%r%=%-14.(%l,%c%V%)\ %P\ %a
Yup, you can configure your status line by just setting a variable, no need for dedicated plugins ...
Note that most of the commands only work if fugitive detects that you are editing a file from a git repository.
Here are the commands I use the most, followed by the effect they have assuming I'm editing a file named `foo.c`: