├── .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() + '
sha
parent
author
at
msg
');
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 | + '
";
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 += "
"}).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() + '
sha
parent
author
at
msg
');
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 | + '
";
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 += "