├── .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 | [![Releases](https://img.shields.io/github/release/jlinoff/git2dot.svg?style=flat)](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 |         test04 example 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 |         example 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 |         example1 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 |         example2 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 |         example2-2 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 |         example2-4 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 |         dog food 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 |         dog food 1 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 |         dog food zoom 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