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