├── .gitignore
├── LICENSE
├── README.md
├── example
├── README.md
├── example.sh
├── example2.sh
├── svg-pan-zoom.min.js
└── webserver.py
├── git2dot.py
├── pkg
├── Makefile
└── README.md
└── test
├── README.txt
├── test-utils.sh
├── test.sh
├── test01.dot.gold
├── test01.dot.keep
├── test01.sh
├── test02.dot.gold
├── test02.dot.keep
├── test02.sh
├── test03.dot.gold
├── test03.dot.keep
├── test03.sh
├── test04.dot.gold
├── test04.dot.keep
├── test04.sh
├── test05.dot.gold
├── test05.dot.keep
├── test05.sh
├── test06.dot.gold
├── test06.dot.keep
├── test06.sh
├── test07.dot.gold
├── test07.dot.keep
├── test07.sh
├── test08.dot.gold
├── test08.dot.keep
├── test08.sh
├── test09.dot.gold
├── test09.dot.keep
├── test09.sh
├── test10.dot.gold
├── test10.dot.keep
└── test10.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | # Ignore generated files.
3 | example/example*.dot*
4 | example/example*.html
5 | example/README
6 | test/test*.dot
7 | test/test*.dot.html
8 | test/test*.dot.png
9 | test/test*.dot.svg
10 | test/test*.html
11 | test/test*.difflog
12 | test/test*.log
13 | test/test*.filter
14 |
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Joe Linoff
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # git2dot
2 | [](https://github.com/jlinoff/git2dot/releases)
3 |
4 | Visualize a git repository using the graphviz dot tool optionally with pan and zoom.
5 |
6 | It is useful for understanding how git works in detail. You can use
7 | it to analyze repositories before and after operations like merge and
8 | rebase to really get a feeling for what happens. It can also be used
9 | for looking at subsets of history on live repositories.
10 |
11 | > I have created a docker image to allow git2dot to be used without installing graphviz on the target system. See https://github.com/jlinoff/docker-images/tree/master/git2dot for details.
12 |
13 | Here is an example that shows the PNG file generated by test04 in the
14 | test directory.
15 |
16 |
17 |
18 | Here is a quick rundown of what you are seeing:
19 |
20 | 1. The bisque (tan) nodes are commits. Each commit has the short id, the commit date, the subject (truncated to 32 characters) and the change-id (if it exists). The fields are the same for merged and squashed nodes as well.
21 | 2. The light red nodes are merged nodes. They are commits that resulted from a merge (they have 2 or more children).
22 | 3. The dark red nodes are squashed nodes. They are the end-points of squashed commit chains. Squashed commit chains do not have any branches, tags, change-ids or merges. They are just a long chain of work. If you turn off squashing, you will see 6 additional commit nodes and the two read squashed nodes will disappear.
23 | 4. The bluish boxes underneath commit and merge nodes are the branches associated with the commit. There is one box for each branch.
24 | 5. The light purple boxes above the commit and merge nodes are the tags associated with the commit. There is one box for each tag.
25 | 6. The arrows on the edges show the back (parent) pointer relationships from the repo. Git does not have child references.
26 | 7. The yellow box at the bottom is an optional graph label with custom text.
27 |
28 | It works by running over the .git repository in the current directory
29 | and generating a commit relationship DAG that has both parent and
30 | child relationships.
31 |
32 | The generated graph shows commits, tags and branches as nodes.
33 | Commits are further broken down into simple commits and merged commits
34 | where merged commits are commits with 2 or more children. There is an
35 | additional option that allows you to squash long chains of simple
36 | commits with no branch or tag data.
37 |
38 | It has a number of different options for customizing the nodes,
39 | using your own custom git command to generate the data, keeping
40 | the generated data for re-use, customizing dot directly and
41 | generating graphical output like PNG, SVG or even HTML files.
42 |
43 | Here is an example run:
44 | ```bash
45 | $ cd SANDBOX
46 | $ git2dot.py --png git.dot
47 | $ open -a Preview git.dot.png # on Mac OS X
48 | $ display git.dot.png # linux
49 | ```
50 | If you want to create a simple HTML page that allows panning and
51 | zooming of the generated SVG then use the --html option like
52 | this.
53 | ```bash
54 | $ cd SANDBOX
55 | $ git2dot.py --svg --html ~/web/index.html ~/web/git.dot
56 | $ $ ls ~/web
57 | git.dot git.dot.svg git.html svg-pan-zoom.js
58 | $ cd ~/web
59 | $ python -m SimpleHTTPServer 8090 # start server
60 | $ # Browse to http://localhost:8090/git.dot.svg
61 | ```
62 |
63 | It assumes that existence of svg-pan-zoom.js from the
64 | https://github.com/ariutta/svg-pan-zoom package.
65 |
66 | The output is pretty customizable. For example, to add the subject and
67 | commit date to the commit node names use `-l '%s|%cr'`. The items come
68 | from the git format placeholders or variables that you define using
69 | `-D`. The | separator is used to define the end of a line. The maximum
70 | width of each line can be specified by `-w`. Variables are defined by `-D`
71 | and come from text in the commit message. See `-D` for more details.
72 |
73 | You can customize the attributes of the different types of nodes and
74 | edges in the graph using the -?node and -?edge attributes. The table
75 | below briefly describes the different node types:
76 |
77 | | Node Type | Brief Description |
78 | | --------- | ----------------- |
79 | | bedge | Edge connecting to a bnode. |
80 | | bnode | Branch node associated with a commit. |
81 | | cnode | Commit node (simple commit node). |
82 | | mnode | Merge node. A commit node with multiple children. |
83 | | snode | Squashed node. End point of a sequence of squashed nodes. |
84 | | tedge | Edge connecting to a tnode. |
85 | | tnode | Tag node associated with a commit. |
86 |
87 | If you have long chains of single commits use the `--squash` option to
88 | squash out the middle ones. That is generally helpful for filtering
89 | out extraneous commit details for moderately sized repos.
90 |
91 | If you find that dot is placing your bnode and tnode nodes in odd
92 | places, use the `--crunch` option to collapse the bnode nodes into
93 | a single node and the tnodes into a single node for each commit.
94 |
95 | If you want to limit the analysis to commits between certain dates,
96 | use the `--since` and `--until` options.
97 |
98 | If you want to limit the analysis to commits in a certain range use
99 | the `--range` option.
100 |
101 | If you want to limit the analysis to a small set of branches or tags
102 | you can use the `--choose-branch` and `--choose-tag` options. These options
103 | prune the graph so that only parents of commits with the choose branch
104 | or tag ids are included in the graph. This gives you more detail
105 | controlled that the git options allowed in the --range command. It
106 | is very useful for determining where branches occurred.
107 |
108 | You can choose to keep the git output to re-use multiple times with
109 | different display options or to share by specifying the `-k` (`--keep`)
110 | option.
111 |
112 | Use the `-h` option to get detailed information about the available options.
113 |
114 | ## Example
115 | The following example shows how to use git2dot by creating a git repository
116 | from scratch and then running the tool to create images.
117 |
118 | First we create a repository with a bunch of commits and branches.
119 | ```bash
120 | git init
121 |
122 | echo 'A' >README
123 | git add README
124 | git commit -m 'master - first'
125 | sleep 1
126 |
127 | echo 'B' >>README
128 | git add README
129 | git commit -m 'master - second' -m 'Change-Id: I001'
130 | sleep 1
131 |
132 | # tag the basis for all of the branches
133 | git tag -a 'v1.0' -m 'Initial version.'
134 | git tag -a 'v1.0a' -m 'Another version.'
135 |
136 | git checkout -b branchX1
137 | git checkout master
138 | git checkout -b branchX2
139 |
140 | git checkout master
141 | git checkout -b branchA
142 | echo 'C' >> README
143 | git add README
144 | git commit -m 'branchA - first'
145 | sleep 1
146 |
147 | echo 'B' >> README
148 | git add README
149 | git commit -m 'branchA - second' -m 'Change-Id: I001'
150 | sleep 1
151 |
152 | git checkout master
153 | git checkout -b branchB
154 | echo 'E' >> README
155 | git add README
156 | git commit -m 'branchB - first'
157 | sleep 1
158 |
159 | echo 'F' >> README
160 | git add README
161 | git commit -m 'branchB - second'
162 | sleep 1
163 |
164 | echo 'B' >> README
165 | git add README
166 | git commit -m 'branchB - third' -m 'Change-Id: I001'
167 | sleep 1
168 |
169 | echo 'H' >> README
170 | git add README
171 | git commit -m 'branchB - fourth' -m 'Change-Id: I002'
172 | sleep 1
173 |
174 | echo 'I' >> README
175 | git add README
176 | git commit -m 'branchB - fifth'
177 | sleep 1
178 |
179 | echo 'J' >> README
180 | git add README
181 | git commit -m 'branchB - sixth'
182 | sleep 1
183 |
184 | echo 'K' >> README
185 | git add README
186 | git commit -m 'branchB - seventh'
187 | sleep 1
188 |
189 | git checkout master
190 | echo 'L' >> README
191 | git add README
192 | git commit -m 'master - third'
193 | ```
194 |
195 | You can verify the repo structure using something like `git log`.
196 | ```bash
197 | $ git log --graph --oneline --decorate --all
198 | * da0165b (HEAD -> master) master - third
199 | | * 8e3cf50 (branchB) branchB - seventh
200 | | * e0420c1 branchB - sixth
201 | | * f51497b branchB - fifth
202 | | * cee652e branchB - fourth
203 | | * 2fc95e6 branchB - third
204 | | * 9c654d8 branchB - second
205 | | * 33fbc07 branchB - first
206 | |/
207 | | * 20ea3d2 (branchA) branchA - second
208 | | * 71a0d0c branchA - first
209 | |/
210 | * ecdc7dc (tag: v1.0a, tag: v1.0, branchX2, branchX1) master - second
211 | * c8ae444 master - first
212 | ```
213 |
214 | Now run the git2dot tool to generate PNG, HTML and SVG files.
215 | ```bash
216 | $ git2dot.py --png --svg --html example.html example.dot
217 | $ ls -1 example.*
218 | example.dot
219 | example.dot.png
220 | example.dot.svg
221 | example.html
222 | ```
223 |
224 | You can now view the PNG and SVG files using local tools.
225 | ```bash
226 | $ open -a Preview example.dot.png # on Mac
227 | $ display example.dot.png # on Linux
228 | ```
229 |
230 | To view the generated SVG file with pan and zoom you must download
231 | the svg-pan-zoom.min.js file from https://github.com/ariutta/svg-pan-zoom
232 | and copy into the current directory.
233 |
234 | ```bash
235 | $ cp ~/work/svg-pan-zoom-3.4.1/dist/svg-pan-zoom.min.js .
236 | $ ls -1 example* svg*
237 | example.dot
238 | example.dot.png
239 | example.dot.svg
240 | example.html
241 | svg-pan-zoom.min.js
242 | ```
243 |
244 | Now you need to start a server.
245 |
246 | ```bash
247 | $ python -m SimpleHTTPServer 8090
248 | ```
249 |
250 | After that you can browse to http://localhost:8090/example.html and you will see this.
251 |
252 |
253 |
254 | As you can see, there is a long chain of commits, to run it again using the `--squash` option.
255 |
256 | ```bash
257 | $ git2dot.py --squash --png --svg --html example1.html example1.dot
258 | ```
259 |
260 | And browse to http://localhost:8090/example1.html and you will see this.
261 |
262 |
263 |
264 | Which is a cleaner view of the overall structure.
265 |
266 | You will also note that there are two branches and two tags on *ecdc7dc*. They can be collapsed using the `--crunch` option like this.
267 |
268 | ```bash
269 | $ git2dot.py --crunch --squash --png --svg --html example1.html example1.dot
270 | ```
271 | When you browse to http://localhost:8090/example2.html and you will see this.
272 |
273 |
274 |
275 | For such a small graph the crunch operation doesn't really make things simpler but for larger graphs where dot may move the branch and tag information around, it can be a much cleaner view.
276 |
277 | ## Example 2 - pruning the graph
278 |
279 | There are two more options you will want to think about for making large graphs
280 | more readable: `--choose-branch` and `--choose-tag`. As described earlier,
281 | they prune the graph so that it only considers the parent chains of the
282 | specified branches or tags. This can be very useful to determining where
283 | branches occurred.
284 |
285 | This example shows how it works.
286 |
287 | First you create a repository like this.
288 |
289 | ```bash
290 | git init
291 |
292 | echo 'A' >example2.txt
293 | git add example2.txt
294 | git commit -m 'master - first'
295 | sleep 1
296 |
297 | echo 'B' >>example2.txt
298 | git add example2.txt
299 | git commit -m 'master - second'
300 | sleep 1
301 |
302 | # tag the basis for all of the branches
303 | git tag -a 'v1.0' -m 'Initial version.'
304 | git tag -a 'v1.0a' -m 'Another version.'
305 |
306 | git checkout -b branchX1
307 | git checkout master
308 | git checkout -b branchX2
309 |
310 | git checkout master
311 | git checkout -b branchA
312 | echo 'C' >> example2.txt
313 | git add example2.txt
314 | git commit -m 'branchA - first'
315 | sleep 1
316 |
317 | echo 'D' >> example2.txt
318 | git add example2.txt
319 | git commit -m 'branchA - second'
320 | sleep 1
321 |
322 | echo 'E' >> example2.txt
323 | git add example2.txt
324 | git commit -m 'branchA - third'
325 | sleep 1
326 |
327 | echo 'F' >> example2.txt
328 | git add example2.txt
329 | git commit -m 'branchA - fourth'
330 | sleep 1
331 |
332 | git checkout master
333 | git checkout -b branchB
334 | echo 'G' >> example2.txt
335 | git add example2.txt
336 | git commit -m 'branchB - first'
337 | sleep 1
338 |
339 | echo 'H' >> example2.txt
340 | git add example2.txt
341 | git commit -m 'branchB - second'
342 | sleep 1
343 |
344 | echo 'I' >> example2.txt
345 | git add example2.txt
346 | git commit -m 'branchB - third'
347 | sleep 1
348 |
349 | echo 'J' >> example2.txt
350 | git add example2.txt
351 | git commit -m 'branchB - fourth'
352 | sleep 1
353 | git tag -a 'v2.0a' -m 'Initial version.'
354 |
355 | echo 'K' >> example2.txt
356 | git add example2.txt
357 | git commit -m 'branchB - fifth'
358 | sleep 1
359 |
360 | echo 'L' >> example2.txt
361 | git add example2.txt
362 | git commit -m 'branchB - sixth'
363 | sleep 1
364 |
365 | echo 'M' >> example2.txt
366 | git add example2.txt
367 | git commit -m 'branchB - seventh'
368 | sleep 1
369 |
370 | git checkout master
371 | echo 'N' >> example2.txt
372 | git add example2.txt
373 | git commit -m 'master - third'
374 | sleep 1
375 |
376 | echo 'O' >> example2.txt
377 | git add example2.txt
378 | git commit -m 'master - fourth'
379 | ```
380 |
381 | You can confirm its layout like this.
382 |
383 | ```bash
384 | $ $ git log --graph --oneline --decorate --all --topo-order
385 | * e4bb699 (HEAD -> master, origin/master, origin/HEAD) Add --topo-order to the default range
386 | * 01bb6de Update comments
387 | * c0bf31e Update comments
388 | * f6f32ac (tag: v0.4) Update to describe --choose-* functionality
389 | * 81fc41c v0.4 - added --choose-* support
390 | * c50cded (tag: v0.3) Merge branch 'master' of https://github.com/jlinoff/git2dot
391 | |\
392 | | * 3c52eae Update README.md
393 | * | be89609 Add example
394 | |/
395 | * 680b2e5 Update documentation
396 | * 0b7fed3 Update README.txt
397 | * 47f1430 Initial load
398 | * b4c73c8 Update README.md
399 | * 54632ac Change bedge/tedge color defaults
400 | * 0136d78 Add support for --crunch
401 | * 10eaf83 Update README.md
402 | * 736b75a Fix bug in --align-by-date handling
403 | * 4fac1b8 Fix bug in --align-by-date handling
404 | * fd20bac Fix bug in --align-by-date handling
405 | * e15199f Update README.md
406 | * a4a6fa8 Initial load
407 | * da4e1d3 Update README.md
408 | * ba50fcf Update README.md
409 | * 2a8038c Update README.md
410 | * 800700f Update README.md
411 | * a3a4ae0 Initial commit
412 | ```
413 |
414 | Create the graph without pruning.
415 |
416 | ```bash
417 | $ ../git2dot.py --graph-label 'graph[label="example2 - compressed initial state"]' --crunch --squash --png --svg --html example2-2.html example2-2.dot
418 | ```
419 |
420 |
421 |
422 | Create the graph with pruning.
423 |
424 | ```bash
425 | $ ../git2dot.py --graph-label 'graph[label="example2 - compressed pruned state"]' --choose-branch 'branchA' --choose-tag 'tag: v2.0a' --crunch --squash --png --svg --html example2-4.html example2-4.dot
426 | ```
427 |
428 |
429 |
430 | As you can see, branchB has been completely removed in the second one.
431 |
432 | ## Eat your own dog food
433 |
434 | Here is the generated image of the git2dot development tree for v0.6.
435 |
436 |
437 |
438 | It was generated with this command.
439 |
440 | ```bash
441 | $ ./git2dot.py -s -c --png --graph-label 'graph[label="git2dot v0.6", fontsize="18"]' git.dot
442 | ```
443 |
444 | Here is how I created a pannable and zoomable version of the "eat your own dog food" graph.
445 |
446 | First I created the HTML and SVG files in an example directory. I also created a PNG file for local testing. Note that I ran the `git2dot.py` command in the git2dot repo and directed the output to the example directory.
447 |
448 | ```bash
449 | $ mkdir ~/work/git2dot-zoom-example
450 | $ cd ~/work/git2dot # the repo
451 | $ git2dot.py -s -c -L 'graph[label="\ngit2dot v0.6", fontsize="24"]' --png --svg --html ~/work/git2dot-zoom-example/git.html --choose-tag 'tag: v0.6' ~/work/git2dot-zoom-example/git.dot
452 | $ open -a Preview ~/work/git2dot-zoom-example/git.png
453 | ```
454 |
455 | I then copied over the svg-pan-zoom.min.js file. Without it, panning and zooming cannot work.
456 |
457 | ```bash
458 | $ cd ~/work/git2dot-zoom-example
459 | $ cp ~/work/svg-pan-zoom/dist/svg-pan-zoom.min.js .
460 | ```
461 |
462 | Once the files were in place, I started a simple HTTP server in the same directory that I created the HTML and SVG files.
463 |
464 | ```bash
465 | $ cd ~/work/git2dot-zoom-example
466 | $ python -m SimpleHTTPServer 8081
467 | ```
468 |
469 | I then navigated to `http://localhost:8081/git.html` in a browser and saw this.
470 |
471 |
472 |
473 | After that I panned to the left (left-mouse-button-down and drag) and zoomed in using the mousewheel to see the most recent tag.
474 |
475 |
476 |
477 | ## Hints
478 |
479 | 1. For large graphs consider using the `--squash` option.
480 | 2. For large graphs consider using the svg-pan-zoom zoom() function when the data is loaded to make the nodes visible.
481 | 3. For graphs that have multiple branches and tags on the same commits consider using the `--crunch` option.
482 | 4. If you only want to see the combined history of a few branches or tags (like release branches) consider using the `--choose-branch` and `--choose-tag` options to prune the graph.
483 | 5. Use the `--since` option if you don't care about ancient history.
484 | 6. The `--graph-label` option can be useful and can be very simple: `--graph-label 'graph[label="MY LABEL"]'`.
485 | 7. Read the program help: `-h` or `--help`, there is a lot of useful information there.
486 |
487 | ## Summary data
488 | The generated dot file has summary fields at the end that can be useful for post processing.
489 |
490 | The fields are written as dot comments like this.
491 |
492 | ```
493 | // summary:num_graph_commit_nodes 5
494 | // summary:num_graph_merge_nodes 1
495 | // summary:num_graph_squash_nodes 2
496 | // summary:total_commits 12
497 | // summary:total_graph_commit_nodes 8
498 | ```
499 |
500 | They are described in the table below.
501 |
502 | | Field | Description |
503 | | ----- | ----------- |
504 | | `// summary:num_graph_commit_nodes INT` | The total number of simple commit nodes in the graph. |
505 | | `// summary:num_graph_merge_nodes INT` | The total nummber of merge commit nodes in the graph. |
506 | | `// summary:num_graph_squash_nodes INT` | The total number of squash commit nodes in the graph. |
507 | | `// summary:total_commits INT` | The total number of commits (incuding merges) with no squashing. |
508 | | `// summary:total_graph_commit_nodes INT` | The number of actual commit nodes in the graph. |
509 |
510 | Note that total_commits and total_graph_commit_nodes will be the same if squashing is not specified.
511 |
512 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # Run the example.
2 |
3 | You can use `example.sh` to create a local version of the example.
4 |
5 | After it runs, the PNG, SVG and HTML files are available to view
6 | by starting a webserver.
7 |
8 | > A simple webserver is provided `webserver.py 8090`
9 | > or you can run something like `python -m SimpleHTTPServer 8090`.
10 |
11 | Once the web server is running you can access the data at the following
12 | URLs if you specified port 8090.
13 |
14 | 1. http://localhost:8090/example.html
15 | 2. http://localhost:8090/example1.html
16 | 3. http://localhost:8090/example2.html
17 |
18 | The example2.sh scripts 4 additional HTML files:
19 |
20 | 1. http://localhost:8090/example2-1.html
21 | 2. http://localhost:8090/example2-2.html
22 | 3. http://localhost:8090/example2-3.html
23 | 4. http://localhost:8090/example2-4.html
24 |
25 | See the project README for more information at
26 | https://github.com/jlinoff/git2dot.
27 |
28 |
29 |
--------------------------------------------------------------------------------
/example/example.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # This script generates the example data in the local directory.
4 | # Note that is creates and then deletes a local git repository.
5 | #
6 | rm -rf .git
7 |
8 | git init
9 |
10 | echo 'A' >README
11 | git add README
12 | git commit -m 'master - first'
13 | sleep 1
14 |
15 | echo 'B' >>README
16 | git add README
17 | git commit -m 'master - second' -m 'Change-Id: I001'
18 | sleep 1
19 |
20 | # tag the basis for all of the branches
21 | git tag -a 'v1.0' -m 'Initial version.'
22 | git tag -a 'v1.0a' -m 'Another version.'
23 |
24 | git checkout -b branchX1
25 | git checkout master
26 | git checkout -b branchX2
27 |
28 | git checkout master
29 | git checkout -b branchA
30 | echo 'C' >> README
31 | git add README
32 | git commit -m 'branchA - first'
33 | sleep 1
34 |
35 | echo 'B' >> README
36 | git add README
37 | git commit -m 'branchA - second' -m 'Change-Id: I001'
38 | sleep 1
39 |
40 | git checkout master
41 | git checkout -b branchB
42 | echo 'E' >> README
43 | git add README
44 | git commit -m 'branchB - first'
45 | sleep 1
46 |
47 | echo 'F' >> README
48 | git add README
49 | git commit -m 'branchB - second'
50 | sleep 1
51 |
52 | echo 'B' >> README
53 | git add README
54 | git commit -m 'branchB - third' -m 'Change-Id: I001'
55 | sleep 1
56 |
57 | echo 'H' >> README
58 | git add README
59 | git commit -m 'branchB - fourth' -m 'Change-Id: I002'
60 | sleep 1
61 |
62 | echo 'I' >> README
63 | git add README
64 | git commit -m 'branchB - fifth'
65 | sleep 1
66 |
67 | echo 'J' >> README
68 | git add README
69 | git commit -m 'branchB - sixth'
70 | sleep 1
71 |
72 | echo 'K' >> README
73 | git add README
74 | git commit -m 'branchB - seventh'
75 | sleep 1
76 |
77 | git checkout master
78 | echo 'L' >> README
79 | git add README
80 | git commit -m 'master - third'
81 |
82 | git log --graph --oneline --decorate --all
83 |
84 | ../git2dot.py --png --svg --html example.html example.dot
85 | ../git2dot.py --squash --png --svg --html example1.html example1.dot
86 | ../git2dot.py --crunch --squash --png --svg --html example2.html example2.dot
87 |
88 | osType=$(uname)
89 | case "$osType" in
90 | Darwin)
91 | open -a Preview example.dot.png
92 | open -a Preview example1.dot.png
93 | open -a Preview example2.dot.png
94 | ;;
95 | Linux)
96 | display example.dot.png
97 | display example1.dot.png
98 | display example2.dot.png
99 | ;;
100 | *)
101 | ;;
102 | esac
103 |
104 | cat <example2.txt
11 | git add example2.txt
12 | git commit -m 'master - first'
13 | sleep 1
14 |
15 | echo 'B' >>example2.txt
16 | git add example2.txt
17 | git commit -m 'master - second'
18 | sleep 1
19 |
20 | # tag the basis for all of the branches
21 | git tag -a 'v1.0' -m 'Initial version.'
22 | git tag -a 'v1.0a' -m 'Another version.'
23 |
24 | git checkout -b branchX1
25 | git checkout master
26 | git checkout -b branchX2
27 |
28 | git checkout master
29 | git checkout -b branchA
30 | echo 'C' >> example2.txt
31 | git add example2.txt
32 | git commit -m 'branchA - first'
33 | sleep 1
34 |
35 | echo 'D' >> example2.txt
36 | git add example2.txt
37 | git commit -m 'branchA - second'
38 | sleep 1
39 |
40 | echo 'E' >> example2.txt
41 | git add example2.txt
42 | git commit -m 'branchA - third'
43 | sleep 1
44 |
45 | echo 'F' >> example2.txt
46 | git add example2.txt
47 | git commit -m 'branchA - fourth'
48 | sleep 1
49 |
50 | git checkout master
51 | git checkout -b branchB
52 | echo 'G' >> example2.txt
53 | git add example2.txt
54 | git commit -m 'branchB - first'
55 | sleep 1
56 |
57 | echo 'H' >> example2.txt
58 | git add example2.txt
59 | git commit -m 'branchB - second'
60 | sleep 1
61 |
62 | echo 'I' >> example2.txt
63 | git add example2.txt
64 | git commit -m 'branchB - third'
65 | sleep 1
66 |
67 | echo 'J' >> example2.txt
68 | git add example2.txt
69 | git commit -m 'branchB - fourth'
70 | sleep 1
71 | git tag -a 'v2.0a' -m 'Initial version.'
72 |
73 | echo 'K' >> example2.txt
74 | git add example2.txt
75 | git commit -m 'branchB - fifth'
76 | sleep 1
77 |
78 | echo 'L' >> example2.txt
79 | git add example2.txt
80 | git commit -m 'branchB - sixth'
81 | sleep 1
82 |
83 | echo 'M' >> example2.txt
84 | git add example2.txt
85 | git commit -m 'branchB - seventh'
86 | sleep 1
87 |
88 | git checkout master
89 | echo 'N' >> example2.txt
90 | git add example2.txt
91 | git commit -m 'master - third'
92 | sleep 1
93 |
94 | echo 'O' >> example2.txt
95 | git add example2.txt
96 | git commit -m 'master - fourth'
97 |
98 | ../git2dot.py --graph-label 'graph[label="example2 - full initial state"]' --png --svg --html example2-1.html example2-1.dot
99 | ../git2dot.py --graph-label 'graph[label="example2 - compressed initial state"]' --crunch --squash --png --svg --html example2-2.html example2-2.dot
100 | ../git2dot.py --graph-label 'graph[label="example2 - full pruned state"]' --choose-branch 'branchA' --choose-tag 'tag: v2.0a' --png --svg --html example2-3.html example2-3.dot
101 | ../git2dot.py --graph-label 'graph[label="example2 - compressed pruned state"]' --choose-branch 'branchA' --choose-tag 'tag: v2.0a' --crunch --squash --png --svg --html example2-4.html example2-4.dot
102 |
103 | osType=$(uname)
104 | case "$osType" in
105 | Darwin)
106 | open -a Preview example2-1.dot.png
107 | open -a Preview example2-2.dot.png
108 | open -a Preview example2-3.dot.png
109 | open -a Preview example2-4.dot.png
110 | ;;
111 | Linux)
112 | display example2-1.dot.png
113 | display example2-2.dot.png
114 | display example2-3.dot.png
115 | display example2-4.dot.png
116 | ;;
117 | *)
118 | ;;
119 | esac
120 |
121 | cat <=0;n--)this.eventListeners.hasOwnProperty(o[n])&&delete this.eventListeners[o[n]]}for(var i in this.eventListeners)(this.options.eventsListenerElement||this.svg).addEventListener(i,this.eventListeners[i],!1);this.options.mouseWheelZoomEnabled&&(this.options.mouseWheelZoomEnabled=!1,this.enableMouseWheelZoom())},l.prototype.enableMouseWheelZoom=function(){if(!this.options.mouseWheelZoomEnabled){var t=this;this.wheelListener=function(e){return t.handleMouseWheel(e)},n.on(this.options.eventsListenerElement||this.svg,this.wheelListener,!1),this.options.mouseWheelZoomEnabled=!0}},l.prototype.disableMouseWheelZoom=function(){this.options.mouseWheelZoomEnabled&&(n.off(this.options.eventsListenerElement||this.svg,this.wheelListener,!1),this.options.mouseWheelZoomEnabled=!1)},l.prototype.handleMouseWheel=function(t){if(this.options.zoomEnabled&&"none"===this.state){this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1);var e=t.deltaY||1,o=Date.now()-this.lastMouseWheelEventTime,n=3+Math.max(0,30-o);this.lastMouseWheelEventTime=Date.now(),"deltaMode"in t&&0===t.deltaMode&&t.wheelDelta&&(e=0===t.deltaY?0:Math.abs(t.wheelDelta)/t.deltaY),e=-.30?1:-1)*Math.log(Math.abs(e)+10)/n;var i=this.svg.getScreenCTM().inverse(),s=r.getEventPoint(t,this.svg).matrixTransform(i),a=Math.pow(1+this.options.zoomScaleSensitivity,-1*e);this.zoomAtPoint(a,s)}},l.prototype.zoomAtPoint=function(t,e,o){var n=this.viewport.getOriginalState();o?(t=Math.max(this.options.minZoom*n.zoom,Math.min(this.options.maxZoom*n.zoom,t)),t/=this.getZoom()):this.getZoom()*tthis.options.maxZoom*n.zoom&&(t=this.options.maxZoom*n.zoom/this.getZoom());var i=this.viewport.getCTM(),s=e.matrixTransform(i.inverse()),r=this.svg.createSVGMatrix().translate(s.x,s.y).scale(t).translate(-s.x,-s.y),a=i.multiply(r);a.a!==i.a&&this.viewport.setCTM(a)},l.prototype.zoom=function(t,e){this.zoomAtPoint(t,r.getSvgCenterPoint(this.svg,this.width,this.height),e)},l.prototype.publicZoom=function(t,e){e&&(t=this.computeFromRelativeZoom(t)),this.zoom(t,e)},l.prototype.publicZoomAtPoint=function(t,e,o){if(o&&(t=this.computeFromRelativeZoom(t)),"SVGPoint"!==s.getType(e)){if(!("x"in e&&"y"in e))throw new Error("Given point is invalid");e=r.createSVGPoint(this.svg,e.x,e.y)}this.zoomAtPoint(t,e,o)},l.prototype.getZoom=function(){return this.viewport.getZoom()},l.prototype.getRelativeZoom=function(){return this.viewport.getRelativeZoom()},l.prototype.computeFromRelativeZoom=function(t){return t*this.viewport.getOriginalState().zoom},l.prototype.resetZoom=function(){var t=this.viewport.getOriginalState();this.zoom(t.zoom,!0)},l.prototype.resetPan=function(){this.pan(this.viewport.getOriginalState())},l.prototype.reset=function(){this.resetZoom(),this.resetPan()},l.prototype.handleDblClick=function(t){if(this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),this.options.controlIconsEnabled){var e=t.target.getAttribute("class")||"";if(e.indexOf("svg-pan-zoom-control")>-1)return!1}var o;o=t.shiftKey?1/(2*(1+this.options.zoomScaleSensitivity)):2*(1+this.options.zoomScaleSensitivity);var n=r.getEventPoint(t,this.svg).matrixTransform(this.svg.getScreenCTM().inverse());this.zoomAtPoint(o,n)},l.prototype.handleMouseDown=function(t,e){this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),s.mouseAndTouchNormalize(t,this.svg),this.options.dblClickZoomEnabled&&s.isDblClick(t,e)?this.handleDblClick(t):(this.state="pan",this.firstEventCTM=this.viewport.getCTM(),this.stateOrigin=r.getEventPoint(t,this.svg).matrixTransform(this.firstEventCTM.inverse()))},l.prototype.handleMouseMove=function(t){if(this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),"pan"===this.state&&this.options.panEnabled){var e=r.getEventPoint(t,this.svg).matrixTransform(this.firstEventCTM.inverse()),o=this.firstEventCTM.translate(e.x-this.stateOrigin.x,e.y-this.stateOrigin.y);this.viewport.setCTM(o)}},l.prototype.handleMouseUp=function(t){this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),"pan"===this.state&&(this.state="none")},l.prototype.fit=function(){var t=this.viewport.getViewBox(),e=Math.min(this.width/t.width,this.height/t.height);this.zoom(e,!0)},l.prototype.contain=function(){var t=this.viewport.getViewBox(),e=Math.max(this.width/t.width,this.height/t.height);this.zoom(e,!0)},l.prototype.center=function(){var t=this.viewport.getViewBox(),e=.5*(this.width-(t.width+2*t.x)*this.getZoom()),o=.5*(this.height-(t.height+2*t.y)*this.getZoom());this.getPublicInstance().pan({x:e,y:o})},l.prototype.updateBBox=function(){this.viewport.simpleViewBoxCache()},l.prototype.pan=function(t){var e=this.viewport.getCTM();e.e=t.x,e.f=t.y,this.viewport.setCTM(e)},l.prototype.panBy=function(t){var e=this.viewport.getCTM();e.e+=t.x,e.f+=t.y,this.viewport.setCTM(e)},l.prototype.getPan=function(){var t=this.viewport.getState();return{x:t.x,y:t.y}},l.prototype.resize=function(){var t=r.getBoundingClientRectNormalized(this.svg);this.width=t.width,this.height=t.height;var e=this.viewport;e.options.width=this.width,e.options.height=this.height,e.processCTM(),this.options.controlIconsEnabled&&(this.getPublicInstance().disableControlIcons(),this.getPublicInstance().enableControlIcons())},l.prototype.destroy=function(){var t=this;this.beforeZoom=null,this.onZoom=null,this.beforePan=null,this.onPan=null,null!=this.options.customEventsHandler&&this.options.customEventsHandler.destroy({svgElement:this.svg,eventsListenerElement:this.options.eventsListenerElement,instance:this.getPublicInstance()});for(var e in this.eventListeners)(this.options.eventsListenerElement||this.svg).removeEventListener(e,this.eventListeners[e],!1);this.disableMouseWheelZoom(),this.getPublicInstance().disableControlIcons(),this.reset(),h=h.filter(function(e){return e.svg!==t.svg}),delete this.options,delete this.publicInstance,delete this.pi,this.getPublicInstance=function(){return null}},l.prototype.getPublicInstance=function(){var t=this;return this.publicInstance||(this.publicInstance=this.pi={enablePan:function(){return t.options.panEnabled=!0,t.pi},disablePan:function(){return t.options.panEnabled=!1,t.pi},isPanEnabled:function(){return!!t.options.panEnabled},pan:function(e){return t.pan(e),t.pi},panBy:function(e){return t.panBy(e),t.pi},getPan:function(){return t.getPan()},setBeforePan:function(e){return t.options.beforePan=null===e?null:s.proxy(e,t.publicInstance),t.pi},setOnPan:function(e){return t.options.onPan=null===e?null:s.proxy(e,t.publicInstance),t.pi},enableZoom:function(){return t.options.zoomEnabled=!0,t.pi},disableZoom:function(){return t.options.zoomEnabled=!1,t.pi},isZoomEnabled:function(){return!!t.options.zoomEnabled},enableControlIcons:function(){return t.options.controlIconsEnabled||(t.options.controlIconsEnabled=!0,i.enable(t)),t.pi},disableControlIcons:function(){return t.options.controlIconsEnabled&&(t.options.controlIconsEnabled=!1,i.disable(t)),t.pi},isControlIconsEnabled:function(){return!!t.options.controlIconsEnabled},enableDblClickZoom:function(){return t.options.dblClickZoomEnabled=!0,t.pi},disableDblClickZoom:function(){return t.options.dblClickZoomEnabled=!1,t.pi},isDblClickZoomEnabled:function(){return!!t.options.dblClickZoomEnabled},enableMouseWheelZoom:function(){return t.enableMouseWheelZoom(),t.pi},disableMouseWheelZoom:function(){return t.disableMouseWheelZoom(),t.pi},isMouseWheelZoomEnabled:function(){return!!t.options.mouseWheelZoomEnabled},setZoomScaleSensitivity:function(e){return t.options.zoomScaleSensitivity=e,t.pi},setMinZoom:function(e){return t.options.minZoom=e,t.pi},setMaxZoom:function(e){return t.options.maxZoom=e,t.pi},setBeforeZoom:function(e){return t.options.beforeZoom=null===e?null:s.proxy(e,t.publicInstance),t.pi},setOnZoom:function(e){return t.options.onZoom=null===e?null:s.proxy(e,t.publicInstance),t.pi},zoom:function(e){return t.publicZoom(e,!0),t.pi},zoomBy:function(e){return t.publicZoom(e,!1),t.pi},zoomAtPoint:function(e,o){return t.publicZoomAtPoint(e,o,!0),t.pi},zoomAtPointBy:function(e,o){return t.publicZoomAtPoint(e,o,!1),t.pi},zoomIn:function(){return this.zoomBy(1+t.options.zoomScaleSensitivity),t.pi},zoomOut:function(){return this.zoomBy(1/(1+t.options.zoomScaleSensitivity)),t.pi},getZoom:function(){return t.getRelativeZoom()},resetZoom:function(){return t.resetZoom(),t.pi},resetPan:function(){return t.resetPan(),t.pi},reset:function(){return t.reset(),t.pi},fit:function(){return t.fit(),t.pi},contain:function(){return t.contain(),t.pi},center:function(){return t.center(),t.pi},updateBBox:function(){return t.updateBBox(),t.pi},resize:function(){return t.resize(),t.pi},getSizes:function(){return{width:t.width,height:t.height,realZoom:t.getZoom(),viewBox:t.viewport.getViewBox()}},destroy:function(){return t.destroy(),t.pi}}),this.publicInstance};var h=[],c=function(t,e){var o=s.getSvg(t);if(null===o)return null;for(var n=h.length-1;n>=0;n--)if(h[n].svg===o)return h[n].instance.getPublicInstance();return h.push({svg:o,instance:new l(o,e)}),h[h.length-1].instance.getPublicInstance()};e.exports=c},{"./control-icons":2,"./shadow-viewport":3,"./svg-utilities":5,"./uniwheel":6,"./utilities":7}],5:[function(t,e,o){var n=t("./utilities"),i="unknown";document.documentMode&&(i="ie"),e.exports={svgNS:"http://www.w3.org/2000/svg",xmlNS:"http://www.w3.org/XML/1998/namespace",xmlnsNS:"http://www.w3.org/2000/xmlns/",xlinkNS:"http://www.w3.org/1999/xlink",evNS:"http://www.w3.org/2001/xml-events",getBoundingClientRectNormalized:function(t){if(t.clientWidth&&t.clientHeight)return{width:t.clientWidth,height:t.clientHeight};if(t.getBoundingClientRect())return t.getBoundingClientRect();throw new Error("Cannot get BoundingClientRect for SVG.")},getOrCreateViewport:function(t,e){var o=null;if(o=n.isElement(e)?e:t.querySelector(e),!o){var i=Array.prototype.slice.call(t.childNodes||t.children).filter(function(t){return"defs"!==t.nodeName&&"#text"!==t.nodeName});1===i.length&&"g"===i[0].nodeName&&null===i[0].getAttribute("transform")&&(o=i[0])}if(!o){var s="viewport-"+(new Date).toISOString().replace(/\D/g,"");o=document.createElementNS(this.svgNS,"g"),o.setAttribute("id",s);var r=t.childNodes||t.children;if(r&&r.length>0)for(var a=r.length;a>0;a--)"defs"!==r[r.length-a].nodeName&&o.appendChild(r[r.length-a]);t.appendChild(o)}var l=[];return o.getAttribute("class")&&(l=o.getAttribute("class").split(" ")),~l.indexOf("svg-pan-zoom_viewport")||(l.push("svg-pan-zoom_viewport"),o.setAttribute("class",l.join(" "))),o},setupSvgAttributes:function(t){if(t.setAttribute("xmlns",this.svgNS),t.setAttributeNS(this.xmlnsNS,"xmlns:xlink",this.xlinkNS),t.setAttributeNS(this.xmlnsNS,"xmlns:ev",this.evNS),null!==t.parentNode){var e=t.getAttribute("style")||"";e.toLowerCase().indexOf("overflow")===-1&&t.setAttribute("style","overflow: hidden; "+e)}},internetExplorerRedisplayInterval:300,refreshDefsGlobal:n.throttle(function(){for(var t=document.querySelectorAll("defs"),e=t.length,o=0;oe?(clearTimeout(a),a=null,l=h,s=t.apply(n,i),a||(n=i=null)):a||o.trailing===!1||(a=setTimeout(u,c)),s}},createRequestAnimationFrame:function(t){var e=null;return"auto"!==t&&t<60&&t>1&&(e=Math.floor(1e3/t)),null===e?window.requestAnimationFrame||n(33):n(e)}}},{}]},{},[1]);
--------------------------------------------------------------------------------
/example/webserver.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | '''
3 | World's simplest multi-threaded socket server for testing
4 | git2dot.py output.
5 |
6 | Run it like this:
7 |
8 | $ webserver.py 8090
9 |
10 | You can then access the contents of the local directory using
11 | http://localhost:8090.
12 | '''
13 | import sys
14 | import SocketServer
15 | import BaseHTTPServer
16 | import SimpleHTTPServer
17 |
18 | class ThreadingHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
19 | pass
20 |
21 | assert len(sys.argv) == 2
22 | port = int(sys.argv[1])
23 | httpd = ThreadingHTTPServer(('', port), SimpleHTTPServer.SimpleHTTPRequestHandler)
24 | try:
25 | print('Serving on port {}'.format(port))
26 | httpd.serve_forever()
27 | except KeyboardInterrupt:
28 | print('Done.')
29 |
30 |
--------------------------------------------------------------------------------
/git2dot.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | r'''
3 | Tool to visualize a git repository using the graphviz dot tool.
4 |
5 | It is useful for understanding how git works in detail. You can use
6 | it to analyze repositories before and after operations like merge and
7 | rebase to really get a feeling for what happens. It can also be used
8 | for looking at subsets of history on live repositories.
9 |
10 | It works by running over the .git repository in the current directory
11 | and generating a commit relationship DAG that has both parent and
12 | child relationships.
13 |
14 | The generated graph shows commits, tags and branches as nodes.
15 | Commits are further broken down into simple commits and merged commits
16 | where merged commits are commits with 2 or more children. There is an
17 | additional option that allows you to squash long chains of simple
18 | commits with no branch or tag data.
19 |
20 | It has a number of different options for customizing the nodes,
21 | using your own custom git command to generate the data, keeping
22 | the generated data for re-use and generating graphical output like
23 | PNG, SVG or even HTML files.
24 |
25 | Here is an example run:
26 |
27 | $ cd SANDBOX
28 | $ git2dot.py --png git.dot
29 | $ open -a Preview git.dot.png # on Mac OS X
30 | $ display git.dot.png # linux
31 |
32 | If you want to create a simple HTML page that allows panning and
33 | zooming of the generated SVG then use the --html option like
34 | this.
35 |
36 | $ cd SANDBOX
37 | $ git2dot.py --svg --html ~/web/index.html ~/web/git.dot
38 | $ $ ls ~/web
39 | git.dot git.dot.svg git.html svg-pan-zoom.min.js
40 | $ cd ~/web
41 | $ python -m SimpleHTTPServer 8090 # start server
42 | $ # Browse to http://localhost:8090/git.dot.svg
43 |
44 | It assumes that existence of svg-pan-zoom.min.js from the
45 | https://github.com/ariutta/svg-pan-zoom package.
46 |
47 | The output is pretty customizable. For example, to add the subject and
48 | commit date to the commit node names use -l '%s|%cr'. The items come
49 | from the git format placeholders or variables that you define using
50 | -D. The | separator is used to define the end of a line. The maximum
51 | width of each line can be specified by -w. Variables are defined by -D
52 | and come from text in the commit message. See -D for more details.
53 |
54 | You can customize the attributes of the different types of nodes and
55 | edges in the graph using the -?node and -?edge attributes. The table
56 | below briefly describes the different node types:
57 |
58 | bedge Edge connecting to a bnode.
59 | bnode Branch node associated with a commit.
60 | cnode Commit node (simple commit node).
61 | mnode Merge node. A commit node with multiple children.
62 | snode Squashed node. End point of a sequence of squashed nodes.
63 | tedge Edge connecting to a tnode.
64 | tnode Tag node associated with a commit.
65 |
66 | If you have long chains of single commits use the --squash option to
67 | squash out the middle ones. That is generally helpful for filtering
68 | out extraneous commit details for moderately sized repos.
69 |
70 | If you find that dot is placing your bnode and tnode nodes in odd
71 | places, use the --crunch option to collapse the bnode nodes into
72 | a single node and the tnodes into a single node for each commit.
73 |
74 | If you want to limit the analysis to commits between certain dates,
75 | use the --since and --until options.
76 |
77 | If you want to limit the analysis to commits in a certain range use
78 | the --range option.
79 |
80 | If you want to limit the analysis to a small set of branches or tags
81 | you can use the --choose-branch and --choose-tag options. These options
82 | prune the graph so that only parents of commits with the choose branch
83 | or tag ids are included in the graph. This gives you more detail
84 | controlled that the git options allowed in the --range command. It
85 | is very useful for determining where branches occurred.
86 |
87 | You can choose to keep the git output to re-use multiple times with
88 | different display options or to share by specifying the -k (--keep)
89 | option.
90 | '''
91 | import argparse
92 | import copy
93 | import datetime
94 | import dateutil.parser
95 | import inspect
96 | import os
97 | import re
98 | import subprocess
99 | import sys
100 |
101 |
102 | VERSION = '0.8.3'
103 | DEFAULT_GITCMD = 'git log --format="|Record:|%h|%p|%d|%ci%n%b"' # --gitcmd
104 | DEFAULT_RANGE = '--all --topo-order' # --range
105 |
106 |
107 | class Node:
108 | r'''
109 | Each node represents a commit.
110 | A commit can have zero or parents.
111 | A parent link is created each time a merge is done.
112 | '''
113 |
114 | m_list = []
115 | m_map = {}
116 | m_list_bydate = []
117 | m_vars_usage = {} # nodes that have var values
118 |
119 | def __init__(self, cid, pids=[], branches=[], tags=[], dts=None):
120 | self.m_cid = cid
121 | self.m_idx = len(Node.m_list)
122 | self.m_parents = pids
123 | self.m_label = ''
124 | self.m_branches = branches
125 | self.m_tags = tags
126 | self.m_children = []
127 |
128 | self.m_vars = {} # user defined variable values
129 |
130 | self.m_choose = True # used by the --choose-* options only
131 |
132 | self.m_extra = []
133 | self.m_dts = dts # date/time stamp, used for invisible constraints.
134 |
135 | # For squashing.
136 | self.m_chain_head = None
137 | self.m_chain_tail = None
138 | self.m_chain_size = -1
139 |
140 | Node.m_list.append(self)
141 | Node.m_map[cid] = self
142 |
143 | def is_squashable(self):
144 | if len(self.m_branches) > 0 or len(self.m_tags) > 0 or len(self.m_parents) > 1 or len(self.m_children) > 1:
145 | return False
146 | return True
147 |
148 | def is_squashed(self):
149 | if self.m_chain_head is None:
150 | return False
151 | if self.m_chain_tail is None:
152 | return False
153 | return self.m_chain_size > 0 and self.m_cid != self.m_chain_head.m_cid and self.m_cid != self.m_chain_tail.m_cid
154 |
155 | def is_squashed_head(self):
156 | if self.m_chain_head is None:
157 | return False
158 | return self.m_chain_head.m_cid == self.m_cid
159 |
160 | def is_squashed_tail(self):
161 | if self.m_chain_tail is None:
162 | return False
163 | return self.m_chain_tail.m_cid == self.m_cid
164 |
165 | def is_merge_node(self):
166 | return len(self.m_children) > 1
167 |
168 | def find_chain_head(self):
169 | if self.is_squashable() == False:
170 | return None
171 | if self.m_chain_head is not None:
172 | return self.m_chain_head
173 |
174 | # Get the head node, traversing via parents.
175 | chain_head = None
176 | chain_next = self
177 | while chain_next is not None and chain_next.is_squashable():
178 | chain_head = chain_next
179 | if len(chain_next.m_parents) > 0:
180 | chain_next = Node.m_map[chain_next.m_parents[0]]
181 | else:
182 | chain_next = None
183 | return chain_head
184 |
185 | def find_chain_tail(self):
186 | if self.is_squashable() == False:
187 | return None
188 | if self.m_chain_tail is not None:
189 | return self.m_chain_tail
190 |
191 | # Get the tail node, traversing via children.
192 | chain_tail = None
193 | chain_next = self
194 | while chain_next is not None and chain_next.is_squashable():
195 | chain_tail = chain_next
196 | if len(chain_next.m_children) > 0:
197 | chain_next = chain_next.m_children[0]
198 | else:
199 | chain_next = None
200 | return chain_tail
201 |
202 | @staticmethod
203 | def squash():
204 | '''
205 | Squash nodes that in a chain of single commits.
206 | '''
207 | update = {}
208 | for nd in Node.m_list:
209 | head = nd.find_chain_head()
210 | if head is not None:
211 | update[head.m_cid] = head
212 |
213 | for key in update:
214 | head = update[key]
215 | tail = head.find_chain_tail()
216 | cnext = head
217 | clast = head
218 | distance = 0
219 | while clast != tail:
220 | distance += 1
221 | clast = cnext
222 | cnext = cnext.m_children[0]
223 |
224 | cnext = head
225 | clast = head
226 | while clast != tail:
227 | idx = cnext.m_idx
228 | cid = cnext.m_cid
229 |
230 | Node.m_list[idx].m_chain_head = head
231 | Node.m_list[idx].m_chain_tail = tail
232 | Node.m_list[idx].m_chain_size = distance
233 |
234 | Node.m_map[cid].m_chain_head = head
235 | Node.m_map[cid].m_chain_tail = tail
236 | Node.m_map[cid].m_chain_size = distance
237 |
238 | clast = cnext
239 | cnext = cnext.m_children[0]
240 |
241 | def rm_parent(self, pcid):
242 | while pcid in self.m_parents:
243 | i = self.m_parents.index(pcid)
244 | self.m_parents = self.m_parents[:i] + self.m_parents[i+1:]
245 |
246 | def rm_child(self, ccid):
247 | for i, cnd in reversed(list(enumerate(self.m_children))):
248 | if cnd.m_cid == ccid:
249 | self.m_children = self.m_children[:i] + self.m_children[i+1:]
250 |
251 |
252 | def info(msg, lev=1):
253 | ''' Print an informational message with the source line number. '''
254 | print('// INFO:{} {}'.format(inspect.stack()[lev][2], msg))
255 |
256 |
257 | def infov(opts, msg, lev=1):
258 | ''' Print an informational message with the source line number. '''
259 | if opts.verbose > 0:
260 | print('// INFO:{} {}'.format(inspect.stack()[lev][2], msg))
261 |
262 |
263 | def warn(msg, lev=1):
264 | ''' Print a warning message with the source line number. '''
265 | print('// WARNING:{} {}'.format(inspect.stack()[lev][2], msg))
266 |
267 |
268 | def err(msg, lev=1):
269 | ''' Print an error message and exit. '''
270 | sys.stderr.write('// ERROR:{} {}\n'.format(inspect.stack()[lev][2], msg))
271 | sys.exit(1)
272 |
273 |
274 | def runcmd_long(cmd, show_output=True):
275 | '''
276 | Execute a long running shell command with no inputs.
277 | Capture output and exit status.
278 | For long running commands, this implementation displays output
279 | information as it is captured.
280 | For fast running commands it would be better to use
281 | subprocess.check_output.
282 | '''
283 | proc = subprocess.Popen(cmd,
284 | shell=True,
285 | stdout=subprocess.PIPE,
286 | stderr=subprocess.STDOUT)
287 |
288 | # Read the output 1 character at a time so that it can be
289 | # displayed in real time.
290 | output = ''
291 | while not proc.returncode:
292 | char = proc.stdout.read(1)
293 | if not char:
294 | # all done, wait for returncode to get populated
295 | break
296 | else:
297 | try:
298 | # There is probably a better way to do this.
299 | char = char.decode('utf-8')
300 | except UnicodeDecodeError:
301 | continue
302 | output += char
303 | if show_output:
304 | sys.stdout.write(char)
305 | sys.stdout.flush()
306 | proc.wait()
307 | return proc.returncode, output
308 |
309 |
310 | def runcmd_short(cmd, show_output=True):
311 | '''
312 | Execute a short running shell command with no inputs.
313 | Capture output and exit status.
314 | '''
315 | try:
316 | output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
317 | status = 0
318 | except subprocess.CalledProcessError as obj:
319 | output = obj.output
320 | status = obj.returncode
321 |
322 | if show_output:
323 | sys.stdout.write(output)
324 |
325 | return status, output
326 |
327 |
328 | def runcmd(cmd, show_output=True):
329 | '''
330 | Wrapper for run commands.
331 | '''
332 | return runcmd_long(cmd, show_output)
333 |
334 |
335 | def read(opts):
336 | '''
337 | Read the input data.
338 | The input can come from two general sources: the output of a git
339 | command or a file that contains the output from a git comment
340 | (-i).
341 | '''
342 | # Run the git command.
343 | infov(opts, 'reading git repo data')
344 | out = ''
345 | if opts.input != '':
346 | # The user specified a file that contains the input data
347 | # via the -i option.
348 | try:
349 | with open(opts.input, 'r') as ifp:
350 | out = ifp.read()
351 | except IOError as e:
352 | err('input read failed: {}'.format(e))
353 | else:
354 | # The user chose to run a git command.
355 | cmd = opts.gitcmd
356 | if cmd.replace('%%', '%') == DEFAULT_GITCMD:
357 | cmd = cmd.replace('%%', '%')
358 | if opts.cnode_label != '':
359 | x = cmd.rindex('"')
360 | cmd = cmd[:x] + '%n{}|{}'.format(opts.cnode_label_recid, opts.cnode_label) + cmd[x:]
361 |
362 | if opts.since != '':
363 | cmd += ' --since="{}"'.format(opts.since)
364 | if opts.until != '':
365 | cmd += ' --until="{}"'.format(opts.until)
366 | if opts.range != '':
367 | cmd += ' {}'.format(opts.range)
368 | else:
369 | # If the user specified a custom command then we
370 | # do not allow the user options to affect it.
371 | if opts.cnode_label != '':
372 | warn('-l