├── .eslintrc.json ├── .gitignore ├── README.md ├── Vis └── Web │ └── Scripts │ └── gitflow-visualize.js ├── dist ├── gitflow-visualize.bundle.js ├── gitflow-visualize.bundle.min.js ├── gitflow-visualize.css ├── gitflow-visualize.js ├── gitflow-visualize.min.css ├── gitflow-visualize.min.js └── gitflow-visualize.node.js ├── examples ├── commits.json ├── multiple_datasets.html └── standalone.html ├── gulpfile.js ├── lib ├── gitflow-visualize.js ├── gitflow-visualize.scss └── wrapper.js ├── package.json └── test ├── index.2.html ├── index.html ├── karma.conf.js ├── test.2.js ├── test.data.2.js ├── test.data.js └── test.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": false, 6 | "node": true 7 | }, 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "sourceType": "module" 13 | }, 14 | "rules": { 15 | "no-const-assign": "warn", 16 | "no-this-before-super": "warn", 17 | "no-undef": "error", 18 | "no-unreachable": "warn", 19 | "no-unused-vars": "error", 20 | "no-invalid-regexp": "error", 21 | "constructor-super": "warn", 22 | "valid-typeof": "warn", 23 | "curly": ["warn", "multi-line"], 24 | "no-irregular-whitespace": "error" 25 | }, 26 | "globals": { 27 | "_": false, 28 | "d3":false, 29 | "moment": false, 30 | "firstBy":false, 31 | "CryptoJS":false, 32 | "define": false, 33 | "GitFlowVisualize": true 34 | } 35 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | 11 | [Dd]ebug/ 12 | [Rr]elease/ 13 | x64/ 14 | build/ 15 | [Bb]in/ 16 | [Oo]bj/ 17 | 18 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets 19 | !packages/*/build/ 20 | 21 | # MSTest test Results 22 | [Tt]est[Rr]esult*/ 23 | [Bb]uild[Ll]og.* 24 | 25 | *_i.c 26 | *_p.c 27 | *.ilk 28 | *.meta 29 | *.obj 30 | *.pch 31 | *.pdb 32 | *.pgc 33 | *.pgd 34 | *.rsp 35 | *.sbr 36 | *.tlb 37 | *.tli 38 | *.tlh 39 | *.tmp 40 | *.tmp_proj 41 | *.log 42 | *.vspscc 43 | *.vssscc 44 | .builds 45 | *.pidb 46 | *.log 47 | *.scc 48 | 49 | # Visual C++ cache files 50 | ipch/ 51 | *.aps 52 | *.ncb 53 | *.opensdf 54 | *.sdf 55 | *.cachefile 56 | 57 | # Visual Studio profiler 58 | *.psess 59 | *.vsp 60 | *.vspx 61 | 62 | # Guidance Automation Toolkit 63 | *.gpState 64 | 65 | # ReSharper is a .NET coding add-in 66 | _ReSharper*/ 67 | *.[Rr]e[Ss]harper 68 | 69 | # TeamCity is a build add-in 70 | _TeamCity* 71 | 72 | # DotCover is a Code Coverage Tool 73 | *.dotCover 74 | 75 | # NCrunch 76 | *.ncrunch* 77 | .*crunch*.local.xml 78 | 79 | # Installshield output folder 80 | [Ee]xpress/ 81 | 82 | # DocProject is a documentation generator add-in 83 | DocProject/buildhelp/ 84 | DocProject/Help/*.HxT 85 | DocProject/Help/*.HxC 86 | DocProject/Help/*.hhc 87 | DocProject/Help/*.hhk 88 | DocProject/Help/*.hhp 89 | DocProject/Help/Html2 90 | DocProject/Help/html 91 | 92 | # Click-Once directory 93 | publish/ 94 | 95 | # Publish Web Output 96 | *.Publish.xml 97 | *.pubxml 98 | 99 | # NuGet Packages Directory 100 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 101 | #packages/ 102 | 103 | # Windows Azure Build Output 104 | csx 105 | *.build.csdef 106 | 107 | # Windows Store app package directory 108 | AppPackages/ 109 | 110 | # Others 111 | sql/ 112 | *.Cache 113 | ClientBin/ 114 | [Ss]tyle[Cc]op.* 115 | ~$* 116 | *~ 117 | *.dbmdl 118 | *.[Pp]ublish.xml 119 | *.pfx 120 | *.publishsettings 121 | 122 | # RIA/Silverlight projects 123 | Generated_Code/ 124 | 125 | # Backup & report files from converting an old project file to a newer 126 | # Visual Studio version. Backup files are not needed, because we have git ;-) 127 | _UpgradeReport_Files/ 128 | Backup*/ 129 | UpgradeLog*.XML 130 | UpgradeLog*.htm 131 | 132 | # SQL Server files 133 | App_Data/*.mdf 134 | App_Data/*.ldf 135 | 136 | # ========================= 137 | # Windows detritus 138 | # ========================= 139 | 140 | # Windows image file caches 141 | Thumbs.db 142 | ehthumbs.db 143 | 144 | # Folder config file 145 | Desktop.ini 146 | 147 | # Recycle Bin used on file shares 148 | $RECYCLE.BIN/ 149 | 150 | # Mac crap 151 | .DS_Store 152 | 153 | # custom ignores 154 | node_modules 155 | test/coverage 156 | gitflowvis.sublime-workspace 157 | gitflowvis.sublime-project 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitFlowVisualize 2 | 3 | GitFlowVisualize is a javascript library to visualize git repositories that are using the Git Flow workflow. It uses conventions to know which lineage should be drawn as the master line and which as the develop line. The output is SVG using d3.js. The different types of branches are color coded (master, develop, feature, release, hotfix). 4 | 5 | ## Installation 6 | 7 | ### In the browser 8 | 9 | You can use GitFlowVisualize directly in your browser by including the `gitflow-visualize.js` file from the `dist/` folder. GitFlowVisualize is registered on the global namespace. Please note that you will also be required to include the following dependencies: 10 | 11 | - d3.js (v3) 12 | - Moment 13 | - ThenBy 14 | - Crypto-JS/MD5 15 | 16 | If you are not using these dependencies in your own project, it might be easier to include `gitflow-visualize.bundle.js`. This version includes all required dependencies and will allow you to immediately start using GitFlowVisualize in your project. 17 | 18 | ### In your NodeJS project 19 | 20 | Simply install GitFlowVisualize by running 21 | 22 | ``` 23 | npm install git-flow-vis 24 | ``` 25 | 26 | and include it in your project by adding 27 | 28 | ``` 29 | const GitFlowVisualize = require('git-flow-vis'); 30 | ``` 31 | 32 | ## Live Examples 33 | 34 | You can find two live examples with dummy data in the `examples/` folder. To see them in action, checkout the repository, run `npm install` to install all dependencies and `npm run dist` to compile the project. Afterwards, you can open the `standalone.html` or `multiple_datasets.html` examples in your browser. 35 | 36 | ## Usage 37 | 38 | GitFlowVisualize is available on the global namespace and can be used directly in your project. You can use the following placeholder to kickstart your project. 39 | 40 | ``` 41 | 42 | 43 | 44 | 45 | 50 | 51 | 52 | 53 | 54 |
55 | 65 | 66 | 67 | ``` 68 | 69 | GitFlowVisualize also has support for AMD and CommonJS dependency resolving. If you wish to include GitFlowVisualise in your NodeJS project you can simply assign it to a variable: 70 | 71 | ``` 72 | const GitFlowVisualize = require('git-flow-vis'); 73 | ``` 74 | 75 | ## `GitFlowVisualize.draw([elem], opts)` 76 | 77 |
78 |
79 | elem 80 |
81 | 82 |
83 | DOM object, placeholder of the Git Flow Chart. 84 |
85 | 86 |
87 | opts 88 |
89 | 90 |
91 | Object. 92 |
93 |
94 | 95 | `elem` is an optional argument, `opts` is required. If `elem` is provided, it must be in the order shown. The `elem` can also be passed using the `opts.drawElem` option. If neither `elem` nor `opts.drawElem` is provided, a `div` placeholder will be appended to the document `body` tag. 96 | 97 | ### `opts` 98 | 99 | `opts.drawElem` is the DOM Element which is used as the placeholder in which the graph is drawn. 100 | 101 | `opts.drawTable` is the DOM Element which is used as the placeholder to hold the commit data table. If not provided, the `opts.drawElem` element is used. 102 | 103 | `opts.masterRef` is the git reference to the master branch. Defaults to 'refs/heads/master'. 104 | 105 | `opts.developRef` is the git reference to the develop branch. Defaults to 'refs/heads/develop'. 106 | 107 | `opts.featurePrefix` is the git reference prefix to all feature branches. Any branch that is prefixed with this value will be considered to be a feature branch. Defaults to 'refs/heads/feature'. 108 | 109 | `opts.releasePrefix` is the git reference prefix to all release branches. Any branch that is prefixed with this value will be considered to be a release branch. Defaults to 'refs/heads/release'. 110 | 111 | `opts.hotfixPrefix` is the git reference prefix to all hotfix release branches. Any branch that is prefixed with this value will be considered to be a hotfix release branch. Defaults to 'refs/heads/hotfix'. 112 | 113 | `opts.releaseZonePattern` is a regular expression (RegExp) that can be used to include other branches in the colored 'Release' zone of the graph. Defaults to '/^refs\/heads\/bugfix/'. 114 | 115 | `opts.releaseTagPattern` is a regular expression (RegExp) that can be used to identify release tags. Defaults to '^/refs\\/tags\\/\d+(\\.\d+)+$/'. 116 | 117 | `opts.showSpinner()` is a function that is called prior to starting processing of commit data and can be used to show a loading message. 118 | 119 | `opts.hideSpinner()` is a function that is called when the commit data has been processed and the graph has been drawn. 120 | 121 | `opts.dataCallback(callback(data))` (required) is a function that is called by the graph to retrieve the commit data from the Git repository. It provides a callback function which expects a `data` parameter. The data parameter should use the following JSON schema: 122 | 123 | ``` 124 | { 125 | branches: { 126 | values: [ 127 | { 128 | "id": STRING - the Git reference to the branch, 129 | "displayId": STRING - the name of the branch to be displayed in the graph, 130 | "latestChangeset": STRING - the SHA hash of the latest changeset on the branch 131 | } 132 | ] 133 | }, 134 | tags: { 135 | values: [ 136 | { 137 | "id": STRING - the Git reference to the tag, 138 | "displayId": STRING - the name of the tag to be displayed in the graph, 139 | "latestChangeset": STRING - the SHA hash of the latest changeset on the tag 140 | } 141 | ] 142 | }, 143 | commits: [ 144 | { 145 | values: [ 146 | { 147 | "id": STRING - the SHA hash of the commit, 148 | "displayId": STRING - the abbreviated SHA hash of the commit, 149 | "author": { 150 | "emailAddress": STRING - the email address of the commit author, 151 | "displayName": STRING - the name of the author to be displayed in the graph 152 | }, 153 | "authorTimestamp": INTEGER - the timestamp of the commit, 154 | "message": STRING - the commit message, 155 | "parents": [ 156 | { 157 | "id": STRING - the SHA hash of the commit parent, 158 | "displayId": STRING - the abbreviated SHA hash of the commit 159 | } 160 | ] 161 | } 162 | ] 163 | } 164 | ] 165 | } 166 | ``` 167 | 168 | `opts.moreDataCallback(from, callback(data, from))` (required) is a function which is used by the commit graph to lazy load additional commit data on scroll. It passes the SHA hash of the last commit that it retrieved and a callback function which expects a `data` and `from` parameter. The `data` parameter should use the following JSON schema: 169 | 170 | ``` 171 | { 172 | "size": INTEGER - the number of commits in the values property, 173 | "values": [ 174 | { 175 | "id": STRING - the SHA hash of the commit, 176 | "displayId": STRING - the abbreviated SHA hash of the commit, 177 | "author": { 178 | "emailAddress": STRING - the email address of the commit author, 179 | "displayName": STRING - the name of the author to be displayed in the graph 180 | }, 181 | "authorTimestamp": INTEGER - the timestamp of the commit, 182 | "message": STRING - the commit message, 183 | "parents": [ 184 | { 185 | "id": STRING - the SHA hash of the commit parent, 186 | "displayId": STRING - the abbreviated SHA hash of the commit 187 | } 188 | ] 189 | } 190 | ] 191 | } 192 | ``` 193 | 194 | In this case, 'values' is an array of commits with the same JSON schema as the 'commits' property of the `opts.dataCallback()` function. The `from` parameter of the callback should be the same SHA hash that was provided in the `from` parameter of the `opts.moreDataCallback()` function. 195 | 196 | `opts.dataProcessed(data)` is a function that is called when the graph has finished processing the commit data. It passes a `data` parameter which includes detailed information of all the commits that have been processed by the graph. 197 | 198 | `opts.createCommitUrl(commit)` is a function that is called for each commit which can be used to generate a URL reference to the commit source. The `commit` parameter has the same JSON schema as a commit in the `commits` property of the `opts.dataCallback()` function. 199 | 200 | `opts.createAuthorAvatarUrl(author)` is a function that is called for each commit which can be used to generate a URL reference to the author avatar/profile picture. The `author` parameter has the same JSON schema as the `author` object of a commit in the `commits` property of the `opts.dataCallback()` function. If this option is not specified, the default behavior is to use the MD5 hash of the author email address and retrieve the picture from [Gravatar](http://gravatar.com). 201 | 202 | `opts.hiddenBranches` is an array of strings (full refs) that indicates which branches should NOT be shown in the diagram initially. 203 | 204 | ## `GitFlowVisualize.branches` 205 | The `branches` property of `GitFlowVisualize` exposes a few functions that allow the host to show/hide branches and provide a nicer UI for this. 206 | 207 | `branches.getAll()` returns a list of all branches in the chart, both visible and invisible. Each item comes with a few useful properties on it. An example: 208 | 209 | ``` 210 | { 211 | id: 'refs/heads/feature/large-font', /*full git ref*/ 212 | name: 'feature/large-font', 213 | lastActivity: 42274874226 /*milliseconds since UNIX epoch. Can be passed to javascript Date() constructor*/, 214 | lastActivityFormatted: '3/22/2017 13:04:12', 215 | visible: true 216 | } 217 | ``` 218 | `branches.setHidden(refs)` allows you to pass an array of refs (like what you can set on opts.hiddenBranches). 219 | 220 | `branches.getHidden()` returns an array of refs of branches that are currently hidden. 221 | 222 | `branches.registerHandler(handler)` allows you to pass an event handler. This handler will be called when users click the "Change..." item on the branches summary. To unregister, pass in `null`. 223 | 224 | ## Legal stuff 225 | 226 | GitFlowVisualize was created as part of the [Git Flow Chart](https://marketplace.atlassian.com/1212520) add-on for Atlassian Bitbucket. As such, this code has a mixed Commercial and Open Source license. It is released to GitHub to share and to accept contributions. The GitHub Terms of Service apply, so feel free to study the code, make forks and contribute. The project can also be used in Open Source projects that are made public using the GPLv3 license or for personal non-commercial use. Commercial exploitation of the code is explicitely prohibited. 227 | 228 | ### License 229 | This program is free software: you can redistribute it and/or modify 230 | it under the terms of the GNU General Public License as published by 231 | the Free Software Foundation, either version 3 of the License, or 232 | (at your option) any later version. 233 | 234 | This program is distributed in the hope that it will be useful, 235 | but WITHOUT ANY WARRANTY; without even the implied warranty of 236 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 237 | GNU General Public License for more details. 238 | 239 | You should have received a copy of the GNU General Public License 240 | along with this program. If not, see 241 | 242 | Copyright 2014-2017 Teun Duynstee -------------------------------------------------------------------------------- /dist/gitflow-visualize.css: -------------------------------------------------------------------------------- 1 | circle.commit-dot { 2 | fill: white; 3 | stroke: black; 4 | stroke-width: 2px; } 5 | 6 | .commit-dot.dim { 7 | opacity: .2; } 8 | 9 | line { 10 | stroke: black; 11 | opacity: 0.2; } 12 | line.m { 13 | stroke: #d04437; 14 | stroke-width: 3px; 15 | opacity: 1; } 16 | line.d0 { 17 | stroke: #8eb021; 18 | stroke-width: 3px; 19 | opacity: 1; } 20 | 21 | .arrow path { 22 | stroke: black; 23 | stroke-width: 2px; 24 | opacity: 1; 25 | fill: none; } 26 | .arrow path.outline { 27 | stroke: white; 28 | stroke-width: 4px; 29 | opacity: .8; } 30 | .arrow path.branch-type-f { 31 | stroke: #3b7fc4; } 32 | .arrow path.branch-type-r { 33 | stroke: #f6c342; } 34 | .arrow path.branch-type-d { 35 | stroke: #8eb021; } 36 | .arrow path.branch-type-m { 37 | stroke: #f6c342; } 38 | .arrow path.branch-type-default { 39 | stroke-width: 1px; } 40 | .arrow path.back { 41 | opacity: 0.5; } 42 | 43 | .messages { 44 | position: relative; } 45 | 46 | .commit-msg { 47 | position: absolute; 48 | white-space: nowrap; 49 | cursor: pointer; 50 | padding-left: 30%; 51 | width: 70%; 52 | overflow-x: hidden; } 53 | .commit-msg.dim { 54 | color: #aaa; } 55 | .commit-msg.selected { 56 | background-color: #ccd9ea; } 57 | .commit-msg:hover { 58 | background-color: #f5f5f5; } 59 | 60 | .commit-link { 61 | font-family: courier; } 62 | 63 | .commit-table { 64 | width: 100%; 65 | table-layout: fixed; } 66 | 67 | td.author { 68 | width: 8em; } 69 | 70 | td.sha { 71 | width: 5em; } 72 | 73 | td.date { 74 | width: 7em; } 75 | 76 | .label { 77 | margin-right: 2px; } 78 | 79 | .branch { 80 | background-color: #ffc; 81 | border-color: #ff0; } 82 | 83 | .legenda-label text { 84 | fill: white; } 85 | 86 | .legenda-label path { 87 | stroke-width: 4; } 88 | 89 | .legenda-label.m rect { 90 | fill: #d04437; } 91 | 92 | .legenda-label.m path { 93 | stroke: #d04437; } 94 | 95 | .legenda-label.r rect { 96 | fill: #f6c342; } 97 | 98 | .legenda-label.r path { 99 | stroke: #f6c342; } 100 | 101 | .legenda-label.d rect { 102 | fill: #8eb021; } 103 | 104 | .legenda-label.d text { 105 | fill: white; } 106 | 107 | .legenda-label.d path { 108 | stroke: #8eb021; } 109 | 110 | .legenda-label.f rect { 111 | fill: #3b7fc4; } 112 | 113 | .legenda-label.f text { 114 | fill: white; } 115 | 116 | .legenda-label.f path { 117 | stroke: #3b7fc4; } 118 | 119 | .tag { 120 | background-color: #eee; 121 | border-color: #ccc; } 122 | 123 | table.commit-table td { 124 | overflow: hidden; 125 | margin: 2px; } 126 | 127 | .author { 128 | font-weight: bold; 129 | width: 120px; } 130 | 131 | .commits-graph-container { 132 | width: 30%; 133 | overflow-x: scroll; 134 | float: left; 135 | z-index: 5; 136 | position: relative; } 137 | 138 | #debug-output { 139 | width: 600px; 140 | height: 300px; 141 | position: absolute; 142 | left: 300px; 143 | top: 100px; 144 | z-index: 100; } 145 | 146 | .branch-btn { 147 | cursor: pointer; } 148 | 149 | .context-menu { 150 | display: inline-block; 151 | position: absolute; 152 | visibility: hidden; 153 | background-color: lightgray; 154 | z-index: 10; 155 | border: 2px outset; } 156 | .context-menu .item { 157 | border-bottom: solid silver 1px; 158 | background-color: #eee; 159 | font-family: sans-serif; 160 | padding: 3px; 161 | cursor: pointer; 162 | white-space: nowrap; 163 | color: #333; } 164 | .context-menu .item:hover { 165 | color: black; 166 | background-color: #ddd; } 167 | -------------------------------------------------------------------------------- /dist/gitflow-visualize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | This file is part of GitFlowVisualize. 5 | 6 | GitFlowVisualize is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | GitFlowVisualize is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with GitFlowVisualize. If not, see . 18 | */ 19 | var GitFlowVisualize = (function () { 20 | 21 | var self = {}; 22 | var data; 23 | var displayState = {style:"none", root:null}; 24 | var constants = { 25 | rowHeight: 35 26 | }; 27 | 28 | var md5 = CryptoJS.MD5; 29 | 30 | var options = { 31 | drawElem: null, 32 | drawTable: false, 33 | 34 | // these are the exact names of the branches that should be drawn as stright lines master and develop 35 | masterRef: "refs/heads/master", 36 | developRef: "refs/heads/develop", 37 | 38 | // feature branches are prefixed by 39 | featurePrefix: "refs/heads/feature/", 40 | releasePrefix: "refs/heads/release/", 41 | hotfixPrefix: "refs/heads/hotfix/", 42 | 43 | // remaing branches will be tested to this regex: if match -> release, if not -> feature 44 | releaseZonePattern: /^refs\/heads\/bugfix/, 45 | 46 | // this pattern should match the tags that are given to release commits on master 47 | releaseTagPattern: /^refs\/tags\/\d+(\.\d+)+$/, 48 | 49 | // UI interaction hooks for loading message 50 | showSpinner: function () {}, 51 | hideSpinner: function () {}, 52 | 53 | // function to provide commit data 54 | dataCallback: function (done) { 55 | console.log("The required option 'dataCallback' is missing, please provide a method to retrieve commit data"); 56 | done({}); 57 | }, 58 | 59 | // function to retrieve additional commit data on scroll 60 | moreDataCallback: function (from, done) { 61 | console.log("The required option 'moreDataCallback' is missing, please provide a method to retrieve commit data"); 62 | done({}); 63 | }, 64 | 65 | // function called after data hase been processed successfully and chart has been drawn 66 | dataProcessed: function (/*data*/) { }, 67 | 68 | // function to provide the appropriate url to the actual commit souce 69 | createCommitUrl: function(/*commit*/){ 70 | return "#"; 71 | }, 72 | 73 | // function to provide the appropriate url to the author avatar 74 | createAuthorAvatarUrl: _.memoize(function(author) { 75 | return "https://secure.gravatar.com/avatar/" + md5(author.emailAddress) + ".jpg?s=48&d=mm"; 76 | }, function(author){return author.emailAddress;}), 77 | hiddenBranches:[] 78 | }; 79 | 80 | var cleanup = function (_data) { 81 | var result = {}; 82 | data = result; 83 | result.commits = {}; 84 | result.openEnds = {}; 85 | if (!_data.commits || !_data.branches || !_data.tags) { 86 | throw "raw data should have a commits, branches and tags property"; 87 | } 88 | for (var i = 0; i < _data.commits.length; i++) { 89 | for (var j = 0; j < _data.commits[i].values.length; j++) { 90 | var commit = _data.commits[i].values[j]; 91 | 92 | // cleanup stuff (when redrawing, this can still be there from last time) 93 | delete commit.columns; 94 | delete commit.labels; 95 | delete commit.orderTimestamp; 96 | delete commit.children; 97 | 98 | result.commits[commit.id] = commit; 99 | } 100 | } 101 | for (var id in result.commits) { 102 | var commit = result.commits[id]; 103 | commit.orderTimestamp = commit.authorTimestamp; 104 | if (!commit.children) commit.children = []; 105 | for (var i = commit.parents.length - 1; i >= 0; i--) { 106 | var parent = result.commits[commit.parents[i].id]; 107 | if (parent) { 108 | setChildToParent(parent, commit.id); 109 | } else { 110 | result.openEnds[commit.id] = result.openEnds[commit.id] || []; 111 | result.openEnds[commit.id].push(commit.parents[i].id); 112 | } 113 | } 114 | } 115 | result.branches = _.filter(_data.branches.values, function(b){return options.hiddenBranches.indexOf(b.id) === -1;}); 116 | result.hiddenBranches = _.filter(_data.branches.values, function(b){return options.hiddenBranches.indexOf(b.id) > -1;}); 117 | for (var i = 0; i < result.branches.length; i++) { 118 | var branch = result.branches[i]; 119 | var commit = result.commits[branch.latestChangeset]; 120 | if (commit) { 121 | commit.labels = (commit.labels || []); 122 | commit.labels.push(branch.id); 123 | branch.lastActivity = commit.authorTimestamp; 124 | } 125 | } 126 | 127 | // fixup orderTimestamp for cases of rebasing and cherrypicking, where the parent can be younger than the child 128 | var fixMyTimeRecursive = function (c, after) { 129 | if (!c) return; 130 | if (c.orderTimestamp <= after) { 131 | //console.log("fixing orderTimestamp for " + c.displayId + " " + c.orderTimestamp + " -> " + after + 1); 132 | c.orderTimestamp = after + 1; 133 | for (var k = 0; k < c.children.length; k++) { 134 | fixMyTimeRecursive(result.commits[c.children[k]], c.orderTimestamp); 135 | } 136 | } 137 | }; 138 | for (var key in result.commits) { 139 | var me = result.commits[key]; 140 | for (var k = 0; k < me.parents.length; k++) { 141 | var parent = result.commits[me.parents[k].id]; 142 | if (parent) fixMyTimeRecursive(me, parent.orderTimestamp); 143 | } 144 | } 145 | 146 | result.tags = _data.tags.values; 147 | for (var i = 0; i < result.tags.length; i++) { 148 | var tag = result.tags[i]; 149 | var commit = result.commits[tag.latestChangeset]; 150 | if (commit) { 151 | commit.labels = (commit.labels || []); 152 | commit.labels.push(tag.id); 153 | } 154 | } 155 | result.labels = result.tags.concat(result.branches); 156 | 157 | result.chronoCommits = []; 158 | for (var id in result.commits) { 159 | result.chronoCommits.push(id); 160 | } 161 | 162 | // evaluate visibility 163 | for(var i = 0; i < result.chronoCommits.length; i++){ 164 | var commit = result.commits[result.chronoCommits[i]]; 165 | if(commit.labels && commit.labels.length){ 166 | commit.visible = true; 167 | }else{ 168 | var visibleChildren = _.filter( 169 | _.map(commit.children, function(id){return result.commits[id];}), 170 | function(child){return child.visible;}); 171 | commit.visible = (visibleChildren.length > 0); 172 | } 173 | } 174 | 175 | result.chronoCommits.sort(function (a, b) { return result.commits[b].orderTimestamp - result.commits[a].orderTimestamp; }); 176 | result.visibleCommits = []; 177 | for (var i = 0, counter = 0; i < result.chronoCommits.length; i++) 178 | { 179 | var commit = result.commits[result.chronoCommits[i]]; 180 | if(commit.visible){ 181 | commit.orderNr = counter; 182 | result.visibleCommits.push(result.chronoCommits[i]); 183 | counter++; 184 | }else{ 185 | delete commit.orderNr; 186 | } 187 | } 188 | 189 | 190 | 191 | 192 | setColumns(result); 193 | return result; 194 | }; 195 | 196 | var setChildToParent = function (parent, childId) { 197 | parent.children = parent.children || []; 198 | parent.children.push(childId); 199 | }; 200 | 201 | var setColumns = function () { 202 | isolateMaster(); 203 | isolateDevelop(); 204 | isolateRest(); 205 | separateReleaseFeatureBranches(); 206 | combineColumnsOfType('d'); 207 | combineColumnsOfType('f'); 208 | combineColumnsOfType('r'); 209 | }; 210 | 211 | var isolateMaster = function () { 212 | var head = _.filter(data.branches, function (item) { return (item.id == options.masterRef); }); 213 | if (head.length == 0) return; 214 | var versionCommitPath = findShortestPathAlong( 215 | /*from*/ head[0].latestChangeset, 216 | /*along*/ _.map(_.filter(data.tags, 217 | function (tag) { return tag.id.match(options.releaseTagPattern); }), 218 | function (i) { return i.latestChangeset; }), 219 | data 220 | ); 221 | for (var i = 0; i < versionCommitPath.length; i++) { 222 | putCommitInColumn(versionCommitPath[i], 'm', data); 223 | } 224 | // add older commits that are the 'first' parents of the oldest master commit 225 | while (true) { 226 | var masterCommits = data.columns['m'].commits; 227 | var oldestMaster = masterCommits[masterCommits.length - 1]; 228 | var evenOlder = data.commits[oldestMaster].parents; 229 | if (!evenOlder || evenOlder.length == 0) break; 230 | if (!putCommitInColumn(evenOlder[0].id, 'm', data)) { 231 | break; 232 | } 233 | } 234 | 235 | }; 236 | 237 | var isolateDevelop = function () { 238 | var head = _.filter(data.branches, function (item) { return (item.id == options.developRef); }); 239 | if (head.length == 0) return; 240 | 241 | var versionCommitPath = findDevelopPathFrom(head[0].latestChangeset); 242 | for (var i = 0; i < versionCommitPath.length; i++) { 243 | putCommitInColumn(versionCommitPath[i], 'd0', data); 244 | } 245 | // find extra develop commits that are on secondary develop columns 246 | var developBranch = options.developRef.substring(options.developRef.lastIndexOf('/') + 1); 247 | var regexMerge = new RegExp("Merge branch '[^']+' (of \\S+ )?into " + developBranch + "$"); 248 | var current = 1; 249 | for (var i = 0; i < data.chronoCommits.length; i++) { 250 | var commit = data.commits[data.chronoCommits[i]]; 251 | if (!commit.columns) { 252 | if (regexMerge.test(commit.message)) { 253 | putCommitInColumn(commit.id, 'd' + current); 254 | current++; 255 | } 256 | } 257 | } 258 | 259 | }; 260 | 261 | var isolateRest = function () { 262 | var current = 0; 263 | for (var i = 0; i < data.chronoCommits.length; i++) { 264 | var commit = data.commits[data.chronoCommits[i]]; 265 | if (!commit.columns) { 266 | var childrenThatAreNotMasterOrDevelopAndAreLastInTheirColumn = _.filter(commit.children, function (childId) { 267 | var child = data.commits[childId]; 268 | var isOnMasterOrDevelop = child.columns && (child.columns[0] == "m" || child.columns[0][0] == "d"); 269 | if (isOnMasterOrDevelop) return false; 270 | if (!data.columns[child.columns[0]]) { 271 | console.log('huh'); 272 | } 273 | var commitsInColumn = data.columns[child.columns[0]].commits; 274 | return child.id == commitsInColumn[commitsInColumn.length - 1]; 275 | }); 276 | if (childrenThatAreNotMasterOrDevelopAndAreLastInTheirColumn.length == 0) { 277 | // if this commit has a child that is master or develop, but it is not on a column yet, we start a new column 278 | putCommitInColumn(commit.id, "c" + current, data); 279 | current++; 280 | } else { 281 | var firstChild = data.commits[childrenThatAreNotMasterOrDevelopAndAreLastInTheirColumn[0]]; 282 | if (firstChild && firstChild.columns) { 283 | putCommitInColumn(commit.id, firstChild.columns[0], data); 284 | firstChild._hasColumnChild = true; 285 | } else { 286 | console.log("Couldn't find appropriate parent"); 287 | } 288 | } 289 | } 290 | } 291 | }; 292 | 293 | var separateReleaseFeatureBranches = function () { 294 | // first find all branches that match a release or bugfix and place columns in appropriate zone 295 | for(var br = 0; br< data.branches.length; br++){ 296 | var branch = data.branches[br]; 297 | if(branch.id.indexOf(options.releasePrefix) === 0 298 | || branch.id.indexOf(options.hotfixPrefix) === 0 299 | || branch.id.match(options.releaseZonePattern) === 0){ 300 | var head = data.commits[branch.latestChangeset]; 301 | if(head){ 302 | var column = data.columns[head.columns[0]]; 303 | if(column.name[0] === 'c'){ 304 | column.name = 'r' + column.name.substring(1); 305 | } 306 | } 307 | } 308 | } 309 | 310 | // then do same with features for unplaced 311 | for(var br = 0; br< data.branches.length; br++){ 312 | var branch = data.branches[br]; 313 | if(branch.id.indexOf(options.featurePrefix) === 0 ){ 314 | var head = data.commits[branch.latestChangeset]; 315 | if(head){ 316 | var column = data.columns[head.columns[0]]; 317 | if(column.name[0] === 'c'){ 318 | column.name = 'f' + column.name.substring(1); 319 | } 320 | } 321 | } 322 | } 323 | 324 | // then start looking for topology hints 325 | for (var col in data.columns) { 326 | var column = data.columns[col]; 327 | if (col == 'm' || col[0] == 'd' || col[0] == 'r') continue; 328 | var allChildren = _.flatMap(column.commits, function (id) { return data.commits[id].children; }); 329 | var allChildrenOnMaster = _.filter(allChildren, function (id) { 330 | var parent = data.commits[id]; 331 | return parent.visible && parent.columns && parent.columns[0] == 'm'; 332 | }); 333 | if (allChildrenOnMaster.length > 0) { 334 | //release branches are branches that are not master or develop, but some commit merges into master 335 | column.name = 'r' + column.name.substring(1); 336 | continue; 337 | } 338 | var lastVisibleCommit = column.lastVisible(); // data.commits[column.commits[0]]; 339 | if(!lastVisibleCommit){ 340 | continue; 341 | } 342 | // if starts with releasePrefix or hotfixPrefix -> r 343 | if (lastVisibleCommit.labels && lastVisibleCommit.labels.filter(function (l) { 344 | return l.indexOf(options.releasePrefix) == 0 345 | || l.indexOf(options.hotfixPrefix) == 0 346 | || options.releaseZonePattern.test(l); 347 | }).length > 0) { 348 | column.name = 'r' + column.name.substring(1); 349 | continue; 350 | } 351 | if (lastVisibleCommit.labels && lastVisibleCommit.labels.filter(function (l) { return l.indexOf(options.featurePrefix) == 0; }).length > 0) { 352 | column.name = 'f' + column.name.substring(1); 353 | continue; 354 | } 355 | 356 | // var visibleChildren = lastVisibleCommit ? _.filter(lastVisibleCommit.children, function(id){return data.commits[id].visible;}) : []; 357 | // if (visibleChildren.length > 0) { 358 | // var developCommits = _.filter(visibleChildren, function (id) { return data.commits[id].columns[0][0] == 'd'; }); 359 | // if (developCommits.length > 0) { 360 | // // feature branches are branches that eventually merge into develop, not master 361 | // column.name = 'f' + column.name.substring(1); 362 | // } else { 363 | // // so we have a child, but not m or d: probably two branches merged together 364 | // // we'll figure this out later 365 | // column.firstChild = data.commits[lastVisibleCommit.children[0]]; 366 | // } 367 | // } else { 368 | // // unmerged branch without useful label. Assume feature branch 369 | // column.name = 'f' + column.name.substring(1); 370 | // } 371 | } 372 | 373 | while (true) { 374 | var connected = false; 375 | var unassignedColumns = _.filter(_.map(Object.keys(data.columns), function (id) { return data.columns[id]; }), function (c) { return c.name[0] == 'c'; }); 376 | for (var j = 0; j < unassignedColumns.length; j++) { 377 | var column = unassignedColumns[j]; 378 | var childCol = column.finallyMergesToColumn(); 379 | var firstLetter = childCol ? childCol.name[0]: 'f'; 380 | if (firstLetter == 'c') continue; 381 | if (firstLetter == 'd') firstLetter = 'f'; 382 | column.name = firstLetter + column.name.substring(1); 383 | connected = true; 384 | } 385 | if(!connected)break; 386 | } 387 | 388 | // now separate the feature branches into groups: 389 | var featureBranches = _.filter(_.map(Object.keys(data.columns), function (k) { return data.columns[k]; }), function (col) { return (col.name[0] == 'f'); }); 390 | var longBranches = _.filter(featureBranches, function (col) { return col.commits.length > 9 }); 391 | var groupNr = 1; 392 | for (var i = 0; i < longBranches.length; i++) { 393 | var thisCol = longBranches[i]; 394 | thisCol.group = groupNr; 395 | groupNr++; 396 | } 397 | // now loop through _all_ feature branches and group them together 398 | for (var i = 0; i < featureBranches.length; i++) { 399 | var thisCol = featureBranches[i]; 400 | var lastVisibleCommit = data.commits[thisCol.commits[0]]; 401 | if (lastVisibleCommit.children && lastVisibleCommit.children.length > 0) { 402 | var childColumn = data.columns[data.commits[lastVisibleCommit.children[0]].columns[0]]; 403 | if (childColumn.group) thisCol.group = childColumn.group; 404 | } else { 405 | var firstCommit = data.commits[thisCol.commits[thisCol.commits.length - 1]]; 406 | if (firstCommit.parents && firstCommit.parents.length > 0) { 407 | var parentCommit = data.commits[firstCommit.parents[0].id]; 408 | if (parentCommit) { 409 | var parentCol = data.columns[parentCommit.columns[0]]; 410 | if (parentCol.group) thisCol.group = parentCol.group; 411 | } 412 | } 413 | } 414 | } 415 | }; 416 | 417 | var combineColumnsOfType = function (type) { 418 | var columns = _.map(data.columns, function (v /*, k*/) { return v; }).filter(function (v) { return v.name[0] == type }); 419 | var groups = {}; 420 | for (var i = 0; i < columns.length; i++) { 421 | if (columns[i].group) { 422 | groups[columns[i].group] = true; 423 | } 424 | } 425 | groups = Object.keys(groups); 426 | groups.unshift(null); 427 | for (var groupCount = 0; groupCount < groups.length; groupCount++) { 428 | var nowGrouping = groups[groupCount]; 429 | var columnsToCombine = _.filter(columns, function (c) { 430 | if (nowGrouping === null) { 431 | return (typeof c.group === "undefined"); 432 | } 433 | return c.group == nowGrouping; 434 | }); 435 | for (var i = 0; i < columnsToCombine.length; i++) { 436 | var column = columnsToCombine[i]; 437 | for (var j = 0; j < i; j++) { 438 | var earlierColumn = columnsToCombine[j]; 439 | if (!earlierColumn.isVisible()) { 440 | continue; 441 | } 442 | // todo: here we must also search the columns already mapped to this one 443 | var earliestCommitOfFirst = earlierColumn.firstRenderedCommit(); 444 | if (earliestCommitOfFirst && earliestCommitOfFirst.parents.length > 0 && data.commits[earliestCommitOfFirst.parents[0].id]) { 445 | earliestCommitOfFirst = data.commits[earliestCommitOfFirst.parents[0].id]; 446 | } 447 | var lastCommitOfSecond = column.lastRenderedCommit(); 448 | if (lastCommitOfSecond && lastCommitOfSecond.children.length > 0 && data.commits[lastCommitOfSecond.children[0]]) { 449 | lastCommitOfSecond = data.commits[lastCommitOfSecond.children[0]]; 450 | } 451 | if ((!lastCommitOfSecond) || (!earliestCommitOfFirst) || lastCommitOfSecond.orderNr >= earliestCommitOfFirst.orderNr) { 452 | // combine columns 453 | column.combine(earlierColumn); 454 | j = i;//next column 455 | } 456 | } 457 | } 458 | 459 | } 460 | }; 461 | function Column(name){ 462 | var self = this; 463 | self.id = name; 464 | self.name = name; 465 | self.commits = []; 466 | var renderedOn = null; 467 | var renderingOthers = []; 468 | self.combine = function(otherCol){ 469 | renderedOn = otherCol; 470 | otherCol.receive(self); 471 | } 472 | self.receive = function(otherCol){ 473 | renderingOthers.push(otherCol); 474 | } 475 | self.isVisible = function(){return renderedOn === null;} 476 | self.renderPos = function(){return renderedOn ? renderedOn.id : self.id;} 477 | var allRendered = function(){ 478 | return renderingOthers.concat(self); 479 | } 480 | var visibleCommit = function(id){ 481 | var commit = data.commits[id]; 482 | return commit && commit.visible === true 483 | } 484 | self.firstVisible = function(){ 485 | var id = _.findLast(self.commits, visibleCommit); 486 | return data.commits[id]; 487 | } 488 | self.lastVisible = function(){ 489 | var id = _.find(self.commits, visibleCommit); 490 | return data.commits[id]; 491 | } 492 | self.firstRenderedCommit = function(){ 493 | if(renderedOn)return null; 494 | var all = allRendered(); 495 | return all.reduce(function(agg, col){ 496 | var first = col.firstVisible(); 497 | if(first && (!agg || agg.orderNr < first.orderNr))return first; 498 | return agg; 499 | }, null); 500 | } 501 | self.lastRenderedCommit = function(){ 502 | if(renderedOn)return null; 503 | var all = allRendered(); 504 | return all.reduce(function(agg, col){ 505 | var last = col.lastVisible(); 506 | if(!agg || agg.orderNr > last.orderNr)return last; 507 | return agg; 508 | }, null); 509 | } 510 | self.finallyMergesToColumn = function(){ 511 | var lastCommit = this.lastVisible(); 512 | if(!lastCommit)return null; 513 | var childrenOfLast = lastCommit.children; 514 | if(childrenOfLast.length === 0)return null; 515 | var childCol = data.columns[data.commits[childrenOfLast[0]].columns[0]]; 516 | return childCol; 517 | } 518 | } 519 | 520 | var putCommitInColumn = function (commitId, columnName) { 521 | if (!data.columns) data.columns = {}; 522 | if (!(columnName in data.columns)) { 523 | data.columns[columnName] = new Column(columnName ); 524 | } 525 | var commit = data.commits[commitId]; 526 | if (commit) { 527 | commit.columns = commit.columns || []; 528 | commit.columns.push(columnName); 529 | data.columns[columnName].commits.push(commitId); 530 | return true; 531 | } else { 532 | return false; 533 | } 534 | }; 535 | 536 | var findShortestPathAlong = function (from, along) { 537 | var scoreForAlong = function (path, childId) { 538 | if (along.indexOf(childId) > -1) return 1000; 539 | return -1; 540 | } 541 | var mostAlong = findBestPathFromBreadthFirst(from, scoreForAlong); 542 | return mostAlong.asArray(); 543 | } 544 | 545 | function makePath(initialPath) { 546 | var self = { score: 0 }; 547 | var arrayPath = initialPath.slice(0); 548 | var length = arrayPath.length; 549 | var last = arrayPath[length - 1]; 550 | self.members = {}; 551 | var prev = null; 552 | for (var i = 0; i < arrayPath.length; i++) { 553 | self.members[arrayPath[i]] = prev; 554 | prev = arrayPath[i]; 555 | } 556 | self.push = function (newStep) { 557 | var currLast = last; 558 | length++; 559 | self.members[newStep] = currLast; 560 | last = newStep; 561 | arrayPath.push(newStep); 562 | }; 563 | 564 | self.last = function () { 565 | return last; 566 | }; 567 | self.clone = function () { 568 | var clone = makePath(arrayPath); 569 | clone.score = self.score; 570 | return clone; 571 | }; 572 | self.asArray = function () { 573 | return arrayPath.slice(0); 574 | }; 575 | return self; 576 | } 577 | 578 | var findBestPathFromBreadthFirst = function (from, score) { 579 | var scoreFunc = score || function () { return -1 }; 580 | var openPaths = []; 581 | var bestPathToPoints = {}; 582 | var fromCommit = data.commits[from]; 583 | var firstPath = makePath([from]); 584 | var furthestPath = 0; 585 | firstPath.score = 0; 586 | bestPathToPoints[fromCommit.orderNr] = firstPath; 587 | furthestPath = fromCommit.orderNr; 588 | openPaths.push(firstPath); 589 | while (openPaths.length > 0) { 590 | var basePath = openPaths.shift(); 591 | var tail = data.commits[basePath.last()]; 592 | for (var i = 0; i < tail.parents.length; i++) { 593 | var nextChild = data.commits[tail.parents[i].id]; 594 | if (!nextChild) continue; 595 | var stepScore = scoreFunc(basePath, nextChild.id); 596 | if (stepScore === false) { 597 | // blocked node 598 | continue; 599 | } 600 | if (bestPathToPoints[nextChild.orderNr]) { 601 | if (bestPathToPoints[nextChild.orderNr].score > basePath.score + stepScore) { 602 | // this is not the best path. We do not place it in the open paths 603 | continue; 604 | } 605 | } 606 | var newPath = basePath.clone(); 607 | newPath.push(nextChild.id); 608 | newPath.score = basePath.score + stepScore; 609 | openPaths.push(newPath); 610 | bestPathToPoints[nextChild.orderNr] = newPath; 611 | if (furthestPath < nextChild.orderNr) furthestPath = nextChild.orderNr; 612 | } 613 | } 614 | var allDistances = Object.keys(bestPathToPoints); 615 | allDistances.sort(function (p1, p2) { 616 | if (!p1) return 0; 617 | if (!p2) return 0; 618 | return bestPathToPoints[p2].score - bestPathToPoints[p1].score; 619 | }); 620 | return bestPathToPoints[allDistances[0]]; 621 | } 622 | 623 | var findDevelopPathFrom = function(from) { 624 | var developBranch = options.developRef.substring(options.developRef.lastIndexOf('/') + 1); 625 | var releasePrefix = options.releasePrefix.split('/')[2]; 626 | var hotfixPrefix = options.hotfixPrefix.split('/')[2]; 627 | var regexSelfMerge = new RegExp("Merge branch '(" + developBranch + ")' of http:\\/\\/\\S+ into \\1"); 628 | var regexRealMerge = new RegExp("Merge branch '[^']+' into " + developBranch + "$"); 629 | var regexReleaseMerge = new RegExp("Merge branch '(" + releasePrefix + "|" + hotfixPrefix + ")[^']+' into " + developBranch + "\\b"); 630 | var score = function (path, nextId) { 631 | var c = data.commits[nextId]; 632 | var last = data.commits[path.last()]; 633 | // no part of m can be d 634 | if (c.columns && c.columns[0] == 'm') return false; 635 | // next commit cannot have a child further down the line 636 | var childrenInPath = c.children.filter(function(child) { 637 | return child in path.members; 638 | }); 639 | if (childrenInPath.length != 1) return false; 640 | // merges of develop onto itself are neutral 641 | if (regexSelfMerge.test(c.message)) return 0; 642 | //merges of a release branch onto develop are a big bonus (we want these on the primary develop branch) 643 | if (regexReleaseMerge.test(c.message)) return 20; 644 | //merges of a local branch onto develop are a bonus 645 | if (regexRealMerge.test(c.message)) return 5; 646 | // following first parent is a bonus 647 | if (last.parents.length > 1 && c.id == last.parents[0].id) return 1; 648 | return -.1; 649 | } 650 | var path = findBestPathFromBreadthFirst(from, score); 651 | return path.asArray(); 652 | }; 653 | 654 | self.state = function () { 655 | var state = JSON.stringify(rawData); 656 | return state; 657 | }; 658 | 659 | var rawData = null; 660 | var downloadedStartPoints = []; 661 | 662 | self.draw = function (elem, opt) { 663 | 664 | // Check if we have a placeholder element 665 | if(elem) { 666 | // Determine if placeholder element was provided 667 | try { 668 | //Using W3 DOM2 (works for FF, Opera and Chrom) 669 | if(!(elem instanceof HTMLElement)) { 670 | opt = elem; 671 | elem = null; 672 | } 673 | } catch(e) { 674 | //Browsers not supporting W3 DOM2 don't have HTMLElement and 675 | //an exception is thrown and we end up here. Testing some 676 | //properties that all elements have. (works on IE7) 677 | if(!((typeof elem==="object") && 678 | (elem.nodeType===1) && (typeof elem.style === "object") && 679 | (typeof elem.ownerDocument ==="object"))) { 680 | opt = elem; 681 | elem = null; 682 | } 683 | } 684 | } 685 | 686 | // Merge options with defaults 687 | options = _.extend(options, opt); 688 | options.drawElem = options.drawElem || elem; 689 | 690 | // Check if we have a placeholder element 691 | if(!options.drawElem) { 692 | options.drawElem = d3.select("body").append("div").attr("id", "gitflow-visualize"); 693 | }else{ 694 | if(! (options.drawElem instanceof d3.selection)){ 695 | options.drawElem = d3.select(options.drawElem); 696 | } 697 | } 698 | 699 | // Start drawing! 700 | options.showSpinner(); 701 | options.dataCallback(function (data) { 702 | rawData = data; 703 | options.hideSpinner(); 704 | drawFromRaw(); 705 | }); 706 | }; 707 | 708 | var appendData = function (newCommits) { 709 | rawData.commits.push(newCommits); 710 | } 711 | 712 | var drawFromRaw = function () { 713 | options.showSpinner(); 714 | data = setTimeout(function () { 715 | cleanup(rawData); 716 | options.hideSpinner(); 717 | options.dataProcessed(data); 718 | if (options.drawElem) { 719 | self.drawing.drawTable(options.drawElem); 720 | self.drawing.drawGraph(options.drawElem); 721 | self.drawing.updateHighlight(); 722 | } 723 | }, 10); 724 | } 725 | 726 | self.drawing = (function () { 727 | var self = {}; 728 | 729 | self.updateHighlight = function () { 730 | var highlightCommits = function (arrIds) { 731 | if (!arrIds || arrIds.length == 0) { 732 | d3.selectAll(".commit-msg").classed("dim", false).classed("highlight", false); 733 | d3.selectAll(".commit-dot").classed("dim", false); 734 | d3.selectAll(".arrow").style("opacity", "1"); 735 | return; 736 | } 737 | for (var id in data.commits) { 738 | if (arrIds.indexOf(id) > -1) { 739 | d3.selectAll("#msg-" + id).classed("dim", false).classed("highlight", true); 740 | d3.selectAll("#commit-" + id).classed("dim", false); 741 | d3.selectAll(".arrow-to-" + id).style("opacity", "1"); 742 | } else { 743 | d3.selectAll("#msg-" + id).classed("dim", true).classed("highlight", false); 744 | d3.selectAll("#commit-" + id).classed("dim", true); 745 | d3.selectAll(".arrow-to-" + id).style("opacity", "0.2"); 746 | 747 | } 748 | } 749 | }; 750 | 751 | d3.selectAll('.commit-msg.selected').classed("selected", false); 752 | 753 | switch (displayState.style) { 754 | case "none": 755 | highlightCommits([]); 756 | break; 757 | case "ancestry": 758 | var root = d3.select("#msg-" + displayState.root); 759 | var toHighlight = {}; 760 | var addIdsAncestry = function (id) { 761 | var commit = data.commits[id]; 762 | if (!commit) return; 763 | if (!toHighlight[id]) { 764 | toHighlight[id] = true; 765 | for (var i = 0; i < commit.parents.length; i++) { 766 | addIdsAncestry(commit.parents[i].id); 767 | } 768 | } else { 769 | // prevent cycles 770 | } 771 | }; 772 | root.classed("selected", true); 773 | addIdsAncestry(displayState.root); 774 | highlightCommits(Object.keys(toHighlight)); 775 | break; 776 | default: 777 | } 778 | 779 | } 780 | 781 | self.drawTable = function (elem) { 782 | if (options.drawTable) { 783 | var table = d3.select(document.createElement('table')); 784 | table.append('tr').html( drawColumnsAsHeaders() + 'shaparentauthoratmsg'); 785 | for (var i = 0 ; i < data.chronoCommits.length; i++) { 786 | var commit = data.commits[data.chronoCommits[i]]; 787 | var time = new Date(commit.authorTimestamp); 788 | table.append('tr').html(drawColumnsAsCells(commit) 789 | + '' + commit.displayId + '' + showCommaSeparated(commit.parents) + 790 | '' + commit.author.name + '' + moment(time).format("M/D/YY HH:mm:ss") + 791 | '' + commit.message + ''); 792 | } 793 | d3.select(elem).append(table); 794 | } 795 | }; 796 | 797 | var showCommaSeparated = function (arr) { 798 | return _.map(arr, function (i) { return i.displayId; }).join(", "); 799 | } 800 | 801 | var keysInOrder = function (obj) { 802 | var keys = _.map(obj, function (v, k) { return k; }); 803 | keys.sort(firstBy(function (k1, k2) { 804 | var groupVal = function (k) { return { 'm': 1, 'd': 3, 'f': 4, 'r': 2 }[obj[k].name[0]] || 5; }; 805 | return groupVal(k1) - groupVal(k2); 806 | }).thenBy(function (k1, k2) { 807 | return (data.columns[k1].group || 0) - (data.columns[k2].group || 0); 808 | }).thenBy(function (k1, k2) { 809 | if (data.columns[k1].name[0] == 'f') { 810 | // for feature branches we want the ones with recent commits closer to develop 811 | var commits1 = data.columns[k1].commits; 812 | var commits2 = data.columns[k2].commits; 813 | // order by last commit 814 | return data.commits[commits1[0]].orderNr - data.commits[commits2[0]].orderNr; 815 | } 816 | return k2 > k1 ? -1 : 1; 817 | })); 818 | return keys; 819 | }; 820 | 821 | var drawColumnsAsCells = function (commit) { 822 | var result = ""; 823 | var keys = keysInOrder(data.columns); 824 | for (var i = 0; i < keys.length; i++) { 825 | var col = keys[i]; 826 | result += ""; 827 | if (commit.columns.indexOf(col) > -1) { 828 | result += "o"; 829 | } 830 | result += ""; 831 | } 832 | return result; 833 | }; 834 | 835 | var drawColumnsAsHeaders = function () { 836 | var result = ""; 837 | var keys = keysInOrder(data.columns); 838 | for (var i = 0; i < keys.length; i++) { 839 | var col = keys[i]; 840 | result += "" + data.columns[col].name + ""; 841 | } 842 | return result; 843 | }; 844 | 845 | var groupScale = function(cols, maxWidth){ 846 | var scaleCol = { 847 | gutter: 0.7, 848 | line: 1, 849 | developLine: 0.4, 850 | }; 851 | var mapping = {}; 852 | var lastGroup = ''; 853 | var here = 0; 854 | var basePositions = {}; 855 | for (var i = 0; i < cols.length; i++) { 856 | var thisColId = cols[i]; 857 | var thisCol = data.columns[thisColId]; 858 | if(!thisCol.isVisible()){ 859 | // draws on other column 860 | mapping[thisColId] = thisCol.renderPos(); 861 | continue; 862 | } 863 | var thisGroup = thisColId[0]; 864 | if(lastGroup != thisGroup) here += scaleCol.gutter; 865 | here += thisGroup == 'd' ? scaleCol.developLine : scaleCol.line; 866 | basePositions[thisColId] = here; 867 | lastGroup = thisGroup; 868 | } 869 | 870 | var baseLinear = d3.scale.linear() 871 | .domain([0,here]) 872 | .range([0, Math.min(maxWidth, 20 * here)]); 873 | return function(d){ 874 | if(d in mapping){ 875 | d = mapping[d]; 876 | } 877 | var offset = 0; 878 | if(d[d.length-1] == "+"){ 879 | d = d.substring(0, d.length-1); 880 | offset = 0.5; 881 | } 882 | return baseLinear(basePositions[d] + offset); 883 | }; 884 | 885 | } 886 | 887 | self.drawGraph = function (elem) { 888 | var calcHeight = Math.max(800, data.visibleCommits.length * constants.rowHeight); 889 | var size = { width: 500, height: calcHeight }; 890 | var margin = 20; 891 | 892 | var svg = elem.select("svg>g"); 893 | if (svg.empty()) { 894 | var cont = elem.append("div"); 895 | cont.attr("class", "commits-graph-container"); 896 | var svg = cont.append("svg") 897 | .attr("class", "commits-graph") 898 | .append("g") 899 | .attr("transform", "translate(" + margin + ",0)"); 900 | elem.select("svg") 901 | .attr("width", size.width + 2 * margin) 902 | .attr("height", size.height + 2 * margin); 903 | var backgroundLayer = svg.append("g").attr("id", "bgLayer"); 904 | var arrowsLayer = svg.append("g").attr("id", "arrowsLayer"); 905 | var mainLinesLayer = svg.append("g").attr("id", "mainLinesLayer"); 906 | var commitsLayer = svg.append("g").attr("id", "commitsLayer"); 907 | } 908 | backgroundLayer = svg.select("g#bgLayer"); 909 | arrowsLayer = svg.select("g#arrowsLayer"); 910 | mainLinesLayer = svg.select("g#mainLinesLayer"); 911 | commitsLayer = svg.select("g#commitsLayer"); 912 | 913 | var columnsInOrder = keysInOrder(data.columns); 914 | 915 | var legendaBlocks = { 916 | "master": { prefix: 'm' }, 917 | "releases": { prefix: 'r' }, 918 | "develop": { prefix: 'd' }, 919 | "features": { prefix: 'f' } 920 | } 921 | for (var key in legendaBlocks) { 922 | var groupColumns = columnsInOrder.filter(function (k) { return data.columns[k].name[0] === legendaBlocks[key].prefix; }); 923 | if (groupColumns.length == 0) { 924 | delete legendaBlocks[key]; 925 | continue; 926 | } 927 | legendaBlocks[key].first = groupColumns[0]; 928 | legendaBlocks[key].last = groupColumns[groupColumns.length - 1]; 929 | } 930 | 931 | var x = groupScale(columnsInOrder, size.width, data.columnMappings); 932 | var y = d3.scale.linear() 933 | .domain([0, data.visibleCommits.length]) 934 | .range([60, 60 + data.visibleCommits.length * constants.rowHeight]); 935 | 936 | var line = d3.svg.line() 937 | //.interpolate("bundle") 938 | .x(function (d) { return x(d.x); }) 939 | .y(function (d) { return y(d.y); }); 940 | 941 | var connector = function (d) { 942 | var childCommit = data.commits[d.c]; 943 | var parentCommit = data.commits[d.p]; 944 | if (!childCommit || !parentCommit || !childCommit.visible) return null; 945 | var intermediateRow = parentCommit.orderNr - .5; 946 | var intermediatCol = childCommit.columns[0]; 947 | var intermediateRow2 = null; 948 | var intermediateCol2 = null; 949 | var childCol = data.columns[childCommit.columns[0]]; 950 | if (!childCol) return null; 951 | var parentCol = data.columns[parentCommit.columns[0]]; 952 | if (childCol.id != parentCol.id) { // merge 953 | var followingCommitOnParent = parentCol.commits[parentCol.commits.indexOf(parentCommit.id) - 1]; 954 | if (!followingCommitOnParent || data.commits[followingCommitOnParent].orderNr < childCommit.orderNr) { 955 | intermediateRow = childCommit.orderNr + .5; 956 | intermediatCol = parentCommit.columns[0]; 957 | } else { 958 | var precedingCommitOnChild = childCol.commits[childCol.commits.indexOf(childCommit.id) + 1]; 959 | if (!precedingCommitOnChild || data.commits[precedingCommitOnChild].orderNr > parentCommit.orderNr) { 960 | // do nothing, the sideways first model of the non-merge commit applies 961 | } else { 962 | // worst case: two bends 963 | intermediateCol2 = childCommit.columns[0] + '+'; 964 | intermediateRow2 = parentCommit.orderNr - 0.5; 965 | intermediatCol = childCommit.columns[0] + '+'; 966 | intermediateRow = childCommit.orderNr + 0.5; 967 | } 968 | } 969 | } 970 | if(!intermediateCol2)intermediateCol2 = intermediatCol; 971 | if(!intermediateRow2)intermediateRow2 = intermediateRow; 972 | var points = [ 973 | { x: childCommit.columns[0], y: childCommit.orderNr }, 974 | { x: intermediatCol, y: intermediateRow }, 975 | { x: intermediateCol2, y: intermediateRow2 }, 976 | { x: parentCommit.columns[0], y: parentCommit.orderNr }]; 977 | return line(points); 978 | }; 979 | 980 | // arrows 981 | var arrows = _.flatMap( 982 | d3.values(data.commits).filter(function(c){return c.visible;}) 983 | , function (c) { 984 | return c.parents.map(function (p) { return { p: p.id, c: c.id }; }); 985 | }); 986 | var arrow = arrowsLayer.selectAll(".arrow") 987 | .data(arrows, function(d){return 'a-' + d.p + '-' + d.c;}); 988 | var addedArrow = arrow 989 | .enter().append("g") 990 | .attr("class", function (d) { return "arrow arrow-to-" + d.c; }); 991 | addedArrow 992 | .append("path") 993 | .attr("stroke-linejoin", "round") 994 | .attr("class", "outline"); 995 | addedArrow 996 | .append("path") 997 | .attr("stroke-linejoin", "round") 998 | .attr("class", function (d) { return "branch-type-" + branchType(d.c, d.p); }); 999 | 1000 | var path = arrow.selectAll("g>path"); 1001 | path.transition().attr("d", connector) 1002 | 1003 | arrow.exit().remove(); 1004 | 1005 | 1006 | var branchLine = backgroundLayer.selectAll(".branch") 1007 | .data(d3.values(data.columns).filter(function(c){return c.isVisible();})); 1008 | branchLine 1009 | .enter().append("g") 1010 | .attr("class", "branch") 1011 | .append("line"); 1012 | branchLine.select("g>line").transition() 1013 | .attr("class", function (d) { return "branch-line " + d.name; }) 1014 | .attr("x1", function (d) { return x(d.id); }) 1015 | .attr("x2", function (d) { return x(d.id); }) 1016 | .attr("y1", y(0)) 1017 | .attr("y2", size.height); 1018 | branchLine.exit().remove(); 1019 | 1020 | var branchLine = mainLinesLayer.selectAll(".branch") 1021 | .data(d3.values(data.columns).filter(function(c){return c.isVisible() && (c.id === "d0" || c.id === "m");})); 1022 | branchLine 1023 | .enter().append("g") 1024 | .attr("class", "branch") 1025 | .append("line"); 1026 | branchLine.select("g>line").transition() 1027 | .attr("class", function (d) { return "branch-line " + d.name; }) 1028 | .attr("x1", function (d) { return x(d.id); }) 1029 | .attr("x2", function (d) { return x(d.id); }) 1030 | .attr("y1", y(0)) 1031 | .attr("y2", size.height); 1032 | 1033 | var commit = commitsLayer.selectAll(".commit") 1034 | .data(d3.values(data.commits).filter(function(c){return c.visible;}), function(c){return 'c-' + c.id;}); 1035 | commit 1036 | .enter().append("g") 1037 | .attr("class", "commit") 1038 | .append("circle") 1039 | .attr("class", "commit-dot") 1040 | .attr("r", 5); 1041 | 1042 | commit.exit().remove(); 1043 | commit 1044 | .transition() 1045 | .select("g>circle") 1046 | .attr("cx", function (d) { return x(d.columns[0]); }) 1047 | .attr("cy", function (d) { return y(d.orderNr); }) 1048 | .attr("id", function (d) { return "commit-" + d.id; }); 1049 | 1050 | var blockLegenda = backgroundLayer.selectAll(".legenda-label") 1051 | .data(Object.keys(legendaBlocks)); 1052 | var entering = blockLegenda.enter(); 1053 | var rotated = entering 1054 | .append("g") 1055 | .attr("class", function (d) { return "legenda-label " + legendaBlocks[d].prefix; }) 1056 | .append("g") 1057 | .attr("transform", function (d) { 1058 | var extraOffset = legendaBlocks[d].first == legendaBlocks[d].last ? -10 : 0; 1059 | return "translate(" + (x(legendaBlocks[d].first) + extraOffset) + ", " + (y(0) - 20) + ") rotate(-40)"; 1060 | }); 1061 | rotated.append("rect") 1062 | .attr("width", 60).attr("height", 15).attr("rx", "2"); 1063 | rotated.append("text").attr("y", "12").attr("x", "3") 1064 | .text(function (d) { return d; }); 1065 | 1066 | blockLegenda 1067 | .select("g").transition() 1068 | .attr("transform", function (d) { 1069 | var extraOffset = legendaBlocks[d].first == legendaBlocks[d].last ? -10 : 0; 1070 | return "translate(" + (x(legendaBlocks[d].first) + extraOffset) + ", " + (y(0) - 20) + ") rotate(-40)"; 1071 | }); 1072 | 1073 | var messages = elem.select("div.messages"); 1074 | if (messages.empty()) { 1075 | messages = elem.append("div") 1076 | .attr("class", "messages"); 1077 | messages 1078 | .append("div").attr("class", "context-menu"); 1079 | } 1080 | var msgHeader = messages.select("div.msg-header"); 1081 | if(msgHeader.empty()){ 1082 | msgHeader = messages.append("div") 1083 | .attr("class", "msg-header"); 1084 | msgHeader.append("span").attr("class", "branch-btn label aui-lozenge aui-lozenge-subtle") 1085 | .on("click", function(){ 1086 | var items = [["Show all", function(){ 1087 | options.hiddenBranches = []; 1088 | drawFromRaw(); 1089 | }]]; 1090 | if(branchVisibilityHandler !== null){ 1091 | items.push(["Change...", branchVisibilityHandler]); 1092 | } 1093 | var pos = d3.mouse(messages.node()); 1094 | menu.show(items, pos[0], pos[1]); 1095 | }); 1096 | 1097 | } 1098 | var branchLabelText = (data.branches.length + data.hiddenBranches.length) + " branches"; 1099 | if(data.hiddenBranches.length > 0) branchLabelText += " (" + data.hiddenBranches.length + " hidden)"; 1100 | msgHeader.select("span.branch-btn").text(branchLabelText); 1101 | 1102 | //labels 1103 | var labelData = messages.selectAll(".commit-msg") 1104 | .data(d3.values(data.commits).filter(function(c){return c.visible;}) 1105 | , function (c) {return c.id + "-" + c.orderNr;}); 1106 | labelData 1107 | .enter().append("div") 1108 | .attr("class", "commit-msg") 1109 | .attr("id", function (c) { return "msg-" + c.id; }) 1110 | .on('click', function (a) { 1111 | if(d3.event.target.tagName == 'A')return true; 1112 | // will show menu. Collect items 1113 | var items = []; 1114 | if(d3.event.target.tagName == 'SPAN'){ 1115 | // on branch label 1116 | var clickedBranch = 'refs/heads/' + d3.event.target.innerHTML; 1117 | items.push(["Hide branch '" + d3.event.target.innerHTML + "'", function(){ 1118 | options.hiddenBranches.push(clickedBranch); 1119 | drawFromRaw(); 1120 | }]); 1121 | } 1122 | if(displayState.style == "ancestry"){ 1123 | items.push(["Stop highlighting", function(){ 1124 | displayState.style = "none"; 1125 | displayState.root = null; 1126 | self.updateHighlight(); 1127 | }]); 1128 | } 1129 | if(displayState.style !== "ancestry" || a.id !== displayState.root){ 1130 | items.push(["Highlight ancestry from here", function(){ 1131 | displayState.style = "ancestry"; 1132 | displayState.root = a.id; 1133 | self.updateHighlight(); 1134 | }]); 1135 | } 1136 | var pos = d3.mouse(messages.node()); 1137 | menu.show(items, pos[0], pos[1]); 1138 | }); 1139 | labelData.exit().remove(); 1140 | labelData 1141 | .html(function (d) { 1142 | var commitUrl = options.createCommitUrl(d); 1143 | var res = ""; 1167 | if (d.author) { 1168 | var authorAvatarUrl = options.createAuthorAvatarUrl(d.author); 1169 | res += ""; 1170 | } else { 1171 | res += ""; 1172 | } 1173 | if (d.authorTimestamp) { 1174 | var dt = new Date(d.authorTimestamp); 1175 | var today = (new Date().toDateString() === dt.toDateString()); 1176 | if (today) { 1177 | res += " "; 1178 | } else { 1179 | res += " "; 1180 | } 1181 | } 1182 | res += " "; 1183 | res += "
"; 1144 | if (d.labels) { 1145 | _.each(d.labels, function (v /*, k*/) { 1146 | if (v.indexOf('refs/heads/') == 0) { 1147 | if (v.indexOf(options.masterRef) == 0) { 1148 | res += "" + v.substring(11) + ""; 1149 | } else if (v.indexOf(options.developRef) == 0) { 1150 | res += "" + v.substring(11) + ""; 1151 | } else if (v.indexOf(options.featurePrefix) == 0) { 1152 | res += "" + v.substring(11) + ""; 1153 | } else if (v.indexOf(options.releasePrefix) == 0 || v.indexOf(options.hotfixPrefix) == 0) { 1154 | res += "" + v.substring(11) + ""; 1155 | } else { 1156 | res += "" + v.substring(11) + ""; 1157 | } 1158 | } else if (v.indexOf('refs/tags/') == 0) { 1159 | res += "" + v.substring(10) + ""; 1160 | } else { 1161 | res += "" + v + ""; 1162 | } 1163 | }); 1164 | } 1165 | res += " " + d.message; 1166 | res += "" + (d.author.displayName || d.author.name || d.author.emailAddress) + " " + moment(dt).format("HH:mm:ss") + " today" + moment(dt).format("dd YYYY-MM-DD") + "" + d.displayId + "
"; 1184 | return res; 1185 | }) 1186 | .transition() 1187 | .attr("style", function (d) { 1188 | var commit = d; 1189 | return "top:" + (y(commit.orderNr) - constants.rowHeight / 2) + "px;"; 1190 | }); 1191 | 1192 | function isElementInViewport(el) { 1193 | var rect = el.getBoundingClientRect(); 1194 | return ( 1195 | rect.top >= 0 && 1196 | rect.left >= 0 && 1197 | rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */ 1198 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */ 1199 | ); 1200 | } 1201 | 1202 | self.lazyLoad = function() { 1203 | //check for openEnded messages in view 1204 | var keyInView = null; 1205 | for (var key in data.openEnds) { 1206 | var elementSelection = d3.select('#msg-' + key); 1207 | if(!elementSelection.empty()) { 1208 | if (isElementInViewport(elementSelection.node())) { 1209 | keyInView = key; 1210 | break; 1211 | } 1212 | } 1213 | } 1214 | if (keyInView) { 1215 | var ourOrderNr = data.commits[keyInView].orderNr; 1216 | for (var key in data.openEnds) { 1217 | if (data.commits[key].orderNr > ourOrderNr + 200) { 1218 | // to far out, skip 1219 | continue; 1220 | } 1221 | for (var i = 0; i < data.openEnds[key].length; i++) { 1222 | var parentId = data.openEnds[key][i]; 1223 | if(downloadedStartPoints.indexOf(parentId) === -1){ 1224 | openEndsToBeDownloaded[parentId] = true; 1225 | console.log("scheduled: " + parentId); 1226 | } 1227 | } 1228 | delete data.openEnds[key]; 1229 | } 1230 | for (var key in openEndsToBeDownloaded) { 1231 | console.log("downloading: " + key); 1232 | delete openEndsToBeDownloaded[key]; 1233 | openEndsBeingDownloaded[key] = true; 1234 | options.moreDataCallback(key, function (commits, thisKey) { 1235 | delete openEndsBeingDownloaded[thisKey]; 1236 | downloadedStartPoints.push(thisKey); 1237 | if (commits) appendData(commits); 1238 | if (Object.keys(openEndsToBeDownloaded).length == 0 && Object.keys(openEndsBeingDownloaded).length == 0) { 1239 | console.log("queues empty, ready to draw"); 1240 | setTimeout(function () { 1241 | drawFromRaw(); 1242 | }, 50); 1243 | } else { 1244 | console.log("waiting, still downloads in progress"); 1245 | console.log(openEndsToBeDownloaded); 1246 | console.log(openEndsBeingDownloaded); 1247 | } 1248 | 1249 | }); 1250 | } 1251 | openEndsToBeDownloaded = {}; 1252 | } 1253 | }; 1254 | }; 1255 | 1256 | var openEndsToBeDownloaded = {}; 1257 | var openEndsBeingDownloaded = {}; 1258 | var branchType = function (childId, parentId) { 1259 | var ct = function (id) { 1260 | var commit = data.commits[id]; 1261 | if (!commit || data.columns.length == 0) return "?"; 1262 | var columns = commit.columns.map(function (d) { return data.columns[d]; }); 1263 | return columns[0].name[0]; 1264 | }; 1265 | var prioHash = { 'm': 0, 'd': 1, 'r': 3, 'f': 2 }; 1266 | var cols = [ct(childId), ct(parentId)]; 1267 | 1268 | // special case for back-merge 1269 | if(cols[0] === 'd' && cols[1] !== 'd')return cols[1] + ' back'; 1270 | 1271 | cols.sort(function (v1, v2) { return prioHash[v2] - prioHash[v1]; }); 1272 | return cols[0] || "default"; 1273 | }; 1274 | 1275 | var menu = function(){ 1276 | var menu = {}; 1277 | var theMenu = null; 1278 | var ensureRef = function(){ 1279 | if(theMenu === null || theMenu.empty()){ 1280 | theMenu = d3.select(".messages .context-menu"); 1281 | theMenu.on("mousemove", function(){ 1282 | console.log("mouse move"); 1283 | timeLastSeen = Date.now(); 1284 | }); 1285 | } 1286 | } 1287 | var timeLastSeen = 0; 1288 | var timer; 1289 | var start = function(){ 1290 | timeLastSeen = Date.now(); 1291 | timer = setInterval(function(){ 1292 | if(timeLastSeen + 10000 < Date.now()){ 1293 | menu.hide(); 1294 | } 1295 | }, 100);} 1296 | var stop = function(){clearInterval(timer);} 1297 | 1298 | menu.show = function(items, x, y){ 1299 | ensureRef(); 1300 | theMenu 1301 | .style("top", y + "px" ) 1302 | .style("left", x + "px") 1303 | .style("visibility", "visible"); 1304 | theMenu.selectAll("div.item").remove(); 1305 | _.each(items, function(item){ 1306 | theMenu.append("div") 1307 | .on("click", function(){ 1308 | item[1](); 1309 | menu.hide(); 1310 | }) 1311 | .attr("class", "item") 1312 | .text(item[0]); 1313 | }); 1314 | d3.event.stopPropagation(); 1315 | d3.select("body").on("click", function(){menu.hide()}); 1316 | start(); 1317 | } 1318 | menu.hide = function(){ 1319 | ensureRef(); 1320 | theMenu.style("visibility", "hidden"); 1321 | stop(); 1322 | } 1323 | 1324 | return menu; 1325 | }(); 1326 | 1327 | return self; 1328 | })(); 1329 | 1330 | var branchVisibilityHandler = null; 1331 | self.branches = { 1332 | setHidden: function(refs){ 1333 | if(!(refs instanceof Array)){ 1334 | throw "pass in refs as an array of strings with full ref descriptors of the branches to hide (like 'refs/heads/develop')"; 1335 | } 1336 | options.hiddenBranches = refs; 1337 | drawFromRaw(); 1338 | }, 1339 | getHidden: function(){ 1340 | return options.hiddenBranches; 1341 | }, 1342 | getAll: function(){ 1343 | return _.map(data.branches.concat(data.hiddenBranches), function(b){ 1344 | return {id: b.id, name: b.displayId, 1345 | lastActivity:b.lastActivity, lastActivityFormatted: moment(b.lastActivity).format("M/D/YY HH:mm:ss"), 1346 | visible: options.hiddenBranches.indexOf(b.id) === -1 1347 | }; 1348 | }); 1349 | }, 1350 | registerHandler: function(handler){ 1351 | branchVisibilityHandler = handler; 1352 | } 1353 | }; 1354 | 1355 | if (document) { 1356 | //d3.select(document).on("scroll resize", function () { 1357 | d3.select(document) 1358 | .on("scroll", function(){GitFlowVisualize.drawing.lazyLoad();}) 1359 | .on("resize", function(){GitFlowVisualize.drawing.lazyLoad();}); 1360 | 1361 | d3.select(document).on("keydown", function () { 1362 | var event = d3.event; 1363 | if (event.ctrlKey && event.shiftKey && event.which == 221) { 1364 | //prompt("Ctrl-C to copy the graph source", GitFlowVisualize.state()); 1365 | var out = d3.select("#debug-output"); 1366 | if (out.empty()) { 1367 | out = d3.select("body").append("textarea").attr("id", "debug-output"); 1368 | } 1369 | out.style("display", ""); 1370 | out.node().value = GitFlowVisualize.state(); 1371 | out.node().focus(); 1372 | out.node().select(); 1373 | out.on('blur', function() { out.style("display", "none");; }); 1374 | } 1375 | }); 1376 | } 1377 | 1378 | return self; 1379 | })(); -------------------------------------------------------------------------------- /dist/gitflow-visualize.min.css: -------------------------------------------------------------------------------- 1 | circle.commit-dot{fill:#fff;stroke:#000;stroke-width:2px}.commit-dot.dim,line{opacity:.2}line{stroke:#000}line.m{stroke:#d04437;stroke-width:3px;opacity:1}line.d0{stroke:#8eb021;stroke-width:3px;opacity:1}.arrow path{stroke:#000;stroke-width:2px;opacity:1;fill:none}.arrow path.outline{stroke:#fff;stroke-width:4px;opacity:.8}.arrow path.branch-type-f{stroke:#3b7fc4}.arrow path.branch-type-r{stroke:#f6c342}.arrow path.branch-type-d{stroke:#8eb021}.arrow path.branch-type-m{stroke:#f6c342}.arrow path.branch-type-default{stroke-width:1px}.arrow path.back{opacity:.5}.messages{position:relative}.commit-msg{position:absolute;white-space:nowrap;cursor:pointer;padding-left:30%;width:70%;overflow-x:hidden}.commit-msg.dim{color:#aaa}.commit-msg.selected{background-color:#ccd9ea}.commit-msg:hover{background-color:#f5f5f5}.commit-link{font-family:courier}.commit-table{width:100%;table-layout:fixed}td.author{width:8em}td.sha{width:5em}td.date{width:7em}.label{margin-right:2px}.branch{background-color:#ffc;border-color:#ff0}.legenda-label text{fill:#fff}.legenda-label path{stroke-width:4}.legenda-label.m rect{fill:#d04437}.legenda-label.m path{stroke:#d04437}.legenda-label.r rect{fill:#f6c342}.legenda-label.r path{stroke:#f6c342}.legenda-label.d rect{fill:#8eb021}.legenda-label.d text{fill:#fff}.legenda-label.d path{stroke:#8eb021}.legenda-label.f rect{fill:#3b7fc4}.legenda-label.f text{fill:#fff}.legenda-label.f path{stroke:#3b7fc4}.tag{background-color:#eee;border-color:#ccc}table.commit-table td{overflow:hidden;margin:2px}.author{font-weight:700;width:120px}.commits-graph-container{width:30%;overflow-x:scroll;float:left;z-index:5;position:relative}#debug-output{width:600px;height:300px;position:absolute;left:300px;top:100px;z-index:100}.branch-btn{cursor:pointer}.context-menu{display:inline-block;position:absolute;visibility:hidden;background-color:#d3d3d3;z-index:10;border:2px outset}.context-menu .item{border-bottom:1px solid silver;background-color:#eee;font-family:sans-serif;padding:3px;cursor:pointer;white-space:nowrap;color:#333}.context-menu .item:hover{color:#000;background-color:#ddd} -------------------------------------------------------------------------------- /dist/gitflow-visualize.min.js: -------------------------------------------------------------------------------- 1 | "use strict";var GitFlowVisualize=function(){function e(e){var t=this;t.id=e,t.name=e,t.commits=[];var r=null,a=[];t.combine=function(e){r=e,e.receive(t)},t.receive=function(e){a.push(e)},t.isVisible=function(){return null===r},t.renderPos=function(){return r?r.id:t.id};var i=function(){return a.concat(t)},s=function(e){var t=n.commits[e];return t&&t.visible===!0};t.firstVisible=function(){var e=_.findLast(t.commits,s);return n.commits[e]},t.lastVisible=function(){var e=_.find(t.commits,s);return n.commits[e]},t.firstRenderedCommit=function(){if(r)return null;var e=i();return e.reduce(function(e,t){var n=t.firstVisible();return n&&(!e||e.orderNrn.orderNr?n:e},null)},t.finallyMergesToColumn=function(){var e=this.lastVisible();if(!e)return null;var t=e.children;if(0===t.length)return null;var r=n.columns[n.commits[t[0]].columns[0]];return r}}function t(e){var n={score:0},r=e.slice(0),a=r.length,i=r[a-1];n.members={};for(var s=null,o=0;o=0;r--){var l=t.commits[i.parents[r].id];l?c(l,i.id):(t.openEnds[i.id]=t.openEnds[i.id]||[],t.openEnds[i.id].push(i.parents[r].id))}}t.branches=_.filter(e.branches.values,function(e){return o.hiddenBranches.indexOf(e.id)===-1}),t.hiddenBranches=_.filter(e.branches.values,function(e){return o.hiddenBranches.indexOf(e.id)>-1});for(var r=0;r0}}t.chronoCommits.sort(function(e,n){return t.commits[n].orderTimestamp-t.commits[e].orderTimestamp}),t.visibleCommits=[];for(var r=0,b=0;r0)a.name="r"+a.name.substring(1);else{var c=a.lastVisible();c&&(c.labels&&c.labels.filter(function(e){return 0==e.indexOf(o.releasePrefix)||0==e.indexOf(o.hotfixPrefix)||o.releaseZonePattern.test(e)}).length>0?a.name="r"+a.name.substring(1):c.labels&&c.labels.filter(function(e){return 0==e.indexOf(o.featurePrefix)}).length>0&&(a.name="f"+a.name.substring(1)))}}}for(;;){for(var m=!1,u=_.filter(_.map(Object.keys(n.columns),function(e){return n.columns[e]}),function(e){return"c"==e.name[0]}),d=0;d9}),v=1,b=0;b0){var x=n.columns[n.commits[c.children[0]].columns[0]];x.group&&(y.group=x.group)}else{var w=n.commits[y.commits[y.commits.length-1]];if(w.parents&&w.parents.length>0){var C=n.commits[w.parents[0].id];if(C){var A=n.columns[C.columns[0]];A.group&&(y.group=A.group)}}}}},g=function(e){for(var t=_.map(n.columns,function(e){return e}).filter(function(t){return t.name[0]==e}),r={},a=0;a0&&n.commits[u.parents[0].id]&&(u=n.commits[u.parents[0].id]);var d=l.lastRenderedCommit();d&&d.children.length>0&&n.commits[d.children[0]]&&(d=n.commits[d.children[0]]),(!d||!u||d.orderNr>=u.orderNr)&&(l.combine(m),c=a)}}},p=function(t,r){n.columns||(n.columns={}),r in n.columns||(n.columns[r]=new e(r));var a=n.commits[t];return!!a&&(a.columns=a.columns||[],a.columns.push(r),n.columns[r].commits.push(t),!0)},v=function(e,t){var n=function(e,n){return t.indexOf(n)>-1?1e3:-1},r=b(e,n);return r.asArray()},b=function(e,r){var a=r||function(){return-1},i=[],s={},o=n.commits[e],l=t([e]),c=0;for(l.score=0,s[o.orderNr]=l,c=o.orderNr,i.push(l);i.length>0;)for(var m=i.shift(),u=n.commits[m.last()],d=0;dm.score+h)){var g=m.clone();g.push(f.id),g.score=m.score+h,i.push(g),s[f.orderNr]=g,c1&&r.id==a.parents[0].id?1:-.1)},m=b(e,c);return m.asArray()};r.state=function(){var e=JSON.stringify(x);return e};var x=null,w=[];r.draw=function(e,t){if(e)try{e instanceof HTMLElement||(t=e,e=null)}catch(n){"object"==typeof e&&1===e.nodeType&&"object"==typeof e.style&&"object"==typeof e.ownerDocument||(t=e,e=null)}o=_.extend(o,t),o.drawElem=o.drawElem||e,o.drawElem?o.drawElem instanceof d3.selection||(o.drawElem=d3.select(o.drawElem)):o.drawElem=d3.select("body").append("div").attr("id","gitflow-visualize"),o.showSpinner(),o.dataCallback(function(e){x=e,o.hideSpinner(),A()})};var C=function(e){x.commits.push(e)},A=function(){o.showSpinner(),n=setTimeout(function(){l(x),o.hideSpinner(),o.dataProcessed(n),o.drawElem&&(r.drawing.drawTable(o.drawElem),r.drawing.drawGraph(o.drawElem),r.drawing.updateHighlight())},10)};r.drawing=function(){var e={};e.updateHighlight=function(){var e=function(e){if(!e||0==e.length)return d3.selectAll(".commit-msg").classed("dim",!1).classed("highlight",!1),d3.selectAll(".commit-dot").classed("dim",!1),void d3.selectAll(".arrow").style("opacity","1");for(var t in n.commits)e.indexOf(t)>-1?(d3.selectAll("#msg-"+t).classed("dim",!1).classed("highlight",!0),d3.selectAll("#commit-"+t).classed("dim",!1),d3.selectAll(".arrow-to-"+t).style("opacity","1")):(d3.selectAll("#msg-"+t).classed("dim",!0).classed("highlight",!1),d3.selectAll("#commit-"+t).classed("dim",!0),d3.selectAll(".arrow-to-"+t).style("opacity","0.2"))};switch(d3.selectAll(".commit-msg.selected").classed("selected",!1),a.style){case"none":e([]);break;case"ancestry":var t=d3.select("#msg-"+a.root),r={},i=function(e){var t=n.commits[e];if(t&&!r[e]){r[e]=!0;for(var a=0;ashaparentauthoratmsg");for(var a=0;a"+i.displayId+""+t(i.parents)+""+i.author.name+""+moment(c).format("M/D/YY HH:mm:ss")+""+i.message+"")}d3.select(e).append(r)}};var t=function(e){return _.map(e,function(e){return e.displayId}).join(", ")},r=function(e){var t=_.map(e,function(e,t){return t});return t.sort(firstBy(function(t,n){var r=function(t){return{m:1,d:3,f:4,r:2}[e[t].name[0]]||5};return r(t)-r(n)}).thenBy(function(e,t){return(n.columns[e].group||0)-(n.columns[t].group||0)}).thenBy(function(e,t){if("f"==n.columns[e].name[0]){var r=n.columns[e].commits,a=n.columns[t].commits;return n.commits[r[0]].orderNr-n.commits[a[0]].orderNr}return t>e?-1:1})),t},s=function(e){for(var t="",a=r(n.columns),i=0;i",e.columns.indexOf(s)>-1&&(t+="o"),t+=""}return t},l=function(){for(var e="",t=r(n.columns),a=0;a"+n.columns[i].name+""}return e},c=function(e,t){for(var r={gutter:.7,line:1,developLine:.4},a={},i="",s=0,o={},l=0;l=0&&t.left>=0&&t.bottom<=(window.innerHeight||document.documentElement.clientHeight)&&t.right<=(window.innerWidth||document.documentElement.clientWidth)}var l=Math.max(800,n.visibleCommits.length*i.rowHeight),h={width:500,height:l},g=20,p=t.select("svg>g");if(p.empty()){var v=t.append("div");v.attr("class","commits-graph-container");var p=v.append("svg").attr("class","commits-graph").append("g").attr("transform","translate("+g+",0)");t.select("svg").attr("width",h.width+2*g).attr("height",h.height+2*g);var b=p.append("g").attr("id","bgLayer"),y=p.append("g").attr("id","arrowsLayer"),x=p.append("g").attr("id","mainLinesLayer"),O=p.append("g").attr("id","commitsLayer")}b=p.select("g#bgLayer"),y=p.select("g#arrowsLayer"),x=p.select("g#mainLinesLayer"),O=p.select("g#commitsLayer");var z=r(n.columns),k={master:{prefix:"m"},releases:{prefix:"r"},develop:{prefix:"d"},features:{prefix:"f"}};for(var T in k){var E=z.filter(function(e){return n.columns[e].name[0]===k[T].prefix});0!=E.length?(k[T].first=E[0],k[T].last=E[E.length-1]):delete k[T]}var H=c(z,h.width,n.columnMappings),P=d3.scale.linear().domain([0,n.visibleCommits.length]).range([60,60+n.visibleCommits.length*i.rowHeight]),M=d3.svg.line().x(function(e){return H(e.x)}).y(function(e){return P(e.y)}),D=function(e){var t=n.commits[e.c],r=n.commits[e.p];if(!t||!r||!t.visible)return null;var a=r.orderNr-.5,i=t.columns[0],s=null,o=null,l=n.columns[t.columns[0]];if(!l)return null;var c=n.columns[r.columns[0]];if(l.id!=c.id){var m=c.commits[c.commits.indexOf(r.id)-1];if(!m||n.commits[m].orderNrr.orderNr||(o=t.columns[0]+"+",s=r.orderNr-.5,i=t.columns[0]+"+",a=t.orderNr+.5)}}o||(o=i),s||(s=a);var d=[{x:t.columns[0],y:t.orderNr},{x:i,y:a},{x:o,y:s},{x:r.columns[0],y:r.orderNr}];return M(d)},L=_.flatMap(d3.values(n.commits).filter(function(e){return e.visible}),function(e){return e.parents.map(function(t){return{p:t.id,c:e.id}})}),R=y.selectAll(".arrow").data(L,function(e){return"a-"+e.p+"-"+e.c}),B=R.enter().append("g").attr("class",function(e){return"arrow arrow-to-"+e.c});B.append("path").attr("stroke-linejoin","round").attr("class","outline"),B.append("path").attr("stroke-linejoin","round").attr("class",function(e){return"branch-type-"+d(e.c,e.p)});var j=R.selectAll("g>path");j.transition().attr("d",D),R.exit().remove();var S=b.selectAll(".branch").data(d3.values(n.columns).filter(function(e){return e.isVisible()}));S.enter().append("g").attr("class","branch").append("line"),S.select("g>line").transition().attr("class",function(e){return"branch-line "+e.name}).attr("x1",function(e){return H(e.id)}).attr("x2",function(e){return H(e.id)}).attr("y1",P(0)).attr("y2",h.height),S.exit().remove();var S=x.selectAll(".branch").data(d3.values(n.columns).filter(function(e){return e.isVisible()&&("d0"===e.id||"m"===e.id)}));S.enter().append("g").attr("class","branch").append("line"),S.select("g>line").transition().attr("class",function(e){return"branch-line "+e.name}).attr("x1",function(e){return H(e.id)}).attr("x2",function(e){return H(e.id)}).attr("y1",P(0)).attr("y2",h.height);var V=O.selectAll(".commit").data(d3.values(n.commits).filter(function(e){return e.visible}),function(e){return"c-"+e.id});V.enter().append("g").attr("class","commit").append("circle").attr("class","commit-dot").attr("r",5),V.exit().remove(),V.transition().select("g>circle").attr("cx",function(e){return H(e.columns[0])}).attr("cy",function(e){return P(e.orderNr)}).attr("id",function(e){return"commit-"+e.id});var Y=b.selectAll(".legenda-label").data(Object.keys(k)),I=Y.enter(),G=I.append("g").attr("class",function(e){return"legenda-label "+k[e].prefix}).append("g").attr("transform",function(e){var t=k[e].first==k[e].last?-10:0;return"translate("+(H(k[e].first)+t)+", "+(P(0)-20)+") rotate(-40)"});G.append("rect").attr("width",60).attr("height",15).attr("rx","2"),G.append("text").attr("y","12").attr("x","3").text(function(e){return e}),Y.select("g").transition().attr("transform",function(e){var t=k[e].first==k[e].last?-10:0;return"translate("+(H(k[e].first)+t)+", "+(P(0)-20)+") rotate(-40)"});var F=t.select("div.messages");F.empty()&&(F=t.append("div").attr("class","messages"),F.append("div").attr("class","context-menu"));var U=F.select("div.msg-header");U.empty()&&(U=F.append("div").attr("class","msg-header"),U.append("span").attr("class","branch-btn label aui-lozenge aui-lozenge-subtle").on("click",function(){var e=[["Show all",function(){o.hiddenBranches=[],A()}]];null!==N&&e.push(["Change...",N]);var t=d3.mouse(F.node());f.show(e,t[0],t[1])}));var q=n.branches.length+n.hiddenBranches.length+" branches";n.hiddenBranches.length>0&&(q+=" ("+n.hiddenBranches.length+" hidden)"),U.select("span.branch-btn").text(q);var Z=F.selectAll(".commit-msg").data(d3.values(n.commits).filter(function(e){return e.visible}),function(e){return e.id+"-"+e.orderNr});Z.enter().append("div").attr("class","commit-msg").attr("id",function(e){return"msg-"+e.id}).on("click",function(t){if("A"==d3.event.target.tagName)return!0;var n=[];if("SPAN"==d3.event.target.tagName){var r="refs/heads/"+d3.event.target.innerHTML;n.push(["Hide branch '"+d3.event.target.innerHTML+"'",function(){o.hiddenBranches.push(r),A()}])}"ancestry"==a.style&&n.push(["Stop highlighting",function(){a.style="none",a.root=null,e.updateHighlight()}]),"ancestry"===a.style&&t.id===a.root||n.push(["Highlight ancestry from here",function(){a.style="ancestry",a.root=t.id,e.updateHighlight()}]);var i=d3.mouse(F.node());f.show(n,i[0],i[1])}),Z.exit().remove(),Z.html(function(e){var t=o.createCommitUrl(e),n="",e.author){var r=o.createAuthorAvatarUrl(e.author);n+=""}else n+="";if(e.authorTimestamp){var a=new Date(e.authorTimestamp),i=(new Date).toDateString()===a.toDateString();n+=i?" ":" "}return n+=" ",n+="
";if(e.labels&&_.each(e.labels,function(e){n+=0==e.indexOf("refs/heads/")?0==e.indexOf(o.masterRef)?""+e.substring(11)+"":0==e.indexOf(o.developRef)?""+e.substring(11)+"":0==e.indexOf(o.featurePrefix)?""+e.substring(11)+"":0==e.indexOf(o.releasePrefix)||0==e.indexOf(o.hotfixPrefix)?""+e.substring(11)+"":""+e.substring(11)+"":0==e.indexOf("refs/tags/")?""+e.substring(10)+"":""+e+""}),n+=" "+e.message,n+=""+(e.author.displayName||e.author.name||e.author.emailAddress)+" "+moment(a).format("HH:mm:ss")+" today"+moment(a).format("dd YYYY-MM-DD")+""+e.displayId+"
"}).transition().attr("style",function(e){var t=e;return"top:"+(P(t.orderNr)-i.rowHeight/2)+"px;"}),e.lazyLoad=function(){var e=null;for(var t in n.openEnds){var r=d3.select("#msg-"+t);if(!r.empty()&&s(r.node())){e=t;break}}if(e){var a=n.commits[e].orderNr;for(var t in n.openEnds)if(!(n.commits[t].orderNr>a+200)){for(var i=0;i 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 56 | 57 | 58 | 59 | 60 | 61 |
62 | 63 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /examples/standalone.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 53 | 54 | 55 | 56 |
57 | 58 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | This file is part of GitFlowVisualize. 5 | 6 | GitFlowVisualize is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | GitFlowVisualize is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with GitFlowVisualize. If not, see . 18 | */ 19 | 20 | // ------------------------------------------------------------------------------------------ Dependencies 21 | 22 | const gulp = require('gulp'); 23 | const rename = require('gulp-rename'); 24 | const uglify = require('gulp-uglify'); 25 | const injectfile = require("gulp-inject-file"); 26 | const del = require('del'); 27 | const vinylPaths = require('vinyl-paths'); 28 | const browserify = require('browserify'); 29 | const source = require('vinyl-source-stream'); 30 | const buffer = require('vinyl-buffer'); 31 | const karma = require('karma').Server; 32 | const sass = require('gulp-sass'); 33 | const postcss = require('gulp-postcss'); 34 | const cssnano = require('cssnano'); 35 | const eslint = require('gulp-eslint'); 36 | 37 | // ------------------------------------------------------------------------------------------ Tasks 38 | 39 | gulp.task('dist', ['build']); 40 | gulp.task('build', ['stylesheet', 'standalone', 'commonjs', 'bundle']); 41 | gulp.task('test', ['lint', 'karma']); 42 | 43 | // ------------------------------------------------------------------------------------------ Task Definitions 44 | 45 | gulp.task('clean', () => 46 | gulp.src('dist/*', {read: false}) 47 | .pipe(vinylPaths(del)) 48 | ) 49 | 50 | gulp.task('stylesheet', () => 51 | gulp.src('lib/gitflow-visualize.scss') 52 | .pipe(sass({ 53 | outputStyle: 'nested' 54 | })) 55 | .pipe(gulp.dest('dist')) 56 | .pipe(rename({ 57 | suffix: '.min' 58 | })) 59 | .pipe(postcss([ 60 | cssnano({ 61 | safe: true, 62 | discardUnused: false, 63 | discardEmpty: false, 64 | discardDuplicates: false, 65 | discardComments: { removeAll: true }, 66 | autoprefixer: false 67 | }) 68 | ])) 69 | .pipe(gulp.dest('dist')) 70 | ) 71 | 72 | gulp.task('standalone', () => 73 | gulp.src('lib/gitflow-visualize.js') 74 | .pipe(gulp.dest('dist')) 75 | .pipe(rename({ 76 | suffix: '.min' 77 | })) 78 | .pipe(uglify({ 79 | dead_code: true, 80 | drop_debugger: true, 81 | drop_console: true 82 | })) 83 | .pipe(gulp.dest('dist')) 84 | ); 85 | 86 | gulp.task('commonjs', () => 87 | gulp.src('lib/wrapper.js') 88 | .pipe(injectfile({ 89 | pattern: '/\\*\\s*inject:\\*/' 90 | })) 91 | .pipe(rename({ 92 | basename: 'gitflow-visualize', 93 | suffix: '.node' 94 | })) 95 | .pipe(gulp.dest('dist')) 96 | ) 97 | 98 | gulp.task('bundle', ['commonjs'], () => 99 | browserify({ entries: ['dist/gitflow-visualize.node.js'] }) 100 | .bundle() 101 | .pipe(source('gitflow-visualize.bundle.js')) 102 | .pipe(gulp.dest('dist')) 103 | .pipe(rename({ 104 | suffix: '.min' 105 | })) 106 | .pipe(buffer()) 107 | .pipe(uglify({ 108 | dead_code: true, 109 | drop_debugger: true, 110 | drop_console: true 111 | })) 112 | .pipe(gulp.dest('dist')) 113 | ) 114 | 115 | gulp.task('lint', () => 116 | gulp.src([ 117 | '**/*.js', 118 | '!**/dist/**', 119 | '!**/coverage/**', 120 | '!node_modules/**' 121 | ]) 122 | .pipe(eslint()) 123 | .pipe(eslint.format()) 124 | .pipe(eslint.failAfterError()) 125 | ) 126 | 127 | gulp.task('karma', ['dist'], (done) => { 128 | new karma({ 129 | configFile: __dirname + '/test/karma.conf.js', 130 | singleRun: true 131 | }, done).start(); 132 | }) 133 | 134 | -------------------------------------------------------------------------------- /lib/gitflow-visualize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | This file is part of GitFlowVisualize. 5 | 6 | GitFlowVisualize is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | GitFlowVisualize is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with GitFlowVisualize. If not, see . 18 | */ 19 | var GitFlowVisualize = (function () { 20 | 21 | var self = {}; 22 | var data; 23 | var displayState = {style:"none", root:null}; 24 | var constants = { 25 | rowHeight: 35 26 | }; 27 | 28 | var md5 = CryptoJS.MD5; 29 | 30 | var options = { 31 | drawElem: null, 32 | drawTable: false, 33 | 34 | // these are the exact names of the branches that should be drawn as stright lines master and develop 35 | masterRef: "refs/heads/master", 36 | developRef: "refs/heads/develop", 37 | 38 | // feature branches are prefixed by 39 | featurePrefix: "refs/heads/feature/", 40 | releasePrefix: "refs/heads/release/", 41 | hotfixPrefix: "refs/heads/hotfix/", 42 | 43 | // remaing branches will be tested to this regex: if match -> release, if not -> feature 44 | releaseZonePattern: /^refs\/heads\/bugfix/, 45 | 46 | // this pattern should match the tags that are given to release commits on master 47 | releaseTagPattern: /^refs\/tags\/\d+(\.\d+)+$/, 48 | 49 | // UI interaction hooks for loading message 50 | showSpinner: function () {}, 51 | hideSpinner: function () {}, 52 | 53 | // function to provide commit data 54 | dataCallback: function (done) { 55 | console.log("The required option 'dataCallback' is missing, please provide a method to retrieve commit data"); 56 | done({}); 57 | }, 58 | 59 | // function to retrieve additional commit data on scroll 60 | moreDataCallback: function (from, done) { 61 | console.log("The required option 'moreDataCallback' is missing, please provide a method to retrieve commit data"); 62 | done({}); 63 | }, 64 | 65 | // function called after data hase been processed successfully and chart has been drawn 66 | dataProcessed: function (/*data*/) { }, 67 | 68 | // function to provide the appropriate url to the actual commit souce 69 | createCommitUrl: function(/*commit*/){ 70 | return "#"; 71 | }, 72 | 73 | // function to provide the appropriate url to the author avatar 74 | createAuthorAvatarUrl: _.memoize(function(author) { 75 | return "https://secure.gravatar.com/avatar/" + md5(author.emailAddress) + ".jpg?s=48&d=mm"; 76 | }, function(author){return author.emailAddress;}), 77 | hiddenBranches:[] 78 | }; 79 | 80 | var cleanup = function (_data) { 81 | var result = {}; 82 | data = result; 83 | result.commits = {}; 84 | result.openEnds = {}; 85 | if (!_data.commits || !_data.branches || !_data.tags) { 86 | throw "raw data should have a commits, branches and tags property"; 87 | } 88 | for (var i = 0; i < _data.commits.length; i++) { 89 | for (var j = 0; j < _data.commits[i].values.length; j++) { 90 | var commit = _data.commits[i].values[j]; 91 | 92 | // cleanup stuff (when redrawing, this can still be there from last time) 93 | delete commit.columns; 94 | delete commit.labels; 95 | delete commit.orderTimestamp; 96 | delete commit.children; 97 | 98 | result.commits[commit.id] = commit; 99 | } 100 | } 101 | for (var id in result.commits) { 102 | var commit = result.commits[id]; 103 | commit.orderTimestamp = commit.authorTimestamp; 104 | if (!commit.children) commit.children = []; 105 | for (var i = commit.parents.length - 1; i >= 0; i--) { 106 | var parent = result.commits[commit.parents[i].id]; 107 | if (parent) { 108 | setChildToParent(parent, commit.id); 109 | } else { 110 | result.openEnds[commit.id] = result.openEnds[commit.id] || []; 111 | result.openEnds[commit.id].push(commit.parents[i].id); 112 | } 113 | } 114 | } 115 | result.branches = _.filter(_data.branches.values, function(b){return options.hiddenBranches.indexOf(b.id) === -1;}); 116 | result.hiddenBranches = _.filter(_data.branches.values, function(b){return options.hiddenBranches.indexOf(b.id) > -1;}); 117 | for (var i = 0; i < result.branches.length; i++) { 118 | var branch = result.branches[i]; 119 | var commit = result.commits[branch.latestChangeset]; 120 | if (commit) { 121 | commit.labels = (commit.labels || []); 122 | commit.labels.push(branch.id); 123 | branch.lastActivity = commit.authorTimestamp; 124 | } 125 | } 126 | 127 | // fixup orderTimestamp for cases of rebasing and cherrypicking, where the parent can be younger than the child 128 | var fixMyTimeRecursive = function (c, after) { 129 | if (!c) return; 130 | if (c.orderTimestamp <= after) { 131 | //console.log("fixing orderTimestamp for " + c.displayId + " " + c.orderTimestamp + " -> " + after + 1); 132 | c.orderTimestamp = after + 1; 133 | for (var k = 0; k < c.children.length; k++) { 134 | fixMyTimeRecursive(result.commits[c.children[k]], c.orderTimestamp); 135 | } 136 | } 137 | }; 138 | for (var key in result.commits) { 139 | var me = result.commits[key]; 140 | for (var k = 0; k < me.parents.length; k++) { 141 | var parent = result.commits[me.parents[k].id]; 142 | if (parent) fixMyTimeRecursive(me, parent.orderTimestamp); 143 | } 144 | } 145 | 146 | result.tags = _data.tags.values; 147 | for (var i = 0; i < result.tags.length; i++) { 148 | var tag = result.tags[i]; 149 | var commit = result.commits[tag.latestChangeset]; 150 | if (commit) { 151 | commit.labels = (commit.labels || []); 152 | commit.labels.push(tag.id); 153 | } 154 | } 155 | result.labels = result.tags.concat(result.branches); 156 | 157 | result.chronoCommits = []; 158 | for (var id in result.commits) { 159 | result.chronoCommits.push(id); 160 | } 161 | 162 | // evaluate visibility 163 | for(var i = 0; i < result.chronoCommits.length; i++){ 164 | var commit = result.commits[result.chronoCommits[i]]; 165 | if(commit.labels && commit.labels.length){ 166 | commit.visible = true; 167 | }else{ 168 | var visibleChildren = _.filter( 169 | _.map(commit.children, function(id){return result.commits[id];}), 170 | function(child){return child.visible;}); 171 | commit.visible = (visibleChildren.length > 0); 172 | } 173 | } 174 | 175 | result.chronoCommits.sort(function (a, b) { return result.commits[b].orderTimestamp - result.commits[a].orderTimestamp; }); 176 | result.visibleCommits = []; 177 | for (var i = 0, counter = 0; i < result.chronoCommits.length; i++) 178 | { 179 | var commit = result.commits[result.chronoCommits[i]]; 180 | if(commit.visible){ 181 | commit.orderNr = counter; 182 | result.visibleCommits.push(result.chronoCommits[i]); 183 | counter++; 184 | }else{ 185 | delete commit.orderNr; 186 | } 187 | } 188 | 189 | 190 | 191 | 192 | setColumns(result); 193 | return result; 194 | }; 195 | 196 | var setChildToParent = function (parent, childId) { 197 | parent.children = parent.children || []; 198 | parent.children.push(childId); 199 | }; 200 | 201 | var setColumns = function () { 202 | isolateMaster(); 203 | isolateDevelop(); 204 | isolateRest(); 205 | separateReleaseFeatureBranches(); 206 | combineColumnsOfType('d'); 207 | combineColumnsOfType('f'); 208 | combineColumnsOfType('r'); 209 | }; 210 | 211 | var isolateMaster = function () { 212 | var head = _.filter(data.branches, function (item) { return (item.id == options.masterRef); }); 213 | if (head.length == 0) return; 214 | var versionCommitPath = findShortestPathAlong( 215 | /*from*/ head[0].latestChangeset, 216 | /*along*/ _.map(_.filter(data.tags, 217 | function (tag) { return tag.id.match(options.releaseTagPattern); }), 218 | function (i) { return i.latestChangeset; }), 219 | data 220 | ); 221 | for (var i = 0; i < versionCommitPath.length; i++) { 222 | putCommitInColumn(versionCommitPath[i], 'm', data); 223 | } 224 | // add older commits that are the 'first' parents of the oldest master commit 225 | while (true) { 226 | var masterCommits = data.columns['m'].commits; 227 | var oldestMaster = masterCommits[masterCommits.length - 1]; 228 | var evenOlder = data.commits[oldestMaster].parents; 229 | if (!evenOlder || evenOlder.length == 0) break; 230 | if (!putCommitInColumn(evenOlder[0].id, 'm', data)) { 231 | break; 232 | } 233 | } 234 | 235 | }; 236 | 237 | var isolateDevelop = function () { 238 | var head = _.filter(data.branches, function (item) { return (item.id == options.developRef); }); 239 | if (head.length == 0) return; 240 | 241 | var versionCommitPath = findDevelopPathFrom(head[0].latestChangeset); 242 | for (var i = 0; i < versionCommitPath.length; i++) { 243 | putCommitInColumn(versionCommitPath[i], 'd0', data); 244 | } 245 | // find extra develop commits that are on secondary develop columns 246 | var developBranch = options.developRef.substring(options.developRef.lastIndexOf('/') + 1); 247 | var regexMerge = new RegExp("Merge branch '[^']+' (of \\S+ )?into " + developBranch + "$"); 248 | var current = 1; 249 | for (var i = 0; i < data.chronoCommits.length; i++) { 250 | var commit = data.commits[data.chronoCommits[i]]; 251 | if (!commit.columns) { 252 | if (regexMerge.test(commit.message)) { 253 | putCommitInColumn(commit.id, 'd' + current); 254 | current++; 255 | } 256 | } 257 | } 258 | 259 | }; 260 | 261 | var isolateRest = function () { 262 | var current = 0; 263 | for (var i = 0; i < data.chronoCommits.length; i++) { 264 | var commit = data.commits[data.chronoCommits[i]]; 265 | if (!commit.columns) { 266 | var childrenThatAreNotMasterOrDevelopAndAreLastInTheirColumn = _.filter(commit.children, function (childId) { 267 | var child = data.commits[childId]; 268 | var isOnMasterOrDevelop = child.columns && (child.columns[0] == "m" || child.columns[0][0] == "d"); 269 | if (isOnMasterOrDevelop) return false; 270 | if (!data.columns[child.columns[0]]) { 271 | console.log('huh'); 272 | } 273 | var commitsInColumn = data.columns[child.columns[0]].commits; 274 | return child.id == commitsInColumn[commitsInColumn.length - 1]; 275 | }); 276 | if (childrenThatAreNotMasterOrDevelopAndAreLastInTheirColumn.length == 0) { 277 | // if this commit has a child that is master or develop, but it is not on a column yet, we start a new column 278 | putCommitInColumn(commit.id, "c" + current, data); 279 | current++; 280 | } else { 281 | var firstChild = data.commits[childrenThatAreNotMasterOrDevelopAndAreLastInTheirColumn[0]]; 282 | if (firstChild && firstChild.columns) { 283 | putCommitInColumn(commit.id, firstChild.columns[0], data); 284 | firstChild._hasColumnChild = true; 285 | } else { 286 | console.log("Couldn't find appropriate parent"); 287 | } 288 | } 289 | } 290 | } 291 | }; 292 | 293 | var separateReleaseFeatureBranches = function () { 294 | // first find all branches that match a release or bugfix and place columns in appropriate zone 295 | for(var br = 0; br< data.branches.length; br++){ 296 | var branch = data.branches[br]; 297 | if(branch.id.indexOf(options.releasePrefix) === 0 298 | || branch.id.indexOf(options.hotfixPrefix) === 0 299 | || branch.id.match(options.releaseZonePattern) === 0){ 300 | var head = data.commits[branch.latestChangeset]; 301 | if(head){ 302 | var column = data.columns[head.columns[0]]; 303 | if(column.name[0] === 'c'){ 304 | column.name = 'r' + column.name.substring(1); 305 | } 306 | } 307 | } 308 | } 309 | 310 | // then do same with features for unplaced 311 | for(var br = 0; br< data.branches.length; br++){ 312 | var branch = data.branches[br]; 313 | if(branch.id.indexOf(options.featurePrefix) === 0 ){ 314 | var head = data.commits[branch.latestChangeset]; 315 | if(head){ 316 | var column = data.columns[head.columns[0]]; 317 | if(column.name[0] === 'c'){ 318 | column.name = 'f' + column.name.substring(1); 319 | } 320 | } 321 | } 322 | } 323 | 324 | // then start looking for topology hints 325 | for (var col in data.columns) { 326 | var column = data.columns[col]; 327 | if (col == 'm' || col[0] == 'd' || col[0] == 'r') continue; 328 | var allChildren = _.flatMap(column.commits, function (id) { return data.commits[id].children; }); 329 | var allChildrenOnMaster = _.filter(allChildren, function (id) { 330 | var parent = data.commits[id]; 331 | return parent.visible && parent.columns && parent.columns[0] == 'm'; 332 | }); 333 | if (allChildrenOnMaster.length > 0) { 334 | //release branches are branches that are not master or develop, but some commit merges into master 335 | column.name = 'r' + column.name.substring(1); 336 | continue; 337 | } 338 | var lastVisibleCommit = column.lastVisible(); // data.commits[column.commits[0]]; 339 | if(!lastVisibleCommit){ 340 | continue; 341 | } 342 | // if starts with releasePrefix or hotfixPrefix -> r 343 | if (lastVisibleCommit.labels && lastVisibleCommit.labels.filter(function (l) { 344 | return l.indexOf(options.releasePrefix) == 0 345 | || l.indexOf(options.hotfixPrefix) == 0 346 | || options.releaseZonePattern.test(l); 347 | }).length > 0) { 348 | column.name = 'r' + column.name.substring(1); 349 | continue; 350 | } 351 | if (lastVisibleCommit.labels && lastVisibleCommit.labels.filter(function (l) { return l.indexOf(options.featurePrefix) == 0; }).length > 0) { 352 | column.name = 'f' + column.name.substring(1); 353 | continue; 354 | } 355 | 356 | // var visibleChildren = lastVisibleCommit ? _.filter(lastVisibleCommit.children, function(id){return data.commits[id].visible;}) : []; 357 | // if (visibleChildren.length > 0) { 358 | // var developCommits = _.filter(visibleChildren, function (id) { return data.commits[id].columns[0][0] == 'd'; }); 359 | // if (developCommits.length > 0) { 360 | // // feature branches are branches that eventually merge into develop, not master 361 | // column.name = 'f' + column.name.substring(1); 362 | // } else { 363 | // // so we have a child, but not m or d: probably two branches merged together 364 | // // we'll figure this out later 365 | // column.firstChild = data.commits[lastVisibleCommit.children[0]]; 366 | // } 367 | // } else { 368 | // // unmerged branch without useful label. Assume feature branch 369 | // column.name = 'f' + column.name.substring(1); 370 | // } 371 | } 372 | 373 | while (true) { 374 | var connected = false; 375 | var unassignedColumns = _.filter(_.map(Object.keys(data.columns), function (id) { return data.columns[id]; }), function (c) { return c.name[0] == 'c'; }); 376 | for (var j = 0; j < unassignedColumns.length; j++) { 377 | var column = unassignedColumns[j]; 378 | var childCol = column.finallyMergesToColumn(); 379 | var firstLetter = childCol ? childCol.name[0]: 'f'; 380 | if (firstLetter == 'c') continue; 381 | if (firstLetter == 'd') firstLetter = 'f'; 382 | column.name = firstLetter + column.name.substring(1); 383 | connected = true; 384 | } 385 | if(!connected)break; 386 | } 387 | 388 | // now separate the feature branches into groups: 389 | var featureBranches = _.filter(_.map(Object.keys(data.columns), function (k) { return data.columns[k]; }), function (col) { return (col.name[0] == 'f'); }); 390 | var longBranches = _.filter(featureBranches, function (col) { return col.commits.length > 9 }); 391 | var groupNr = 1; 392 | for (var i = 0; i < longBranches.length; i++) { 393 | var thisCol = longBranches[i]; 394 | thisCol.group = groupNr; 395 | groupNr++; 396 | } 397 | // now loop through _all_ feature branches and group them together 398 | for (var i = 0; i < featureBranches.length; i++) { 399 | var thisCol = featureBranches[i]; 400 | var lastVisibleCommit = data.commits[thisCol.commits[0]]; 401 | if (lastVisibleCommit.children && lastVisibleCommit.children.length > 0) { 402 | var childColumn = data.columns[data.commits[lastVisibleCommit.children[0]].columns[0]]; 403 | if (childColumn.group) thisCol.group = childColumn.group; 404 | } else { 405 | var firstCommit = data.commits[thisCol.commits[thisCol.commits.length - 1]]; 406 | if (firstCommit.parents && firstCommit.parents.length > 0) { 407 | var parentCommit = data.commits[firstCommit.parents[0].id]; 408 | if (parentCommit) { 409 | var parentCol = data.columns[parentCommit.columns[0]]; 410 | if (parentCol.group) thisCol.group = parentCol.group; 411 | } 412 | } 413 | } 414 | } 415 | }; 416 | 417 | var combineColumnsOfType = function (type) { 418 | var columns = _.map(data.columns, function (v /*, k*/) { return v; }).filter(function (v) { return v.name[0] == type }); 419 | var groups = {}; 420 | for (var i = 0; i < columns.length; i++) { 421 | if (columns[i].group) { 422 | groups[columns[i].group] = true; 423 | } 424 | } 425 | groups = Object.keys(groups); 426 | groups.unshift(null); 427 | for (var groupCount = 0; groupCount < groups.length; groupCount++) { 428 | var nowGrouping = groups[groupCount]; 429 | var columnsToCombine = _.filter(columns, function (c) { 430 | if (nowGrouping === null) { 431 | return (typeof c.group === "undefined"); 432 | } 433 | return c.group == nowGrouping; 434 | }); 435 | for (var i = 0; i < columnsToCombine.length; i++) { 436 | var column = columnsToCombine[i]; 437 | for (var j = 0; j < i; j++) { 438 | var earlierColumn = columnsToCombine[j]; 439 | if (!earlierColumn.isVisible()) { 440 | continue; 441 | } 442 | // todo: here we must also search the columns already mapped to this one 443 | var earliestCommitOfFirst = earlierColumn.firstRenderedCommit(); 444 | if (earliestCommitOfFirst && earliestCommitOfFirst.parents.length > 0 && data.commits[earliestCommitOfFirst.parents[0].id]) { 445 | earliestCommitOfFirst = data.commits[earliestCommitOfFirst.parents[0].id]; 446 | } 447 | var lastCommitOfSecond = column.lastRenderedCommit(); 448 | if (lastCommitOfSecond && lastCommitOfSecond.children.length > 0 && data.commits[lastCommitOfSecond.children[0]]) { 449 | lastCommitOfSecond = data.commits[lastCommitOfSecond.children[0]]; 450 | } 451 | if ((!lastCommitOfSecond) || (!earliestCommitOfFirst) || lastCommitOfSecond.orderNr >= earliestCommitOfFirst.orderNr) { 452 | // combine columns 453 | column.combine(earlierColumn); 454 | j = i;//next column 455 | } 456 | } 457 | } 458 | 459 | } 460 | }; 461 | function Column(name){ 462 | var self = this; 463 | self.id = name; 464 | self.name = name; 465 | self.commits = []; 466 | var renderedOn = null; 467 | var renderingOthers = []; 468 | self.combine = function(otherCol){ 469 | renderedOn = otherCol; 470 | otherCol.receive(self); 471 | } 472 | self.receive = function(otherCol){ 473 | renderingOthers.push(otherCol); 474 | } 475 | self.isVisible = function(){return renderedOn === null;} 476 | self.renderPos = function(){return renderedOn ? renderedOn.id : self.id;} 477 | var allRendered = function(){ 478 | return renderingOthers.concat(self); 479 | } 480 | var visibleCommit = function(id){ 481 | var commit = data.commits[id]; 482 | return commit && commit.visible === true 483 | } 484 | self.firstVisible = function(){ 485 | var id = _.findLast(self.commits, visibleCommit); 486 | return data.commits[id]; 487 | } 488 | self.lastVisible = function(){ 489 | var id = _.find(self.commits, visibleCommit); 490 | return data.commits[id]; 491 | } 492 | self.firstRenderedCommit = function(){ 493 | if(renderedOn)return null; 494 | var all = allRendered(); 495 | return all.reduce(function(agg, col){ 496 | var first = col.firstVisible(); 497 | if(first && (!agg || agg.orderNr < first.orderNr))return first; 498 | return agg; 499 | }, null); 500 | } 501 | self.lastRenderedCommit = function(){ 502 | if(renderedOn)return null; 503 | var all = allRendered(); 504 | return all.reduce(function(agg, col){ 505 | var last = col.lastVisible(); 506 | if(!agg || agg.orderNr > last.orderNr)return last; 507 | return agg; 508 | }, null); 509 | } 510 | self.finallyMergesToColumn = function(){ 511 | var lastCommit = this.lastVisible(); 512 | if(!lastCommit)return null; 513 | var childrenOfLast = lastCommit.children; 514 | if(childrenOfLast.length === 0)return null; 515 | var childCol = data.columns[data.commits[childrenOfLast[0]].columns[0]]; 516 | return childCol; 517 | } 518 | } 519 | 520 | var putCommitInColumn = function (commitId, columnName) { 521 | if (!data.columns) data.columns = {}; 522 | if (!(columnName in data.columns)) { 523 | data.columns[columnName] = new Column(columnName ); 524 | } 525 | var commit = data.commits[commitId]; 526 | if (commit) { 527 | commit.columns = commit.columns || []; 528 | commit.columns.push(columnName); 529 | data.columns[columnName].commits.push(commitId); 530 | return true; 531 | } else { 532 | return false; 533 | } 534 | }; 535 | 536 | var findShortestPathAlong = function (from, along) { 537 | var scoreForAlong = function (path, childId) { 538 | if (along.indexOf(childId) > -1) return 1000; 539 | return -1; 540 | } 541 | var mostAlong = findBestPathFromBreadthFirst(from, scoreForAlong); 542 | return mostAlong.asArray(); 543 | } 544 | 545 | function makePath(initialPath) { 546 | var self = { score: 0 }; 547 | var arrayPath = initialPath.slice(0); 548 | var length = arrayPath.length; 549 | var last = arrayPath[length - 1]; 550 | self.members = {}; 551 | var prev = null; 552 | for (var i = 0; i < arrayPath.length; i++) { 553 | self.members[arrayPath[i]] = prev; 554 | prev = arrayPath[i]; 555 | } 556 | self.push = function (newStep) { 557 | var currLast = last; 558 | length++; 559 | self.members[newStep] = currLast; 560 | last = newStep; 561 | arrayPath.push(newStep); 562 | }; 563 | 564 | self.last = function () { 565 | return last; 566 | }; 567 | self.clone = function () { 568 | var clone = makePath(arrayPath); 569 | clone.score = self.score; 570 | return clone; 571 | }; 572 | self.asArray = function () { 573 | return arrayPath.slice(0); 574 | }; 575 | return self; 576 | } 577 | 578 | var findBestPathFromBreadthFirst = function (from, score) { 579 | var scoreFunc = score || function () { return -1 }; 580 | var openPaths = []; 581 | var bestPathToPoints = {}; 582 | var fromCommit = data.commits[from]; 583 | var firstPath = makePath([from]); 584 | var furthestPath = 0; 585 | firstPath.score = 0; 586 | bestPathToPoints[fromCommit.orderNr] = firstPath; 587 | furthestPath = fromCommit.orderNr; 588 | openPaths.push(firstPath); 589 | while (openPaths.length > 0) { 590 | var basePath = openPaths.shift(); 591 | var tail = data.commits[basePath.last()]; 592 | for (var i = 0; i < tail.parents.length; i++) { 593 | var nextChild = data.commits[tail.parents[i].id]; 594 | if (!nextChild) continue; 595 | var stepScore = scoreFunc(basePath, nextChild.id); 596 | if (stepScore === false) { 597 | // blocked node 598 | continue; 599 | } 600 | if (bestPathToPoints[nextChild.orderNr]) { 601 | if (bestPathToPoints[nextChild.orderNr].score > basePath.score + stepScore) { 602 | // this is not the best path. We do not place it in the open paths 603 | continue; 604 | } 605 | } 606 | var newPath = basePath.clone(); 607 | newPath.push(nextChild.id); 608 | newPath.score = basePath.score + stepScore; 609 | openPaths.push(newPath); 610 | bestPathToPoints[nextChild.orderNr] = newPath; 611 | if (furthestPath < nextChild.orderNr) furthestPath = nextChild.orderNr; 612 | } 613 | } 614 | var allDistances = Object.keys(bestPathToPoints); 615 | allDistances.sort(function (p1, p2) { 616 | if (!p1) return 0; 617 | if (!p2) return 0; 618 | return bestPathToPoints[p2].score - bestPathToPoints[p1].score; 619 | }); 620 | return bestPathToPoints[allDistances[0]]; 621 | } 622 | 623 | var findDevelopPathFrom = function(from) { 624 | var developBranch = options.developRef.substring(options.developRef.lastIndexOf('/') + 1); 625 | var releasePrefix = options.releasePrefix.split('/')[2]; 626 | var hotfixPrefix = options.hotfixPrefix.split('/')[2]; 627 | var regexSelfMerge = new RegExp("Merge branch '(" + developBranch + ")' of http:\\/\\/\\S+ into \\1"); 628 | var regexRealMerge = new RegExp("Merge branch '[^']+' into " + developBranch + "$"); 629 | var regexReleaseMerge = new RegExp("Merge branch '(" + releasePrefix + "|" + hotfixPrefix + ")[^']+' into " + developBranch + "\\b"); 630 | var score = function (path, nextId) { 631 | var c = data.commits[nextId]; 632 | var last = data.commits[path.last()]; 633 | // no part of m can be d 634 | if (c.columns && c.columns[0] == 'm') return false; 635 | // next commit cannot have a child further down the line 636 | var childrenInPath = c.children.filter(function(child) { 637 | return child in path.members; 638 | }); 639 | if (childrenInPath.length != 1) return false; 640 | // merges of develop onto itself are neutral 641 | if (regexSelfMerge.test(c.message)) return 0; 642 | //merges of a release branch onto develop are a big bonus (we want these on the primary develop branch) 643 | if (regexReleaseMerge.test(c.message)) return 20; 644 | //merges of a local branch onto develop are a bonus 645 | if (regexRealMerge.test(c.message)) return 5; 646 | // following first parent is a bonus 647 | if (last.parents.length > 1 && c.id == last.parents[0].id) return 1; 648 | return -.1; 649 | } 650 | var path = findBestPathFromBreadthFirst(from, score); 651 | return path.asArray(); 652 | }; 653 | 654 | self.state = function () { 655 | var state = JSON.stringify(rawData); 656 | return state; 657 | }; 658 | 659 | var rawData = null; 660 | var downloadedStartPoints = []; 661 | 662 | self.draw = function (elem, opt) { 663 | 664 | // Check if we have a placeholder element 665 | if(elem) { 666 | // Determine if placeholder element was provided 667 | try { 668 | //Using W3 DOM2 (works for FF, Opera and Chrom) 669 | if(!(elem instanceof HTMLElement)) { 670 | opt = elem; 671 | elem = null; 672 | } 673 | } catch(e) { 674 | //Browsers not supporting W3 DOM2 don't have HTMLElement and 675 | //an exception is thrown and we end up here. Testing some 676 | //properties that all elements have. (works on IE7) 677 | if(!((typeof elem==="object") && 678 | (elem.nodeType===1) && (typeof elem.style === "object") && 679 | (typeof elem.ownerDocument ==="object"))) { 680 | opt = elem; 681 | elem = null; 682 | } 683 | } 684 | } 685 | 686 | // Merge options with defaults 687 | options = _.extend(options, opt); 688 | options.drawElem = options.drawElem || elem; 689 | 690 | // Check if we have a placeholder element 691 | if(!options.drawElem) { 692 | options.drawElem = d3.select("body").append("div").attr("id", "gitflow-visualize"); 693 | }else{ 694 | if(! (options.drawElem instanceof d3.selection)){ 695 | options.drawElem = d3.select(options.drawElem); 696 | } 697 | } 698 | 699 | // Start drawing! 700 | options.showSpinner(); 701 | options.dataCallback(function (data) { 702 | rawData = data; 703 | options.hideSpinner(); 704 | drawFromRaw(); 705 | }); 706 | }; 707 | 708 | var appendData = function (newCommits) { 709 | rawData.commits.push(newCommits); 710 | } 711 | 712 | var drawFromRaw = function () { 713 | options.showSpinner(); 714 | data = setTimeout(function () { 715 | cleanup(rawData); 716 | options.hideSpinner(); 717 | options.dataProcessed(data); 718 | if (options.drawElem) { 719 | self.drawing.drawTable(options.drawElem); 720 | self.drawing.drawGraph(options.drawElem); 721 | self.drawing.updateHighlight(); 722 | } 723 | }, 10); 724 | } 725 | 726 | self.drawing = (function () { 727 | var self = {}; 728 | 729 | self.updateHighlight = function () { 730 | var highlightCommits = function (arrIds) { 731 | if (!arrIds || arrIds.length == 0) { 732 | d3.selectAll(".commit-msg").classed("dim", false).classed("highlight", false); 733 | d3.selectAll(".commit-dot").classed("dim", false); 734 | d3.selectAll(".arrow").style("opacity", "1"); 735 | return; 736 | } 737 | for (var id in data.commits) { 738 | if (arrIds.indexOf(id) > -1) { 739 | d3.selectAll("#msg-" + id).classed("dim", false).classed("highlight", true); 740 | d3.selectAll("#commit-" + id).classed("dim", false); 741 | d3.selectAll(".arrow-to-" + id).style("opacity", "1"); 742 | } else { 743 | d3.selectAll("#msg-" + id).classed("dim", true).classed("highlight", false); 744 | d3.selectAll("#commit-" + id).classed("dim", true); 745 | d3.selectAll(".arrow-to-" + id).style("opacity", "0.2"); 746 | 747 | } 748 | } 749 | }; 750 | 751 | d3.selectAll('.commit-msg.selected').classed("selected", false); 752 | 753 | switch (displayState.style) { 754 | case "none": 755 | highlightCommits([]); 756 | break; 757 | case "ancestry": 758 | var root = d3.select("#msg-" + displayState.root); 759 | var toHighlight = {}; 760 | var addIdsAncestry = function (id) { 761 | var commit = data.commits[id]; 762 | if (!commit) return; 763 | if (!toHighlight[id]) { 764 | toHighlight[id] = true; 765 | for (var i = 0; i < commit.parents.length; i++) { 766 | addIdsAncestry(commit.parents[i].id); 767 | } 768 | } else { 769 | // prevent cycles 770 | } 771 | }; 772 | root.classed("selected", true); 773 | addIdsAncestry(displayState.root); 774 | highlightCommits(Object.keys(toHighlight)); 775 | break; 776 | default: 777 | } 778 | 779 | } 780 | 781 | self.drawTable = function (elem) { 782 | if (options.drawTable) { 783 | var table = d3.select(document.createElement('table')); 784 | table.append('tr').html( drawColumnsAsHeaders() + 'shaparentauthoratmsg'); 785 | for (var i = 0 ; i < data.chronoCommits.length; i++) { 786 | var commit = data.commits[data.chronoCommits[i]]; 787 | var time = new Date(commit.authorTimestamp); 788 | table.append('tr').html(drawColumnsAsCells(commit) 789 | + '' + commit.displayId + '' + showCommaSeparated(commit.parents) + 790 | '' + commit.author.name + '' + moment(time).format("M/D/YY HH:mm:ss") + 791 | '' + commit.message + ''); 792 | } 793 | d3.select(elem).append(table); 794 | } 795 | }; 796 | 797 | var showCommaSeparated = function (arr) { 798 | return _.map(arr, function (i) { return i.displayId; }).join(", "); 799 | } 800 | 801 | var keysInOrder = function (obj) { 802 | var keys = _.map(obj, function (v, k) { return k; }); 803 | keys.sort(firstBy(function (k1, k2) { 804 | var groupVal = function (k) { return { 'm': 1, 'd': 3, 'f': 4, 'r': 2 }[obj[k].name[0]] || 5; }; 805 | return groupVal(k1) - groupVal(k2); 806 | }).thenBy(function (k1, k2) { 807 | return (data.columns[k1].group || 0) - (data.columns[k2].group || 0); 808 | }).thenBy(function (k1, k2) { 809 | if (data.columns[k1].name[0] == 'f') { 810 | // for feature branches we want the ones with recent commits closer to develop 811 | var commits1 = data.columns[k1].commits; 812 | var commits2 = data.columns[k2].commits; 813 | // order by last commit 814 | return data.commits[commits1[0]].orderNr - data.commits[commits2[0]].orderNr; 815 | } 816 | return k2 > k1 ? -1 : 1; 817 | })); 818 | return keys; 819 | }; 820 | 821 | var drawColumnsAsCells = function (commit) { 822 | var result = ""; 823 | var keys = keysInOrder(data.columns); 824 | for (var i = 0; i < keys.length; i++) { 825 | var col = keys[i]; 826 | result += ""; 827 | if (commit.columns.indexOf(col) > -1) { 828 | result += "o"; 829 | } 830 | result += ""; 831 | } 832 | return result; 833 | }; 834 | 835 | var drawColumnsAsHeaders = function () { 836 | var result = ""; 837 | var keys = keysInOrder(data.columns); 838 | for (var i = 0; i < keys.length; i++) { 839 | var col = keys[i]; 840 | result += "" + data.columns[col].name + ""; 841 | } 842 | return result; 843 | }; 844 | 845 | var groupScale = function(cols, maxWidth){ 846 | var scaleCol = { 847 | gutter: 0.7, 848 | line: 1, 849 | developLine: 0.4, 850 | }; 851 | var mapping = {}; 852 | var lastGroup = ''; 853 | var here = 0; 854 | var basePositions = {}; 855 | for (var i = 0; i < cols.length; i++) { 856 | var thisColId = cols[i]; 857 | var thisCol = data.columns[thisColId]; 858 | if(!thisCol.isVisible()){ 859 | // draws on other column 860 | mapping[thisColId] = thisCol.renderPos(); 861 | continue; 862 | } 863 | var thisGroup = thisColId[0]; 864 | if(lastGroup != thisGroup) here += scaleCol.gutter; 865 | here += thisGroup == 'd' ? scaleCol.developLine : scaleCol.line; 866 | basePositions[thisColId] = here; 867 | lastGroup = thisGroup; 868 | } 869 | 870 | var baseLinear = d3.scale.linear() 871 | .domain([0,here]) 872 | .range([0, Math.min(maxWidth, 20 * here)]); 873 | return function(d){ 874 | if(d in mapping){ 875 | d = mapping[d]; 876 | } 877 | var offset = 0; 878 | if(d[d.length-1] == "+"){ 879 | d = d.substring(0, d.length-1); 880 | offset = 0.5; 881 | } 882 | return baseLinear(basePositions[d] + offset); 883 | }; 884 | 885 | } 886 | 887 | self.drawGraph = function (elem) { 888 | var calcHeight = Math.max(800, data.visibleCommits.length * constants.rowHeight); 889 | var size = { width: 500, height: calcHeight }; 890 | var margin = 20; 891 | 892 | var svg = elem.select("svg>g"); 893 | if (svg.empty()) { 894 | var cont = elem.append("div"); 895 | cont.attr("class", "commits-graph-container"); 896 | var svg = cont.append("svg") 897 | .attr("class", "commits-graph") 898 | .append("g") 899 | .attr("transform", "translate(" + margin + ",0)"); 900 | elem.select("svg") 901 | .attr("width", size.width + 2 * margin) 902 | .attr("height", size.height + 2 * margin); 903 | var backgroundLayer = svg.append("g").attr("id", "bgLayer"); 904 | var arrowsLayer = svg.append("g").attr("id", "arrowsLayer"); 905 | var mainLinesLayer = svg.append("g").attr("id", "mainLinesLayer"); 906 | var commitsLayer = svg.append("g").attr("id", "commitsLayer"); 907 | } 908 | backgroundLayer = svg.select("g#bgLayer"); 909 | arrowsLayer = svg.select("g#arrowsLayer"); 910 | mainLinesLayer = svg.select("g#mainLinesLayer"); 911 | commitsLayer = svg.select("g#commitsLayer"); 912 | 913 | var columnsInOrder = keysInOrder(data.columns); 914 | 915 | var legendaBlocks = { 916 | "master": { prefix: 'm' }, 917 | "releases": { prefix: 'r' }, 918 | "develop": { prefix: 'd' }, 919 | "features": { prefix: 'f' } 920 | } 921 | for (var key in legendaBlocks) { 922 | var groupColumns = columnsInOrder.filter(function (k) { return data.columns[k].name[0] === legendaBlocks[key].prefix; }); 923 | if (groupColumns.length == 0) { 924 | delete legendaBlocks[key]; 925 | continue; 926 | } 927 | legendaBlocks[key].first = groupColumns[0]; 928 | legendaBlocks[key].last = groupColumns[groupColumns.length - 1]; 929 | } 930 | 931 | var x = groupScale(columnsInOrder, size.width, data.columnMappings); 932 | var y = d3.scale.linear() 933 | .domain([0, data.visibleCommits.length]) 934 | .range([60, 60 + data.visibleCommits.length * constants.rowHeight]); 935 | 936 | var line = d3.svg.line() 937 | //.interpolate("bundle") 938 | .x(function (d) { return x(d.x); }) 939 | .y(function (d) { return y(d.y); }); 940 | 941 | var connector = function (d) { 942 | var childCommit = data.commits[d.c]; 943 | var parentCommit = data.commits[d.p]; 944 | if (!childCommit || !parentCommit || !childCommit.visible) return null; 945 | var intermediateRow = parentCommit.orderNr - .5; 946 | var intermediatCol = childCommit.columns[0]; 947 | var intermediateRow2 = null; 948 | var intermediateCol2 = null; 949 | var childCol = data.columns[childCommit.columns[0]]; 950 | if (!childCol) return null; 951 | var parentCol = data.columns[parentCommit.columns[0]]; 952 | if (childCol.id != parentCol.id) { // merge 953 | var followingCommitOnParent = parentCol.commits[parentCol.commits.indexOf(parentCommit.id) - 1]; 954 | if (!followingCommitOnParent || data.commits[followingCommitOnParent].orderNr < childCommit.orderNr) { 955 | intermediateRow = childCommit.orderNr + .5; 956 | intermediatCol = parentCommit.columns[0]; 957 | } else { 958 | var precedingCommitOnChild = childCol.commits[childCol.commits.indexOf(childCommit.id) + 1]; 959 | if (!precedingCommitOnChild || data.commits[precedingCommitOnChild].orderNr > parentCommit.orderNr) { 960 | // do nothing, the sideways first model of the non-merge commit applies 961 | } else { 962 | // worst case: two bends 963 | intermediateCol2 = childCommit.columns[0] + '+'; 964 | intermediateRow2 = parentCommit.orderNr - 0.5; 965 | intermediatCol = childCommit.columns[0] + '+'; 966 | intermediateRow = childCommit.orderNr + 0.5; 967 | } 968 | } 969 | } 970 | if(!intermediateCol2)intermediateCol2 = intermediatCol; 971 | if(!intermediateRow2)intermediateRow2 = intermediateRow; 972 | var points = [ 973 | { x: childCommit.columns[0], y: childCommit.orderNr }, 974 | { x: intermediatCol, y: intermediateRow }, 975 | { x: intermediateCol2, y: intermediateRow2 }, 976 | { x: parentCommit.columns[0], y: parentCommit.orderNr }]; 977 | return line(points); 978 | }; 979 | 980 | // arrows 981 | var arrows = _.flatMap( 982 | d3.values(data.commits).filter(function(c){return c.visible;}) 983 | , function (c) { 984 | return c.parents.map(function (p) { return { p: p.id, c: c.id }; }); 985 | }); 986 | var arrow = arrowsLayer.selectAll(".arrow") 987 | .data(arrows, function(d){return 'a-' + d.p + '-' + d.c;}); 988 | var addedArrow = arrow 989 | .enter().append("g") 990 | .attr("class", function (d) { return "arrow arrow-to-" + d.c; }); 991 | addedArrow 992 | .append("path") 993 | .attr("stroke-linejoin", "round") 994 | .attr("class", "outline"); 995 | addedArrow 996 | .append("path") 997 | .attr("stroke-linejoin", "round") 998 | .attr("class", function (d) { return "branch-type-" + branchType(d.c, d.p); }); 999 | 1000 | var path = arrow.selectAll("g>path"); 1001 | path.transition().attr("d", connector) 1002 | 1003 | arrow.exit().remove(); 1004 | 1005 | 1006 | var branchLine = backgroundLayer.selectAll(".branch") 1007 | .data(d3.values(data.columns).filter(function(c){return c.isVisible();})); 1008 | branchLine 1009 | .enter().append("g") 1010 | .attr("class", "branch") 1011 | .append("line"); 1012 | branchLine.select("g>line").transition() 1013 | .attr("class", function (d) { return "branch-line " + d.name; }) 1014 | .attr("x1", function (d) { return x(d.id); }) 1015 | .attr("x2", function (d) { return x(d.id); }) 1016 | .attr("y1", y(0)) 1017 | .attr("y2", size.height); 1018 | branchLine.exit().remove(); 1019 | 1020 | var branchLine = mainLinesLayer.selectAll(".branch") 1021 | .data(d3.values(data.columns).filter(function(c){return c.isVisible() && (c.id === "d0" || c.id === "m");})); 1022 | branchLine 1023 | .enter().append("g") 1024 | .attr("class", "branch") 1025 | .append("line"); 1026 | branchLine.select("g>line").transition() 1027 | .attr("class", function (d) { return "branch-line " + d.name; }) 1028 | .attr("x1", function (d) { return x(d.id); }) 1029 | .attr("x2", function (d) { return x(d.id); }) 1030 | .attr("y1", y(0)) 1031 | .attr("y2", size.height); 1032 | 1033 | var commit = commitsLayer.selectAll(".commit") 1034 | .data(d3.values(data.commits).filter(function(c){return c.visible;}), function(c){return 'c-' + c.id;}); 1035 | commit 1036 | .enter().append("g") 1037 | .attr("class", "commit") 1038 | .append("circle") 1039 | .attr("class", "commit-dot") 1040 | .attr("r", 5); 1041 | 1042 | commit.exit().remove(); 1043 | commit 1044 | .transition() 1045 | .select("g>circle") 1046 | .attr("cx", function (d) { return x(d.columns[0]); }) 1047 | .attr("cy", function (d) { return y(d.orderNr); }) 1048 | .attr("id", function (d) { return "commit-" + d.id; }); 1049 | 1050 | var blockLegenda = backgroundLayer.selectAll(".legenda-label") 1051 | .data(Object.keys(legendaBlocks)); 1052 | var entering = blockLegenda.enter(); 1053 | var rotated = entering 1054 | .append("g") 1055 | .attr("class", function (d) { return "legenda-label " + legendaBlocks[d].prefix; }) 1056 | .append("g") 1057 | .attr("transform", function (d) { 1058 | var extraOffset = legendaBlocks[d].first == legendaBlocks[d].last ? -10 : 0; 1059 | return "translate(" + (x(legendaBlocks[d].first) + extraOffset) + ", " + (y(0) - 20) + ") rotate(-40)"; 1060 | }); 1061 | rotated.append("rect") 1062 | .attr("width", 60).attr("height", 15).attr("rx", "2"); 1063 | rotated.append("text").attr("y", "12").attr("x", "3") 1064 | .text(function (d) { return d; }); 1065 | 1066 | blockLegenda 1067 | .select("g").transition() 1068 | .attr("transform", function (d) { 1069 | var extraOffset = legendaBlocks[d].first == legendaBlocks[d].last ? -10 : 0; 1070 | return "translate(" + (x(legendaBlocks[d].first) + extraOffset) + ", " + (y(0) - 20) + ") rotate(-40)"; 1071 | }); 1072 | 1073 | var messages = elem.select("div.messages"); 1074 | if (messages.empty()) { 1075 | messages = elem.append("div") 1076 | .attr("class", "messages"); 1077 | messages 1078 | .append("div").attr("class", "context-menu"); 1079 | } 1080 | var msgHeader = messages.select("div.msg-header"); 1081 | if(msgHeader.empty()){ 1082 | msgHeader = messages.append("div") 1083 | .attr("class", "msg-header"); 1084 | msgHeader.append("span").attr("class", "branch-btn label aui-lozenge aui-lozenge-subtle") 1085 | .on("click", function(){ 1086 | var items = [["Show all", function(){ 1087 | options.hiddenBranches = []; 1088 | drawFromRaw(); 1089 | }]]; 1090 | if(branchVisibilityHandler !== null){ 1091 | items.push(["Change...", branchVisibilityHandler]); 1092 | } 1093 | var pos = d3.mouse(messages.node()); 1094 | menu.show(items, pos[0], pos[1]); 1095 | }); 1096 | 1097 | } 1098 | var branchLabelText = (data.branches.length + data.hiddenBranches.length) + " branches"; 1099 | if(data.hiddenBranches.length > 0) branchLabelText += " (" + data.hiddenBranches.length + " hidden)"; 1100 | msgHeader.select("span.branch-btn").text(branchLabelText); 1101 | 1102 | //labels 1103 | var labelData = messages.selectAll(".commit-msg") 1104 | .data(d3.values(data.commits).filter(function(c){return c.visible;}) 1105 | , function (c) {return c.id + "-" + c.orderNr;}); 1106 | labelData 1107 | .enter().append("div") 1108 | .attr("class", "commit-msg") 1109 | .attr("id", function (c) { return "msg-" + c.id; }) 1110 | .on('click', function (a) { 1111 | if(d3.event.target.tagName == 'A')return true; 1112 | // will show menu. Collect items 1113 | var items = []; 1114 | if(d3.event.target.tagName == 'SPAN'){ 1115 | // on branch label 1116 | var clickedBranch = 'refs/heads/' + d3.event.target.innerHTML; 1117 | items.push(["Hide branch '" + d3.event.target.innerHTML + "'", function(){ 1118 | options.hiddenBranches.push(clickedBranch); 1119 | drawFromRaw(); 1120 | }]); 1121 | } 1122 | if(displayState.style == "ancestry"){ 1123 | items.push(["Stop highlighting", function(){ 1124 | displayState.style = "none"; 1125 | displayState.root = null; 1126 | self.updateHighlight(); 1127 | }]); 1128 | } 1129 | if(displayState.style !== "ancestry" || a.id !== displayState.root){ 1130 | items.push(["Highlight ancestry from here", function(){ 1131 | displayState.style = "ancestry"; 1132 | displayState.root = a.id; 1133 | self.updateHighlight(); 1134 | }]); 1135 | } 1136 | var pos = d3.mouse(messages.node()); 1137 | menu.show(items, pos[0], pos[1]); 1138 | }); 1139 | labelData.exit().remove(); 1140 | labelData 1141 | .html(function (d) { 1142 | var commitUrl = options.createCommitUrl(d); 1143 | var res = ""; 1167 | if (d.author) { 1168 | var authorAvatarUrl = options.createAuthorAvatarUrl(d.author); 1169 | res += ""; 1170 | } else { 1171 | res += ""; 1172 | } 1173 | if (d.authorTimestamp) { 1174 | var dt = new Date(d.authorTimestamp); 1175 | var today = (new Date().toDateString() === dt.toDateString()); 1176 | if (today) { 1177 | res += " "; 1178 | } else { 1179 | res += " "; 1180 | } 1181 | } 1182 | res += " "; 1183 | res += "
"; 1144 | if (d.labels) { 1145 | _.each(d.labels, function (v /*, k*/) { 1146 | if (v.indexOf('refs/heads/') == 0) { 1147 | if (v.indexOf(options.masterRef) == 0) { 1148 | res += "" + v.substring(11) + ""; 1149 | } else if (v.indexOf(options.developRef) == 0) { 1150 | res += "" + v.substring(11) + ""; 1151 | } else if (v.indexOf(options.featurePrefix) == 0) { 1152 | res += "" + v.substring(11) + ""; 1153 | } else if (v.indexOf(options.releasePrefix) == 0 || v.indexOf(options.hotfixPrefix) == 0) { 1154 | res += "" + v.substring(11) + ""; 1155 | } else { 1156 | res += "" + v.substring(11) + ""; 1157 | } 1158 | } else if (v.indexOf('refs/tags/') == 0) { 1159 | res += "" + v.substring(10) + ""; 1160 | } else { 1161 | res += "" + v + ""; 1162 | } 1163 | }); 1164 | } 1165 | res += " " + d.message; 1166 | res += "" + (d.author.displayName || d.author.name || d.author.emailAddress) + " " + moment(dt).format("HH:mm:ss") + " today" + moment(dt).format("dd YYYY-MM-DD") + "" + d.displayId + "
"; 1184 | return res; 1185 | }) 1186 | .transition() 1187 | .attr("style", function (d) { 1188 | var commit = d; 1189 | return "top:" + (y(commit.orderNr) - constants.rowHeight / 2) + "px;"; 1190 | }); 1191 | 1192 | function isElementInViewport(el) { 1193 | var rect = el.getBoundingClientRect(); 1194 | return ( 1195 | rect.top >= 0 && 1196 | rect.left >= 0 && 1197 | rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */ 1198 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */ 1199 | ); 1200 | } 1201 | 1202 | self.lazyLoad = function() { 1203 | //check for openEnded messages in view 1204 | var keyInView = null; 1205 | for (var key in data.openEnds) { 1206 | var elementSelection = d3.select('#msg-' + key); 1207 | if(!elementSelection.empty()) { 1208 | if (isElementInViewport(elementSelection.node())) { 1209 | keyInView = key; 1210 | break; 1211 | } 1212 | } 1213 | } 1214 | if (keyInView) { 1215 | var ourOrderNr = data.commits[keyInView].orderNr; 1216 | for (var key in data.openEnds) { 1217 | if (data.commits[key].orderNr > ourOrderNr + 200) { 1218 | // to far out, skip 1219 | continue; 1220 | } 1221 | for (var i = 0; i < data.openEnds[key].length; i++) { 1222 | var parentId = data.openEnds[key][i]; 1223 | if(downloadedStartPoints.indexOf(parentId) === -1){ 1224 | openEndsToBeDownloaded[parentId] = true; 1225 | console.log("scheduled: " + parentId); 1226 | } 1227 | } 1228 | delete data.openEnds[key]; 1229 | } 1230 | for (var key in openEndsToBeDownloaded) { 1231 | console.log("downloading: " + key); 1232 | delete openEndsToBeDownloaded[key]; 1233 | openEndsBeingDownloaded[key] = true; 1234 | options.moreDataCallback(key, function (commits, thisKey) { 1235 | delete openEndsBeingDownloaded[thisKey]; 1236 | downloadedStartPoints.push(thisKey); 1237 | if (commits) appendData(commits); 1238 | if (Object.keys(openEndsToBeDownloaded).length == 0 && Object.keys(openEndsBeingDownloaded).length == 0) { 1239 | console.log("queues empty, ready to draw"); 1240 | setTimeout(function () { 1241 | drawFromRaw(); 1242 | }, 50); 1243 | } else { 1244 | console.log("waiting, still downloads in progress"); 1245 | console.log(openEndsToBeDownloaded); 1246 | console.log(openEndsBeingDownloaded); 1247 | } 1248 | 1249 | }); 1250 | } 1251 | openEndsToBeDownloaded = {}; 1252 | } 1253 | }; 1254 | }; 1255 | 1256 | var openEndsToBeDownloaded = {}; 1257 | var openEndsBeingDownloaded = {}; 1258 | var branchType = function (childId, parentId) { 1259 | var ct = function (id) { 1260 | var commit = data.commits[id]; 1261 | if (!commit || data.columns.length == 0) return "?"; 1262 | var columns = commit.columns.map(function (d) { return data.columns[d]; }); 1263 | return columns[0].name[0]; 1264 | }; 1265 | var prioHash = { 'm': 0, 'd': 1, 'r': 3, 'f': 2 }; 1266 | var cols = [ct(childId), ct(parentId)]; 1267 | 1268 | // special case for back-merge 1269 | if(cols[0] === 'd' && cols[1] !== 'd')return cols[1] + ' back'; 1270 | 1271 | cols.sort(function (v1, v2) { return prioHash[v2] - prioHash[v1]; }); 1272 | return cols[0] || "default"; 1273 | }; 1274 | 1275 | var menu = function(){ 1276 | var menu = {}; 1277 | var theMenu = null; 1278 | var ensureRef = function(){ 1279 | if(theMenu === null || theMenu.empty()){ 1280 | theMenu = d3.select(".messages .context-menu"); 1281 | theMenu.on("mousemove", function(){ 1282 | console.log("mouse move"); 1283 | timeLastSeen = Date.now(); 1284 | }); 1285 | } 1286 | } 1287 | var timeLastSeen = 0; 1288 | var timer; 1289 | var start = function(){ 1290 | timeLastSeen = Date.now(); 1291 | timer = setInterval(function(){ 1292 | if(timeLastSeen + 10000 < Date.now()){ 1293 | menu.hide(); 1294 | } 1295 | }, 100);} 1296 | var stop = function(){clearInterval(timer);} 1297 | 1298 | menu.show = function(items, x, y){ 1299 | ensureRef(); 1300 | theMenu 1301 | .style("top", y + "px" ) 1302 | .style("left", x + "px") 1303 | .style("visibility", "visible"); 1304 | theMenu.selectAll("div.item").remove(); 1305 | _.each(items, function(item){ 1306 | theMenu.append("div") 1307 | .on("click", function(){ 1308 | item[1](); 1309 | menu.hide(); 1310 | }) 1311 | .attr("class", "item") 1312 | .text(item[0]); 1313 | }); 1314 | d3.event.stopPropagation(); 1315 | d3.select("body").on("click", function(){menu.hide()}); 1316 | start(); 1317 | } 1318 | menu.hide = function(){ 1319 | ensureRef(); 1320 | theMenu.style("visibility", "hidden"); 1321 | stop(); 1322 | } 1323 | 1324 | return menu; 1325 | }(); 1326 | 1327 | return self; 1328 | })(); 1329 | 1330 | var branchVisibilityHandler = null; 1331 | self.branches = { 1332 | setHidden: function(refs){ 1333 | if(!(refs instanceof Array)){ 1334 | throw "pass in refs as an array of strings with full ref descriptors of the branches to hide (like 'refs/heads/develop')"; 1335 | } 1336 | options.hiddenBranches = refs; 1337 | drawFromRaw(); 1338 | }, 1339 | getHidden: function(){ 1340 | return options.hiddenBranches; 1341 | }, 1342 | getAll: function(){ 1343 | return _.map(data.branches.concat(data.hiddenBranches), function(b){ 1344 | return {id: b.id, name: b.displayId, 1345 | lastActivity:b.lastActivity, lastActivityFormatted: moment(b.lastActivity).format("M/D/YY HH:mm:ss"), 1346 | visible: options.hiddenBranches.indexOf(b.id) === -1 1347 | }; 1348 | }); 1349 | }, 1350 | registerHandler: function(handler){ 1351 | branchVisibilityHandler = handler; 1352 | } 1353 | }; 1354 | 1355 | if (document) { 1356 | //d3.select(document).on("scroll resize", function () { 1357 | d3.select(document) 1358 | .on("scroll", function(){GitFlowVisualize.drawing.lazyLoad();}) 1359 | .on("resize", function(){GitFlowVisualize.drawing.lazyLoad();}); 1360 | 1361 | d3.select(document).on("keydown", function () { 1362 | var event = d3.event; 1363 | if (event.ctrlKey && event.shiftKey && event.which == 221) { 1364 | //prompt("Ctrl-C to copy the graph source", GitFlowVisualize.state()); 1365 | var out = d3.select("#debug-output"); 1366 | if (out.empty()) { 1367 | out = d3.select("body").append("textarea").attr("id", "debug-output"); 1368 | } 1369 | out.style("display", ""); 1370 | out.node().value = GitFlowVisualize.state(); 1371 | out.node().focus(); 1372 | out.node().select(); 1373 | out.on('blur', function() { out.style("display", "none");; }); 1374 | } 1375 | }); 1376 | } 1377 | 1378 | return self; 1379 | })(); -------------------------------------------------------------------------------- /lib/gitflow-visualize.scss: -------------------------------------------------------------------------------- 1 | 2 | circle.commit-dot { 3 | fill: white; 4 | stroke: black; 5 | stroke-width:2px; 6 | } 7 | 8 | .commit-dot { 9 | &.dim { 10 | opacity: .2; 11 | } 12 | } 13 | 14 | line { 15 | stroke: black; 16 | opacity: 0.2; 17 | 18 | &.m { 19 | stroke:#d04437; 20 | stroke-width:3px; 21 | opacity: 1; 22 | } 23 | 24 | &.d0 { 25 | stroke:#8eb021; 26 | stroke-width:3px; 27 | opacity: 1; 28 | } 29 | } 30 | 31 | .arrow { 32 | path { 33 | stroke: black; 34 | stroke-width: 2px; 35 | opacity: 1; 36 | fill:none; 37 | 38 | &.outline { 39 | stroke:white; 40 | stroke-width:4px; 41 | opacity: .8; 42 | } 43 | 44 | &.branch-type-f { 45 | stroke: #3b7fc4; 46 | } 47 | 48 | &.branch-type-r { 49 | stroke: #f6c342; 50 | } 51 | 52 | &.branch-type-d { 53 | stroke: #8eb021; 54 | } 55 | 56 | &.branch-type-m { 57 | stroke: #f6c342; 58 | } 59 | 60 | &.branch-type-default { 61 | stroke-width:1px; 62 | } 63 | 64 | &.back { 65 | opacity:0.5; 66 | } 67 | } 68 | } 69 | 70 | // .commits-graph {} 71 | 72 | .messages { 73 | position:relative; 74 | } 75 | 76 | .commit-msg { 77 | position:absolute; 78 | white-space:nowrap; 79 | cursor:pointer; 80 | padding-left:30%; 81 | width:70%; 82 | overflow-x:hidden; 83 | 84 | &.dim { 85 | color:#aaa; 86 | } 87 | 88 | &.selected { 89 | background-color:#ccd9ea; 90 | } 91 | 92 | &:hover { 93 | background-color:#f5f5f5; 94 | } 95 | } 96 | 97 | .commit-link { 98 | font-family:courier; 99 | } 100 | 101 | .commit-table { 102 | width:100%; 103 | table-layout: fixed; 104 | } 105 | 106 | td { 107 | &.author { 108 | width:8em; 109 | } 110 | 111 | &.sha { 112 | width:5em; 113 | } 114 | 115 | &.date { 116 | width:7em; 117 | } 118 | } 119 | 120 | .label { 121 | margin-right:2px; 122 | } 123 | 124 | .branch { 125 | background-color:#ffc; 126 | border-color:#ff0; 127 | } 128 | 129 | .legenda-label { 130 | text { 131 | fill:white; 132 | } 133 | 134 | path { 135 | stroke-width:4 136 | } 137 | 138 | &.m { 139 | rect { 140 | fill:#d04437; 141 | } 142 | 143 | path { 144 | stroke:#d04437; 145 | } 146 | } 147 | 148 | &.r { 149 | rect { 150 | fill:#f6c342; 151 | } 152 | 153 | path { 154 | stroke:#f6c342; 155 | } 156 | } 157 | 158 | &.d { 159 | rect { 160 | fill:#8eb021; 161 | } 162 | 163 | text { 164 | fill:white; 165 | } 166 | 167 | path { 168 | stroke:#8eb021; 169 | } 170 | } 171 | 172 | &.f { 173 | rect { 174 | fill:#3b7fc4; 175 | } 176 | 177 | text { 178 | fill:white; 179 | } 180 | 181 | path { 182 | stroke:#3b7fc4; 183 | } 184 | } 185 | } 186 | 187 | .tag { 188 | background-color:#eee; 189 | border-color:#ccc; 190 | } 191 | 192 | table { 193 | &.commit-table { 194 | td { 195 | overflow:hidden; 196 | margin:2px; 197 | } 198 | } 199 | } 200 | 201 | .author { 202 | font-weight:bold; 203 | width:120px; 204 | } 205 | 206 | .commits-graph-container { 207 | width:30%; 208 | overflow-x:scroll; 209 | float:left; 210 | z-index:5; 211 | position:relative; 212 | } 213 | 214 | #debug-output { 215 | width:600px; 216 | height:300px; 217 | position:absolute; 218 | left:300px; 219 | top:100px; 220 | z-index:100; 221 | } 222 | .branch-btn{ 223 | cursor:pointer; 224 | } 225 | .context-menu{ 226 | display: inline-block; 227 | position: absolute; 228 | visibility: hidden; 229 | background-color: lightgray; 230 | //width:100px; 231 | z-index: 10; 232 | border: 2px outset; 233 | .item{ 234 | border-bottom: solid silver 1px; 235 | background-color:#eee; 236 | font-family: sans-serif; 237 | padding:3px; 238 | cursor: pointer; 239 | white-space: nowrap; 240 | color: #333; 241 | &:hover{ 242 | color: black; 243 | background-color: #ddd; 244 | 245 | } 246 | } 247 | } 248 | 249 | -------------------------------------------------------------------------------- /lib/wrapper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | This file is part of GitFlowVisualize. 5 | 6 | GitFlowVisualize is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | GitFlowVisualize is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with GitFlowVisualize. If not, see . 18 | */ 19 | /*eslint no-unused-vars: ["error", { "varsIgnorePattern": "_|d3|firstBy|moment|CryptoJS" }]*/ 20 | 21 | // ------------------------------------------------------------------------------------------ Dependencies 22 | 23 | var d3 = require('d3'); 24 | var firstBy = require('thenby'); 25 | var moment = require('moment'); 26 | var md5 = require('crypto-js/md5'); 27 | var forEach = require('lodash/forEach'); 28 | var extend = require('lodash/extend'); 29 | var filter = require('lodash/filter'); 30 | var map = require('lodash/map'); 31 | var flatMap = require('lodash/flatmap'); 32 | var find = require('lodash/find'); 33 | var findLast = require('lodash/findlast'); 34 | var memoize = require('lodash/memoize'); 35 | 36 | // ------------------------------------------------------------------------------------------ Wrapper 37 | 38 | // CommonJS & AMD wrapper 39 | // Inspiration and code was stolen with gratitude from both the Moment and jQuery projects 40 | // https://github.com/moment/moment/ | http://github.com/jquery 41 | ( function( global, factory ) { 42 | 43 | if(!global.document) { 44 | throw new Error( "GitFlowVisualize requires a window with a document" ); 45 | } else { 46 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 47 | typeof define === 'function' && define.amd ? define(factory) : 48 | global.GitFlowVisualize = factory() 49 | } 50 | 51 | } )( typeof window !== "undefined" ? window : this, function() { 52 | 53 | // Creating a cherry-picked version of lodash 54 | var _ = { 55 | each: forEach, 56 | extend: extend, 57 | filter: filter, 58 | map: map, 59 | flatMap: flatMap, 60 | find: find, 61 | findLast: findLast, 62 | memoize: memoize 63 | } 64 | 65 | // Creating a cherry-picked version of CryptoJS 66 | var CryptoJS = { 67 | MD5: md5 68 | }; 69 | 70 | // Gitflow-visualize 71 | /* inject: ./gitflow-visualize.js*/ 72 | 73 | return window.GitFlowVisualize = GitFlowVisualize; 74 | }); 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-flow-vis", 3 | "version": "1.2.1", 4 | "description": "Visualize your commits when using the Git Flow branching strategy", 5 | "main": "dist/gitflow-visualize.node.js", 6 | "scripts": { 7 | "test": "gulp clean && gulp test", 8 | "dist": "gulp clean && gulp dist", 9 | "prepublish": "npm test", 10 | "prepush": "npm test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/Teun/git-flow-vis.git" 15 | }, 16 | "author": "Teun Duynstee", 17 | "contributors": [ 18 | { 19 | "name": "Remie Bolte", 20 | "email": "r.bolte@gmail.com" 21 | } 22 | ], 23 | "license": "GPL-3.0", 24 | "bugs": { 25 | "url": "https://github.com/Teun/git-flow-vis/issues" 26 | }, 27 | "homepage": "https://github.com/Teun/git-flow-vis#readme", 28 | "dependencies": { 29 | "crypto-js": "^3.1.9-1", 30 | "d3": "3.5.17", 31 | "lodash": "^4.17.4", 32 | "moment": "^2.17.1", 33 | "thenby": "^1.2.3" 34 | }, 35 | "devDependencies": { 36 | "browserify": "^13.3.0", 37 | "chai": "^3.5.0", 38 | "cssnano": "^3.10.0", 39 | "del": "^2.2.2", 40 | "gulp": "^3.9.1", 41 | "gulp-eslint": "^3.0.1", 42 | "gulp-inject-file": "0.0.18", 43 | "gulp-istanbul": "^1.1.1", 44 | "gulp-mocha": "^3.0.1", 45 | "gulp-postcss": "^6.3.0", 46 | "gulp-rename": "^1.2.2", 47 | "gulp-sass": "^3.1.0", 48 | "gulp-uglify": "^2.0.0", 49 | "husky": "^0.12.0", 50 | "karma": "^1.4.0", 51 | "karma-browserify": "^5.1.0", 52 | "karma-chai": "^0.1.0", 53 | "karma-coverage": "^1.1.1", 54 | "karma-mocha": "^1.3.0", 55 | "karma-phantomjs-launcher": "^1.0.2", 56 | "karma-spec-reporter": "0.0.26", 57 | "mocha": "^3.2.0", 58 | "proxyquireify": "^3.2.1", 59 | "vinyl-buffer": "^1.0.0", 60 | "vinyl-paths": "^2.1.0", 61 | "vinyl-source-stream": "^1.1.0", 62 | "watchify": "^3.8.0" 63 | }, 64 | "files": [ 65 | "dist", 66 | "lib", 67 | "test", 68 | "!test/coverage", 69 | "gulpfile.js", 70 | "README.md" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /test/index.2.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Mocha Tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 20 | 21 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Mocha Tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 20 | 21 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | module.exports = function (config) { 3 | config.set({ 4 | 5 | // base path that will be used to resolve all patterns (eg. files, exclude) 6 | basePath: '../', 7 | 8 | 9 | // frameworks to use 10 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 11 | frameworks: ['mocha', 'chai'], 12 | 13 | 14 | //list of files / patterns to load in the browser 15 | files: [ 16 | { pattern: 'node_modules/d3/d3.js' }, 17 | { pattern: 'node_modules/moment/moment.js' }, 18 | { pattern: 'node_modules/thenby/thenBy.min.js' }, 19 | { pattern: 'node_modules/crypto-js/crypto-js.js' }, 20 | { pattern: 'node_modules/lodash/lodash.js' }, 21 | { pattern: 'lib/gitflow-visualize.js' }, 22 | { pattern: 'test/test.data.js' }, 23 | { pattern: 'test/test.js' }, 24 | { pattern: 'test/test.data.2.js' }, 25 | { pattern: 'test/test.2.js' } 26 | ], 27 | 28 | // Configure Mocha client 29 | client: { 30 | mocha: { 31 | ui: 'tdd' 32 | } 33 | }, 34 | 35 | preprocessors: { 36 | // source files, that you wanna generate coverage for 37 | // do not include tests or libraries 38 | // (these files will be instrumented by Istanbul) 39 | 'lib/gitflow-visualize.js': ['coverage'] 40 | }, 41 | 42 | // optionally, configure the reporter 43 | coverageReporter: { 44 | type : 'html', 45 | dir : 'test/coverage/' 46 | }, 47 | 48 | // test results reporter to use 49 | // possible values: 'dots', 'progress' 50 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 51 | reporters: ['spec', 'coverage' ], 52 | 53 | // web server port 54 | port: 9999, 55 | 56 | // enable / disable colors in the output (reporters and logs) 57 | colors: true, 58 | 59 | // level of logging 60 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 61 | logLevel: config.LOG_WARN, 62 | 63 | // enable / disable watching file and executing tests whenever any file changes 64 | autoWatch: false, 65 | 66 | // start these browsers 67 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 68 | browsers: ['PhantomJS'], 69 | 70 | // Continuous Integration mode 71 | // if true, Karma captures browsers, runs the tests and exits 72 | singleRun: false 73 | }); 74 | }; -------------------------------------------------------------------------------- /test/test.2.js: -------------------------------------------------------------------------------- 1 |  2 | /* 3 | This file is part of GitFlowVisualize. 4 | 5 | GitFlowVisualize is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | GitFlowVisualize is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with GitFlowVisualize. If not, see . 17 | */ 18 | /* global _:false , suite:false, test:false, suiteSetup:false, assert:false, GitFlowVisualize:false , Dummy2:false*/ 19 | 20 | // ------------------------------------------------------------------------------------------ Test Definitions 21 | 22 | suite('Library set up', function () { 23 | test('GitFlowVisualize should be in global scope', function (done) { 24 | if (GitFlowVisualize) { 25 | done(); 26 | } 27 | }); 28 | 29 | }); 30 | 31 | suite('Data set 0', function () { 32 | var data; 33 | suiteSetup(function(done) { 34 | var dataCallback = function(d) { d(Dummy2.Data[0]); }; 35 | var dataClean = function(d) { 36 | data = d; 37 | done(); 38 | }; 39 | GitFlowVisualize.draw(null, { dataCallback: dataCallback, dataProcessed: dataClean, releaseTagPattern: /refs\/tags\/(r|h)\d$/, showSpinner: function () { } }); 40 | }); 41 | test('Unrecognized branch should be in release zone', function() { 42 | var commit = data.commits["a71a7a23456f22f7390d547a8a28782530d191367"]; 43 | var column = data.columns[commit.columns[0]]; 44 | 45 | assert(column.name[0] === 'r', "unrecognized branch shoul remain in release zone, because release branch is direct ancestor"); 46 | 47 | }); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /test/test.data.2.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | This file is part of GitFlowVisualize. 4 | 5 | GitFlowVisualize is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | GitFlowVisualize is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with GitFlowVisualize. If not, see . 17 | */ 18 | 19 | var Dummy2 = { 20 | 21 | Data: [ 22 | /* a simple set of commits including one finishe feature, one release, one hotfix (unfinished) */ 23 | { 24 | branches: { "size": 6, "limit": 100, "isLastPage": true, "values": [{ "id": "refs/heads/any-branch-that-doesnt-match", "displayId": "any-branch-that-doesnt-match", "latestChangeset": "a71a7a23456f22f7390d547a8a28782530d191367", "isDefault": false }, { "id": "refs/heads/bugfix/b1", "displayId": "bugfix/b1", "latestChangeset": "771a7a651cf22f7390d547a8a28782530d191367", "isDefault": false }, { "id": "refs/heads/hotfix/h1", "displayId": "hotfix/h1", "latestChangeset": "871a7a651cf22f7390d547a8a28782530d191367", "isDefault": false }, { "id": "refs/heads/release/r2", "displayId": "release/r2", "latestChangeset": "0aabee3cc5a668e1dffd3c464b18890caf98e6e9", "isDefault": false }, { "id": "refs/heads/feature/f3", "displayId": "feature/f3", "latestChangeset": "fcda73616bf16fc0d4560c628ed3876ccc9762f5", "isDefault": false }, { "id": "refs/heads/develop", "displayId": "develop", "latestChangeset": "035b319c4dfa9f6baa8580edcaf9f40557bbfd80", "isDefault": false }, { "id": "refs/heads/master", "displayId": "master", "latestChangeset": "12a048fd7a67e4a1690e67cb242e589d7a2594f4", "isDefault": true }, { "id": "refs/heads/feature/F1", "displayId": "feature/F1", "latestChangeset": "ea08c2c5f4fa9778baec512b28603ff763ef9022", "isDefault": false }], "start": 0 } 25 | , 26 | tags: { "size": 1, "limit": 100, "isLastPage": true, "values": [{ "id": "refs/tags/r1", "displayId": "r1", "latestChangeset": "12a048fd7a67e4a1690e67cb242e589d7a2594f4", "hash": "b9e9d72be9e1e04adbc2b3635aadee3b3714c4d2" }], "start": 0 } 27 | , 28 | commits: [ 29 | { "values": [{ "id": "a71a7a23456f22f7390d547a8a28782530d191367", "displayId": "a71a7a2", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405529398000, "message": "Extra commit on release zone branch", "parents": [{ "id": "771a7a651cf22f7390d547a8a28782530d191367", "displayId": "771a7a6" }] }, { "id": "771a7a651cf22f7390d547a8a28782530d191367", "displayId": "771a7a6", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405529298000, "message": "bugfix on release branch", "parents": [{ "id": "0aabee3cc5a668e1dffd3c464b18890caf98e6e9", "displayId": "0aabee3" }] }, { "id": "871a7a651cf22f7390d547a8a28782530d191367", "displayId": "871a7a6", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405528298000, "message": "extra commit on hotfix", "parents": [{ "id": "33e12f603fec10963eb1da11fdd0e0457f9923f0", "displayId": "33e12f6" }] }, { "id": "33e12f603fec10963eb1da11fdd0e0457f9923f0", "displayId": "33e12f6", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527791000, "message": "no message", "parents": [{ "id": "12a048fd7a67e4a1690e67cb242e589d7a2594f4", "displayId": "12a048f" }] }, { "id": "12a048fd7a67e4a1690e67cb242e589d7a2594f4", "displayId": "12a048f", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527578000, "message": "Merge branch 'release/r1'", "parents": [{ "id": "f615f275f2d9082bbc718d01cbbb1f68c2c5a180", "displayId": "f615f27" }, { "id": "39d50e3d5293fcecb3f83043af1372f2d9861f6d", "displayId": "39d50e3" }] }, { "id": "39d50e3d5293fcecb3f83043af1372f2d9861f6d", "displayId": "39d50e3", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527561000, "message": "fix", "parents": [{ "id": "55aa691047c7e235c7d27d2922fd78b6d384ea25", "displayId": "55aa691" }] }, { "id": "55aa691047c7e235c7d27d2922fd78b6d384ea25", "displayId": "55aa691", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527507000, "message": "Merge branch 'feature/F2' into develop", "parents": [{ "id": "24b067d7222531023d0cb50d73a1855198c92e67", "displayId": "24b067d" }, { "id": "e0eeec7e3c999ad09a6a86ed292b89327791755e", "displayId": "e0eeec7" }] }, { "id": "e0eeec7e3c999ad09a6a86ed292b89327791755e", "displayId": "e0eeec7", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527483000, "message": "Merge branch 'feature/f1' into develop", "parents": [{ "id": "c9548d2e3d8a061187aed23023d0419f52d87c83", "displayId": "c9548d2" }] }, { "id": "c9548d2e3d8a061187aed23023d0419f52d87c83", "displayId": "c9548d2", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527413000, "message": "f3", "parents": [{ "id": "24b067d7222531023d0cb50d73a1855198c92e67", "displayId": "24b067d" }] }, { "id": "24b067d7222531023d0cb50d73a1855198c92e67", "displayId": "24b067d", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527316000, "message": "Merge branch 'release/R0' into develop", "parents": [{ "id": "f615f275f2d9082bbc718d01cbbb1f68c2c5a180", "displayId": "f615f27" }] }, { "id": "f615f275f2d9082bbc718d01cbbb1f68c2c5a180", "displayId": "f615f27", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527279000, "message": "2", "parents": [{ "id": "0a971a835aa695970494c16aa9a96bcdf45f0e3c", "displayId": "0a971a8" }] }, { "id": "0a971a835aa695970494c16aa9a96bcdf45f0e3c", "displayId": "0a971a8", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527258000, "message": "initial commit", "parents": [] }], "size": 10, "isLastPage": true, "start": 0, "limit": 100, "nextPageStart": null } 30 | , { "values": [{ "id": "fcda73616bf16fc0d4560c628ed3876ccc9762f5", "displayId": "fcda736", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527730000, "message": "Merge branch 'develop' into feature/f3\n\nConflicts:\n\ttest.txt", "parents": [{ "id": "9271572e68256f05a747014e119af1779f37e8e5", "displayId": "9271572" }, { "id": "035b319c4dfa9f6baa8580edcaf9f40557bbfd80", "displayId": "035b319" }] }, { "id": "035b319c4dfa9f6baa8580edcaf9f40557bbfd80", "displayId": "035b319", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527668000, "message": "commit directly on develop", "parents": [{ "id": "4b9d6fcfe8cbac8d3ad5334e13f115f60ab13b40", "displayId": "4b9d6fc" }] }, { "id": "9271572e68256f05a747014e119af1779f37e8e5", "displayId": "9271572", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527627000, "message": "no message", "parents": [{ "id": "4b9d6fcfe8cbac8d3ad5334e13f115f60ab13b40", "displayId": "4b9d6fc" }] }, { "id": "4b9d6fcfe8cbac8d3ad5334e13f115f60ab13b40", "displayId": "4b9d6fc", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527578000, "message": "Merge branch 'release/r1' into develop", "parents": [{ "id": "55aa691047c7e235c7d27d2922fd78b6d384ea25", "displayId": "55aa691" }, { "id": "39d50e3d5293fcecb3f83043af1372f2d9861f6d", "displayId": "39d50e3" }] }, { "id": "39d50e3d5293fcecb3f83043af1372f2d9861f6d", "displayId": "39d50e3", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527561000, "message": "fix", "parents": [{ "id": "55aa691047c7e235c7d27d2922fd78b6d384ea25", "displayId": "55aa691" }] }, { "id": "55aa691047c7e235c7d27d2922fd78b6d384ea25", "displayId": "55aa691", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527507000, "message": "Merge branch 'feature/F2' into develop", "parents": [{ "id": "24b067d7222531023d0cb50d73a1855198c92e67", "displayId": "24b067d" }, { "id": "e0eeec7e3c999ad09a6a86ed292b89327791755e", "displayId": "e0eeec7" }] }, { "id": "e0eeec7e3c999ad09a6a86ed292b89327791755e", "displayId": "e0eeec7", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527483000, "message": "Merge branch 'feature/f1' into develop", "parents": [{ "id": "c9548d2e3d8a061187aed23023d0419f52d87c83", "displayId": "c9548d2" }] }, { "id": "c9548d2e3d8a061187aed23023d0419f52d87c83", "displayId": "c9548d2", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527413000, "message": "f3", "parents": [{ "id": "24b067d7222531023d0cb50d73a1855198c92e67", "displayId": "24b067d" }] }, { "id": "24b067d7222531023d0cb50d73a1855198c92e67", "displayId": "24b067d", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527316000, "message": "Merge branch 'release/R0' into develop", "parents": [{ "id": "f615f275f2d9082bbc718d01cbbb1f68c2c5a180", "displayId": "f615f27" }] }, { "id": "f615f275f2d9082bbc718d01cbbb1f68c2c5a180", "displayId": "f615f27", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527279000, "message": "2", "parents": [{ "id": "0a971a835aa695970494c16aa9a96bcdf45f0e3c", "displayId": "0a971a8" }] }, { "id": "0a971a835aa695970494c16aa9a96bcdf45f0e3c", "displayId": "0a971a8", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527258000, "message": "initial commit", "parents": [] }], "size": 11, "isLastPage": true, "start": 0, "limit": 100, "nextPageStart": null } 31 | , { "values": [{ "id": "ea08c2c5f4fa9778baec512b28603ff763ef9022", "displayId": "ea08c2c", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527358000, "message": "3", "parents": [{ "id": "24b067d7222531023d0cb50d73a1855198c92e67", "displayId": "24b067d" }] }, { "id": "24b067d7222531023d0cb50d73a1855198c92e67", "displayId": "24b067d", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527316000, "message": "Merge branch 'release/R0' into develop", "parents": [{ "id": "f615f275f2d9082bbc718d01cbbb1f68c2c5a180", "displayId": "f615f27" }] }, { "id": "f615f275f2d9082bbc718d01cbbb1f68c2c5a180", "displayId": "f615f27", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527279000, "message": "2", "parents": [{ "id": "0a971a835aa695970494c16aa9a96bcdf45f0e3c", "displayId": "0a971a8" }] }, { "id": "0a971a835aa695970494c16aa9a96bcdf45f0e3c", "displayId": "0a971a8", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527258000, "message": "initial commit", "parents": [] }], "size": 4, "isLastPage": true, "start": 0, "limit": 100, "nextPageStart": null } 32 | , { "values": [{ "id": "0aabee3cc5a668e1dffd3c464b18890caf98e6e9", "displayId": "0aabee3", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405528185000, "message": "r2", "parents": [{ "id": "035b319c4dfa9f6baa8580edcaf9f40557bbfd80", "displayId": "035b319" }] }, { "id": "035b319c4dfa9f6baa8580edcaf9f40557bbfd80", "displayId": "035b319", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527668000, "message": "commit directly on develop", "parents": [{ "id": "4b9d6fcfe8cbac8d3ad5334e13f115f60ab13b40", "displayId": "4b9d6fc" }] }, { "id": "4b9d6fcfe8cbac8d3ad5334e13f115f60ab13b40", "displayId": "4b9d6fc", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527578000, "message": "Merge branch 'release/r1' into develop", "parents": [{ "id": "55aa691047c7e235c7d27d2922fd78b6d384ea25", "displayId": "55aa691" }, { "id": "39d50e3d5293fcecb3f83043af1372f2d9861f6d", "displayId": "39d50e3" }] }, { "id": "39d50e3d5293fcecb3f83043af1372f2d9861f6d", "displayId": "39d50e3", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527561000, "message": "fix", "parents": [{ "id": "55aa691047c7e235c7d27d2922fd78b6d384ea25", "displayId": "55aa691" }] }, { "id": "55aa691047c7e235c7d27d2922fd78b6d384ea25", "displayId": "55aa691", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527507000, "message": "Merge branch 'feature/F2' into develop", "parents": [{ "id": "24b067d7222531023d0cb50d73a1855198c92e67", "displayId": "24b067d" }, { "id": "e0eeec7e3c999ad09a6a86ed292b89327791755e", "displayId": "e0eeec7" }] }, { "id": "e0eeec7e3c999ad09a6a86ed292b89327791755e", "displayId": "e0eeec7", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527483000, "message": "Merge branch 'feature/f1' into develop", "parents": [{ "id": "c9548d2e3d8a061187aed23023d0419f52d87c83", "displayId": "c9548d2" }] }, { "id": "c9548d2e3d8a061187aed23023d0419f52d87c83", "displayId": "c9548d2", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527413000, "message": "f3", "parents": [{ "id": "24b067d7222531023d0cb50d73a1855198c92e67", "displayId": "24b067d" }] }, { "id": "24b067d7222531023d0cb50d73a1855198c92e67", "displayId": "24b067d", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527316000, "message": "Merge branch 'release/R0' into develop", "parents": [{ "id": "f615f275f2d9082bbc718d01cbbb1f68c2c5a180", "displayId": "f615f27" }] }, { "id": "f615f275f2d9082bbc718d01cbbb1f68c2c5a180", "displayId": "f615f27", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527279000, "message": "2", "parents": [{ "id": "0a971a835aa695970494c16aa9a96bcdf45f0e3c", "displayId": "0a971a8" }] }, { "id": "0a971a835aa695970494c16aa9a96bcdf45f0e3c", "displayId": "0a971a8", "author": { "name": "teun", "emailAddress": "teun@funda.nl", "id": 301, "displayName": "Teun Duynstee", "active": true, "slug": "teun", "type": "NORMAL", "link": { "url": "/users/teun", "rel": "self" }, "links": { "self": [{ "href": "https://git.funda.nl/users/teun" }] } }, "authorTimestamp": 1405527258000, "message": "initial commit", "parents": [] }], "size": 10, "isLastPage": true, "start": 0, "limit": 100, "nextPageStart": null } 33 | ] 34 | }, 35 | 36 | ] 37 | }; 38 | 39 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = Dummy2 : 40 | typeof define === 'function' && define.amd ? define(Dummy2) : 41 | window.Dummy2 = Dummy2 42 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 |  2 | /* 3 | This file is part of GitFlowVisualize. 4 | 5 | GitFlowVisualize is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | GitFlowVisualize is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with GitFlowVisualize. If not, see . 17 | */ 18 | /* global _:false , suite:false, test:false, suiteSetup:false, assert:false, GitFlowVisualize:false , Dummy:false*/ 19 | 20 | // ------------------------------------------------------------------------------------------ Test Definitions 21 | 22 | suite('Library set up', function () { 23 | test('GitFlowVisualize should be in global scope', function (done) { 24 | if (GitFlowVisualize) { 25 | done(); 26 | } 27 | }); 28 | 29 | }); 30 | 31 | suite('Data set 1', function () { 32 | var data; 33 | suiteSetup(function(done) { 34 | var dataCallback = function(d) { d(Dummy.Data[2]); }; 35 | var dataClean = function(d) { 36 | data = d; 37 | done(); 38 | }; 39 | GitFlowVisualize.draw(null, { dataCallback: dataCallback, dataProcessed: dataClean, releaseTagPattern: /refs\/tags\/(r|h)\d$/, showSpinner: function () { } }); 40 | }); 41 | test('Master branch should be isolated', function() { 42 | assert(data.columns['m'].commits.length > 0, "No master branch found"); 43 | }); 44 | test('Open feature branch should be on f* column', function () { 45 | var colF1 = data.commits["ea08c2c5f4fa9778baec512b28603ff763ef9022"].columns[0]; 46 | assert(data.columns[colF1].name[0] == "f", "open feature is on column " + data.columns[colF1].name); 47 | }); 48 | test('Needs two release columns and two feature columns', function () { 49 | var releaseColumns = _.filter(Object.keys(data.columns), function(c) { 50 | var col = data.columns[c]; 51 | return col.isVisible() && col.name[0] == "r"; 52 | }); 53 | assert(releaseColumns.length == 2, "found " + _.map(releaseColumns, function(c) { return data.columns[c].name; }).join(',') ); 54 | var featureColumns = _.filter(Object.keys(data.columns), function (c) { 55 | var col = data.columns[c]; 56 | return col.isVisible() && col.name[0] == "f"; 57 | }); 58 | assert(featureColumns.length == 2, "found " + _.map(featureColumns, function (c) { return data.columns[c].name; }).join(',')); 59 | }); 60 | test('Bugfix branch in same column as release', function() { 61 | var commitBugfix = data.commits["771a7a651cf22f7390d547a8a28782530d191367"]; 62 | var commitR2 = data.commits["0aabee3cc5a668e1dffd3c464b18890caf98e6e9"]; 63 | 64 | assert(commitBugfix.columns[0] === commitR2.columns[0], "Bugfix commit and release tip should be on the same column"); 65 | 66 | 67 | }); 68 | 69 | }); 70 | suite('Data set 2', function() { 71 | var data; 72 | suiteSetup(function(done) { 73 | var dataCallback = function(d) { d(Dummy.Data[0]); }; 74 | var dataClean = function(d) { 75 | data = d; 76 | done(); 77 | }; 78 | GitFlowVisualize.draw(null, { dataCallback: dataCallback, dataProcessed: dataClean, releaseTagPattern: /refs\/tags\/(r|h)\d$/, showSpinner: function () { } }); 79 | }); 80 | test('Master branch should be isolated', function() { 81 | assert(data.columns['m'].commits.length > 0, "No master branch found"); 82 | }); 83 | test('Last 5 commits were all on develop', function() { 84 | var latestShas = data.chronoCommits.slice(0, 5); 85 | var latestCommits = _.map(latestShas, function(sha) { 86 | return data.commits[sha]; 87 | }).filter(function(c) { 88 | return c.columns[0][0] == "d"; 89 | }); 90 | 91 | assert(latestCommits.length == 5, "The recent commits that were done directly on develop should be in the d column. Even though a shorter number of commits leads over the featur branch feature/f4"); 92 | }); 93 | test("Cherry picked commits should appear in parentage-order, not in authorTimestamp order", function() { 94 | var cherrypicked = data.commits["19f5bebe9d537f56385f2e7a18f41358dba35013"]; 95 | assert(cherrypicked.orderNr == 2, "Cherrypicked commit appears at " + cherrypicked.orderNr); 96 | }); 97 | test('Open feature branch should be on f* column', function() { 98 | var colF1 = data.commits["ea08c2c5f4fa9778baec512b28603ff763ef9022"].columns[0]; 99 | assert(data.columns[colF1].name[0] == "f", "open feature is on column " + data.columns[colF1].name); 100 | }); 101 | test('Needs two release columns and three feature columns', function() { 102 | var releaseColumns = _.filter(Object.keys(data.columns), function(c) { 103 | var col = data.columns[c]; 104 | return col.isVisible() && col.name[0] == "r"; 105 | }); 106 | assert(releaseColumns.length == 2, "found " + _.map(releaseColumns, function(c) { return data.columns[c].name; }).join(',')); 107 | var featureColumns = _.filter(Object.keys(data.columns), function(c) { 108 | var col = data.columns[c]; 109 | return col.isVisible() && col.name[0] == "f"; 110 | }); 111 | assert(featureColumns.length == 3, "found " + _.map(featureColumns, function(c) { return data.columns[c].name; }).join(',')); 112 | }); 113 | test('Branches f3 and f1 should not be place on one column', function() { 114 | var commitF4 = data.commits["ee268f9d33d940722d974ac1c12cd20cb85bc768"]; 115 | var commitF1 = data.commits["ea08c2c5f4fa9778baec512b28603ff763ef9022"]; 116 | 117 | assert(commitF1.columns[0] != commitF4.columns[0], "Branches are on the same column: " + commitF4.columns[0]); 118 | 119 | 120 | }); 121 | }); 122 | suite('Realistic data set (3)', function () { 123 | var data; 124 | suiteSetup(function (done) { 125 | var dataCallback = function (d) { d(Dummy.Data[1]); }; 126 | var dataClean = function (d) { 127 | data = d; 128 | done(); 129 | }; 130 | GitFlowVisualize.draw(null, { dataCallback: dataCallback, dataProcessed: dataClean, showSpinner: function () { } }); 131 | }); 132 | test('Master branch should be isolated', function () { 133 | assert(data.columns['m'].commits.length > 0, "No master branch found"); 134 | }); 135 | test('Certain known commits should be on develop', function () { 136 | var shas = [ 137 | "c0a3b213f475f6b6fa367a18620ce6e15cc6ca63", 138 | "780166cccae94196cccfcc25ab3377ffebdc9c77", 139 | "d91595f546a0076b4da305dbc562d330de69c225", 140 | "95cc45b24338a7489dd7468aeefd284821f84532", 141 | "aef22550ae3d7112be370cb7c55bd3b7a0d51e86", 142 | "419ee2a7bfe63fa0e74a3f7b2206e593272951f4", 143 | ]; 144 | for (var i = 0; i < shas.length; i++) { 145 | var commit = data.commits[shas[i]]; 146 | assert(commit.columns[0][0] == 'd', "Commit " + commit.id + " (" + commit.message + ") should be on develop column. Now on " + commit.columns[0]); 147 | } 148 | }); 149 | test('Related branches should be in the same group', function () { 150 | var shas = ["9f21aa01d191e9342c71df16c87d5d9fe78a0a8c", "97ced13be8581ec0fd8b9394a40e6cec868317e4"]; 151 | assert(data.columns[data.commits[shas[0]].columns[0]].group == data.columns[data.commits[shas[1]].columns[0]].group, 152 | "These two commits should be in the same group: " + shas.join(',')); 153 | }); 154 | test('Some commits should be on develop, although not in primary line', function() { 155 | var shas = [ 156 | "88c1a2b70a36eae0ac1e14791467f428293001a9", 157 | "b922548bfc7dafb8fe6ad5e8042adacc3817fe1a" 158 | ]; 159 | for (var i = 0; i < shas.length; i++) { 160 | var commit = data.commits[shas[i]]; 161 | assert(commit.columns[0][0] == 'd', "Commit " + commit.id + " (" + commit.message + ") should be on develop column. Now on " + commit.columns[0]); 162 | } 163 | 164 | }); 165 | 166 | }); 167 | 168 | suite('Realistic dataset (4)', function() { 169 | var data; 170 | suiteSetup(function (done) { 171 | var dataCallback = function (d) { d(Dummy.Data[4]); }; 172 | var dataClean = function (d) { 173 | data = d; 174 | done(); 175 | }; 176 | GitFlowVisualize.draw(null, { 177 | dataCallback: dataCallback, dataProcessed: dataClean, showSpinner: function() {} 178 | }); 179 | }); 180 | test('Master branch should be isolated', function() { 181 | assert(data.columns['m'].commits.length > 0, "No master branch found"); 182 | }); 183 | test('OBJ-847 commits should not be on develop', function () { 184 | var commit = data.commits['c8d172c52045ca6bd8da96b50492c97fc2c0d492']; 185 | assert(commit.columns[0][0] != 'd', "Commit " + commit.id + " (" + commit.message + ") should not be on develop column. Now it is."); 186 | }); 187 | }); 188 | 189 | 190 | suite('Realistic dataset (5)', function () { 191 | var data; 192 | suiteSetup(function (done) { 193 | var dataCallback = function (d) { d(Dummy.Data[5]); }; 194 | var dataClean = function (d) { 195 | data = d; 196 | done(); 197 | }; 198 | GitFlowVisualize.draw(null, { 199 | dataCallback: dataCallback, dataProcessed: dataClean, showSpinner: function () { } 200 | }); 201 | }); 202 | test('Master branch should be isolated', function () { 203 | assert(data.columns['m'].commits.length > 0, "No master branch found"); 204 | }); 205 | test('Commit 8a1355 should be on first develop column', function () { 206 | var commit = data.commits['8a1355bed1bb8be4dcffa179b3f5c2f61e70f325']; 207 | var column = data.columns[commit.columns[0]]; 208 | assert(column.name === 'd0', "Commit " + commit.id + " (" + commit.message + ") should be on first develop column. Now on " + column.name); 209 | }); 210 | test('Commit 40ebb6 should be on a release column', function () { 211 | var commit = data.commits['40ebb6d70110a304f0e7fa72854e35a9577bc4f4']; 212 | var column = data.columns[commit.columns[0]]; 213 | assert(column.name[0] === 'r', "Commit " + commit.id + " (" + commit.message + ") should be on a release column. Now on " + column.name); 214 | }); 215 | 216 | 217 | }); 218 | suite('Situation with feature branch as release branch', function () { 219 | var data; 220 | suiteSetup(function (done) { 221 | var dataCallback = function (d) { d(Dummy.Data[6]); }; 222 | var dataClean = function (d) { 223 | data = d; 224 | done(); 225 | }; 226 | GitFlowVisualize.draw(null, { 227 | dataCallback: dataCallback, dataProcessed: dataClean, showSpinner: function () { } 228 | }); 229 | }); 230 | test('Commit 367bee should be on a feature column', function () { 231 | var commit = data.commits['367bee2cb6a85217cb23d405cd9ac68a6039d096']; 232 | var column = data.columns[commit.columns[0]]; 233 | assert(column.name[0] === 'f', "Commit " + commit.id + " (" + commit.message + ") should be on a feature column. Now on " + column.name); 234 | }); 235 | test('Commit d125ab should be on the develop column', function () { 236 | var commit = data.commits['d125ab84c422febd4dabca6012409848ca4a09bb']; 237 | var column = data.columns[commit.columns[0]]; 238 | assert(column.name[0] === 'd', "Commit " + commit.id + " (" + commit.message + ") should be on the develop branch. Now on " + column.name); 239 | }); 240 | 241 | 242 | }); 243 | suite('Situation non-standard branch names', function () { 244 | var data; 245 | suiteSetup(function (done) { 246 | var dataCallback = function (d) { d(Dummy.Data[7]); }; 247 | var dataClean = function (d) { 248 | data = d; 249 | done(); 250 | }; 251 | GitFlowVisualize.draw(null, { 252 | dataCallback: dataCallback, dataProcessed: dataClean, showSpinner: function () { } 253 | }); 254 | }); 255 | test('Commit bac09e5 should be on a feature column', function () { 256 | var commit = data.commits['bac09e5378cae08a4c52107ba58c0318577cf557']; 257 | var column = data.columns[commit.columns[0]]; 258 | assert(column.name[0] === 'f', "Commit " + commit.id + " (" + commit.message + ") should be on a feature column. Now on " + column.name); 259 | }); 260 | test('Commit aac09e5 should be on a release column', function () { 261 | var commit = data.commits['aac09e5378cae08a4c52107ba58c0318577cf557']; 262 | var column = data.columns[commit.columns[0]]; 263 | assert(column.name[0] === 'r', "Commit " + commit.id + " (" + commit.message + ") should be on the release column. Now on " + column.name); 264 | }); 265 | }); 266 | suite('Situation non-standard branch names with different config', function () { 267 | var data; 268 | suiteSetup(function (done) { 269 | var dataCallback = function (d) { d(Dummy.Data[7]); }; 270 | var dataClean = function (d) { 271 | data = d; 272 | done(); 273 | }; 274 | GitFlowVisualize.draw(null, { 275 | dataCallback: dataCallback, dataProcessed: dataClean, showSpinner: function () { }, 276 | releaseZonePattern: /^(?!.*bugfix)/ /* switch the meaning using a negative lookahaed expression*/ 277 | }); 278 | }); 279 | test('Commit bac09e5 should be on a release column', function () { 280 | var commit = data.commits['bac09e5378cae08a4c52107ba58c0318577cf557']; 281 | var column = data.columns[commit.columns[0]]; 282 | assert(column.name[0] === 'r', "Commit " + commit.id + " (" + commit.message + ") should be on a release column. Now on " + column.name); 283 | }); 284 | test('Commit aac09e5 should be on a feature column', function () { 285 | var commit = data.commits['aac09e5378cae08a4c52107ba58c0318577cf557']; 286 | var column = data.columns[commit.columns[0]]; 287 | assert(column.name[0] === 'f', "Commit " + commit.id + " (" + commit.message + ") should be on the feature column. Now on " + column.name); 288 | }); 289 | }); 290 | suite('Showing and hiding', function () { 291 | var data; 292 | suiteSetup(function(done) { 293 | var dataCallback = function(d) { return d(Dummy.Data[2]); }; 294 | var setup = false; 295 | var dataClean = function(d) { 296 | data = d; 297 | if(!setup){ 298 | done(); 299 | setup = true; 300 | } 301 | }; 302 | GitFlowVisualize.draw(null, { 303 | dataCallback: dataCallback, dataProcessed: dataClean, 304 | releaseTagPattern: /refs\/tags\/(r|h)\d$/, showSpinner: function () { } , 305 | hiddenBranches:["refs/heads/feature/F1"] 306 | }); 307 | }); 308 | test('Commit on feature/f1 should not be visible', function() { 309 | var commit = data.commits['ea08c2c5f4fa9778baec512b28603ff763ef9022']; 310 | assert(commit.visible === false, "Commit should not be visible"); 311 | }); 312 | test('Commit on feature/f3 should be visible', function() { 313 | var commit = data.commits['fcda73616bf16fc0d4560c628ed3876ccc9762f5']; 314 | assert(commit.visible === true, "Commit should be visible"); 315 | }); 316 | test('Commit on feature/f1 should not have an orderNr', function() { 317 | var commit = data.commits['ea08c2c5f4fa9778baec512b28603ff763ef9022']; 318 | assert(!('orderNr' in commit), "Hidden commit has an orderNr " + commit.orderNr); 319 | }); 320 | test('Get all branches from outside', function() { 321 | var branches = GitFlowVisualize.branches.getAll(); 322 | assert(branches.length > 0, "Branches should be available"); 323 | assert(branches.filter(function(b){return b.visible}).length === 6, "Not the right number of visible branches"); 324 | }); 325 | test('Branches can be unhidden from outside', function(d) { 326 | GitFlowVisualize.branches.setHidden([]); 327 | setTimeout(function(){ 328 | var commit = data.commits['ea08c2c5f4fa9778baec512b28603ff763ef9022']; 329 | assert(commit.visible === true, "Commit should be visible after unhiding"); 330 | d(); 331 | }, 11); 332 | }); 333 | test('Branches can be hidden from outside', function(d) { 334 | GitFlowVisualize.branches.setHidden(['refs/heads/feature/f3', 'refs/heads/feature/F1', ]); 335 | setTimeout(function(){ 336 | var commit = data.commits['fcda73616bf16fc0d4560c628ed3876ccc9762f5']; 337 | assert(commit.visible === false, "Commit should not be visible after hiding"); 338 | commit = data.commits['ea08c2c5f4fa9778baec512b28603ff763ef9022']; 339 | assert(commit.visible === false, "Commit should not be visible after hiding"); 340 | d(); 341 | }, 11); 342 | }); 343 | }); 344 | suite('Reproduce several issues', function () { 345 | var data; 346 | suiteSetup(function (done) { 347 | var dataCallback = function(d) { return d(Dummy.Data[2]); }; 348 | var setup = false; 349 | var dataClean = function(d) { 350 | data = d; 351 | if(!setup){ 352 | done(); 353 | setup = true; 354 | } 355 | }; 356 | GitFlowVisualize.draw(null, { 357 | dataCallback: dataCallback, dataProcessed: dataClean, showSpinner: function () { } 358 | }); 359 | }); 360 | test('When bugfix branch hidden, release/r2 should be visible and in release zone', function (done) { 361 | GitFlowVisualize.branches.setHidden(['refs/heads/bugfix/b1']); 362 | setTimeout(function() { 363 | var commit = data.commits['0aabee3cc5a668e1dffd3c464b18890caf98e6e9']; 364 | assert(commit.visible, "Commit " + commit.id + " should be visible"); 365 | var column = data.columns[commit.columns[0]]; 366 | assert(column.name[0] === 'r', "Commit " + commit.id + " (" + commit.message + ") should be on a release column. Now on " + column.name); 367 | done(); 368 | }, 11); 369 | 370 | }); 371 | }); 372 | --------------------------------------------------------------------------------