├── LICENSE.md ├── README.md ├── css └── explaingit.css ├── images └── prompt.gif ├── index.html ├── js ├── controlbox.js ├── explaingit.js ├── historyview.js └── main.js └── memtest.html /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Wei Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | explain-git-with-d3 2 | =================== 3 | 4 | Use D3 to visualize simple git branching operations. 5 | 6 | This simple project is designed to help people understand some basic git concepts visually. 7 | 8 | This is my first attempt at using both SVG and D3. I hope it is helpful to you. 9 | 10 | The page can be accessed via: http://onlywei.github.io/explain-git-with-d3/ 11 | -------------------------------------------------------------------------------- /css/explaingit.css: -------------------------------------------------------------------------------- 1 | /* styles */ 2 | 3 | body, html { 4 | height: 100%; 5 | } 6 | 7 | .intro p, .concept-container p { 8 | padding-top: 10px; 9 | } 10 | 11 | a.openswitch { 12 | display: block; 13 | } 14 | 15 | a.openswitch.selected { 16 | font-weight: bold; 17 | } 18 | 19 | .command-list, .example-list { 20 | margin-top: 10px; 21 | margin-bottom: 10px; 22 | padding: 10px 0; 23 | border-bottom: 2px dashed #888; 24 | border-top: 2px dashed #888; 25 | background-color: #EEE; 26 | } 27 | 28 | .command-list a.openswitch { 29 | font-family: Courier New; 30 | } 31 | 32 | .concept-area { 33 | padding-bottom: 20px; 34 | } 35 | 36 | .concept-container { 37 | display: none; 38 | } 39 | 40 | .playground-container { 41 | margin-top: 20px; 42 | position: relative; 43 | } 44 | 45 | span.cmd { 46 | background-color: #222222; 47 | color: #FFFFFF; 48 | font-family: Courier New; 49 | padding: 0 0.2em; 50 | } 51 | 52 | .svg-container { 53 | margin-left:250px; 54 | display: block; 55 | overflow: auto; 56 | border: 1px dotted #AAA; 57 | } 58 | 59 | .svg-container.remote-container { 60 | position: absolute; 61 | top: 0px; right: 0px; 62 | background-color: #EFF1FF; 63 | border-left: 1px dotted #AAA; 64 | border-bottom: 1px dotted #AAA; 65 | } 66 | 67 | #ExplainGitZen-Container { 68 | position: absolute; 69 | top: 0; bottom: 0; right: 0; left: 0; 70 | } 71 | 72 | #ExplainGitZen-Container .svg-container { 73 | display: inline-block; 74 | border: 1px dotted #AAA; 75 | position: absolute; 76 | top: 0; bottom: 0; right: 0; left: 250px; 77 | margin-left: 0; 78 | } 79 | 80 | #ExplainGitZen-Container .svg-container.remote-container { 81 | position: absolute; 82 | top: 0px; right: 0px; left: auto; bottom: auto; 83 | background-color: #EFF1FF; 84 | border-left: 1px dotted #AAA; 85 | border-bottom: 1px dotted #AAA; 86 | } 87 | 88 | #ExplainGitZen-Container .playground-container { 89 | position: absolute; 90 | top: 0; bottom: 20px; right: 20px; left: 20px; 91 | } 92 | 93 | .remote-name-display { 94 | font-weight: bold; 95 | text-align: right; 96 | } 97 | 98 | .control-box { 99 | display: inline-block; 100 | position: absolute; 101 | top: 0px; bottom: 0; 102 | width: 250px; 103 | vertical-align: bottom; 104 | background-color: #000; 105 | border: 1px dotted #AAA; 106 | } 107 | 108 | .control-box button { 109 | font-family: Courier New; 110 | font-size: 12px; 111 | margin-right: 5px; 112 | margin-bottom: 5px; 113 | } 114 | 115 | .control-box .log { 116 | overflow-y: auto; 117 | position: absolute; 118 | top: 0px; bottom: 20px; left: 0; right: 0; 119 | border-bottom: 1px solid #AAA; 120 | } 121 | 122 | .control-box .log, 123 | .control-box input[type="text"] { 124 | font-family: Courier New; 125 | font-size: 14px; 126 | } 127 | 128 | .control-box .log .command-entry { 129 | padding-left: 15px; 130 | color: #FFF; 131 | line-height: 14px; 132 | background: url(../images/prompt.gif) no-repeat left center transparent; 133 | } 134 | 135 | .control-box input[type="text"] { 136 | position: absolute; 137 | bottom: 0; 138 | padding-left: 15px; 139 | color: #FFF; 140 | line-height: 14px; 141 | background: url(../images/prompt.gif) no-repeat left center transparent; 142 | } 143 | 144 | .control-box .log .info, 145 | .control-box .log .error { 146 | font-size: 12px; 147 | padding: 5px; 148 | } 149 | 150 | .control-box .log .info { 151 | color: #FFC; 152 | } 153 | 154 | .control-box .log .error { 155 | color: #FCC; 156 | } 157 | 158 | .control-box input[type="text"] { 159 | width: 235px; 160 | border: none; 161 | } 162 | 163 | circle.commit { 164 | fill: #EEEEEE; 165 | stroke: #888888; 166 | stroke-width: 3; 167 | } 168 | 169 | circle.commit.checked-out { 170 | fill: #CCFFCC !important; 171 | stroke: #339900; 172 | } 173 | 174 | circle.commit.merge-commit { 175 | stroke: #663300; 176 | fill: #FFFFCC; 177 | } 178 | 179 | circle.commit.reverted { 180 | fill: #FFC; 181 | stroke: #933; 182 | } 183 | 184 | circle.commit.rebased { 185 | stroke: #3300CC; 186 | fill: #CCCCFF; 187 | } 188 | 189 | circle.commit.branchless { 190 | fill: #FEFEFE; 191 | stroke: #DDD; 192 | } 193 | 194 | .commit-pointer { 195 | stroke: #666; 196 | stroke-width: 4; 197 | } 198 | 199 | .merge-pointer { 200 | stroke: #663300; 201 | stroke-width: 4; 202 | } 203 | 204 | .commit-pointer.branchless, 205 | .merge-pointer.branchless { 206 | stroke: #DDD; 207 | stroke-width: 2; 208 | } 209 | 210 | text.id-label { 211 | text-anchor: middle; 212 | font-family: Courier New; 213 | font-weight: bolder; 214 | fill: #666; 215 | font-size: 10px; 216 | } 217 | 218 | text.message-label { 219 | text-anchor: middle; 220 | font-family: Courier New; 221 | fill: #666; 222 | font-size: 10px; 223 | } 224 | 225 | g.branch-tag > rect { 226 | fill: #FFCC66; 227 | stroke: #CC9900; 228 | stroke-width: 2; 229 | } 230 | 231 | g.branch-tag.git-tag > rect { 232 | fill: #7FC9FF; 233 | stroke: #0026FF; 234 | } 235 | 236 | g.branch-tag.remote-branch > rect { 237 | fill: #CCC; 238 | stroke: #888; 239 | } 240 | 241 | g.branch-tag > text { 242 | text-anchor: middle; 243 | fill: #000; 244 | font-size: 11px; 245 | font-family: Arial; 246 | } 247 | 248 | g.head-tag > rect { 249 | fill: #CCFFCC; 250 | stroke: #339900; 251 | stroke-width: 2; 252 | } 253 | 254 | g.head-tag > text { 255 | text-anchor: middle; 256 | fill: #000; 257 | font-size: 11px; 258 | font-family: Arial; 259 | font-weight: bold; 260 | text-transform: uppercase; 261 | } 262 | -------------------------------------------------------------------------------- /images/prompt.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onlywei/explain-git-with-d3/b5b7373d0ecde0f1214dccce4d31c57cc9541a74/images/prompt.gif -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Explain Git with D3 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Fork me on GitHub 16 | 17 |
18 |
19 |
20 |

Visualizing Git Concepts with D3

21 |

22 | This website is designed to help you understand some basic git concepts visually. 23 | This is my first attempt at using both SVG and D3. I hope it is helpful to you. 24 |

25 |

26 | Adding/staging your files for commit will not be covered by this site. In all sandbox playgrounds 27 | on this site, just pretend that you always have files staged and ready to commit at all times. 28 | If you need a refresher on how to add or stage files for commit, please read 29 | Git Basics. 30 |

31 |

32 | Sandboxes are split by specific git commands, listed below. 33 |

34 |
35 |
36 |
37 |
38 |

Basic Commands

39 | git commit 40 | git branch 41 |
42 |
43 |

 

44 | git checkout 45 | git checkout -b 46 |
47 |
48 |

Undo Commits

49 | git reset 50 | git revert 51 |
52 |
53 |

Combine Branches

54 | git merge 55 | git rebase 56 |
57 |
58 |

Remote Server

59 | git fetch 60 | git pull 61 |
62 |
63 |

 

64 | git push 65 | git tag 66 |
67 |
68 |
69 |
70 |

71 | We are going to skip instructing you on how to add your files for commit in this explanation. 72 | Let's assume you already know how to do that. If you don't, go read some other tutorials. 73 |

74 |

75 | Pretend that you already have your files staged for commit and enter git commit 76 | as many times as you like in the terminal box. 77 |

78 |
79 |
80 |
81 |

82 | git tag name will create a new tag named "name". 83 | Creating tags just creates a new tag pointing to the currently checked out commit. 84 |

85 |

86 | Tags can be deleted using the command git tag -d name (coming soon). 87 |

88 |

89 | Type git commit and git tag commands 90 | to your hearts desire until you understand this concept. 91 |

92 |
93 |
94 |
95 |

96 | git branch name will create a new branch named "name". 97 | Creating branches just creates a new tag pointing to the currently checked out commit. 98 |

99 |

100 | Branches can be deleted using the command git branch -d name. 101 |

102 |

103 | Type git commit and git branch commands 104 | to your hearts desire until you understand this concept. 105 |

106 |
107 |
108 |
109 |

110 | git checkout has many uses, 111 | but the main one is to switch between branches.
112 | For example, to switch from master branch to dev branch, 113 | I would type git checkout dev. 114 | After that, if I do a git commit, notice where it goes. Try it. 115 |

116 |

117 | In addition to checking out branches, you can also checkout individual commits. Try it.
118 | Make a new commit and then type git checkout bb92e0e 119 | and see what happens. 120 |

121 |

122 | Type git commit, git branch, 123 | and git checkout commands to your hearts desire 124 | until you understand this concept. 125 |

126 |
127 |
128 |
129 |

130 | You can combine git branch and git checkout 131 | into a single command by typing git checkout -b branchname. 132 | This will create the branch if it does not already exist and immediately check it out. 133 |

134 |
135 |
136 |
137 |

138 | git reset will move HEAD and the current branch back to wherever 139 | you specify, abandoning any commits that may be left behind. This is useful to undo a commit 140 | that you no longer need. 141 |

142 |

143 | This command is normally used with one of three flags: "--soft", "--mixed", and "--hard". 144 | The soft and mixed flags deal with what to do with the work that was inside the commit after 145 | you reset, and you can read about it here. 146 | Since this visualization cannot graphically display that work, only the "--hard" flag will work 147 | on this site. 148 |

149 |

150 | The ref "HEAD^" is usually used together with this command. "HEAD^" means "the commit right 151 | before HEAD. "HEAD^^" means "two commits before HEAD", and so on. 152 |

153 |

154 | Note that you must never use git reset to abandon commits 155 | that have already been pushed and merged into the origin. This can cause your local repository 156 | to become out of sync with the origin. Don't do it unless you really know what you're doing. 157 |

158 |
159 |
160 |
161 |

162 | To undo commits that have already been pushed and shared with the team, we cannot use the 163 | git reset command. Instead, we have to use git revert. 164 |

165 |

166 | git revert will create a new commit that will undo all of the work that 167 | was done in the commit you want to revert. 168 |

169 |
170 |
171 |
172 |

173 | git merge will create a new commit with two parents. The resulting 174 | commit snapshot will have the all of the work that has been done in both branches. 175 |

176 |

177 | If there was no divergence between the two commits, git will do a "fast-forward" method merge.
178 | To see this happen, checkout the 'ff' branch and then type git merge dev. 179 |

180 |
181 |
182 |
183 |

184 | git rebase will take the commits on this branch and "move" them so that their 185 | new "base" is at the point you specify. 186 |

187 |

188 | You should pay close attention to the commit IDs of the circles as they move when you do this exercise. 189 |

190 |

191 | The reason I put "move" in quotations because this process actually generates brand new commits with 192 | completely different IDs than the old commits, and leaves the old commits where they were. For this reason, 193 | you never want to rebase commits that have already been shared with the team you are working with. 194 |

195 |
196 |
197 |
198 |

199 | git fetch will update all of the "remote tracking branches" in your local repository. 200 | Remote tracking branches are tagged in grey. 201 |

202 |
203 |
204 |
205 |

206 | A git pull is a two step process that first does a git fetch, 207 | and then does a git merge of the remote tracking branch associated with your current branch. 208 | If you have no current branch, the process will stop after fetching. 209 |

210 |

211 | If the argument "--rebase" was given by typing git pull --rebase, the second step of 212 | pull process will be a rebase instead of a merge. This can be set to the default behavior by configuration by typing: 213 | git config branch.BRANCHNAME.rebase true. 214 |

215 |
216 |
217 |
218 |

219 | A git push will find the commits you have on your local branch that the corresponding branch 220 | on the origin server does not have, and send them to the remote repository. 221 |

222 |

223 | By default, all pushes must cause a fast-forward merge on the remote repository. If there is any divergence between 224 | your local branch and the remote branch, your push will be rejected. In this scenario, you need to pull first and then 225 | you will be able to push again. 226 |

227 |
228 |
229 |
230 |

231 | One simple example of the use of git reset is to completely restore your local repository 232 | state to that of the origin.
233 | You can do so by typing git reset origin/master. 234 |

235 |

236 | Note that this won't delete untracked files, you will have to delete those separately with 237 | the command git clean -df. 238 |

239 |
240 |
241 |
242 |

243 | Below is a situation in which you are working in a local branch that is all your own. You want to receive the latest code 244 | from the origin server's master branch. To update your local branch, you can do it without having to switch branches! 245 |

246 |

247 | First do a git fetch, then type git rebase origin/master! 248 |

249 |
250 |
251 |
252 |

253 | git branch -d is used to delete branches. 254 | I have pre-created a bunch of branches for you to delete in the playground below. 255 | Have at it. 256 |

257 |
258 |
259 |
260 |

261 | Do whatever you want in this free playground. 262 |

263 |
264 |
265 |
266 |
267 |
268 |

Specific Examples

269 |

Below I have created some specific real-world scenarios that I feel are quite common and useful.

270 |
271 |
272 |
273 | 278 |
279 | Free Playground 280 | Zen Mode 281 |
282 |
283 |
284 | 285 | 288 | 289 | 294 | 295 | 297 | 298 | 299 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 309 | 559 | 560 | 561 | -------------------------------------------------------------------------------- /js/controlbox.js: -------------------------------------------------------------------------------- 1 | define(['d3'], function () { 2 | "use strict"; 3 | 4 | /** 5 | * @class ControlBox 6 | * @constructor 7 | */ 8 | function ControlBox(config) { 9 | this.historyView = config.historyView; 10 | this.originView = config.originView; 11 | this.initialMessage = config.initialMessage || 'Enter git commands below.'; 12 | this._commandHistory = []; 13 | this._currentCommand = -1; 14 | this._tempCommand = ''; 15 | this.rebaseConfig = {}; // to configure branches for rebase 16 | } 17 | 18 | ControlBox.prototype = { 19 | render: function (container) { 20 | var cBox = this, 21 | cBoxContainer, log, input; 22 | 23 | cBoxContainer = container.append('div') 24 | .classed('control-box', true); 25 | 26 | 27 | log = cBoxContainer.append('div') 28 | .classed('log', true); 29 | 30 | input = cBoxContainer.append('input') 31 | .attr('type', 'text') 32 | .attr('placeholder', 'enter git command'); 33 | 34 | input.on('keyup', function () { 35 | var e = d3.event; 36 | 37 | switch (e.keyCode) { 38 | case 13: 39 | if (this.value.trim() === '') { 40 | break; 41 | } 42 | 43 | cBox._commandHistory.unshift(this.value); 44 | cBox._tempCommand = ''; 45 | cBox._currentCommand = -1; 46 | cBox.command(this.value); 47 | this.value = ''; 48 | e.stopImmediatePropagation(); 49 | break; 50 | case 38: 51 | var previousCommand = cBox._commandHistory[cBox._currentCommand + 1]; 52 | if (cBox._currentCommand === -1) { 53 | cBox._tempCommand = this.value; 54 | } 55 | 56 | if (typeof previousCommand === 'string') { 57 | cBox._currentCommand += 1; 58 | this.value = previousCommand; 59 | this.value = this.value; // set cursor to end 60 | } 61 | e.stopImmediatePropagation(); 62 | break; 63 | case 40: 64 | var nextCommand = cBox._commandHistory[cBox._currentCommand - 1]; 65 | if (typeof nextCommand === 'string') { 66 | cBox._currentCommand -= 1; 67 | this.value = nextCommand; 68 | this.value = this.value; // set cursor to end 69 | } else { 70 | cBox._currentCommand = -1; 71 | this.value = cBox._tempCommand; 72 | this.value = this.value; // set cursor to end 73 | } 74 | e.stopImmediatePropagation(); 75 | break; 76 | } 77 | }); 78 | 79 | this.container = cBoxContainer; 80 | this.log = log; 81 | this.input = input; 82 | 83 | this.info(this.initialMessage); 84 | }, 85 | 86 | destroy: function () { 87 | this.log.remove(); 88 | this.input.remove(); 89 | this.container.remove(); 90 | 91 | for (var prop in this) { 92 | if (this.hasOwnProperty(prop)) { 93 | this[prop] = null; 94 | } 95 | } 96 | }, 97 | 98 | _scrollToBottom: function () { 99 | var log = this.log.node(); 100 | log.scrollTop = log.scrollHeight; 101 | }, 102 | 103 | command: function (entry) { 104 | if (entry.trim === '') { 105 | return; 106 | } 107 | 108 | var split = entry.split(' '); 109 | 110 | this.log.append('div') 111 | .classed('command-entry', true) 112 | .html(entry); 113 | 114 | this._scrollToBottom(); 115 | 116 | if (split[0] !== 'git') { 117 | return this.error(); 118 | } 119 | 120 | var method = split[1], 121 | args = split.slice(2); 122 | 123 | try { 124 | if (typeof this[method] === 'function') { 125 | this[method](args); 126 | } else { 127 | this.error(); 128 | } 129 | } catch (ex) { 130 | var msg = (ex && ex.message) ? ex.message: null; 131 | this.error(msg); 132 | } 133 | }, 134 | 135 | info: function (msg) { 136 | this.log.append('div').classed('info', true).html(msg); 137 | this._scrollToBottom(); 138 | }, 139 | 140 | error: function (msg) { 141 | msg = msg || 'I don\'t understand that.'; 142 | this.log.append('div').classed('error', true).html(msg); 143 | this._scrollToBottom(); 144 | }, 145 | 146 | commit: function (args) { 147 | if (args.length >= 2) { 148 | var arg = args.shift(); 149 | 150 | switch (arg) { 151 | case '-m': 152 | var message = args.join(" "); 153 | this.historyView.commit({},message); 154 | break; 155 | default: 156 | this.historyView.commit(); 157 | break; 158 | } 159 | } else { 160 | this.historyView.commit(); 161 | } 162 | }, 163 | 164 | branch: function (args) { 165 | if (args.length < 1) { 166 | this.info( 167 | 'You need to give a branch name. ' + 168 | 'Normally if you don\'t give a name, ' + 169 | 'this command will list your local branches on the screen.' 170 | ); 171 | 172 | return; 173 | } 174 | 175 | while (args.length > 0) { 176 | var arg = args.shift(); 177 | 178 | switch (arg) { 179 | case '--remote': 180 | case '-r': 181 | this.info( 182 | 'This command normally displays all of your remote tracking branches.' 183 | ); 184 | args.length = 0; 185 | break; 186 | case '--all': 187 | case '-a': 188 | this.info( 189 | 'This command normally displays all of your tracking branches, both remote and local.' 190 | ); 191 | break; 192 | case '--delete': 193 | case '-d': 194 | var name = args.pop(); 195 | this.historyView.deleteBranch(name); 196 | break; 197 | default: 198 | if (arg.charAt(0) === '-') { 199 | this.error(); 200 | } else { 201 | var remainingArgs = [arg].concat(args); 202 | args.length = 0; 203 | this.historyView.branch(remainingArgs.join(' ')); 204 | } 205 | } 206 | } 207 | }, 208 | 209 | checkout: function (args) { 210 | while (args.length > 0) { 211 | var arg = args.shift(); 212 | 213 | switch (arg) { 214 | case '-b': 215 | var name = args[args.length - 1]; 216 | try { 217 | this.historyView.branch(name); 218 | } catch (err) { 219 | if (err.message.indexOf('already exists') === -1) { 220 | throw new Error(err.message); 221 | } 222 | } 223 | break; 224 | default: 225 | var remainingArgs = [arg].concat(args); 226 | args.length = 0; 227 | this.historyView.checkout(remainingArgs.join(' ')); 228 | } 229 | } 230 | }, 231 | 232 | tag: function (args) { 233 | if (args.length < 1) { 234 | this.info( 235 | 'You need to give a tag name. ' + 236 | 'Normally if you don\'t give a name, ' + 237 | 'this command will list your local tags on the screen.' 238 | ); 239 | 240 | return; 241 | } 242 | 243 | while (args.length > 0) { 244 | var arg = args.shift(); 245 | 246 | try { 247 | this.historyView.tag(arg); 248 | } catch (err) { 249 | if (err.message.indexOf('already exists') === -1) { 250 | throw new Error(err.message); 251 | } 252 | } 253 | } 254 | }, 255 | 256 | reset: function (args) { 257 | while (args.length > 0) { 258 | var arg = args.shift(); 259 | 260 | switch (arg) { 261 | case '--soft': 262 | this.info( 263 | 'The "--soft" flag works in real git, but ' + 264 | 'I am unable to show you how it works in this demo. ' + 265 | 'So I am just going to show you what "--hard" looks like instead.' 266 | ); 267 | break; 268 | case '--mixed': 269 | this.info( 270 | 'The "--mixed" flag works in real git, but ' + 271 | 'I am unable to show you how it works in this demo.' 272 | ); 273 | break; 274 | case '--hard': 275 | this.historyView.reset(args.join(' ')); 276 | args.length = 0; 277 | break; 278 | default: 279 | var remainingArgs = [arg].concat(args); 280 | args.length = 0; 281 | this.info('Assuming "--hard".'); 282 | this.historyView.reset(remainingArgs.join(' ')); 283 | } 284 | } 285 | }, 286 | 287 | clean: function (args) { 288 | this.info('Deleting all of your untracked files...'); 289 | }, 290 | 291 | revert: function (args) { 292 | this.historyView.revert(args.shift()); 293 | }, 294 | 295 | merge: function (args) { 296 | var noFF = false; 297 | var branch = args[0]; 298 | if (args.length === 2) 299 | { 300 | if (args[0] === '--no-ff') { 301 | noFF = true; 302 | branch = args[1]; 303 | } else if (args[1] === '--no-ff') { 304 | noFF = true; 305 | branch = args[0]; 306 | } else { 307 | this.info('This demo only supports the --no-ff switch..'); 308 | } 309 | } 310 | var result = this.historyView.merge(branch, noFF); 311 | 312 | if (result === 'Fast-Forward') { 313 | this.info('You have performed a fast-forward merge.'); 314 | } 315 | }, 316 | 317 | rebase: function (args) { 318 | var ref = args.shift(), 319 | result = this.historyView.rebase(ref); 320 | 321 | if (result === 'Fast-Forward') { 322 | this.info('Fast-forwarded to ' + ref + '.'); 323 | } 324 | }, 325 | 326 | fetch: function () { 327 | if (!this.originView) { 328 | throw new Error('There is no remote server to fetch from.'); 329 | } 330 | 331 | var origin = this.originView, 332 | local = this.historyView, 333 | remotePattern = /^origin\/([^\/]+)$/, 334 | rtb, isRTB, fb, 335 | fetchBranches = {}, 336 | fetchIds = [], // just to make sure we don't fetch the same commit twice 337 | fetchCommits = [], fetchCommit, 338 | resultMessage = ''; 339 | 340 | // determine which branches to fetch 341 | for (rtb = 0; rtb < local.branches.length; rtb++) { 342 | isRTB = remotePattern.exec(local.branches[rtb]); 343 | if (isRTB) { 344 | fetchBranches[isRTB[1]] = 0; 345 | } 346 | } 347 | 348 | // determine which commits the local repo is missing from the origin 349 | for (fb in fetchBranches) { 350 | if (origin.branches.indexOf(fb) > -1) { 351 | fetchCommit = origin.getCommit(fb); 352 | 353 | var notInLocal = local.getCommit(fetchCommit.id) === null; 354 | while (notInLocal) { 355 | if (fetchIds.indexOf(fetchCommit.id) === -1) { 356 | fetchCommits.unshift(fetchCommit); 357 | fetchIds.unshift(fetchCommit.id); 358 | } 359 | fetchBranches[fb] += 1; 360 | fetchCommit = origin.getCommit(fetchCommit.parent); 361 | notInLocal = local.getCommit(fetchCommit.id) === null; 362 | } 363 | } 364 | } 365 | 366 | // add the fetched commits to the local commit data 367 | for (var fc = 0; fc < fetchCommits.length; fc++) { 368 | fetchCommit = fetchCommits[fc]; 369 | local.commitData.push({ 370 | id: fetchCommit.id, 371 | parent: fetchCommit.parent, 372 | tags: [] 373 | }); 374 | } 375 | 376 | // update the remote tracking branch tag locations 377 | for (fb in fetchBranches) { 378 | if (origin.branches.indexOf(fb) > -1) { 379 | var remoteLoc = origin.getCommit(fb).id; 380 | local.moveTag('origin/' + fb, remoteLoc); 381 | } 382 | 383 | resultMessage += 'Fetched ' + fetchBranches[fb] + ' commits on ' + fb + '.
'; 384 | } 385 | 386 | this.info(resultMessage); 387 | 388 | local.renderCommits(); 389 | }, 390 | 391 | pull: function (args) { 392 | var control = this, 393 | local = this.historyView, 394 | currentBranch = local.currentBranch, 395 | rtBranch = 'origin/' + currentBranch, 396 | isFastForward = false; 397 | 398 | this.fetch(); 399 | 400 | if (!currentBranch) { 401 | throw new Error('You are not currently on a branch.'); 402 | } 403 | 404 | if (local.branches.indexOf(rtBranch) === -1) { 405 | throw new Error('Current branch is not set up for pulling.'); 406 | } 407 | 408 | setTimeout(function () { 409 | try { 410 | if (args[0] === '--rebase' || control.rebaseConfig[currentBranch] === 'true') { 411 | isFastForward = local.rebase(rtBranch) === 'Fast-Forward'; 412 | } else { 413 | isFastForward = local.merge(rtBranch) === 'Fast-Forward'; 414 | } 415 | } catch (error) { 416 | control.error(error.message); 417 | } 418 | 419 | if (isFastForward) { 420 | control.info('Fast-forwarded to ' + rtBranch + '.'); 421 | } 422 | }, 750); 423 | }, 424 | 425 | push: function (args) { 426 | var control = this, 427 | local = this.historyView, 428 | remoteName = args.shift() || 'origin', 429 | remote = this[remoteName + 'View'], 430 | branchArgs = args.pop(), 431 | localRef = local.currentBranch, 432 | remoteRef = local.currentBranch, 433 | localCommit, remoteCommit, 434 | findCommitsToPush, 435 | isCommonCommit, 436 | toPush = []; 437 | 438 | if (remoteName === 'history') { 439 | throw new Error('Sorry, you can\'t have a remote named "history" in this example.'); 440 | } 441 | 442 | if (!remote) { 443 | throw new Error('There is no remote server named "' + remoteName + '".'); 444 | } 445 | 446 | if (branchArgs) { 447 | branchArgs = /^([^:]*)(:?)(.*)$/.exec(branchArgs); 448 | 449 | branchArgs[1] && (localRef = branchArgs[1]); 450 | branchArgs[2] === ':' && (remoteRef = branchArgs[3]); 451 | } 452 | 453 | if (local.branches.indexOf(localRef) === -1) { 454 | throw new Error('Local ref: ' + localRef + ' does not exist.'); 455 | } 456 | 457 | if (!remoteRef) { 458 | throw new Error('No remote branch was specified to push to.'); 459 | } 460 | 461 | localCommit = local.getCommit(localRef); 462 | remoteCommit = remote.getCommit(remoteRef); 463 | 464 | findCommitsToPush = function findCommitsToPush(localCommit) { 465 | var commitToPush, 466 | isCommonCommit = remote.getCommit(localCommit.id) !== null; 467 | 468 | while (!isCommonCommit) { 469 | commitToPush = { 470 | id: localCommit.id, 471 | parent: localCommit.parent, 472 | tags: [] 473 | }; 474 | 475 | if (typeof localCommit.parent2 === 'string') { 476 | commitToPush.parent2 = localCommit.parent2; 477 | findCommitsToPush(local.getCommit(localCommit.parent2)); 478 | } 479 | 480 | toPush.unshift(commitToPush); 481 | localCommit = local.getCommit(localCommit.parent); 482 | isCommonCommit = remote.getCommit(localCommit.id) !== null; 483 | } 484 | }; 485 | 486 | // push to an existing branch on the remote 487 | if (remoteCommit && remote.branches.indexOf(remoteRef) > -1) { 488 | if (!local.isAncestor(remoteCommit.id, localCommit.id)) { 489 | throw new Error('Push rejected. Non fast-forward.'); 490 | } 491 | 492 | isCommonCommit = localCommit.id === remoteCommit.id; 493 | 494 | if (isCommonCommit) { 495 | return this.info('Everything up-to-date.'); 496 | } 497 | 498 | findCommitsToPush(localCommit); 499 | 500 | remote.commitData = remote.commitData.concat(toPush); 501 | remote.moveTag(remoteRef, toPush[toPush.length - 1].id); 502 | remote.renderCommits(); 503 | } else { 504 | this.info('Sorry, creating new remote branches is not supported yet.'); 505 | } 506 | }, 507 | 508 | config: function (args) { 509 | var path = args.shift().split('.'); 510 | 511 | if (path[0] === 'branch') { 512 | if (path[2] === 'rebase') { 513 | this.rebase[path[1]] = args.pop(); 514 | } 515 | } 516 | } 517 | }; 518 | 519 | return ControlBox; 520 | }); 521 | -------------------------------------------------------------------------------- /js/explaingit.js: -------------------------------------------------------------------------------- 1 | define(['historyview', 'controlbox', 'd3'], function (HistoryView, ControlBox, d3) { 2 | var prefix = 'ExplainGit', 3 | openSandBoxes = [], 4 | open, 5 | reset, 6 | explainGit; 7 | 8 | open = function (_args) { 9 | var args = Object.create(_args), 10 | name = prefix + args.name, 11 | containerId = name + '-Container', 12 | container = d3.select('#' + containerId), 13 | playground = container.select('.playground-container'), 14 | historyView, originView = null, 15 | controlBox; 16 | 17 | container.style('display', 'block'); 18 | 19 | args.name = name; 20 | historyView = new HistoryView(args); 21 | 22 | if (args.originData) { 23 | originView = new HistoryView({ 24 | name: name + '-Origin', 25 | width: 300, 26 | height: 225, 27 | commitRadius: 15, 28 | remoteName: 'origin', 29 | commitData: args.originData 30 | }); 31 | 32 | originView.render(playground); 33 | } 34 | 35 | controlBox = new ControlBox({ 36 | historyView: historyView, 37 | originView: originView, 38 | initialMessage: args.initialMessage 39 | }); 40 | 41 | controlBox.render(playground); 42 | historyView.render(playground); 43 | 44 | openSandBoxes.push({ 45 | hv: historyView, 46 | cb: controlBox, 47 | container: container 48 | }); 49 | }; 50 | 51 | reset = function () { 52 | for (var i = 0; i < openSandBoxes.length; i++) { 53 | var osb = openSandBoxes[i]; 54 | osb.hv.destroy(); 55 | osb.cb.destroy(); 56 | osb.container.style('display', 'none'); 57 | } 58 | 59 | openSandBoxes.length = 0; 60 | d3.selectAll('a.openswitch').classed('selected', false); 61 | }; 62 | 63 | explainGit = { 64 | HistoryView: HistoryView, 65 | ControlBox: ControlBox, 66 | generateId: HistoryView.generateId, 67 | open: open, 68 | reset: reset 69 | }; 70 | 71 | window.explainGit = explainGit; 72 | 73 | return explainGit; 74 | }); -------------------------------------------------------------------------------- /js/historyview.js: -------------------------------------------------------------------------------- 1 | define(['d3'], function () { 2 | "use strict"; 3 | 4 | var REG_MARKER_END = 'url(#triangle)', 5 | MERGE_MARKER_END = 'url(#brown-triangle)', 6 | FADED_MARKER_END = 'url(#faded-triangle)', 7 | 8 | preventOverlap, 9 | applyBranchlessClass, 10 | cx, cy, fixCirclePosition, 11 | px1, py1, fixPointerStartPosition, 12 | px2, py2, fixPointerEndPosition, 13 | fixIdPosition, tagY; 14 | 15 | preventOverlap = function preventOverlap(commit, view) { 16 | var commitData = view.commitData, 17 | baseLine = view.baseLine, 18 | shift = view.commitRadius * 4.5, 19 | overlapped = null; 20 | 21 | for (var i = 0; i < commitData.length; i++) { 22 | var c = commitData[i]; 23 | if (c.cx === commit.cx && c.cy === commit.cy && c !== commit) { 24 | overlapped = c; 25 | break; 26 | } 27 | } 28 | 29 | if (overlapped) { 30 | var oParent = view.getCommit(overlapped.parent), 31 | parent = view.getCommit(commit.parent); 32 | 33 | if (overlapped.cy < baseLine) { 34 | overlapped = oParent.cy < parent.cy ? overlapped : commit; 35 | overlapped.cy -= shift; 36 | } else { 37 | overlapped = oParent.cy > parent.cy ? overlapped : commit; 38 | overlapped.cy += shift; 39 | } 40 | 41 | preventOverlap(overlapped, view); 42 | } 43 | }; 44 | 45 | applyBranchlessClass = function (selection) { 46 | if (selection.empty()) { 47 | return; 48 | } 49 | 50 | selection.classed('branchless', function (d) { 51 | return d.branchless; 52 | }); 53 | 54 | if (selection.classed('commit-pointer')) { 55 | selection.attr('marker-end', function (d) { 56 | return d.branchless ? FADED_MARKER_END : REG_MARKER_END; 57 | }); 58 | } else if (selection.classed('merge-pointer')) { 59 | selection.attr('marker-end', function (d) { 60 | return d.branchless ? FADED_MARKER_END : MERGE_MARKER_END; 61 | }); 62 | } 63 | }; 64 | 65 | cx = function (commit, view) { 66 | var parent = view.getCommit(commit.parent), 67 | parentCX = parent.cx; 68 | 69 | if (typeof commit.parent2 === 'string') { 70 | var parent2 = view.getCommit(commit.parent2); 71 | 72 | parentCX = parent.cx > parent2.cx ? parent.cx : parent2.cx; 73 | } 74 | 75 | return parentCX + (view.commitRadius * 4.5); 76 | }; 77 | 78 | cy = function (commit, view) { 79 | var parent = view.getCommit(commit.parent), 80 | parentCY = parent.cy || cy(parent, view), 81 | baseLine = view.baseLine, 82 | shift = view.commitRadius * 4.5, 83 | branches = [], // count the existing branches 84 | branchIndex = 0; 85 | 86 | for (var i = 0; i < view.commitData.length; i++) { 87 | var d = view.commitData[i]; 88 | 89 | if (d.parent === commit.parent) { 90 | branches.push(d.id); 91 | } 92 | } 93 | 94 | branchIndex = branches.indexOf(commit.id); 95 | 96 | if (commit.isNoFFBranch === true) { 97 | branchIndex++; 98 | } 99 | if (commit.isNoFFCommit === true) { 100 | branchIndex--; 101 | } 102 | 103 | if (parentCY === baseLine) { 104 | var direction = 1; 105 | for (var bi = 0; bi < branchIndex; bi++) { 106 | direction *= -1; 107 | } 108 | 109 | shift *= Math.ceil(branchIndex / 2); 110 | 111 | return parentCY + (shift * direction); 112 | } 113 | 114 | if (parentCY < baseLine) { 115 | return parentCY - (shift * branchIndex); 116 | } else if (parentCY > baseLine) { 117 | return parentCY + (shift * branchIndex); 118 | } 119 | }; 120 | 121 | fixCirclePosition = function (selection) { 122 | selection 123 | .attr('cx', function (d) { 124 | return d.cx; 125 | }) 126 | .attr('cy', function (d) { 127 | return d.cy; 128 | }); 129 | }; 130 | 131 | // calculates the x1 point for commit pointer lines 132 | px1 = function (commit, view, pp) { 133 | pp = pp || 'parent'; 134 | 135 | var parent = view.getCommit(commit[pp]), 136 | startCX = commit.cx, 137 | diffX = startCX - parent.cx, 138 | diffY = parent.cy - commit.cy, 139 | length = Math.sqrt((diffX * diffX) + (diffY * diffY)); 140 | 141 | return startCX - (view.pointerMargin * (diffX / length)); 142 | }; 143 | 144 | // calculates the y1 point for commit pointer lines 145 | py1 = function (commit, view, pp) { 146 | pp = pp || 'parent'; 147 | 148 | var parent = view.getCommit(commit[pp]), 149 | startCY = commit.cy, 150 | diffX = commit.cx - parent.cx, 151 | diffY = parent.cy - startCY, 152 | length = Math.sqrt((diffX * diffX) + (diffY * diffY)); 153 | 154 | return startCY + (view.pointerMargin * (diffY / length)); 155 | }; 156 | 157 | fixPointerStartPosition = function (selection, view) { 158 | selection.attr('x1', function (d) { 159 | return px1(d, view); 160 | }).attr('y1', function (d) { 161 | return py1(d, view); 162 | }); 163 | }; 164 | 165 | px2 = function (commit, view, pp) { 166 | pp = pp || 'parent'; 167 | 168 | var parent = view.getCommit(commit[pp]), 169 | endCX = parent.cx, 170 | diffX = commit.cx - endCX, 171 | diffY = parent.cy - commit.cy, 172 | length = Math.sqrt((diffX * diffX) + (diffY * diffY)); 173 | 174 | return endCX + (view.pointerMargin * 1.2 * (diffX / length)); 175 | }; 176 | 177 | py2 = function (commit, view, pp) { 178 | pp = pp || 'parent'; 179 | 180 | var parent = view.getCommit(commit[pp]), 181 | endCY = parent.cy, 182 | diffX = commit.cx - parent.cx, 183 | diffY = endCY - commit.cy, 184 | length = Math.sqrt((diffX * diffX) + (diffY * diffY)); 185 | 186 | return endCY - (view.pointerMargin * 1.2 * (diffY / length)); 187 | }; 188 | 189 | fixPointerEndPosition = function (selection, view) { 190 | selection.attr('x2', function (d) { 191 | return px2(d, view); 192 | }).attr('y2', function (d) { 193 | return py2(d, view); 194 | }); 195 | }; 196 | 197 | fixIdPosition = function (selection, view, delta) { 198 | selection.attr('x', function (d) { 199 | return d.cx; 200 | }).attr('y', function (d) { 201 | return d.cy + view.commitRadius + delta; 202 | }); 203 | }; 204 | 205 | tagY = function tagY(t, view) { 206 | var commit = view.getCommit(t.commit), 207 | commitCY = commit.cy, 208 | tags = commit.tags, 209 | tagIndex = tags.indexOf(t.name); 210 | 211 | if (tagIndex === -1) { 212 | tagIndex = tags.length; 213 | } 214 | 215 | if (commitCY < (view.baseLine)) { 216 | return commitCY - 45 - (tagIndex * 25); 217 | } else { 218 | return commitCY + 50 + (tagIndex * 25); 219 | } 220 | }; 221 | 222 | /** 223 | * @class HistoryView 224 | * @constructor 225 | */ 226 | function HistoryView(config) { 227 | var commitData = config.commitData || [], 228 | commit; 229 | 230 | for (var i = 0; i < commitData.length; i++) { 231 | commit = commitData[i]; 232 | !commit.parent && (commit.parent = 'initial'); 233 | !commit.tags && (commit.tags = []); 234 | } 235 | 236 | this.name = config.name || 'UnnamedHistoryView'; 237 | this.commitData = commitData; 238 | 239 | this.branches = []; 240 | this.currentBranch = config.currentBranch || 'master'; 241 | 242 | this.width = config.width; 243 | this.height = config.height || 400; 244 | this.orginalBaseLine = config.baseLine; 245 | this.baseLine = this.height * (config.baseLine || 0.6); 246 | 247 | this.commitRadius = config.commitRadius || 20; 248 | this.pointerMargin = this.commitRadius * 1.3; 249 | 250 | this.isRemote = typeof config.remoteName === 'string'; 251 | this.remoteName = config.remoteName; 252 | 253 | this.initialCommit = { 254 | id: 'initial', 255 | parent: null, 256 | cx: -(this.commitRadius * 2), 257 | cy: this.baseLine 258 | }; 259 | } 260 | 261 | HistoryView.generateId = function () { 262 | return Math.floor((1 + Math.random()) * 0x10000000).toString(16).substring(1); 263 | }; 264 | 265 | HistoryView.prototype = { 266 | /** 267 | * @method getCommit 268 | * @param ref {String} the id or a tag name that refers to the commit 269 | * @return {Object} the commit datum object 270 | */ 271 | getCommit: function getCommit(ref) { 272 | var commitData = this.commitData, 273 | headMatcher = /HEAD(\^+)/.exec(ref), 274 | matchedCommit = null; 275 | 276 | if (ref === 'initial') { 277 | return this.initialCommit; 278 | } 279 | 280 | if (headMatcher) { 281 | ref = 'HEAD'; 282 | } 283 | 284 | for (var i = 0; i < commitData.length; i++) { 285 | var commit = commitData[i]; 286 | if (commit === ref) { 287 | matchedCommit = commit; 288 | break; 289 | } 290 | 291 | if (commit.id === ref) { 292 | matchedCommit = commit; 293 | break; 294 | } 295 | 296 | var matchedTag = function() { 297 | for (var j = 0; j < commit.tags.length; j++) { 298 | var tag = commit.tags[j]; 299 | if (tag === ref) { 300 | matchedCommit = commit; 301 | return true; 302 | } 303 | 304 | if (tag.indexOf('[') === 0 && tag.indexOf(']') === tag.length - 1) { 305 | tag = tag.substring(1, tag.length - 1); 306 | } 307 | if (tag === ref) { 308 | matchedCommit = commit; 309 | return true; 310 | } 311 | } 312 | }(); 313 | if (matchedTag === true) { 314 | break; 315 | } 316 | } 317 | 318 | if (headMatcher && matchedCommit) { 319 | for (var h = 0; h < headMatcher[1].length; h++) { 320 | matchedCommit = getCommit.call(this, matchedCommit.parent); 321 | } 322 | } 323 | 324 | return matchedCommit; 325 | }, 326 | 327 | /** 328 | * @method getCircle 329 | * @param ref {String} the id or a tag name that refers to the commit 330 | * @return {d3 Selection} the d3 selected SVG circle 331 | */ 332 | getCircle: function (ref) { 333 | var circle = this.svg.select('#' + this.name + '-' + ref), 334 | commit; 335 | 336 | if (circle && !circle.empty()) { 337 | return circle; 338 | } 339 | 340 | commit = this.getCommit(ref); 341 | 342 | if (!commit) { 343 | return null; 344 | } 345 | 346 | return this.svg.select('#' + this.name + '-' + commit.id); 347 | }, 348 | 349 | getCircles: function () { 350 | return this.svg.selectAll('circle.commit'); 351 | }, 352 | 353 | /** 354 | * @method render 355 | * @param container {String} selector for the container to render the SVG into 356 | */ 357 | render: function (container) { 358 | var svgContainer, svg; 359 | 360 | svgContainer = container.append('div') 361 | .classed('svg-container', true) 362 | .classed('remote-container', this.isRemote); 363 | 364 | svg = svgContainer.append('svg:svg'); 365 | 366 | svg.attr('id', this.name) 367 | .attr('width', this.width) 368 | .attr('height', this.height); 369 | 370 | if (this.isRemote) { 371 | svg.append('svg:text') 372 | .classed('remote-name-display', true) 373 | .text(this.remoteName) 374 | .attr('x', 10) 375 | .attr('y', 25); 376 | } else { 377 | svg.append('svg:text') 378 | .classed('remote-name-display', true) 379 | .text('Local Repository') 380 | .attr('x', 10) 381 | .attr('y', 25); 382 | 383 | svg.append('svg:text') 384 | .classed('current-branch-display', true) 385 | .attr('x', 10) 386 | .attr('y', 45); 387 | } 388 | 389 | this.svgContainer = svgContainer; 390 | this.svg = svg; 391 | this.arrowBox = svg.append('svg:g').classed('pointers', true); 392 | this.commitBox = svg.append('svg:g').classed('commits', true); 393 | this.tagBox = svg.append('svg:g').classed('tags', true); 394 | 395 | this.renderCommits(); 396 | 397 | this._setCurrentBranch(this.currentBranch); 398 | }, 399 | 400 | destroy: function () { 401 | this.svg.remove(); 402 | this.svgContainer.remove(); 403 | clearInterval(this.refreshSizeTimer); 404 | 405 | for (var prop in this) { 406 | if (this.hasOwnProperty(prop)) { 407 | this[prop] = null; 408 | } 409 | } 410 | }, 411 | 412 | _calculatePositionData: function () { 413 | for (var i = 0; i < this.commitData.length; i++) { 414 | var commit = this.commitData[i]; 415 | commit.cx = cx(commit, this); 416 | commit.cy = cy(commit, this); 417 | preventOverlap(commit, this); 418 | } 419 | }, 420 | 421 | _resizeSvg: function() { 422 | var ele = document.getElementById(this.svg.node().id); 423 | var container = ele.parentNode; 424 | var currentWidth = ele.offsetWidth; 425 | var newWidth; 426 | 427 | if (ele.getBBox().width > container.offsetWidth) 428 | newWidth = Math.round(ele.getBBox().width); 429 | else 430 | newWidth = container.offsetWidth - 5; 431 | 432 | if (currentWidth != newWidth) { 433 | this.svg.attr('width', newWidth); 434 | container.scrollLeft = container.scrollWidth; 435 | } 436 | }, 437 | 438 | renderCommits: function () { 439 | if (typeof this.height === 'string' && this.height.indexOf('%') >= 0) { 440 | var perc = this.height.substring(0, this.height.length - 1) / 100.0; 441 | var baseLineCalcHeight = Math.round(this.svg.node().parentNode.offsetHeight * perc) - 65; 442 | var newBaseLine = Math.round(baseLineCalcHeight * (this.originalBaseLine || 0.6)); 443 | if (newBaseLine !== this.baseLine) { 444 | this.baseLine = newBaseLine; 445 | this.initialCommit.cy = newBaseLine; 446 | this.svg.attr('height', baseLineCalcHeight); 447 | } 448 | } 449 | this._calculatePositionData(); 450 | this._calculatePositionData(); // do this twice to make sure 451 | this._renderCircles(); 452 | this._renderPointers(); 453 | this._renderMergePointers(); 454 | this._renderIdLabels(); 455 | this._resizeSvg(); 456 | this.checkout(this.currentBranch); 457 | }, 458 | 459 | _renderCircles: function () { 460 | var view = this, 461 | existingCircles, 462 | newCircles; 463 | 464 | existingCircles = this.commitBox.selectAll('circle.commit') 465 | .data(this.commitData, function (d) { return d.id; }) 466 | .attr('id', function (d) { 467 | return view.name + '-' + d.id; 468 | }) 469 | .classed('reverted', function (d) { 470 | return d.reverted; 471 | }) 472 | .classed('rebased', function (d) { 473 | return d.rebased; 474 | }); 475 | 476 | existingCircles.transition() 477 | .duration(500) 478 | .call(fixCirclePosition); 479 | 480 | newCircles = existingCircles.enter() 481 | .append('svg:circle') 482 | .attr('id', function (d) { 483 | return view.name + '-' + d.id; 484 | }) 485 | .classed('commit', true) 486 | .classed('merge-commit', function (d) { 487 | return typeof d.parent2 === 'string'; 488 | }) 489 | .call(fixCirclePosition) 490 | .attr('r', 1) 491 | .transition("inflate") 492 | .duration(500) 493 | .attr('r', this.commitRadius); 494 | 495 | }, 496 | 497 | _renderPointers: function () { 498 | var view = this, 499 | existingPointers, 500 | newPointers; 501 | 502 | existingPointers = this.arrowBox.selectAll('line.commit-pointer') 503 | .data(this.commitData, function (d) { return d.id; }) 504 | .attr('id', function (d) { 505 | return view.name + '-' + d.id + '-to-' + d.parent; 506 | }); 507 | 508 | existingPointers.transition() 509 | .duration(500) 510 | .call(fixPointerStartPosition, view) 511 | .call(fixPointerEndPosition, view); 512 | 513 | newPointers = existingPointers.enter() 514 | .append('svg:line') 515 | .attr('id', function (d) { 516 | return view.name + '-' + d.id + '-to-' + d.parent; 517 | }) 518 | .classed('commit-pointer', true) 519 | .call(fixPointerStartPosition, view) 520 | .attr('x2', function () { return d3.select(this).attr('x1'); }) 521 | .attr('y2', function () { return d3.select(this).attr('y1'); }) 522 | .attr('marker-end', REG_MARKER_END) 523 | .transition() 524 | .duration(500) 525 | .call(fixPointerEndPosition, view); 526 | }, 527 | 528 | _renderMergePointers: function () { 529 | var view = this, 530 | mergeCommits = [], 531 | existingPointers, newPointers; 532 | 533 | for (var i = 0; i < this.commitData.length; i++) { 534 | var commit = this.commitData[i]; 535 | if (typeof commit.parent2 === 'string') { 536 | mergeCommits.push(commit); 537 | } 538 | } 539 | 540 | existingPointers = this.arrowBox.selectAll('polyline.merge-pointer') 541 | .data(mergeCommits, function (d) { return d.id; }) 542 | .attr('id', function (d) { 543 | return view.name + '-' + d.id + '-to-' + d.parent2; 544 | }); 545 | 546 | existingPointers.transition().duration(500) 547 | .attr('points', function (d) { 548 | var p1 = px1(d, view, 'parent2') + ',' + py1(d, view, 'parent2'), 549 | p2 = px2(d, view, 'parent2') + ',' + py2(d, view, 'parent2'); 550 | 551 | return [p1, p2].join(' '); 552 | }); 553 | 554 | newPointers = existingPointers.enter() 555 | .append('svg:polyline') 556 | .attr('id', function (d) { 557 | return view.name + '-' + d.id + '-to-' + d.parent2; 558 | }) 559 | .classed('merge-pointer', true) 560 | .attr('points', function (d) { 561 | var x1 = px1(d, view, 'parent2'), 562 | y1 = py1(d, view, 'parent2'), 563 | p1 = x1 + ',' + y1; 564 | 565 | return [p1, p1].join(' '); 566 | }) 567 | .attr('marker-end', MERGE_MARKER_END) 568 | .transition() 569 | .duration(500) 570 | .attr('points', function (d) { 571 | var points = d3.select(this).attr('points').split(' '), 572 | x2 = px2(d, view, 'parent2'), 573 | y2 = py2(d, view, 'parent2'); 574 | 575 | points[1] = x2 + ',' + y2; 576 | return points.join(' '); 577 | }); 578 | }, 579 | 580 | _renderIdLabels: function () { 581 | this._renderText('id-label', function (d) { return d.id + '..'; }, 14); 582 | this._renderText('message-label', function (d) { return d.message; }, 24); 583 | }, 584 | 585 | _renderText: function(className, getText, delta) { 586 | var view = this, 587 | existingTexts, 588 | newtexts; 589 | 590 | existingTexts = this.commitBox.selectAll('text.' + className) 591 | .data(this.commitData, function (d) { return d.id; }) 592 | .text(getText); 593 | 594 | existingTexts.transition().call(fixIdPosition, view, delta); 595 | 596 | newtexts = existingTexts.enter() 597 | .insert('svg:text', ':first-child') 598 | .classed(className, true) 599 | .text(getText) 600 | .call(fixIdPosition, view, delta); 601 | }, 602 | 603 | _parseTagData: function () { 604 | var tagData = [], i, 605 | headCommit = null; 606 | 607 | for (i = 0; i < this.commitData.length; i++) { 608 | var c = this.commitData[i]; 609 | 610 | for (var t = 0; t < c.tags.length; t++) { 611 | var tagName = c.tags[t]; 612 | if (tagName.toUpperCase() === 'HEAD') { 613 | headCommit = c; 614 | } else if (this.branches.indexOf(tagName) === -1) { 615 | this.branches.push(tagName); 616 | } 617 | 618 | tagData.push({name: tagName, commit: c.id}); 619 | } 620 | } 621 | 622 | if (!headCommit) { 623 | headCommit = this.getCommit(this.currentBranch); 624 | headCommit.tags.push('HEAD'); 625 | tagData.push({name: 'HEAD', commit: headCommit.id}); 626 | } 627 | 628 | // find out which commits are not branchless 629 | 630 | 631 | return tagData; 632 | }, 633 | 634 | _markBranchlessCommits: function () { 635 | var branch, commit, parent, parent2, c, b; 636 | 637 | // first mark every commit as branchless 638 | for (c = 0; c < this.commitData.length; c++) { 639 | this.commitData[c].branchless = true; 640 | } 641 | 642 | for (b = 0; b < this.branches.length; b++) { 643 | branch = this.branches[b]; 644 | if (branch.indexOf('/') === -1) { 645 | commit = this.getCommit(branch); 646 | parent = this.getCommit(commit.parent); 647 | parent2 = this.getCommit(commit.parent2); 648 | 649 | commit.branchless = false; 650 | 651 | while (parent) { 652 | parent.branchless = false; 653 | parent = this.getCommit(parent.parent); 654 | } 655 | 656 | // just in case this is a merge commit 657 | while (parent2) { 658 | parent2.branchless = false; 659 | parent2 = this.getCommit(parent2.parent); 660 | } 661 | } 662 | } 663 | 664 | this.svg.selectAll('circle.commit').call(applyBranchlessClass); 665 | this.svg.selectAll('line.commit-pointer').call(applyBranchlessClass); 666 | this.svg.selectAll('polyline.merge-pointer').call(applyBranchlessClass); 667 | }, 668 | 669 | renderTags: function () { 670 | var view = this, 671 | tagData = this._parseTagData(), 672 | existingTags, newTags; 673 | 674 | existingTags = this.tagBox.selectAll('g.branch-tag') 675 | .data(tagData, function (d) { return d.name; }); 676 | 677 | existingTags.exit().remove(); 678 | 679 | existingTags.select('rect') 680 | .transition() 681 | .duration(500) 682 | .attr('y', function (d) { return tagY(d, view); }) 683 | .attr('x', function (d) { 684 | var commit = view.getCommit(d.commit), 685 | width = Number(d3.select(this).attr('width')); 686 | 687 | return commit.cx - (width / 2); 688 | }); 689 | 690 | existingTags.select('text') 691 | .transition() 692 | .duration(500) 693 | .attr('y', function (d) { return tagY(d, view) + 14; }) 694 | .attr('x', function (d) { 695 | var commit = view.getCommit(d.commit); 696 | return commit.cx; 697 | }); 698 | 699 | newTags = existingTags.enter() 700 | .append('g') 701 | .attr('class', function (d) { 702 | var classes = 'branch-tag'; 703 | if (d.name.indexOf('[') === 0 && d.name.indexOf(']') === d.name.length - 1) { 704 | classes += ' git-tag'; 705 | } else if (d.name.indexOf('/') >= 0) { 706 | classes += ' remote-branch'; 707 | } else if (d.name.toUpperCase() === 'HEAD') { 708 | classes += ' head-tag'; 709 | } 710 | return classes; 711 | }); 712 | 713 | newTags.append('svg:rect') 714 | .attr('width', function (d) { 715 | return (d.name.length * 6) + 10; 716 | }) 717 | .attr('height', 20) 718 | .attr('y', function (d) { return tagY(d, view); }) 719 | .attr('x', function (d) { 720 | var commit = view.getCommit(d.commit), 721 | width = Number(d3.select(this).attr('width')); 722 | 723 | return commit.cx - (width / 2); 724 | }); 725 | 726 | newTags.append('svg:text') 727 | .text(function (d) { 728 | if (d.name.indexOf('[') === 0 && d.name.indexOf(']') === d.name.length - 1) 729 | return d.name.substring(1, d.name.length - 1); 730 | return d.name; 731 | }) 732 | .attr('y', function (d) { 733 | return tagY(d, view) + 14; 734 | }) 735 | .attr('x', function (d) { 736 | var commit = view.getCommit(d.commit); 737 | return commit.cx; 738 | }); 739 | 740 | this._markBranchlessCommits(); 741 | }, 742 | 743 | _setCurrentBranch: function (branch) { 744 | var display = this.svg.select('text.current-branch-display'), 745 | text = 'Current Branch: '; 746 | 747 | if (branch && branch.indexOf('/') === -1) { 748 | text += branch; 749 | this.currentBranch = branch; 750 | } else { 751 | text += ' DETACHED HEAD'; 752 | this.currentBranch = null; 753 | } 754 | 755 | display.text(text); 756 | }, 757 | 758 | moveTag: function (tag, ref) { 759 | var currentLoc = this.getCommit(tag), 760 | newLoc = this.getCommit(ref); 761 | 762 | if (currentLoc) { 763 | currentLoc.tags.splice(currentLoc.tags.indexOf(tag), 1); 764 | } 765 | 766 | newLoc.tags.push(tag); 767 | return this; 768 | }, 769 | 770 | /** 771 | * @method isAncestor 772 | * @param ref1 773 | * @param ref2 774 | * @return {Boolean} whether or not ref1 is an ancestor of ref2 775 | */ 776 | isAncestor: function isAncestor(ref1, ref2) { 777 | var currentCommit = this.getCommit(ref1), 778 | targetTree = this.getCommit(ref2), 779 | inTree = false, 780 | additionalTrees = []; 781 | 782 | if (!currentCommit) { 783 | return false; 784 | } 785 | 786 | while (targetTree) { 787 | if (targetTree.id === currentCommit.id) { 788 | inTree = true; 789 | targetTree = null; 790 | } else { 791 | if (targetTree.parent2) { 792 | additionalTrees.push(targetTree.parent2); 793 | } 794 | targetTree = this.getCommit(targetTree.parent); 795 | } 796 | } 797 | 798 | if (inTree) { 799 | return true; 800 | } 801 | 802 | for (var i = 0; i < additionalTrees.length; i++) { 803 | inTree = isAncestor.call(this, currentCommit, additionalTrees[i]); 804 | if (inTree) break; 805 | } 806 | 807 | return inTree; 808 | }, 809 | 810 | commit: function (commit, message) { 811 | commit = commit || {}; 812 | 813 | !commit.id && (commit.id = HistoryView.generateId()); 814 | !commit.tags && (commit.tags = []); 815 | 816 | commit.message = message; 817 | if (!commit.parent) { 818 | if (!this.currentBranch) { 819 | throw new Error('Not a good idea to make commits while in a detached HEAD state.'); 820 | } 821 | 822 | commit.parent = this.getCommit(this.currentBranch).id; 823 | } 824 | 825 | this.commitData.push(commit); 826 | this.moveTag(this.currentBranch, commit.id); 827 | 828 | this.renderCommits(); 829 | 830 | this.checkout(this.currentBranch); 831 | return this; 832 | }, 833 | 834 | branch: function (name) { 835 | if (!name || name.trim() === '') { 836 | throw new Error('You need to give a branch name.'); 837 | } 838 | 839 | if (name === 'HEAD') { 840 | throw new Error('You cannot name your branch "HEAD".'); 841 | } 842 | 843 | if (name.indexOf(' ') > -1) { 844 | throw new Error('Branch names cannot contain spaces.'); 845 | } 846 | 847 | if (this.branches.indexOf(name) > -1) { 848 | throw new Error('Branch "' + name + '" already exists.'); 849 | } 850 | 851 | this.getCommit('HEAD').tags.push(name); 852 | this.renderTags(); 853 | return this; 854 | }, 855 | 856 | tag: function (name) { 857 | this.branch('[' + name + ']'); 858 | }, 859 | 860 | deleteBranch: function (name) { 861 | var branchIndex, 862 | commit; 863 | 864 | if (!name || name.trim() === '') { 865 | throw new Error('You need to give a branch name.'); 866 | } 867 | 868 | if (name === this.currentBranch) { 869 | throw new Error('Cannot delete the currently checked-out branch.'); 870 | } 871 | 872 | branchIndex = this.branches.indexOf(name); 873 | 874 | if (branchIndex === -1) { 875 | throw new Error('That branch doesn\'t exist.'); 876 | } 877 | 878 | this.branches.splice(branchIndex, 1); 879 | commit = this.getCommit(name); 880 | branchIndex = commit.tags.indexOf(name); 881 | 882 | if (branchIndex > -1) { 883 | commit.tags.splice(branchIndex, 1); 884 | } 885 | 886 | this.renderTags(); 887 | }, 888 | 889 | checkout: function (ref) { 890 | var commit = this.getCommit(ref); 891 | 892 | if (!commit) { 893 | throw new Error('Cannot find commit: ' + ref); 894 | } 895 | 896 | var previousHead = this.getCircle('HEAD'), 897 | newHead = this.getCircle(commit.id); 898 | 899 | if (previousHead && !previousHead.empty()) { 900 | previousHead.classed('checked-out', false); 901 | } 902 | 903 | this._setCurrentBranch(ref === commit.id ? null : ref); 904 | this.moveTag('HEAD', commit.id); 905 | this.renderTags(); 906 | 907 | newHead.classed('checked-out', true); 908 | 909 | return this; 910 | }, 911 | 912 | reset: function (ref) { 913 | var commit = this.getCommit(ref); 914 | 915 | if (!commit) { 916 | throw new Error('Cannot find ref: ' + ref); 917 | } 918 | 919 | if (this.currentBranch) { 920 | this.moveTag(this.currentBranch, commit.id); 921 | this.checkout(this.currentBranch); 922 | } else { 923 | this.checkout(commit.id); 924 | } 925 | 926 | return this; 927 | }, 928 | 929 | revert: function (ref) { 930 | var commit = this.getCommit(ref); 931 | 932 | if (!commit) { 933 | throw new Error('Cannot find ref: ' + ref); 934 | } 935 | 936 | if (this.isAncestor(commit, 'HEAD')) { 937 | commit.reverted = true; 938 | this.commit({reverts: commit.id}); 939 | } else { 940 | throw new Error(ref + 'is not an ancestor of HEAD.'); 941 | } 942 | }, 943 | 944 | fastForward: function (ref) { 945 | var targetCommit = this.getCommit(ref); 946 | 947 | if (this.currentBranch) { 948 | this.moveTag(this.currentBranch, targetCommit.id); 949 | this.checkout(this.currentBranch); 950 | } else { 951 | this.checkout(targetCommit.id); 952 | } 953 | }, 954 | 955 | merge: function (ref, noFF) { 956 | var mergeTarget = this.getCommit(ref), 957 | currentCommit = this.getCommit('HEAD'); 958 | 959 | if (!mergeTarget) { 960 | throw new Error('Cannot find ref: ' + ref); 961 | } 962 | 963 | if (currentCommit.id === mergeTarget.id) { 964 | throw new Error('Already up-to-date.'); 965 | } else if (currentCommit.parent2 === mergeTarget.id) { 966 | throw new Error('Already up-to-date.'); 967 | } else if (noFF === true) { 968 | var branchStartCommit = this.getCommit(mergeTarget.parent); 969 | while (branchStartCommit.parent !== currentCommit.id) { 970 | branchStartCommit = this.getCommit(branchStartCommit.parent); 971 | } 972 | 973 | branchStartCommit.isNoFFBranch = true; 974 | 975 | this.commit({parent2: mergeTarget.id, isNoFFCommit: true}); 976 | } else if (this.isAncestor(currentCommit, mergeTarget)) { 977 | this.fastForward(mergeTarget); 978 | return 'Fast-Forward'; 979 | } else { 980 | this.commit({parent2: mergeTarget.id}); 981 | } 982 | }, 983 | 984 | rebase: function (ref) { 985 | var rebaseTarget = this.getCommit(ref), 986 | currentCommit = this.getCommit('HEAD'), 987 | isCommonAncestor, 988 | rebaseTreeLoc, 989 | rebaseMessage, 990 | toRebase = [], rebasedCommit, 991 | remainingHusk; 992 | 993 | if (!rebaseTarget) { 994 | throw new Error('Cannot find ref: ' + ref); 995 | } 996 | 997 | if (currentCommit.id === rebaseTarget.id) { 998 | throw new Error('Already up-to-date.'); 999 | } else if (currentCommit.parent2 === rebaseTarget.id) { 1000 | throw new Error('Already up-to-date.'); 1001 | } 1002 | 1003 | isCommonAncestor = this.isAncestor(currentCommit, rebaseTarget); 1004 | 1005 | if (isCommonAncestor) { 1006 | this.fastForward(rebaseTarget); 1007 | return 'Fast-Forward'; 1008 | } 1009 | 1010 | rebaseTreeLoc = rebaseTarget.id; 1011 | 1012 | while (!isCommonAncestor) { 1013 | toRebase.unshift(currentCommit); 1014 | currentCommit = this.getCommit(currentCommit.parent); 1015 | isCommonAncestor = this.isAncestor(currentCommit, rebaseTarget); 1016 | } 1017 | 1018 | for (var i = 0; i < toRebase.length; i++) { 1019 | rebasedCommit = toRebase[i]; 1020 | rebaseMessage = rebasedCommit.message; 1021 | 1022 | remainingHusk = { 1023 | id: rebasedCommit.id, 1024 | parent: rebasedCommit.parent, 1025 | message: rebasedCommit.message, 1026 | tags: [] 1027 | }; 1028 | 1029 | for (var t = 0; t < rebasedCommit.tags.length; t++) { 1030 | var tagName = rebasedCommit.tags[t]; 1031 | if (tagName !== this.currentBranch && tagName !== 'HEAD') { 1032 | remainingHusk.tags.unshift(tagName); 1033 | } 1034 | } 1035 | 1036 | this.commitData.push(remainingHusk); 1037 | 1038 | rebasedCommit.parent = rebaseTreeLoc; 1039 | rebaseTreeLoc = HistoryView.generateId() 1040 | rebasedCommit.id = rebaseTreeLoc; 1041 | rebasedCommit.message = rebaseMessage; 1042 | rebasedCommit.tags.length = 0; 1043 | rebasedCommit.rebased = true; 1044 | } 1045 | 1046 | if (this.currentBranch) { 1047 | rebasedCommit.tags.push(this.currentBranch); 1048 | } 1049 | 1050 | this.renderCommits(); 1051 | 1052 | if (this.currentBranch) { 1053 | this.checkout(this.currentBranch); 1054 | } else { 1055 | this.checkout(rebasedCommit.id); 1056 | } 1057 | } 1058 | }; 1059 | 1060 | return HistoryView; 1061 | }); 1062 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | if (!String.prototype.trim) { 2 | String.prototype.trim = function () { 3 | return this.replace(/^\s+|\s+$/g,''); 4 | }; 5 | } 6 | 7 | if (!Array.isArray) { 8 | Array.isArray = function (vArg) { 9 | return Object.prototype.toString.call(vArg) === "[object Array]"; 10 | }; 11 | } 12 | 13 | if (!Array.prototype.indexOf) { 14 | Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) { 15 | "use strict"; 16 | if (this == null) { 17 | throw new TypeError(); 18 | } 19 | var t = Object(this); 20 | var len = t.length >>> 0; 21 | if (len === 0) { 22 | return -1; 23 | } 24 | var n = 0; 25 | if (arguments.length > 1) { 26 | n = Number(arguments[1]); 27 | if (n != n) { // shortcut for verifying if it's NaN 28 | n = 0; 29 | } else if (n != 0 && n != Infinity && n != -Infinity) { 30 | n = (n > 0 || -1) * Math.floor(Math.abs(n)); 31 | } 32 | } 33 | if (n >= len) { 34 | return -1; 35 | } 36 | var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); 37 | for (; k < len; k++) { 38 | if (k in t && t[k] === searchElement) { 39 | return k; 40 | } 41 | } 42 | return -1; 43 | } 44 | } 45 | 46 | require.config({ 47 | paths: { 48 | 'd3': 'https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.12/d3.min' 49 | }, 50 | shim: { 51 | 'd3': { 52 | exports: 'd3' 53 | } 54 | } 55 | }); -------------------------------------------------------------------------------- /memtest.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Explain Git with D3 5 | 6 | 7 | 8 | 9 |

Memtest Page

10 |

This page exists to help me find any memory leaks that may happen.

11 | 12 |

explain git memtest

13 |
14 |

15 | Create and destroy many git history views and control boxes to find memory leaks. 16 |

17 |
18 |

Start Test

19 | 42 |

Back to Home

43 | 44 | --------------------------------------------------------------------------------