leah blogs

January 2013

06jan2013 · A grab bag of Git tricks

Since its release I’ve been a fan of Git. (I still can remember downloading the initial version.) The thing I like most is that it can be extended and customized in an unixy way. Over time, I have collected some scripts and tricks that I would like to present to a wider audience. Git information online abounds (I especially recommend Mark J. Dominus in-depth posts on Git), thus I will only show stuff I haven’t seen elsewhere.

git news

Let’s start with a simple alias which you can simply add to your .gitconfig:

[alias]
        news = log -p HEAD@{1}..HEAD@{0}

I am tracking quite a lot of open source projects by cloning them into ~/src and running git pull on them occasionally. Next, I run git news and see only the commits (with diff) that have arrived since the last pull.

Of course it is a very simplistic alias and it probably won’t do what you want if you actually change the HEAD yourself—e.g. by committing. (A more robust version could, for example, parse the output of git reflog and search for the last pull.) On the other hand, as it is, it also can be useful for showing what came in with a merge. I also use it for repositories where I git cvsimport into, with the same benefits.

git comma

Admittedly, I’m a fan of dirty working trees, which is why—when I don’t use magit or finely-grained git add -p/git commit -p already—I commit whole files at once like git commit foo.c bar.c.

One thing that has always annoyed me is that I cannot git commit files unknown to Git, enforcing an explicit git add step only for these new files! One day I took the plunge and wrote git-comma (a portmanteau of commit and add) which gives its best to behave exactly like git commit except for adding the yet-unknown files beforehand. This was a bit more tricky than I expected because I wanted it to work correctly even in the face of partially staged files, thus a stupid git add on all arguments would not work (also, you only want to add explicitly named files, not whole directories and so on). Finally, git comma tries to clean up properly if you decide to abort the commit, unstaging the files again.

(IMO, this should be a flag or configuration option for git commit.)

git attic

A newer script, but a very useful one, is git attic, whose namesake perhaps gives you a shiver down the spine, being reminded of this CVS quirk.

Yet, CVS’ manner with deleted files—moving them into a folder called Attic—had one benefit which cannot be denied: it was easy to see what had been removed and to access the contents again.

Of course, Git has no problem with file removal, but having a look at the old contents can be laborious.

Thus I wrote git-attic, which presents you a nice list of files together with their deletion date:

% git attic
2012-08-14 441e782^:Etc/ChangeLog-5.0
2012-05-31 0793393^:Completion/Unix/Command/_systemctl
2012-01-31 6a364de^:Test/Y04compgen.ztst
2012-01-31 6a364de^:Test/compgentest
2011-08-18 f0eaa57^:Completion/Zsh/Command/_schedtool
...

The output is designed to be copy’n’pasted: Pass the second field to git show to display the file contents, or just select the hash without ^ to see the commit where removal happened.

(By default, I don’t detect renames, since I want to see which paths don’t exist anymore. If you are looking for “lost” content, feel free to pass -M to the script to detect renames and only show truly deleted files.)

A minimalist, yet powerful zsh prompt

As an avid zsh user for years, I have been using a simple but powerful shell prompt which looks like hecate src/zsh% for years (since 2010-02-11 actually, thanks to homegit, see below.) and ridiculed experiments to make the zsh prompt a kitchen sink. However, my Git usage grew and I started occasionally mixing up branches.

Thus I decided to grin and bear it and wondered how to make a minimalist nevertheless useful Git-enhanced prompt. One feature of my prompt was that it only shows the last few segments of the current working directory (usually 2, which is enough for me unless I need to work in some javaesque file labyrinth). One day I decided to integrate the current Git branch into these path segments. Now, my prompt looks like this:

hecate src/zsh@master% cd Doc

… and it actually sticks to the repository root:

hecate zsh@master/Doc% cd Zsh

When the level gets too deep, the branch and repository moves to the front:

hecate zsh@master Doc/Zsh%

The depth is still configurable:

hecate zsh@master Doc/Zsh% NDIRS=4
hecate src/zsh@master/Doc/Zsh%

I’ve quite come to like this presentation. Additionally, it also works with detached heads (useful when rebasing):

hecate src/zsh@master/Doc/Zsh% git checkout HEAD~42
...
hecate src/zsh@master~42/Doc/Zsh%

For free, you get some feedback when bisecting:

hecate ~/src/zsh@master% git bisect bad
hecate ~/src/zsh@bisect/bad% git bisect good HEAD~42
hecate ~/src/zsh@bisect/bad~21% git bisect good
hecate ~/src/zsh@bisect/bad~5% git bisect reset
hecate ~/src/zsh@master%

This is the code in all its glory:

# gitpwd - print %~, limited to $NDIR segments, with inline git branch
NDIRS=2
gitpwd() {
  local -a segs splitprefix; local prefix gitbranch
  segs=("${(Oas:/:)${(D)PWD}}")

  if gitprefix=$(git rev-parse --show-prefix 2>/dev/null); then
    splitprefix=("${(s:/:)gitprefix}")
    branch=$(git name-rev --name-only HEAD 2>/dev/null)
    if (( $#splitprefix > NDIRS )); then
      print -n "${segs[$#splitprefix]}@$branch "
    else
      segs[$#splitprefix]+=@$branch
    fi
  fi

  print "${(j:/:)${(@Oa)segs[1,NDIRS]}}"
}

Perhaps it turned out to be a bit more challenging than expected. ;) Integration into the prompt is trivial, however:

function cnprompt6 {
  case "$TERM" in
    xterm*|rxvt*)
      precmd() {  print -Pn "\e]0;%m: %~\a" }
      preexec() { printf "\e]0;$HOST: %s\a" $1 };;
  esac
  setopt PROMPT_SUBST
  PS1='%B%m%(?.. %??)%(1j. %j&.)%b $(gitpwd)%B%(!.%F{red}.%F{yellow})%#${SSH_CLIENT:+%#} %b'
  RPROMPT=''
}

cnprompt6

homegit

For the last five years I have used Git to manage my dotfiles and I use the repository on a plethora of machines.

I found the following zsh alias to be the simplest and best method to use Git for this purpose:

alias homegit="GIT_DIR=~/prj/dotfiles/.git GIT_WORK_TREE=~ git"

Why not a function? Because an alias will make zsh autocomplete homegit just like it completes git already, without any additional work.

Why not a ~/.git? I decided against it because I didn’t want to accidentally commit stuff from any subdirectory and feared a git clean could wipe my sweet home directory.

The homegit approach works very well for me and I have not felt a need for more complex solutions which symlink dotfiles or copy them around.

Note that the git-* scripts presented here can be called transparently from homegit as well, e.g. with homegit attic. And since $GIT_DIR is set in the environment, the scripts can just call git and will just work correctly!

411 commits as of now tell me I perhaps should scale back customizing stuff all the time, but it can be very helpful indeed to see how things changed over time. Also, tracking changes other programs make to your files (and being able to revert them) is totally worth it.

git trail

One of the newest additions to my Git zoo is git trail, a tool I wanted for years, really. With many branches, it’s easy to get confused about what branched off where and what actually is part of this topic branch and whether this topic branch has been merged but then forgotten or…

Perhaps you feel my pain. Perhaps you tried git show-branch once to get an overview of such a mess, but I feel it’s easier to see stereographic projections of a T-Rex in its output than the state of your branches.

Thus I wrote git-trail, which shows how to reach commits in the current branch from other branches. Since we don’t have enough local branches to make it interesting, lets show remote branches too (-r):

hecate tmp/rack@master% git trail -r
2013-01-04 7e1f081 master
2013-01-04 7e1f081 remotes/origin/HEAD
2013-01-04 1e75faa remotes/origin/hijack~2
2013-01-04 1e75faa remotes/origin/master~1
2012-11-03 1824547 remotes/origin/unstandard_uri_escape~1
2012-03-18 7d7977f remotes/origin/rack-1.4~77
2011-05-22 a50dda5 remotes/origin/rack-1.3~99
2010-06-15 dc6b54e remotes/origin/rack-1.2~38
2010-01-03 e6ebd83 remotes/origin/rack-1.1~23
2009-04-25 d221938 remotes/origin/rack-1.0~24
2009-01-05 7fed4c7 remotes/origin/rack-0.9~15
2008-08-09 e9f9f27 remotes/origin/rack-0.4~6

What you see is the first common commit between every branch and the current branch, together with the commit date. If the branch is listed without suffixes, it is completely included. Else, you effectively see how the branch diverges. For example, in rack-1.4, there have been 77 patches since branching from master. The feature branch hijack consists of two commits. Lets look at the view from that feature branch:

hecate tmp/rack@master% git trail -r origin/hijack
2013-01-04 8a311fb remotes/origin/hijack
2013-01-04 1e75faa master~1
2013-01-04 1e75faa remotes/origin/HEAD~1
2012-11-03 1824547 remotes/origin/unstandard_uri_escape~1
2012-03-18 7d7977f remotes/origin/rack-1.4~77
2011-05-22 a50dda5 remotes/origin/rack-1.3~99
2010-06-15 dc6b54e remotes/origin/rack-1.2~38
2010-01-03 e6ebd83 remotes/origin/rack-1.1~23
2009-04-25 d221938 remotes/origin/rack-1.0~24
2009-01-05 7fed4c7 remotes/origin/rack-0.9~15
2008-08-09 e9f9f27 remotes/origin/rack-0.4~6

We see that there have been commits to master since hijack was branched, and we should perhaps rebase hijack if we wanted to submit it.

Let’s say we simply merged it into master:

hecate tmp/rack@master% git merge origin/hijack
...
hecate tmp/rack@master% git trail -r
2013-01-06 68de794 master
2013-01-04 8a311fb remotes/origin/hijack
2013-01-04 7e1f081 remotes/origin/HEAD
2012-11-03 1824547 remotes/origin/unstandard_uri_escape~1
2012-03-18 7d7977f remotes/origin/rack-1.4~77
...

Now hijack appears undecorated: it is completely contained in the current branch history.

Let’s say we work on the other feature branch next, unstandard_uri_escape:

hecate tmp/rack@master% git checkout unstandard_uri_escape
hecate tmp/rack@unstandard_uri_escape% git trail
2012-11-03 decaa23 unstandard_uri_escape
2012-11-03 1824547 master~10^2~1

We can now rebase it to make it a proper child of master:

hecate tmp/rack@unstandard_uri_escape% git rebase master
hecate tmp/rack@unstandard_uri_escape% git trail
2013-01-06 92b40fa unstandard_uri_escape
2013-01-06 c30da33 master

And then master can be fast-forwarded:

hecate tmp/rack@unstandard_uri_escape% git checkout master
hecate tmp/rack@master% git trail
2013-01-06 c30da33 master
2013-01-06 c30da33 unstandard_uri_escape~1
hecate tmp/rack@master% git merge unstandard_uri_escape 
Updating c30da33..92b40fa
Fast-forward
...
hecate tmp/rack@master% git trail
2013-01-06 92b40fa master
2013-01-06 92b40fa unstandard_uri_escape

I hope this exposed how git trail helps me to keep track of dealing with branches.

git neck

The perfect match for git-trail is git-neck, which show commits from the HEAD until the first branching point… that should explain the name.

So, what is the “neck” of our master branch as above?

hecate tmp/rack@master% git neck -r
92b40fa Add a decoder that supports ECMA unicode uris
c30da33 Merge remote-tracking branch 'origin/hijack'
7e1f081 Merge pull request #480 from udzura/master
3edd1e8 Add a rackup option for one-liner rack app server
6d41179 Extract Builder.new_from_string from Builder.parse_file

Likewise, let’s have a look at that remote feature branch sticking around:

% git neck -r origin/unstandard_uri_escape
decaa23 Add a decoder that supports ECMA unicode uris

It was just a single commit. We can also look at the neck of an old release branch:

hecate tmp/rack@master% git neck -r origin/rack-0.4
92f79ea Make Rack::Lint::InputWrapper delegate size method to underlying IO object.
e33cc65 Update to version 0.4
ab9a95e Fix packaging script
1ccdf73 Update README
1b56583 Document REQUEST_METHOD future changes
f0977a8 Disarm and document Content-Length checking in Rack::Lint for 0.4

And we see the 6 commits that are only in rack-0.4.

If you remember the situation before merging the feature branches:

hecate tmp/r2@master% git trail -r
2013-01-04 7e1f081 master
2013-01-04 7e1f081 remotes/origin/HEAD
2013-01-04 1e75faa remotes/origin/hijack~2
2013-01-04 1e75faa remotes/origin/master~1
2012-11-03 1824547 remotes/origin/unstandard_uri_escape~1

Here, the neck is the part until master forked off:

hecate tmp/r2@master% git neck -r
7e1f081 Merge pull request #480 from udzura/master
3edd1e8 Add a rackup option for one-liner rack app server
6d41179 Extract Builder.new_from_string from Builder.parse_file

git neck is most useful if you are working in a feature branch which no other branch forks off, because then the neck goes until where you forked it.

Using git diff without Git

At last, another small trick: git diff works between any two files (or directories), even if you don’t use Git at all to track them. But you gain some advantages over regular diff, like --word-diff, --color or --stat without having additional tools beyond Git installed.

Also, you can use git diff --binary to generate efficient binary deltas which you can apply again provided you have the unpatched file. (Possibly you need to edit the patch to make both filenames the same, so git apply finds everything.)

NP: Sophie Hunger—What it is

Copyright © 2004–2022