-
Notifications
You must be signed in to change notification settings - Fork 71
/
git-draw
executable file
·369 lines (343 loc) · 13 KB
/
git-draw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
#!/bin/sh
#
# NAME
# git-draw - draws nearly the full content of a tiny git repository as a graph
#
# SYNOPSIS
# git-draw [OPTION]...
#
# PREREQUISITES
# You don't need graphviz or imagemagick if you use git-draw with certain
# options.
#
# - perl
# - graphviz (http://www.graphviz.org/)
# - imagemagick (http://www.imagemagick.org)
#
# If you have apt you can install these with:
#
# sudo apt-get install perl graphviz imagemagick
#
# DESCRIPTION
# git-draw is composed of three main steps, where the 2nd and 3rd are just
# for convenience and are not part of git-draw's core responsibility.
#
# 1) A .dot file describing the repository's content as a graph is created.
# 2) dot (see graphviz) is called to produce an image out of that .dot file.
# 3) display (see imagemagick) is called to display that image.
#
# Because git-draw currently is quite dumb, the current working directory
# must be at the root of the working tree of your project, i.e. the
# directory which contains the .git directory.
#
# The intention is to help learning Git's basic concepts (references, Git
# objects, SHA-1 checksum over content as id). Virtually all information
# concerning Git's basic concepts is contained in the drawing. Thus git-draw
# is aimed at tiny toy Git repositories and at users with an engineer
# background, i.e. users which are not scared off by terms like checksum,
# references aka pointers and graphs.
#
# OPTIONS
# -p, --print-only
# Only prints the .dot file to STDOUT. Mutually exclusive with
# --image-only.
#
# -i, --image-only
# Only generates an image of the graph, and a .dot file beforehand.
# Mutually exclusive with --print-only.
#
# AUTHOR
# Written by Florian Kaufmann <sensorflo@gmail.com>
#
# COPYRIGHT
# Florian Kaufmann 2014. License GPLv3+: GNU GPL version 3 or later
# <http://gnu.org/licenses/gpl.html>. This is free software: you are free to
# change and redistribute it. There is NO WARRANTY, to the extent permitted
# by law.
#
# SEE ALSO
# git(1)
# $1 = exit code
print_usage_and_exit() {
cat <<EOF
Usage: git-draw [--help] [-p|--print-only] [-i|--image-only]"
-p|--print-only Only print .dot to stdout."
-i|--image-only Only create an image, and a .dot file beforehand."
The primary documentation is at the top of the git-draw script itself, see
$(type -p git-draw)
EOF
exit $1
}
# TODO
# - Layout graph so commit DAG is top-down and on the left, probably using
# graphviz's ranks.
# - Auto-number output files so user can see / compare the different states
# of the repository was in.
# - Optionally use temporary files which the user does not directly see
# - Optionally omit drawing the content of all/certain objects/refs so a
# bit
# larger than tiny repositories still draw usefully.
# - tree objects / reflogs shall fan out graph edges at corresponding line
# in
# content
# - Amend type cell (top left) and id cell (top right) with the prefix
# 'type=' and 'id=' to be very clear. Optionally turn off with options.
# - add a graph title / caption which tells the git command(s) since the
# last
# version of the repository content.
# - Provide options to choose the tool to display the image outputted by
# dot.
# - Support for all those possible other layouts of git repositories. Bare
# repository, a .git file at the root pointing to the real directory,
# environment variables etc. Most probably by using git commands
# exclusive opposed to directly access files within .git, or by asking
# git for the path to the repository.
# - Read gitrepository-layou(1) to find out more things which could be
# displayed
# - To be on the safe side, quote _all_ non-alphanumeric characters in
# label strings of a dot 'program'.
# - Draw multiple repos in one image. Usefull to demonstrate distributed git.
# - Add a fourth element (beside type/id/content) to the things: also the
# config properties. E.g for a branch it's remote tracking branch.
# - Draw packed objects and refs in a subgraph respectively.
# - Allow that the current working directory can be any subdirectory of a
# git project. Similarly, allow to specify the git repository via command
# line arguments.
abbreviate_sha1() {
# BUG: in a blob or commit-msg, would also replace what looks like a sha1
perl -pe 's/([0-9a-f]{40})/substr(`git rev-parse --short=4 $1`,0,-1)/eg'
}
ls_all_objects() {
# unpacked objects
find .git/objects/ -type f |
perl -ne 'print "$1$2\n" if m@^.*/([a-f0-9]{2})/([a-f0-9]{38})@'
# packed objects
# note that the .idx is not always present
find .git/objects/pack/ -iname '*.idx' | while read idxfile; do
cat $idxfile | git show-index |
perl -pe 's@^.*?([a-f0-9]{40}).*$@$1@'
done
}
ls_all_refs() {
# todo: refs like MERGE_HEAD are still not printed
git show-ref --abbrev=4
cat .git/HEAD | perl -ne '
m/^(?:ref: )?(.*?)$/;
if ($1 =~ m/([a-f0-9]{40})/) {
$idshort = substr(`git rev-parse --short=4 $1`,0,-1);
}
else { $idshort = $1 }
print $idshort . " HEAD\n";'
}
ls_all_objects_short() {
ls_all_objects | while read sha1; do
git rev-parse --short=4 $sha1
done
}
print_dot_objects() {
ls_all_objects_short | while read id; do
dotid="_$id"
object_type=$(git cat-file -t $id)
objcontent=$(git cat-file -p $id | abbreviate_sha1 |
perl -pe 's/([^a-zA-Z0-9\n\r])/\\$1/g; s/(\r?\n)?$/\\l/;')
case $object_type in
commit) fillcolor=palegreen1;;
tag) fillcolor=lightyellow;;
*) fillcolor=white;;
esac
echo " $dotid [fillcolor=$fillcolor, style=\"filled,rounded\", "\
"label=\"{{obj:$object_type|$id}|$objcontent}\"]"
# todo: use git's commands to extract the object references
# BUG: must escape stuff that .dot interprets (\n,\l,\l,|,{},...)
# BUG(obsolete when using git's cmds): cannot deal with multiple sha1 on one line
# BUG(obsolete when using git's cmds): in a blob or commit-msg, would also
# replace what looks like a sha1
git cat-file -p $id |
perl -ne 'print " '$dotid' -> _" .
`git rev-parse --short=4 $1` if /([a-f0-9]{40})/'
done
git fsck --cache --unreachable --dangling 2>/dev/null |
perl -ne 'print " _" . substr(`git rev-parse --short=4 $1`,0,-1) .
" [style=dotted]\n" if /^(?:dangling|unreachable)\b.*?([a-f0-9]{40})/'
}
print_dot_references() {
ls_all_refs |
perl -ne '
if (m@(\S+)\s+(\S+?)$@) {
$me = $2;
$other = $1;
$dotid_me = "_" . (($tmp = $me) =~ s@([^a-zA-Z0-9_])@___@g,$tmp);
$dotid_other = "_" . (($tmp = $other) =~ s@([^a-zA-Z0-9_])@___@g,$tmp);
($otherquoted = $other) =~ s/([^a-zA-Z0-9\n])/\\$1/g;
($mequoted = $me) =~ s/([^a-zA-Z0-9\n])/\\$1/g;
$reftype = "";
$fillcolor = "gray";
$configmetadata = "";
if ($me =~ m@^refs/heads/@) {
$reftype = ":local branch";
($me_short = $me) =~ s@^refs/heads/@@;
$remote = substr(`git config --get branch\\.$me_short\\.remote`,0,-1);
$merge = substr(`git config --get branch\\.$me_short\\.merge`,0,-1);
if ($remote && $merge) {
if ($remote eq ".") {
$dotid_merge = "_" . (($tmp = $merge) =~ s@([^a-zA-Z0-9_])@___@g,$tmp);
} else {
$dotid_merge_core = $merge;
$dotid_merge_core =~ s@^refs/heads/@@;
$dotid_merge_core =~ s@([^a-zA-Z0-9_])@___@g;
$dotid_remote = (($tmp = $remote) =~ s@([^a-zA-Z0-9_])@___@g,$tmp);
$dotid_merge = "_refs___remotes___${dotid_remote}___${dotid_merge_core}";
}
print " $dotid_me -> $dotid_merge [style=dotted, color=gray, fontcolor=gray, " .
"label=\"upstream branch\"]\n";
$tmp = "remote = $remote\nmerge = $merge\n";
$tmp =~ s/([^a-zA-Z0-9\n])/\\$1/g; # quote for dot
$tmp =~ s/\n/\\l/g; # \l instead \n newline
$configmetadata = "|$tmp"
}
}
elsif ($me =~ m@^refs/remotes/@) {
$reftype = ":remote tracking branch";
$fillcolor = "yellow";
}
elsif ($me =~ m@^refs/tags/@) {
$reftype = ":tag";
$fillcolor = "lightyellow";
}
if ( ($other !~ m/^[a-f0-9]+$/) &&
0!=system("git show-ref --verify --quiet -- $other") ) {
print " $dotid_me [style=filled, fillcolor=red, " .
"label=\"{{ref$reftype|$mequoted}|" .
"$otherquoted (referee does not exist)\\l$configmetadata}\"]\n";
} else {
if ($me eq "HEAD") {
$fillcolor="gray30";
$fontcolorelement = "fontcolor=white, ";
}
print " $dotid_me [style=filled, fillcolor=$fillcolor, $fontcolorelement " .
"label=\"{{ref$reftype|$mequoted}|$otherquoted\\l$configmetadata}\"]\n";
print " $dotid_me -> $dotid_other\n";
}
}'
}
print_dot_ref_logs() {
firstiter="non-empty-string" # i.e. true
git show-ref --abbrev=4 |
# The following code depends upon HEAD being the last in the list
perl -pe 's@^.*?(\brefs/\S*)$@$1@; END { print "HEAD\n";}' |
while read refname; do
# work around the problem that 'git reflog show HEAD' results in an
# error when HEAD contains refs/heads/master but refs/heads/master does
# not exist, which is the case after 'git init'.
if [ ! \( "$firstiter" -a \( "$refname" = HEAD \) \) ]; then
# 8eb068f master@{11}: commit: tempo-ext: new version
git reflog show $refname | perl -ne '
BEGIN {
$refname = "'$refname'";
$dotid_reflog = "reflog_" . (($tmp = $refname) =~ s@([^a-zA-Z0-9_])@___@g,$tmp);
}
if (m/^([a-f0-9]+).*@\{\d+\}: (.*?)$/) { $id_any=$1; $msg=$2; }
elsif (m/^([a-f0-9]+)/) { $id_any=$1; $msg="<lastentry>"; }
$id_short = substr(`git rev-parse --short=4 $id_any`,0,-1);
print " $dotid_reflog -> _$id_short [color=gray90]\n";
s/^[a-f0-9]+//; # strip sha1; $id_short will be used instead
s/([^a-zA-Z0-9\n])/\\$1/g; # quote for dot
s/(\r?\n)?$/\\l/; # \l instead \n newline, and ensure \l at
# end of content
$content = $content . $id_short . $_;
END {
$trailing = (substr($content,-2) eq "\\l") ? "" : "\\l";
print " $dotid_reflog [color=gray90, fontcolor=gray, " .
"label=\"{{reflog|logs/$refname}|$content$trailing}\"]\n";
}'
fi
firstiter="" # empty stringt, i.e. false
done
}
print_dot_index() {
git ls-files --stage --abbrev=4 | perl -ne '
if (/^[0-9]+\s+([a-f0-9]+)/) {
print " index -> _$1\n";
s/([^a-zA-Z0-9\r\n])/\\$1/g; # quote for dot
s/(\r?\n)?$/\\l/; # \l instead \n newline, and ensure \l at
# end of content
$content = $content . $_;
}
END {
$trailing = (substr($content,-2) eq "\\l") ? "" : "\\l";
print " index [style=filled, fillcolor=lightcyan, " .
"label=\"{{index}|$content$trailing}\"]\n";
}'
}
print_dot() {
echo "digraph structs {"
echo " node [shape=record,fontsize=11];"
echo " subgraph cluster_0 {"
echo " color=gray80;"
echo " label = \"legend\\l\";"
echo " legend_node [label=\"{{type:subtype|id/name}|content\\l|metadata from config\\l}\"]"
echo " }"
print_dot_objects
print_dot_references
print_dot_ref_logs
print_dot_index
echo "}"
}
# process options
dotfilename=git-draw.dot
imgfilename=git-draw.png
if [ $# = 0 ]; then
:
elif [ \( $# = 1 \) -a \( "$1" = "-p" -o "$1" = "--print-only" \) ] ; then
print_only=1
elif [ \( $# = 1 \) -a \( "$1" = "-i" -o "$1" = "--image-only" \) ] ; then
image_only=1
elif [ \( $# = 1 \) -a \( "$1" = "--help" \) ] ; then
print_usage_and_exit 0
else
echo "Invalid options" >&2
print_usage_and_exit 1 >&2
exit 1
fi
# check preconditions
if [ ! -d .git ]; then
echo "Not a git repository" >&2
exit 1
fi
if ! which perl >&2 >/dev/null; then
cat >&2 <<EOF
perl not found. If you have apt-get, you can install it with 'sudo apt-get
install perl'.
EOF
exit 1
fi
# generate .dot file
if [ "$print_only" = 1 ] ; then
print_dot
exit 0
fi
print_dot >"$dotfilename" || exit 1
# build and image out of the .dot file
if ! which dot >&2 >/dev/null; then
cat >&2 <<EOF
dot (part of graphviz) not found. Either use the --print-only option, or
install graphviz. If you have apt-get, you can do that with 'sudo apt-get
install graphviz'.
EOF
exit 1
fi
dot -Tpng "$dotfilename" > "$imgfilename" || exit 1
# display image
if [ "$image_only" = 1 ] ; then
exit 0
fi
if ! which display >&2 >/dev/null; then
cat >&2 <<EOF
display (part of imagemagick) not found. Image '$imgfilename' was generated,
but I cannot display it. Either use the --image-only option, or install
imagemagick. If you have apt-get, you can do that with 'sudo apt-get install
imagemagick'.
EOF
exit 1
fi
display "$imgfilename"