💾 Archived View for dmerej.info › en › blog › 0049-how-i-use-git.gmi captured on 2022-07-16 at 14:34:52. Gemini links have been rewritten to link to archived content

View Raw

More Information

-=-=-=-=-=-=-

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:

Introduction

Talk is cheap, show me the code

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.

Location of the config files

I prefer having my git configuration files in `~/.config/git/config` rather than in `~/.gitconfig`.

That's a matter of personal taste, of course, but it's also conforming to the XDG directory specification

Why did you bother writing this?

Several reasons:

With this out of the way, let's dive in!

2: How I Lint My Python

Options

Excluding files

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


# QtCreator
CMakeLists.txt.user

rerere

`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

pull

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

rebase

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.

fixing history

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

[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.

Aliases

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

Log

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.

Cherry-pick

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

Rebase

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

`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}

`@{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

Helper scripts

As aliases

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.

As standalone files

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.

Lazy push

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

Rebase

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

Returning to the top directory

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)

Gui tools

I do most of my git commands from a shell, but I sometimes need a graphical interface (And use a mouse)

gitk

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.

git-gui

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

Neovim

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`:

4: https://github.com/rking/ag.vim

Conclusion

git takes quite some time to learn, but also offers tons of way to customize its behavior.

I hope I've given you an idea of everything that is possible.

Until next time!

----

Back to Index

Contact me