├── LICENSE.md ├── README.md ├── css └── explaingit.css ├── glossary.md ├── images ├── gitbranch.png ├── gitcheckout.png ├── gitcheckoutb.png ├── gitcommit.png ├── gitreset.png ├── gitrevert.png └── prompt.gif ├── index.html ├── js ├── controlbox.js ├── explaingit.js ├── historyview.js ├── main.js └── vendor │ └── yargs-parser.js └── memtest.html /LICENSE.md: -------------------------------------------------------------------------------- 1 | [git 튜토리얼](https://github.com/Violet-Bora-Lee)(이하 튜토리얼)은 MIT License를 준수하는 Wei Wang의 https://github.com/onlywei/explain-git-with-d3를 기반으로 만든 프로젝트입니다. 2 | 3 | 튜토리얼의 라이센스는 [CC-BY-NC](https://creativecommons.org/licenses/by-nc/4.0/legalcode)를 따릅니다. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git 튜토리얼 2 | 3 | [git 튜토리얼](https://violet-bora-lee.github.io/git-tutorial/)은 git 명령어를 입력 했을 때 어떤 변화가 생기는지 시각적으로 보여주면서 git을 학습할 수 있도록 만든 튜토리얼입니다. 4 | 5 | ## git commit 6 | ![git commit](images/gitcommit.png) 7 | 8 | ## git branch 9 | ![git branch](images/gitbranch.png) 10 | 11 | ## git checkout 12 | ![git checkout](images/gitcheckout.png) 13 | 14 | ## git checkout -b 15 | ![git checkout -b](images/gitcheckoutb.png) 16 | 17 | ## git reset 18 | ![git reset](images/gitreset.png) 19 | 20 | ## git revert 21 | ![git revert](images/gitrevert.png) -------------------------------------------------------------------------------- /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; 62 | right: 0px; 63 | background-color: #EFF1FF; 64 | border-left: 1px dotted #AAA; 65 | border-bottom: 1px dotted #AAA; 66 | } 67 | 68 | #ExplainGitZen-Container { 69 | position: absolute; 70 | top: 0; 71 | bottom: 0; 72 | right: 0; 73 | left: 0; 74 | } 75 | 76 | #ExplainGitZen-Container .svg-container { 77 | display: inline-block; 78 | border: 1px dotted #AAA; 79 | position: absolute; 80 | top: 0; 81 | bottom: 0; 82 | right: 0; 83 | left: 250px; 84 | margin-left: 0; 85 | } 86 | 87 | #ExplainGitZen-Container .svg-container.remote-container { 88 | position: absolute; 89 | top: 0px; 90 | right: 0px; 91 | left: auto; 92 | bottom: auto; 93 | background-color: #EFF1FF; 94 | border-left: 1px dotted #AAA; 95 | border-bottom: 1px dotted #AAA; 96 | } 97 | 98 | #ExplainGitZen-Container .playground-container { 99 | position: absolute; 100 | top: 0; 101 | bottom: 20px; 102 | right: 20px; 103 | left: 20px; 104 | } 105 | 106 | .remote-name-display { 107 | font-weight: bold; 108 | text-align: right; 109 | } 110 | 111 | .control-box { 112 | display: inline-block; 113 | position: absolute; 114 | top: 0px; 115 | bottom: 0; 116 | width: 250px; 117 | vertical-align: bottom; 118 | background-color: #000; 119 | border: 1px dotted #AAA; 120 | } 121 | 122 | .control-box button { 123 | font-family: Courier New; 124 | font-size: 12px; 125 | margin-right: 5px; 126 | margin-bottom: 5px; 127 | } 128 | 129 | .control-box .log { 130 | overflow-y: auto; 131 | position: absolute; 132 | top: 0px; 133 | bottom: 20px; 134 | left: 0; 135 | right: 0; 136 | border-bottom: 1px solid #AAA; 137 | } 138 | 139 | .control-box .log, .control-box input[type="text"] { 140 | font-family: Courier New; 141 | font-size: 14px; 142 | } 143 | 144 | .control-box .log .command-entry { 145 | padding-left: 15px; 146 | color: #FFF; 147 | line-height: 14px; 148 | background: url(../images/prompt.gif) no-repeat left center transparent; 149 | } 150 | 151 | .control-box input[type="text"] { 152 | position: absolute; 153 | bottom: 0; 154 | padding-left: 15px; 155 | color: #FFF; 156 | line-height: 14px; 157 | background: url(../images/prompt.gif) no-repeat left center transparent; 158 | } 159 | 160 | .control-box .log .info, .control-box .log .error { 161 | font-size: 12px; 162 | padding: 5px; 163 | } 164 | 165 | .control-box .log .info { 166 | color: #FFC; 167 | } 168 | 169 | .control-box .log .error { 170 | color: #FCC; 171 | } 172 | 173 | .control-box input[type="text"] { 174 | width: 235px; 175 | border: none; 176 | } 177 | 178 | circle.commit { 179 | fill: #EEEEEE; 180 | stroke: #888888; 181 | stroke-width: 3; 182 | transition-property: stroke, fill; 183 | transition-duration: 500ms; 184 | transition-timing-function: ease-out; 185 | } 186 | 187 | circle.commit.merge-commit { 188 | stroke: #663300; 189 | fill: #FFFFCC; 190 | } 191 | 192 | circle.commit.merge-commit.checked-out { 193 | fill: #FFFFCC; 194 | } 195 | 196 | circle.commit.reverted { 197 | fill: #CCEEFF; 198 | stroke: #0066CC; 199 | } 200 | 201 | circle.commit.reverted.checked-out { 202 | fill: #CCEEFF; 203 | } 204 | 205 | circle.commit.rebased { 206 | stroke: #3300CC; 207 | fill: #CCCCFF; 208 | } 209 | 210 | circle.commit.rebased.checked-out { 211 | fill: #CCCCFF; 212 | } 213 | 214 | circle.commit.cherry-picked { 215 | stroke: #FF0000; 216 | fill: #FFEEEE; 217 | } 218 | 219 | circle.commit.cherry-picked.checked-out { 220 | fill: #FFEEEE; 221 | } 222 | 223 | circle.commit.branchless { 224 | fill: #FEFEFE; 225 | stroke: #DDD; 226 | } 227 | 228 | circle.commit.branchless.checked-out { 229 | fill: #FEFEFE; 230 | } 231 | 232 | circle.commit.checked-out { 233 | fill: #CCFFCC; 234 | stroke: #339900; 235 | } 236 | 237 | circle.commit.logging { 238 | stroke: #0066CC; 239 | fill: #0099EE; 240 | } 241 | 242 | .commit-pointer { 243 | stroke: #666; 244 | stroke-width: 4; 245 | } 246 | 247 | .merge-pointer { 248 | stroke: #663300; 249 | stroke-width: 4; 250 | } 251 | 252 | .commit-pointer.branchless, .merge-pointer.branchless { 253 | stroke: #DDD; 254 | stroke-width: 2; 255 | } 256 | 257 | text.id-label { 258 | text-anchor: middle; 259 | font-family: Courier New; 260 | font-weight: bolder; 261 | fill: #666; 262 | font-size: 10px; 263 | } 264 | 265 | text.message-label { 266 | text-anchor: middle; 267 | font-family: Courier New; 268 | fill: #666; 269 | font-size: 10px; 270 | } 271 | 272 | g.branch-tag > rect { 273 | fill: #FFCC66; 274 | stroke: #CC9900; 275 | stroke-width: 2; 276 | } 277 | 278 | g.branch-tag.git-tag > rect { 279 | fill: #7FC9FF; 280 | stroke: #0026FF; 281 | } 282 | 283 | g.branch-tag.remote-branch > rect { 284 | fill: #CCC; 285 | stroke: #888; 286 | } 287 | 288 | g.branch-tag > text { 289 | text-anchor: middle; 290 | fill: #000; 291 | font-size: 11px; 292 | font-family: Arial; 293 | } 294 | 295 | g.head-tag > rect { 296 | fill: #CCFFCC; 297 | stroke: #339900; 298 | stroke-width: 2; 299 | } 300 | 301 | g.head-tag > text { 302 | text-anchor: middle; 303 | fill: #000; 304 | font-size: 11px; 305 | font-family: Arial; 306 | font-weight: bold; 307 | text-transform: uppercase; 308 | } 309 | 310 | #fork-me svg { 311 | position: absolute; 312 | top: 0; 313 | right: 0; 314 | border: 0; 315 | color: #3f3f3f; 316 | fill: #ea6f5a; 317 | height: 80px; 318 | width: 80px; 319 | } -------------------------------------------------------------------------------- /glossary.md: -------------------------------------------------------------------------------- 1 | # 용어집 2 | 3 | - repository: 리포지토리 4 | - branch: 브랜치 5 | - commit: 커밋 6 | - local: local 7 | - remote: remote -------------------------------------------------------------------------------- /images/gitbranch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Violet-Bora-Lee/git-tutorial/438ad3fd7ba297a26b383343461827f64af14449/images/gitbranch.png -------------------------------------------------------------------------------- /images/gitcheckout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Violet-Bora-Lee/git-tutorial/438ad3fd7ba297a26b383343461827f64af14449/images/gitcheckout.png -------------------------------------------------------------------------------- /images/gitcheckoutb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Violet-Bora-Lee/git-tutorial/438ad3fd7ba297a26b383343461827f64af14449/images/gitcheckoutb.png -------------------------------------------------------------------------------- /images/gitcommit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Violet-Bora-Lee/git-tutorial/438ad3fd7ba297a26b383343461827f64af14449/images/gitcommit.png -------------------------------------------------------------------------------- /images/gitreset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Violet-Bora-Lee/git-tutorial/438ad3fd7ba297a26b383343461827f64af14449/images/gitreset.png -------------------------------------------------------------------------------- /images/gitrevert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Violet-Bora-Lee/git-tutorial/438ad3fd7ba297a26b383343461827f64af14449/images/gitrevert.png -------------------------------------------------------------------------------- /images/prompt.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Violet-Bora-Lee/git-tutorial/438ad3fd7ba297a26b383343461827f64af14449/images/prompt.gif -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | git 튜토리얼 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |
19 |

git 튜토리얼

20 |
21 |

22 | git 튜토리얼은 git 명령어를 시각 자료를 사용해 이해할 수 있도록 만든 웹페이지 기반 튜토리얼입니다. 23 |

24 |

25 | add, stash는 본 튜토리얼에서 다루지 않습니다. 명령어를 직접 입력해 볼 수 있는 샌드박스에서는 버전 관리하고자 26 | 하는 파일이 모두 staging 영역에 올라와 있다고 가정하고 진행됩니다. add나 staging 영역에 관한 개념을 알고 싶다면 27 | 링크를 참고하시기 바랍니다. 28 |

29 |

30 | 학습할 수 있는 명령어는 다음과 같습니다. 31 |

32 |
33 |
34 |
35 |
36 |

기본 명령어

37 | git commit 38 | git branch 39 |
40 |
41 |

 

42 | git checkout 43 | git checkout -b 44 |
45 |
46 |

commit 되돌리기

47 | git reset 48 | git revert 49 |
50 |
51 |

브랜치 병합하기

52 | git merge 53 | git rebase 54 |
55 |
56 |

remote 리포지토리

57 | git fetch 58 | git pull 59 |
60 |
61 |

 

62 | git push 63 | git tag 64 |
65 |
66 |
67 |
68 |

69 | commit 하고자 하는 파일들이 staging 영역에 올라와 있다고 가정합시다. 왼쪽 하단에 git commit 70 | 을 입력하고 엔터를 누르면 커밋이 완료됩니다. 원하는 만큼 커밋을 자유롭게 만들어보세요. 71 |

72 |
73 |
74 |
75 |

76 | git tag name will create a new tag named "name". 77 | Creating tags just creates a new tag pointing to the currently checked out commit. 78 |

79 |

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

82 |

83 | Type git commit and git tag commands 84 | to your hearts desire until you understand this concept. 85 |

86 |
87 |
88 |
89 |

90 | git branch name을 입력하면 'name'이라는 이름을 가진 브랜치가 만들어집니다. 91 | 브랜치를 만드는 것은 현재 체크아웃된 커밋을 가르키는 이름표를 만드는 것이라고 생각하시면 됩니다. 92 |

93 |

94 | git branch -d name을 입력하면 name 브랜치를 삭제할 수 있습니다. 95 |

96 |

97 | 브랜치 개념에 대해 잘 알 수 있을 때까지 git commitgit branch를 98 | 자유롭게 입력해보세요. 99 |

100 |
101 |
102 |
103 |

104 | git checkout은 다양한 상황에서 쓸 수 있는 명령어인데, 주로 브랜치 사이를 왔다 갔다 할 때 105 | 사용합니다.
git checkout dev을 입력하면 master 브랜치에서 dev 브랜치로 이동할 수 106 | 있죠. 직접 명령어를 입력해 dev 브랜치로 이동한 후, commit을 하나 만들어 어떤 일이 일어나는지 살펴봅시다. 107 |

108 |

109 | 브랜치를 체크아웃 할 수도 있지만, 개별 커밋을 체크아웃하는 것도 가능합니다. 실제로 해봅시다.
110 | 새로운 커밋을 몇 개 만든 후 git checkout bb92e0e을 입력해 ID가 bb92e0e인 커밋을 111 | 체크아웃하고 무슨 일이 일어나는지 살펴봅시다. 112 |

113 |

114 | git commit, git branch, git checkout을 115 | 여러 번 번갈아 입력하면서 세 명령어가 익숙해질 때까지 자유롭게 실습해보세요. 116 |

117 |
118 |
119 |
120 |

121 | git checkout -b namegit branch name과 122 | git checkout name을 하나로 합친 명령어입니다. 123 | name이라는 브랜치가 생성되어이있지 않다는 가정하에, 124 | git checkout -b name을 입력하면 name이라는 브랜치가 만들어지면서 125 | 동시에 해당 브랜치를 체크아웃하게 됩니다. 126 |

127 |
128 |
129 |
130 |

131 | git reset을 사용하면 현재 체크아웃한 브랜치의 원하는 과거 커밋으로 HEAD를 이동시키고, 132 | 그 이후의 커밋들은 모두 버릴 수 있습니다. 원치 않는 커밋을 되돌릴 때 유용한 명령어 입니다. 133 |

134 |

135 | reset 명령어는 보통 --soft, --mixed, --hard 136 | 플래그(flag) 중 하나를 붙여서 사용합니다. soft와 mixed 플래그는 reset으로 되돌린 커밋에 있는 내용을 가지고 무언가를 137 | 다시 하고 싶을 때 주로 사용합니다. 자세한 내용은 138 | 링크 139 | 에서 확인해 보시기 바랍니다. 두 플래그는 시각적으로 표현할 수 없기 때문에 본 튜토리얼에선 hard 플래그만 사용하겠습니다. 140 |

141 |

142 | reset은 HEAD^와 함께 사용할 때가 많습니다. HEAD^는 'HEAD 바로 앞의 커밋'을 의미합니다. 143 | HEAD^^는 'HEAD 앞의 커밋 두 개'를 의미합니다. ^는 원하는 만큼 붙여서 사용할 수 있습니다. 144 |

145 |

146 | remote 리포지토리에 이미 푸시했거나 머지된 커밋은 절대 git reset을 사용해서 버리면 안 됩니다. 147 | 이렇게 하면 local 리포지토리와 remote 리포지토리의 싱크가 맞지 않기 때문입니다. reset, push 등에 대해 정확히 알기 전까지 148 | reset을 함부로 사용하지 마세요! 149 |

150 |
151 |
152 |
153 |

154 | 이미 push되어서 팀원간에 공유되고 있는 커밋을 되돌릴 땐 git reset을 사용할 수 없습니다. 155 | 대신 git revert를 사용해야 합니다. 156 |

157 |

158 | git revert를 입력하면 되돌리려고 하는 커밋 안에 있는 작업을 삭제해주는 새로운 커밋이 159 | 만들어집니다. 160 |

161 |
162 |
163 |
164 |

165 | git merge를 사용하면 두 개의 부모 커밋을 가지는 새로운 커밋이 하나 만들어집니다. 166 | 이 커밋에는 두 브랜치에서 했던 작업의 스냅숏이 포함됩니다. 167 |

168 |

169 | 합치려는 두 개의 브랜치에 분기가 없었다면 'fast-forward' 전략을 사용해 머지합니다.
170 | ff 브랜치를 체크아웃하고 git merge dev를 입력해 직접 확인해봅시다. 171 |

172 |
173 |
174 |
175 |

176 | git rebase를 사용하면 현재 체크아웃한 브랜치의 커밋들이 '베이스(기준)' 역할을 하는 브랜치 177 | 끝으로 '이동'합니다. dev 브랜치를 체크아웃한 후, git rebase master를 178 | 입력해 직접 확인해봅시다. 179 |

180 |

181 | rebase를 사용할 때는 커밋 ID가 변한다는 점에 주의하셔야 합니다. 182 |

183 |

184 | rebase를 하면 기존 커밋과 동일한 내용을 담은 '새로운 커밋'들이 '이동'하고, 기존 커밋들은 185 | 그 자리에 남아있습니다. 따라서 다른 사람에게 공유된 커밋이 들어있는 브랜치를 rebase하면 안됩니다. 186 |

187 |
188 |
189 |
190 |

191 | git fetch will update all of the "remote tracking branches" in your local repository. 192 | Remote tracking branches are tagged in grey. 193 |

194 |
195 |
196 |
197 |

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

202 |

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

207 |
208 |
209 |
210 |

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

214 |

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

219 |
220 |
221 |
222 |

223 | One simple example of the use of git reset is to completely restore your local repository 224 | state to that of the origin.
225 | You can do so by typing git reset origin/master. 226 |

227 |

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

231 |
232 |
233 |
234 |

235 | 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 236 | from the origin server's master branch. To update your local branch, you can do it without having to switch branches! 237 |

238 |

239 | First do a git fetch, then type git rebase origin/master! 240 |

241 |
242 |
243 |
244 |

245 | git branch -d is used to delete branches. 246 | I have pre-created a bunch of branches for you to delete in the playground below. 247 | Have at it. 248 |

249 |
250 |
251 |
252 |

253 | Do whatever you want in this free playground. 254 |

255 |
256 |
257 |
258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 280 | 281 | 286 | 287 | 289 | 290 | 291 | 293 | 294 | 295 | 297 | 298 | 299 | 300 | 301 | 553 | 554 | 555 | -------------------------------------------------------------------------------- /js/controlbox.js: -------------------------------------------------------------------------------- 1 | define(['vendor/yargs-parser', 'd3'], 2 | function(_yargs) { 3 | "use strict"; 4 | 5 | function yargs(str, opts) { 6 | var result = _yargs(str, opts) 7 | 8 | // make every value in result._ a string 9 | result._ = result._.map(function(val) { 10 | return "" + val 11 | }) 12 | 13 | return result 14 | } 15 | 16 | /** 17 | * @class ControlBox 18 | * @constructor 19 | */ 20 | function ControlBox(config) { 21 | this.historyView = config.historyView; 22 | this.originView = config.originView; 23 | this.initialMessage = config.initialMessage || '아래에 git 명령어를 입력해보세요.'; 24 | this._commandHistory = []; 25 | this._currentCommand = -1; 26 | this._tempCommand = ''; 27 | this.rebaseConfig = {}; // to configure branches for rebase 28 | 29 | this.undoHistory = { 30 | pointer: 0, 31 | stack: [ 32 | { hv: this.historyView.serialize() } 33 | ] 34 | } 35 | 36 | this.historyView.on('lock', this.lock.bind(this)) 37 | this.historyView.on('unlock', this.unlock.bind(this)) 38 | } 39 | 40 | ControlBox.prototype = { 41 | lock: function () { 42 | this.locked = true 43 | }, 44 | 45 | unlock: function () { 46 | this.locked = false 47 | this.createUndoSnapshot(true) 48 | }, 49 | 50 | createUndoSnapshot: function (replace) { 51 | var state = this.historyView.serialize() 52 | if (!replace) { 53 | this.undoHistory.pointer++ 54 | this.undoHistory.stack.length = this.undoHistory.pointer 55 | this.undoHistory.stack.push({ hv: state }) 56 | } else { 57 | this.undoHistory.stack[this.undoHistory.pointer] = { hv: state } 58 | } 59 | }, 60 | 61 | render: function(container) { 62 | var cBox = this, 63 | cBoxContainer, log, input; 64 | 65 | cBoxContainer = container.append('div') 66 | .classed('control-box', true); 67 | 68 | 69 | log = cBoxContainer.append('div') 70 | .classed('log', true); 71 | 72 | input = cBoxContainer.append('input') 73 | .attr('type', 'text') 74 | .attr('placeholder', 'git 명령어 입력'); 75 | 76 | input.on('keyup', function() { 77 | var e = d3.event; 78 | 79 | switch (e.keyCode) { 80 | case 13: 81 | if (this.value.trim() === '' || cBox.locked) { 82 | return; 83 | } 84 | 85 | cBox._commandHistory.unshift(this.value); 86 | cBox._tempCommand = ''; 87 | cBox._currentCommand = -1; 88 | cBox.command(this.value); 89 | this.value = ''; 90 | e.stopImmediatePropagation(); 91 | break; 92 | case 38: 93 | var previousCommand = cBox._commandHistory[cBox._currentCommand + 1]; 94 | if (cBox._currentCommand === -1) { 95 | cBox._tempCommand = this.value; 96 | } 97 | 98 | if (typeof previousCommand === 'string') { 99 | cBox._currentCommand += 1; 100 | this.value = previousCommand; 101 | this.value = this.value; // set cursor to end 102 | } 103 | e.stopImmediatePropagation(); 104 | break; 105 | case 40: 106 | var nextCommand = cBox._commandHistory[cBox._currentCommand - 1]; 107 | if (typeof nextCommand === 'string') { 108 | cBox._currentCommand -= 1; 109 | this.value = nextCommand; 110 | this.value = this.value; // set cursor to end 111 | } else { 112 | cBox._currentCommand = -1; 113 | this.value = cBox._tempCommand; 114 | this.value = this.value; // set cursor to end 115 | } 116 | e.stopImmediatePropagation(); 117 | break; 118 | } 119 | }); 120 | 121 | this.container = cBoxContainer; 122 | this.terminalOutput = log; 123 | this.input = input; 124 | 125 | this.info(this.initialMessage); 126 | }, 127 | 128 | destroy: function() { 129 | this.terminalOutput.remove(); 130 | this.input.remove(); 131 | this.container.remove(); 132 | 133 | for (var prop in this) { 134 | if (this.hasOwnProperty(prop)) { 135 | this[prop] = null; 136 | } 137 | } 138 | }, 139 | 140 | _scrollToBottom: function() { 141 | var log = this.terminalOutput.node(); 142 | log.scrollTop = log.scrollHeight; 143 | }, 144 | 145 | command: function(entry) { 146 | if (entry.trim() === '') { 147 | return; 148 | } 149 | 150 | if (entry.trim().toLowerCase() === 'undo') { 151 | var lastId = this.undoHistory.pointer - 1 152 | var lastState = this.undoHistory.stack[lastId] 153 | if (lastState) { 154 | this.historyView.deserialize(lastState.hv) 155 | this.undoHistory.pointer = lastId 156 | } else { 157 | this.error("Nothing to undo") 158 | } 159 | this.terminalOutput.append('div') 160 | .classed('command-entry', true) 161 | .html(entry); 162 | return 163 | } 164 | 165 | if (entry.trim().toLowerCase() === 'redo') { 166 | var lastId = this.undoHistory.pointer + 1 167 | var lastState = this.undoHistory.stack[lastId] 168 | if (lastState) { 169 | this.historyView.deserialize(lastState.hv) 170 | this.undoHistory.pointer = lastId 171 | } else { 172 | this.error("Nothing to redo") 173 | } 174 | this.terminalOutput.append('div') 175 | .classed('command-entry', true) 176 | .html(entry); 177 | return 178 | } 179 | 180 | var split = entry.split(' '); 181 | 182 | this.terminalOutput.append('div') 183 | .classed('command-entry', true) 184 | .html(entry); 185 | 186 | this._scrollToBottom(); 187 | 188 | if (split[0] !== 'git') { 189 | return this.error(); 190 | } 191 | 192 | var method = split[1].replace(/-/g, '_'), 193 | args = split.slice(2), 194 | argsStr = args.join(' ') 195 | 196 | var options = yargs(argsStr) 197 | 198 | try { 199 | if (typeof this[method] === 'function') { 200 | this[method](args, options, argsStr); 201 | this.createUndoSnapshot() 202 | } else { 203 | this.error(); 204 | } 205 | } catch (ex) { 206 | console.error(ex.stack) 207 | var msg = (ex && ex.message) ? ex.message : null; 208 | this.error(msg); 209 | } 210 | }, 211 | 212 | info: function(msg) { 213 | this.terminalOutput.append('div').classed('info', true).html(msg); 214 | this._scrollToBottom(); 215 | }, 216 | 217 | error: function(msg) { 218 | msg = msg || '알 수 없는 명령어입니다.'; 219 | this.terminalOutput.append('div').classed('error', true).html(msg); 220 | this._scrollToBottom(); 221 | }, 222 | 223 | transact: function(action, after) { 224 | var oldCommit = this.historyView.getCommit('HEAD') 225 | var oldBranch = this.historyView.currentBranch 226 | var oldRef = oldBranch || oldCommit.id 227 | action.call(this) 228 | var newCommit = this.historyView.getCommit('HEAD') 229 | var newBranch = this.historyView.currentBranch 230 | var newRef = newBranch || newCommit.id 231 | after.call(this, { 232 | commit: oldCommit, 233 | branch: oldBranch, 234 | ref: oldRef 235 | }, { 236 | commit: newCommit, 237 | branch: newBranch, 238 | ref: newRef 239 | }) 240 | }, 241 | 242 | commit: function(args, opts, cmdStr) { 243 | opts = yargs(cmdStr, { 244 | boolean: ['amend'], 245 | string: ['m'] 246 | }) 247 | var msg = "" 248 | this.transact(function() { 249 | if (opts.amend) { 250 | this.historyView.amendCommit(opts.m || this.historyView.getCommit('head').message) 251 | } else { 252 | this.historyView.commit(null, opts.m); 253 | } 254 | }, function(before, after) { 255 | var reflogMsg = 'commit: ' + msg 256 | this.historyView.addReflogEntry( 257 | 'HEAD', after.commit.id, reflogMsg 258 | ) 259 | if(before.branch) { 260 | this.historyView.addReflogEntry( 261 | before.branch, after.commit.id, reflogMsg 262 | ) 263 | } 264 | }) 265 | }, 266 | 267 | log: function(args) { 268 | if (args.length > 1) { 269 | return this.error("'git log' can take at most one argument in this tool") 270 | } 271 | var logs = this.historyView.getLogEntries(args[0] || 'head').join('
') 272 | this.info(logs) 273 | }, 274 | 275 | rev_parse: function(args) { 276 | args.forEach(function(arg) { 277 | this.info(this.historyView.revparse(arg)) 278 | }, this) 279 | }, 280 | 281 | cherry_pick: function (args, opt, cmdStr) { 282 | opt = yargs(cmdStr, { 283 | number: ['m'] 284 | }) 285 | 286 | if (!opt._.length) { 287 | this.error('You must specify one or more commits to cherry-pick'); 288 | return 289 | } 290 | 291 | if (opt.m !== undefined && isNaN(opt.m)) { 292 | this.error("switch 'm' expects a numerical value"); 293 | return 294 | } 295 | 296 | // FIXME: because `cherryPick` is asynchronous, 297 | // it is responsible for its own reflog entries 298 | this.historyView.cherryPick(opt._, opt.m); 299 | }, 300 | 301 | branch: function(args, options, cmdStr) { 302 | options = yargs(cmdStr, { 303 | alias: { delete: ['d'], remote: ['r'], all: ['a'] }, 304 | boolean: ['a', 'r'] 305 | }) 306 | var branchName = options._[0] 307 | var startPoint = options._[1] || 'head' 308 | 309 | if (options.delete) { 310 | return this.historyView.deleteBranch(options.delete); 311 | } 312 | 313 | if (options.remote) { 314 | return this.info('This command normally displays all of your remote tracking branches.'); 315 | } 316 | 317 | if (options.all) { 318 | return this.info('This command normally displays all of your tracking branches, both remote and local.'); 319 | } 320 | 321 | if (options._[2]) { 322 | return this.error('Incorrect usage - supplied too many arguments') 323 | } 324 | 325 | if (!branchName) { 326 | var branches = this.historyView.getBranchList().join('
') 327 | return this.info(branches) 328 | } 329 | 330 | this.transact(function() { 331 | this.historyView.branch(branchName, startPoint) 332 | }, function(before, after) { 333 | var branchCommit = this.historyView.getCommit(branchName) 334 | var reflogMsg = "branch: created from " + before.ref 335 | this.historyView.addReflogEntry(branchName, branchCommit.id, reflogMsg) 336 | }) 337 | 338 | }, 339 | 340 | checkout: function(args, opts) { 341 | if (opts.b) { 342 | if (opts._[0]) { 343 | this.branch(null, null, opts.b + ' ' + opts._[0]) 344 | } else { 345 | this.branch(null, null, opts.b) 346 | } 347 | } 348 | 349 | var name = opts.b || opts._[0] 350 | 351 | this.transact(function() { 352 | this.historyView.checkout(name); 353 | }, function(before, after) { 354 | this.historyView.addReflogEntry( 355 | 'HEAD', after.commit.id, 356 | 'checkout: moving from ' + before.ref + 357 | ' to ' + name 358 | ) 359 | }) 360 | }, 361 | 362 | tag: function(args) { 363 | if (args.length < 1) { 364 | this.info( 365 | 'You need to give a tag name. ' + 366 | 'Normally if you don\'t give a name, ' + 367 | 'this command will list your local tags on the screen.' 368 | ); 369 | 370 | return; 371 | } 372 | 373 | while (args.length > 0) { 374 | var arg = args.shift(); 375 | 376 | try { 377 | this.historyView.tag(arg); 378 | } catch (err) { 379 | if (err.message.indexOf('already exists') === -1) { 380 | throw new Error(err.message); 381 | } 382 | } 383 | } 384 | }, 385 | 386 | doReset: function (name) { 387 | this.transact(function() { 388 | this.historyView.reset(name); 389 | }, function(before, after) { 390 | var reflogMsg = "reset: moving to " + name 391 | this.historyView.addReflogEntry( 392 | 'HEAD', after.commit.id, reflogMsg 393 | ) 394 | if (before.branch) { 395 | this.historyView.addReflogEntry( 396 | before.branch, after.commit.id, reflogMsg 397 | ) 398 | } 399 | }) 400 | }, 401 | 402 | reset: function(args) { 403 | while (args.length > 0) { 404 | var arg = args.shift(); 405 | 406 | switch (arg) { 407 | case '--soft': 408 | this.info( 409 | '실제 환경에선 --soft 플래그를 붙여서 사용할 수 있는데,' + 410 | '이를 시각화 할 수 있는 방법이 없기 때문에 본 튜토리얼에선 ' + 411 | '--hard 플래그를 붙여서 reset을 실행했다고 가정하고 그 결과를 보여줍니다.' 412 | ); 413 | break; 414 | case '--mixed': 415 | this.info( 416 | '실제 환경에선 --soft 플래그를 붙여서 사용할 수 있는데,' + 417 | '이를 시각화 할 수 있는 방법이 없기 때문에 본 튜토리얼에선 ' + 418 | '--hard 플래그를 붙여서 reset을 실행했다고 가정하고 그 결과를 보여줍니다.' 419 | ); 420 | break; 421 | case '--hard': 422 | this.doReset(args.join(' ')); 423 | args.length = 0; 424 | break; 425 | default: 426 | var remainingArgs = [arg].concat(args); 427 | args.length = 0; 428 | this.info('(--hard를 붙여서 실행했다고 가정)'); 429 | this.doReset(remainingArgs.join(' ')); 430 | } 431 | } 432 | }, 433 | 434 | clean: function(args) { 435 | this.info('Deleting all of your untracked files...'); 436 | }, 437 | 438 | revert: function(args, opt, cmdStr) { 439 | opt = yargs(cmdStr, { 440 | number: ['m'] 441 | }) 442 | 443 | if (!opt._.length) { 444 | this.error('되돌릴 커밋을 명시해주세요.'); 445 | return 446 | } 447 | 448 | if (opt.m !== undefined && isNaN(opt.m)) { 449 | this.error("switch 'm' expects a numerical value"); 450 | return 451 | } 452 | 453 | this.transact(function() { 454 | this.historyView.revert(opt._, opt.m); 455 | }, function(before, after) { 456 | var reflogMsg = 'revert: ' + before.commit.message || before.commit.id 457 | this.historyView.addReflogEntry( 458 | 'HEAD', after.commit.id, reflogMsg 459 | ) 460 | if(before.branch) { 461 | this.historyView.addReflogEntry( 462 | before.branch, after.commit.id, reflogMsg 463 | ) 464 | } 465 | }) 466 | }, 467 | 468 | merge: function(args) { 469 | var noFF = false; 470 | var branch = args[0]; 471 | var result 472 | if (args.length === 2) { 473 | if (args[0] === '--no-ff') { 474 | noFF = true; 475 | branch = args[1]; 476 | } else if (args[1] === '--no-ff') { 477 | noFF = true; 478 | branch = args[0]; 479 | } else { 480 | this.info('This demo only supports the --no-ff switch..'); 481 | } 482 | } 483 | 484 | this.transact(function() { 485 | result = this.historyView.merge(branch, noFF); 486 | 487 | if (result === 'Fast-Forward') { 488 | this.info('fast-forward로 머지하였습니다.'); 489 | } 490 | }, function(before, after) { 491 | var reflogMsg = "merge " + branch + ": " 492 | if (result === 'Fast-Forward') { 493 | reflogMsg += "Fast-forward" 494 | } else { 495 | reflogMsg += "Merge made by the 'recursive' strategy." 496 | } 497 | this.historyView.addReflogEntry( 498 | 'HEAD', after.commit.id, reflogMsg 499 | ) 500 | if (before.branch) { 501 | this.historyView.addReflogEntry( 502 | before.branch, after.commit.id, reflogMsg 503 | ) 504 | } 505 | }) 506 | }, 507 | 508 | rebase: function(args) { 509 | var ref = args.shift(), 510 | result = this.historyView.rebase(ref); 511 | 512 | // FIXME: rebase is async, so manages its own 513 | // reflog entries 514 | if (result === 'Fast-Forward') { 515 | this.info('Fast-forwarded to ' + ref + '.'); 516 | } 517 | }, 518 | 519 | fetch: function() { 520 | if (!this.originView) { 521 | throw new Error('There is no remote server to fetch from.'); 522 | } 523 | 524 | var origin = this.originView, 525 | local = this.historyView, 526 | remotePattern = /^origin\/([^\/]+)$/, 527 | rtb, isRTB, fb, 528 | fetchBranches = {}, 529 | fetchIds = [], // just to make sure we don't fetch the same commit twice 530 | fetchCommits = [], 531 | fetchCommit, 532 | resultMessage = ''; 533 | 534 | // determine which branches to fetch 535 | for (rtb = 0; rtb < local.branches.length; rtb++) { 536 | isRTB = remotePattern.exec(local.branches[rtb]); 537 | if (isRTB) { 538 | fetchBranches[isRTB[1]] = 0; 539 | } 540 | } 541 | 542 | // determine which commits the local repo is missing from the origin 543 | for (fb in fetchBranches) { 544 | if (origin.branches.indexOf(fb) > -1) { 545 | fetchCommit = origin.getCommit(fb); 546 | 547 | var notInLocal = local.getCommit(fetchCommit.id) === null; 548 | while (notInLocal) { 549 | if (fetchIds.indexOf(fetchCommit.id) === -1) { 550 | fetchCommits.unshift(fetchCommit); 551 | fetchIds.unshift(fetchCommit.id); 552 | } 553 | fetchBranches[fb] += 1; 554 | fetchCommit = origin.getCommit(fetchCommit.parent); 555 | notInLocal = local.getCommit(fetchCommit.id) === null; 556 | } 557 | } 558 | } 559 | 560 | // add the fetched commits to the local commit data 561 | for (var fc = 0; fc < fetchCommits.length; fc++) { 562 | fetchCommit = fetchCommits[fc]; 563 | local.commitData.push({ 564 | id: fetchCommit.id, 565 | parent: fetchCommit.parent, 566 | tags: [] 567 | }); 568 | } 569 | 570 | // update the remote tracking branch tag locations 571 | for (fb in fetchBranches) { 572 | if (origin.branches.indexOf(fb) > -1) { 573 | var remoteLoc = origin.getCommit(fb).id; 574 | local.moveTag('origin/' + fb, remoteLoc); 575 | } 576 | 577 | resultMessage += 'Fetched ' + fetchBranches[fb] + ' commits on ' + fb + '.
'; 578 | } 579 | 580 | this.info(resultMessage); 581 | 582 | local.renderCommits(); 583 | }, 584 | 585 | pull: function(args) { 586 | var control = this, 587 | local = this.historyView, 588 | currentBranch = local.currentBranch, 589 | rtBranch = 'origin/' + currentBranch, 590 | isFastForward = false; 591 | 592 | this.fetch(); 593 | 594 | if (!currentBranch) { 595 | throw new Error('You are not currently on a branch.'); 596 | } 597 | 598 | if (local.branches.indexOf(rtBranch) === -1) { 599 | throw new Error('Current branch is not set up for pulling.'); 600 | } 601 | 602 | this.lock() 603 | setTimeout(function() { 604 | try { 605 | if (args[0] === '--rebase' || control.rebaseConfig[currentBranch] === 'true') { 606 | isFastForward = local.rebase(rtBranch) === 'Fast-Forward'; 607 | } else { 608 | isFastForward = local.merge(rtBranch) === 'Fast-Forward'; 609 | } 610 | } catch (error) { 611 | control.error(error.message); 612 | } finally { 613 | this.unlock() 614 | } 615 | 616 | if (isFastForward) { 617 | control.info('Fast-forwarded to ' + rtBranch + '.'); 618 | } 619 | }, 750); 620 | }, 621 | 622 | push: function(args) { 623 | var control = this, 624 | local = this.historyView, 625 | remoteName = args.shift() || 'origin', 626 | remote = this[remoteName + 'View'], 627 | branchArgs = args.pop(), 628 | localRef = local.currentBranch, 629 | remoteRef = local.currentBranch, 630 | localCommit, remoteCommit, 631 | findCommitsToPush, 632 | isCommonCommit, 633 | toPush = []; 634 | 635 | if (remoteName === 'history') { 636 | throw new Error('Sorry, you can\'t have a remote named "history" in this example.'); 637 | } 638 | 639 | if (!remote) { 640 | throw new Error('There is no remote server named "' + remoteName + '".'); 641 | } 642 | 643 | if (branchArgs) { 644 | branchArgs = /^([^:]*)(:?)(.*)$/.exec(branchArgs); 645 | 646 | branchArgs[1] && (localRef = branchArgs[1]); 647 | branchArgs[2] === ':' && (remoteRef = branchArgs[3]); 648 | } 649 | 650 | if (local.branches.indexOf(localRef) === -1) { 651 | throw new Error('Local ref: ' + localRef + ' does not exist.'); 652 | } 653 | 654 | if (!remoteRef) { 655 | throw new Error('No remote branch was specified to push to.'); 656 | } 657 | 658 | localCommit = local.getCommit(localRef); 659 | remoteCommit = remote.getCommit(remoteRef); 660 | 661 | findCommitsToPush = function findCommitsToPush(localCommit) { 662 | var commitToPush, 663 | isCommonCommit = remote.getCommit(localCommit.id) !== null; 664 | 665 | while (!isCommonCommit) { 666 | commitToPush = { 667 | id: localCommit.id, 668 | parent: localCommit.parent, 669 | tags: [] 670 | }; 671 | 672 | if (typeof localCommit.parent2 === 'string') { 673 | commitToPush.parent2 = localCommit.parent2; 674 | findCommitsToPush(local.getCommit(localCommit.parent2)); 675 | } 676 | 677 | toPush.unshift(commitToPush); 678 | localCommit = local.getCommit(localCommit.parent); 679 | isCommonCommit = remote.getCommit(localCommit.id) !== null; 680 | } 681 | }; 682 | 683 | // push to an existing branch on the remote 684 | if (remoteCommit && remote.branches.indexOf(remoteRef) > -1) { 685 | if (!local.isAncestorOf(remoteCommit.id, localCommit.id)) { 686 | throw new Error('Push rejected. Non fast-forward.'); 687 | } 688 | 689 | isCommonCommit = localCommit.id === remoteCommit.id; 690 | 691 | if (isCommonCommit) { 692 | return this.info('Everything up-to-date.'); 693 | } 694 | 695 | findCommitsToPush(localCommit); 696 | 697 | remote.commitData = remote.commitData.concat(toPush); 698 | remote.moveTag(remoteRef, toPush[toPush.length - 1].id); 699 | remote.renderCommits(); 700 | } else { 701 | this.info('Sorry, creating new remote branches is not supported yet.'); 702 | } 703 | }, 704 | 705 | config: function(args) { 706 | var path = args.shift().split('.'); 707 | 708 | if (path[0] === 'branch') { 709 | if (path[2] === 'rebase') { 710 | this.rebase[path[1]] = args.pop(); 711 | } 712 | } 713 | }, 714 | 715 | reflog: function (args) { 716 | var reflogExistsFor = function (ref) { 717 | return this.historyView.logs[ref.toLowerCase()] 718 | }.bind(this) 719 | 720 | var ref = "" 721 | var subcommand = "show" 722 | if (args.length === 0) { 723 | ref = "HEAD" 724 | } else if (args.length === 1) { 725 | ref = args[0].trim() 726 | if (ref === "show" || ref === "expire" || ref === "delete" || ref === "exists") { 727 | subcommand = ref 728 | ref = "HEAD" 729 | } 730 | } else if (args.length === 2) { 731 | subcommand = args[0] 732 | ref = args[1] 733 | } else { 734 | this.error("'git reflog' can take at most two arguments in this tool") 735 | return 736 | } 737 | 738 | if (!ref) { 739 | this.error("No ref specified") 740 | return 741 | } 742 | 743 | if (subcommand === "exists") { 744 | if (reflogExistsFor(ref)) { 745 | this.info("Reflog for ref " + ref + " exists") 746 | } else { 747 | this.error("Reflog for ref " + ref + " does not exist") 748 | } 749 | } else if (subcommand === "show") { 750 | var logs = this.historyView.getReflogEntries(ref) 751 | this.info(logs.join("
")) 752 | } else if (subcommand === "expire" || subcommand === "delete") { 753 | this.info("Real git reflog supports the '" + subcommand + 754 | "' subcommand but this tool only supports 'show' and 'exists'") 755 | } 756 | } 757 | }; 758 | 759 | return ControlBox; 760 | }); 761 | -------------------------------------------------------------------------------- /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 | window.hv = historyView; 22 | 23 | if (args.originData) { 24 | originView = new HistoryView({ 25 | name: name + '-Origin', 26 | width: 300, 27 | height: 225, 28 | commitRadius: 15, 29 | remoteName: 'origin', 30 | commitData: args.originData 31 | }); 32 | 33 | originView.render(playground); 34 | window.ov = originView; 35 | } 36 | 37 | controlBox = new ControlBox({ 38 | historyView: historyView, 39 | originView: originView, 40 | initialMessage: args.initialMessage 41 | }); 42 | window.cb = controlBox; 43 | 44 | controlBox.render(playground); 45 | historyView.render(playground); 46 | 47 | openSandBoxes.push({ 48 | hv: historyView, 49 | cb: controlBox, 50 | container: container 51 | }); 52 | }; 53 | 54 | reset = function() { 55 | for (var i = 0; i < openSandBoxes.length; i++) { 56 | var osb = openSandBoxes[i]; 57 | osb.hv.destroy(); 58 | osb.cb.destroy(); 59 | osb.container.style('display', 'none'); 60 | } 61 | 62 | openSandBoxes.length = 0; 63 | d3.selectAll('a.openswitch').classed('selected', false); 64 | }; 65 | 66 | explainGit = { 67 | HistoryView: HistoryView, 68 | ControlBox: ControlBox, 69 | generateId: HistoryView.generateId, 70 | open: open, 71 | reset: reset 72 | }; 73 | 74 | window.explainGit = explainGit; 75 | 76 | return explainGit; 77 | }); 78 | -------------------------------------------------------------------------------- /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, getUniqueSetItems; 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 | getUniqueSetItems = function(set1, set2) { 223 | var uniqueSet1 = JSON.parse(JSON.stringify(set1)) 224 | var uniqueSet2 = JSON.parse(JSON.stringify(set2)) 225 | for (var id in set1) { 226 | delete uniqueSet2[id] 227 | } 228 | for (var id in set2) { 229 | delete uniqueSet1[id] 230 | } 231 | return [uniqueSet1, uniqueSet2] 232 | }; 233 | 234 | /** 235 | * @class HistoryView 236 | * @constructor 237 | */ 238 | function HistoryView(config) { 239 | var commitData = config.commitData || [], 240 | commit; 241 | 242 | for (var i = 0; i < commitData.length; i++) { 243 | commit = commitData[i]; 244 | !commit.parent && (commit.parent = 'initial'); 245 | !commit.tags && (commit.tags = []); 246 | } 247 | 248 | this.name = config.name || 'UnnamedHistoryView'; 249 | this.commitData = commitData; 250 | 251 | this.branches = ['master']; 252 | this.currentBranch = config.currentBranch || 'master'; 253 | 254 | this.width = config.width; 255 | this.height = config.height || 400; 256 | this.orginalBaseLine = config.baseLine; 257 | this.baseLine = this.height * (config.baseLine || 0.6); 258 | 259 | this.commitRadius = config.commitRadius || 20; 260 | this.pointerMargin = this.commitRadius * 1.3; 261 | 262 | this.isRemote = typeof config.remoteName === 'string'; 263 | this.remoteName = config.remoteName; 264 | 265 | this.logs = {} 266 | 267 | this.initialCommit = { 268 | id: 'initial', 269 | parent: null, 270 | cx: -(this.commitRadius * 2), 271 | cy: this.baseLine 272 | }; 273 | 274 | this.locks = 0 275 | this._eventCallbacks = {} 276 | } 277 | 278 | HistoryView.generateId = function() { 279 | return Math.floor((1 + Math.random()) * 0x10000000).toString(16).substring(1); 280 | }; 281 | 282 | HistoryView.prototype = { 283 | serialize: function () { 284 | var data = { 285 | commitData: this.commitData, 286 | branches: this.branches, 287 | logs: this.logs, 288 | currentBranch: this.currentBranch, 289 | } 290 | 291 | return JSON.stringify(data) 292 | }, 293 | 294 | deserialize: function (data) { 295 | data = JSON.parse(data) 296 | this.commitData = data.commitData 297 | this.branches = data.branches 298 | this.logs = data.logs 299 | this.currentBranch = data.currentBranch 300 | this.renderCommits() 301 | this.renderTags() 302 | }, 303 | 304 | emit: function (event) { 305 | var callbacks = this._eventCallbacks[event] || [] 306 | callbacks.forEach(function(callback) { 307 | try { 308 | callback(event) 309 | } finally { 310 | // nothing 311 | } 312 | }) 313 | }, 314 | 315 | on: function (event, callback) { 316 | var callbacks = this._eventCallbacks[event] || [] 317 | callbacks.push(callback) 318 | this._eventCallbacks[event] = callbacks 319 | 320 | return function () { 321 | var cbs = this._eventCallbacks[event] || [] 322 | var idx = cbs.indexOf(callback) 323 | if (idx > -1) { 324 | cbs.splice(idx, 1) 325 | this._eventCallbacks[event] = cbs 326 | } 327 | }.bind(this) 328 | }, 329 | 330 | lock: function () { 331 | this.locks++ 332 | if (this.locks === 1) { 333 | this.emit('lock') 334 | } 335 | }, 336 | 337 | unlock: function () { 338 | if (this.locks <= 0) { 339 | throw new Error('cannot unlock! not locked') 340 | } 341 | 342 | this.locks-- 343 | if (this.locks === 0) { 344 | this.emit('unlock') 345 | } 346 | }, 347 | 348 | /** 349 | * @method getCommit 350 | * @param ref {String} the id or a tag name that refers to the commit 351 | * @return {Object} the commit datum object 352 | */ 353 | getCommit: function getCommit(ref) { 354 | // Optimization, doesn't seem to break anything 355 | if (!ref) return null; 356 | if (ref.id) return ref 357 | 358 | var commitData = this.commitData, 359 | matchedCommit = null; 360 | 361 | var reflogMatch 362 | if (reflogMatch = ref.match(/^(.*)@\{(\d+)\}(.*)$/)) { 363 | var branchName = reflogMatch[1].toLowerCase() 364 | var count = parseInt(reflogMatch[2], 10) 365 | var rest = reflogMatch[3] 366 | 367 | if (this.logs[branchName] && this.logs[branchName][count]) { 368 | ref = this.logs[branchName][count].destination + rest 369 | } 370 | } 371 | 372 | var parts = /^([^\^\~]+)(.*)$/.exec(ref), 373 | ref = parts[1], 374 | modifier = parts[2]; 375 | 376 | if (ref === 'initial') { 377 | return this.initialCommit; 378 | } 379 | 380 | if (ref.toLowerCase() === 'head') { 381 | ref = 'HEAD'; 382 | } 383 | 384 | var commitsThatStartWith = commitData 385 | .filter(function(c) { 386 | return c.id.indexOf(ref) === 0 387 | }) 388 | 389 | if (commitsThatStartWith.length === 1) { 390 | matchedCommit = commitsThatStartWith[0] 391 | } else if (commitsThatStartWith.length > 1) { 392 | throw new Error("Ref " + ref + " is ambiguous") 393 | } 394 | 395 | for (var i = 0; i < commitData.length; i++) { 396 | var commit = commitData[i]; 397 | if (commit === ref) { 398 | matchedCommit = commit; 399 | break; 400 | } 401 | 402 | if (commit.id === ref) { 403 | matchedCommit = commit; 404 | break; 405 | } 406 | 407 | var matchedTag = function() { 408 | for (var j = 0; j < commit.tags.length; j++) { 409 | var tag = commit.tags[j]; 410 | if (tag === ref) { 411 | matchedCommit = commit; 412 | return true; 413 | } 414 | 415 | if (tag.indexOf('[') === 0 && tag.indexOf(']') === tag.length - 1) { 416 | tag = tag.substring(1, tag.length - 1); 417 | } 418 | if (tag === ref) { 419 | matchedCommit = commit; 420 | return true; 421 | } 422 | } 423 | }(); 424 | if (matchedTag === true) { 425 | break; 426 | } 427 | } 428 | 429 | if (matchedCommit && modifier) { 430 | while (modifier) { 431 | var nextToken = modifier[0] 432 | modifier = modifier.substr(1) 433 | var amountMatch = modifier.match(/^(\d+)(.*)$/), 434 | amount = 1; 435 | 436 | if (amountMatch) { 437 | var amount = ~~amountMatch[1] 438 | } 439 | 440 | if (nextToken === '^') { 441 | if (amount === 0) { 442 | /* do nothing, refers to this commit */ 443 | } else if (amount === 1) { 444 | matchedCommit = this.getCommit(matchedCommit.parent) 445 | } else if (amount === 2) { 446 | matchedCommit = this.getCommit(matchedCommit.parent2) 447 | } else { 448 | matchedCommit = null 449 | } 450 | } else if (nextToken === '~') { 451 | for (var i = 0; i < amount; i++) { 452 | if (matchedCommit && matchedCommit.parent) { 453 | matchedCommit = this.getCommit(matchedCommit.parent) 454 | } 455 | } 456 | } 457 | } 458 | } 459 | 460 | return matchedCommit; 461 | }, 462 | 463 | revparse: function(refspec) { 464 | var commit 465 | if (commit = this.getCommit(refspec)) { 466 | return commit.id 467 | } else { 468 | throw new Error("Cannot find object from refspec " + refspec) 469 | } 470 | }, 471 | 472 | /** 473 | * @method getCircle 474 | * @param ref {String} the id or a tag name that refers to the commit 475 | * @return {d3 Selection} the d3 selected SVG circle 476 | */ 477 | getCircle: function(ref) { 478 | var circle = this.svg.select('#' + this.name + '-' + ref), 479 | commit; 480 | 481 | if (circle && !circle.empty()) { 482 | return circle; 483 | } 484 | 485 | commit = this.getCommit(ref); 486 | 487 | if (!commit) { 488 | return null; 489 | } 490 | 491 | return this.svg.select('#' + this.name + '-' + commit.id); 492 | }, 493 | 494 | getCircles: function() { 495 | return this.svg.selectAll('circle.commit'); 496 | }, 497 | 498 | /** 499 | * @method render 500 | * @param container {String} selector for the container to render the SVG into 501 | */ 502 | render: function(container) { 503 | var svgContainer, svg; 504 | 505 | svgContainer = container.append('div') 506 | .classed('svg-container', true) 507 | .classed('remote-container', this.isRemote); 508 | 509 | svg = svgContainer.append('svg:svg'); 510 | 511 | svg.attr('id', this.name) 512 | .attr('width', this.width) 513 | .attr('height', this.height); 514 | 515 | if (this.isRemote) { 516 | svg.append('svg:text') 517 | .classed('remote-name-display', true) 518 | .text(this.remoteName) 519 | .attr('x', 10) 520 | .attr('y', 25); 521 | } else { 522 | svg.append('svg:text') 523 | .classed('remote-name-display', true) 524 | .text('local 리포지토리') 525 | .attr('x', 10) 526 | .attr('y', 25); 527 | 528 | svg.append('svg:text') 529 | .classed('current-branch-display', true) 530 | .attr('x', 10) 531 | .attr('y', 45); 532 | } 533 | 534 | this.svgContainer = svgContainer; 535 | this.svg = svg; 536 | this.arrowBox = svg.append('svg:g').classed('pointers', true); 537 | this.commitBox = svg.append('svg:g').classed('commits', true); 538 | this.tagBox = svg.append('svg:g').classed('tags', true); 539 | 540 | this.renderCommits(); 541 | 542 | this._setCurrentBranch(this.currentBranch); 543 | }, 544 | 545 | destroy: function() { 546 | this.svg.remove(); 547 | this.svgContainer.remove(); 548 | clearInterval(this.refreshSizeTimer); 549 | 550 | for (var prop in this) { 551 | if (this.hasOwnProperty(prop)) { 552 | this[prop] = null; 553 | } 554 | } 555 | }, 556 | 557 | _calculatePositionData: function() { 558 | for (var i = 0; i < this.commitData.length; i++) { 559 | var commit = this.commitData[i]; 560 | commit.cx = cx(commit, this); 561 | commit.cy = cy(commit, this); 562 | preventOverlap(commit, this); 563 | } 564 | }, 565 | 566 | _resizeSvg: function() { 567 | var ele = document.getElementById(this.svg.node().id); 568 | var container = ele.parentNode; 569 | var currentWidth = ele.offsetWidth; 570 | var newWidth; 571 | 572 | if (ele.getBBox().width > container.offsetWidth) 573 | newWidth = Math.round(ele.getBBox().width); 574 | else 575 | newWidth = container.offsetWidth - 5; 576 | 577 | if (currentWidth != newWidth) { 578 | this.svg.attr('width', newWidth); 579 | container.scrollLeft = container.scrollWidth; 580 | } 581 | }, 582 | 583 | renderCommits: function() { 584 | if (typeof this.height === 'string' && this.height.indexOf('%') >= 0) { 585 | var perc = this.height.substring(0, this.height.length - 1) / 100.0; 586 | var baseLineCalcHeight = Math.round(this.svg.node().parentNode.offsetHeight * perc) - 65; 587 | var newBaseLine = Math.round(baseLineCalcHeight * (this.originalBaseLine || 0.6)); 588 | if (newBaseLine !== this.baseLine) { 589 | this.baseLine = newBaseLine; 590 | this.initialCommit.cy = newBaseLine; 591 | this.svg.attr('height', baseLineCalcHeight); 592 | } 593 | } 594 | this._calculatePositionData(); 595 | this._calculatePositionData(); // do this twice to make sure 596 | this._renderCircles(); 597 | this._renderPointers(); 598 | this._renderMergePointers(); 599 | this._renderIdLabels(); 600 | this._resizeSvg(); 601 | this.currentBranch && this.checkout(this.currentBranch); 602 | }, 603 | 604 | _renderCircles: function() { 605 | var view = this, 606 | existingCircles, 607 | newCircles; 608 | 609 | existingCircles = this.commitBox.selectAll('circle.commit') 610 | .data(this.commitData, function(d) { 611 | return d.id; 612 | }) 613 | .attr('id', function(d) { 614 | return view.name + '-' + d.id; 615 | }) 616 | .classed('reverted', function(d) { 617 | return d.reverted || d.revertSource; 618 | }) 619 | .classed('rebased', function(d) { 620 | return d.rebased || d.rebaseSource; 621 | }) 622 | .classed('logging', function(d) { 623 | return d.logging; 624 | }) 625 | .classed('cherry-picked', function(d) { 626 | return d.cherryPicked || d.cherryPickSource; 627 | }); 628 | 629 | existingCircles.transition() 630 | .duration(500) 631 | .call(fixCirclePosition); 632 | 633 | newCircles = existingCircles.enter() 634 | .append('svg:circle') 635 | .attr('id', function(d) { 636 | return view.name + '-' + d.id; 637 | }) 638 | .classed('commit', true) 639 | .classed('merge-commit', function(d) { 640 | return typeof d.parent2 === 'string'; 641 | }) 642 | .classed('rebased', function(d) { 643 | return d.rebased || d.rebaseSource 644 | }) 645 | .classed('cherry-picked', function(d) { 646 | return d.cherryPicked || d.cherryPickSource; 647 | }) 648 | .call(fixCirclePosition) 649 | .attr('r', 1) 650 | .transition("inflate") 651 | .duration(500) 652 | .attr('r', this.commitRadius) 653 | 654 | existingCircles.exit() 655 | .remove() 656 | 657 | }, 658 | 659 | _renderPointers: function() { 660 | var view = this, 661 | existingPointers, 662 | newPointers; 663 | 664 | existingPointers = this.arrowBox.selectAll('line.commit-pointer') 665 | .data(this.commitData, function(d) { 666 | return d.id; 667 | }) 668 | .attr('id', function(d) { 669 | return view.name + '-' + d.id + '-to-' + d.parent; 670 | }); 671 | 672 | existingPointers.transition() 673 | .duration(500) 674 | .call(fixPointerStartPosition, view) 675 | .call(fixPointerEndPosition, view); 676 | 677 | newPointers = existingPointers.enter() 678 | .append('svg:line') 679 | .attr('id', function(d) { 680 | return view.name + '-' + d.id + '-to-' + d.parent; 681 | }) 682 | .classed('commit-pointer', true) 683 | .call(fixPointerStartPosition, view) 684 | .attr('x2', function() { 685 | return d3.select(this).attr('x1'); 686 | }) 687 | .attr('y2', function() { 688 | return d3.select(this).attr('y1'); 689 | }) 690 | .attr('marker-end', REG_MARKER_END) 691 | .transition() 692 | .duration(500) 693 | .call(fixPointerEndPosition, view); 694 | 695 | existingPointers.exit() 696 | .remove() 697 | }, 698 | 699 | _renderMergePointers: function() { 700 | var view = this, 701 | mergeCommits = [], 702 | existingPointers, newPointers; 703 | 704 | for (var i = 0; i < this.commitData.length; i++) { 705 | var commit = this.commitData[i]; 706 | if (typeof commit.parent2 === 'string') { 707 | mergeCommits.push(commit); 708 | } 709 | } 710 | 711 | existingPointers = this.arrowBox.selectAll('polyline.merge-pointer') 712 | .data(mergeCommits, function(d) { 713 | return d.id; 714 | }) 715 | .attr('id', function(d) { 716 | return view.name + '-' + d.id + '-to-' + d.parent2; 717 | }); 718 | 719 | existingPointers.transition().duration(500) 720 | .attr('points', function(d) { 721 | var p1 = px1(d, view, 'parent2') + ',' + py1(d, view, 'parent2'), 722 | p2 = px2(d, view, 'parent2') + ',' + py2(d, view, 'parent2'); 723 | 724 | return [p1, p2].join(' '); 725 | }); 726 | 727 | newPointers = existingPointers.enter() 728 | .append('svg:polyline') 729 | .attr('id', function(d) { 730 | return view.name + '-' + d.id + '-to-' + d.parent2; 731 | }) 732 | .classed('merge-pointer', true) 733 | .attr('points', function(d) { 734 | var x1 = px1(d, view, 'parent2'), 735 | y1 = py1(d, view, 'parent2'), 736 | p1 = x1 + ',' + y1; 737 | 738 | return [p1, p1].join(' '); 739 | }) 740 | .attr('marker-end', MERGE_MARKER_END) 741 | .transition() 742 | .duration(500) 743 | .attr('points', function(d) { 744 | var points = d3.select(this).attr('points').split(' '), 745 | x2 = px2(d, view, 'parent2'), 746 | y2 = py2(d, view, 'parent2'); 747 | 748 | points[1] = x2 + ',' + y2; 749 | return points.join(' '); 750 | }); 751 | 752 | existingPointers.exit() 753 | .remove() 754 | }, 755 | 756 | _renderIdLabels: function() { 757 | this._renderText('id-label', function(d) { 758 | return d.id + '..'; 759 | }, 14); 760 | this._renderText('message-label', function(d) { 761 | return d.message; 762 | }, 24); 763 | }, 764 | 765 | _renderText: function(className, getText, delta) { 766 | var view = this, 767 | existingTexts, 768 | newtexts; 769 | 770 | existingTexts = this.commitBox.selectAll('text.' + className) 771 | .data(this.commitData, function(d) { 772 | return d.id; 773 | }) 774 | .text(getText); 775 | 776 | existingTexts.transition().call(fixIdPosition, view, delta); 777 | 778 | newtexts = existingTexts.enter() 779 | .insert('svg:text', ':first-child') 780 | .classed(className, true) 781 | .text(getText) 782 | .call(fixIdPosition, view, delta); 783 | 784 | existingTexts.exit() 785 | .remove() 786 | }, 787 | 788 | _parseTagData: function() { 789 | var tagData = [], 790 | i, 791 | headCommit = null; 792 | 793 | for (i = 0; i < this.commitData.length; i++) { 794 | var c = this.commitData[i]; 795 | 796 | for (var t = 0; t < c.tags.length; t++) { 797 | var tagName = c.tags[t]; 798 | if (tagName.toUpperCase() === 'HEAD') { 799 | headCommit = c; 800 | } else if (this.branches.indexOf(tagName) === -1) { 801 | this.branches.push(tagName); 802 | } 803 | 804 | tagData.push({ 805 | name: tagName, 806 | commit: c.id 807 | }); 808 | } 809 | } 810 | 811 | if (!headCommit) { 812 | headCommit = this.getCommit(this.currentBranch); 813 | headCommit.tags.push('HEAD'); 814 | tagData.push({ 815 | name: 'HEAD', 816 | commit: headCommit.id 817 | }); 818 | } 819 | 820 | // find out which commits are not branchless 821 | 822 | 823 | return tagData; 824 | }, 825 | 826 | _markBranchlessCommits: function() { 827 | var branch, commit, parent, parent2, c, b; 828 | 829 | // first mark every commit as branchless 830 | for (c = 0; c < this.commitData.length; c++) { 831 | this.commitData[c].branchless = true; 832 | } 833 | 834 | for (b = 0; b < this.branches.length; b++) { 835 | branch = this.branches[b]; 836 | if (branch.indexOf('/') === -1) { 837 | commit = this.getCommit(branch); 838 | parent = this.getCommit(commit.parent); 839 | parent2 = this.getCommit(commit.parent2); 840 | 841 | commit.branchless = false; 842 | 843 | while (parent) { 844 | parent.branchless = false; 845 | parent = this.getCommit(parent.parent); 846 | } 847 | 848 | // just in case this is a merge commit 849 | while (parent2) { 850 | parent2.branchless = false; 851 | parent2 = this.getCommit(parent2.parent); 852 | } 853 | } 854 | } 855 | 856 | this.svg.selectAll('circle.commit').call(applyBranchlessClass); 857 | this.svg.selectAll('line.commit-pointer').call(applyBranchlessClass); 858 | this.svg.selectAll('polyline.merge-pointer').call(applyBranchlessClass); 859 | }, 860 | 861 | renderTags: function() { 862 | var view = this, 863 | tagData = this._parseTagData(), 864 | existingTags, newTags; 865 | 866 | existingTags = this.tagBox.selectAll('g.branch-tag') 867 | .data(tagData, function(d) { 868 | return d.name; 869 | }); 870 | 871 | existingTags.exit().remove(); 872 | 873 | existingTags.select('rect') 874 | .transition() 875 | .duration(500) 876 | .attr('y', function(d) { 877 | return tagY(d, view); 878 | }) 879 | .attr('x', function(d) { 880 | var commit = view.getCommit(d.commit), 881 | width = Number(d3.select(this).attr('width')); 882 | 883 | return commit.cx - (width / 2); 884 | }); 885 | 886 | existingTags.select('text') 887 | .transition() 888 | .duration(500) 889 | .attr('y', function(d) { 890 | return tagY(d, view) + 14; 891 | }) 892 | .attr('x', function(d) { 893 | var commit = view.getCommit(d.commit); 894 | return commit.cx; 895 | }); 896 | 897 | newTags = existingTags.enter() 898 | .append('g') 899 | .attr('class', function(d) { 900 | var classes = 'branch-tag'; 901 | if (d.name.indexOf('[') === 0 && d.name.indexOf(']') === d.name.length - 1) { 902 | classes += ' git-tag'; 903 | } else if (d.name.indexOf('/') >= 0) { 904 | classes += ' remote-branch'; 905 | } else if (d.name.toUpperCase() === 'HEAD') { 906 | classes += ' head-tag'; 907 | } 908 | return classes; 909 | }); 910 | 911 | newTags.append('svg:rect') 912 | .attr('width', function(d) { 913 | return (d.name.length * 6) + 10; 914 | }) 915 | .attr('height', 20) 916 | .attr('y', function(d) { 917 | return tagY(d, view); 918 | }) 919 | .attr('x', function(d) { 920 | var commit = view.getCommit(d.commit), 921 | width = Number(d3.select(this).attr('width')); 922 | 923 | return commit.cx - (width / 2); 924 | }); 925 | 926 | newTags.append('svg:text') 927 | .text(function(d) { 928 | if (d.name.indexOf('[') === 0 && d.name.indexOf(']') === d.name.length - 1) 929 | return d.name.substring(1, d.name.length - 1); 930 | return d.name; 931 | }) 932 | .attr('y', function(d) { 933 | return tagY(d, view) + 14; 934 | }) 935 | .attr('x', function(d) { 936 | var commit = view.getCommit(d.commit); 937 | return commit.cx; 938 | }); 939 | 940 | existingTags.exit() 941 | .remove() 942 | 943 | this._markBranchlessCommits(); 944 | }, 945 | 946 | _setCurrentBranch: function(branch) { 947 | var display = this.svg.select('text.current-branch-display'), 948 | text = '현재 브랜치: '; 949 | 950 | if (branch && branch.indexOf('/') === -1) { 951 | text += branch; 952 | this.currentBranch = branch; 953 | } else { 954 | text += ' DETACHED HEAD'; 955 | this.currentBranch = null; 956 | } 957 | 958 | display.text(text); 959 | }, 960 | 961 | addReflogEntry: function(ref, destination, reason) { 962 | ref = ref.toLowerCase() 963 | this.logs[ref] = this.logs[ref] || [] 964 | this.logs[ref].unshift({ 965 | destination: destination, 966 | reason: reason 967 | }) 968 | }, 969 | 970 | getReflogEntries: function(ref) { 971 | if (!this.logs[ref.toLowerCase()]) { 972 | throw new Error("no reflog for " + ref) 973 | } 974 | 975 | return this.logs[ref.toLowerCase()].map(function(entry, idx) { 976 | return entry.destination + " " + ref + "@{" + idx + "} " + " " + entry.reason 977 | }) 978 | }, 979 | 980 | moveTag: function(tag, ref) { 981 | var currentLoc = this.getCommit(tag), 982 | newLoc = this.getCommit(ref); 983 | 984 | if (currentLoc) { 985 | currentLoc.tags.splice(currentLoc.tags.indexOf(tag), 1); 986 | } 987 | 988 | newLoc.tags.push(tag); 989 | return this; 990 | }, 991 | 992 | amendCommit: function(message) { 993 | this.commit({parent: this.getCommit('head^').id}, message) 994 | }, 995 | 996 | commit: function(commit, message) { 997 | commit = commit || {}; 998 | 999 | !commit.id && (commit.id = HistoryView.generateId()); 1000 | !commit.tags && (commit.tags = []); 1001 | 1002 | commit.message = message 1003 | if (!commit.parent) { 1004 | commit.parent = this.getCommit('HEAD').id; 1005 | } 1006 | 1007 | this.commitData.push(commit); 1008 | if (this.currentBranch) { 1009 | this.moveTag(this.currentBranch, commit.id); 1010 | } 1011 | 1012 | this.renderCommits(); 1013 | 1014 | if (this.currentBranch) { 1015 | this.checkout(this.currentBranch); 1016 | } else { 1017 | this.checkout(commit.id) 1018 | } 1019 | return this; 1020 | }, 1021 | 1022 | getLogEntries: function(refspec) { 1023 | var ancestors = this.getAncestorSet(refspec) 1024 | delete ancestors.initial 1025 | ancestors[refspec] = -1 1026 | var commitIds = Object.keys(ancestors) 1027 | this.lock() 1028 | this.flashProperty(commitIds, 'logging', function () { 1029 | this.unlock() 1030 | }) 1031 | return commitIds.map(function(commitId) { 1032 | return {commit: this.getCommit(commitId), order: ancestors[commitId]} 1033 | }, this).sort(function(a,b) { 1034 | return a.order - b.order 1035 | }).map(function(commitInfo) { 1036 | var commit = commitInfo.commit 1037 | return commit.id + ' ' + (commit.message || "(no message)") 1038 | }, this) 1039 | }, 1040 | 1041 | setProperty: function(refs, property) { 1042 | refs.forEach(function(ref) { 1043 | this.getCommit(ref)[property] = true 1044 | }, this) 1045 | }, 1046 | 1047 | unsetProperty: function(refs, property) { 1048 | refs.forEach(function(ref) { 1049 | var commit = this.getCommit(ref) 1050 | delete commit[property] 1051 | }, this) 1052 | }, 1053 | 1054 | cherryPick: function(refs, mainline) { 1055 | refs.forEach(function(ref) { 1056 | if (!this.getCommit(ref)) { 1057 | throw new Error("fatal: bad revision '" + ref + "'") 1058 | return 1059 | } 1060 | }, this) 1061 | 1062 | if (mainline) { 1063 | if (mainline > 2 || mainline < 1) { 1064 | throw new Error("Commit " + refs[0] + " does not have parent " + mainline) 1065 | return 1066 | } 1067 | var nonMergeRefs = refs.filter(function(ref) { 1068 | var commit = this.getCommit(ref) 1069 | return !commit.parent || !commit.parent2 1070 | }, this) 1071 | 1072 | if (nonMergeRefs.length) { 1073 | throw new Error('mainline specified but ' + nonMergeRefs[0] + ' is not a merge') 1074 | } 1075 | } else { 1076 | var mergeRefs = refs.filter(function(ref) { 1077 | var commit = this.getCommit(ref) 1078 | return commit.parent && commit.parent2 1079 | }, this) 1080 | 1081 | if (mergeRefs.length) { 1082 | throw new Error('cannot cherry-pick merge commit ' + mergeRefs[0] + ' without specifying a mainline with -m') 1083 | } 1084 | } 1085 | 1086 | if (!mainline) { 1087 | refs.forEach(function(ref) { 1088 | var commit = this.getCommit(ref) 1089 | var message = commit.message || "" 1090 | this.lock() 1091 | this.flashProperty([commit.id], 'cherryPicked', function() { 1092 | this.commit({cherryPickSource: [commit.id]}, message) 1093 | var reflogMessage = "cherry-pick: " + message 1094 | this.addReflogEntry( 1095 | 'HEAD', this.getCommit('HEAD').id, reflogMessage 1096 | ) 1097 | if (this.currentBranch) { 1098 | this.addReflogEntry( 1099 | this.currentBranch, this.getCommit('HEAD').id, reflogMessage 1100 | ) 1101 | } 1102 | this.unlock() 1103 | }) 1104 | }, this) 1105 | } else { 1106 | refs.forEach(function(ref) { 1107 | var commit = this.getCommit(ref) 1108 | var message = commit.message || "" 1109 | var cherryPickSource = this.getNonMainlineCommits(commit.id, mainline) 1110 | 1111 | this.lock() 1112 | this.flashProperty(cherryPickSource, 'cherryPicked', function() { 1113 | this.commit({cherryPickSource: cherryPickSource}, message) 1114 | var reflogMessage = "cherry-pick: " + message 1115 | this.addReflogEntry( 1116 | 'HEAD', this.getCommit('HEAD').id, reflogMessage 1117 | ) 1118 | if (this.currentBranch) { 1119 | this.addReflogEntry( 1120 | this.currentBranch, this.getCommit('HEAD').id, reflogMessage 1121 | ) 1122 | } 1123 | this.unlock() 1124 | }) 1125 | }, this) 1126 | } 1127 | }, 1128 | 1129 | getNonMainlineCommits: function(ref, mainline) { 1130 | if (mainline === 1) mainline = 2 1131 | else if (mainline === 2) mainline = 1 1132 | else throw new Error("Mainline " + mainline + " isn't supported") 1133 | var ancestor1Set = this.getAncestorSet(ref, 1) 1134 | var ancestor2Set = this.getAncestorSet(ref, 2) 1135 | var uniqueAncestors = getUniqueSetItems(ancestor1Set, ancestor2Set) 1136 | return Object.keys(uniqueAncestors[mainline-1]).concat(ref) 1137 | }, 1138 | 1139 | flashProperty: function(refs, property, callback) { 1140 | this.setProperty(refs, property) 1141 | this.renderCommits() 1142 | setTimeout(function() { 1143 | callback && callback.call(this) 1144 | setTimeout(function() { 1145 | this.unsetProperty(refs, property) 1146 | this.renderCommits() 1147 | }.bind(this), 500) 1148 | }.bind(this), 1000) 1149 | }, 1150 | 1151 | getParents: function(ref, mainline) { 1152 | var commit, 1153 | parents = [] 1154 | if (ref.id) { 1155 | commit = ref 1156 | } else { 1157 | commit = this.getCommit(ref) 1158 | } 1159 | if ((!mainline || mainline === 1) && commit.parent) parents.push(commit.parent) 1160 | if ((!mainline || mainline === 2) && commit.parent2) parents.push(commit.parent2) 1161 | return parents 1162 | }, 1163 | 1164 | getAncestorSet: function(ref, mainline) { 1165 | var ancestors = {} 1166 | var i = 1; 1167 | function getAncestor(currentRef, currentMainline) { 1168 | var parents = this.getParents(currentRef, currentMainline) 1169 | parents.forEach(function(parentRef) { 1170 | ancestors[parentRef] = i++ 1171 | getAncestor.call(this, parentRef) 1172 | }, this) 1173 | } 1174 | getAncestor.call(this, ref, mainline) 1175 | return ancestors 1176 | }, 1177 | 1178 | getBranchList: function() { 1179 | return this.commitData.reduce(function(acc, commit) { 1180 | return acc.concat(commit.tags.filter(function(tag) { 1181 | return !tag.match(/^\[.*\]$/) && tag !== 'HEAD' 1182 | })) 1183 | }, []).map(function(tag) { 1184 | if (this.currentBranch && (tag.toLowerCase() === this.currentBranch.toLowerCase())) { 1185 | return '* ' + tag 1186 | } else { 1187 | return '  ' + tag 1188 | } 1189 | }, this) 1190 | }, 1191 | 1192 | branch: function(name, startCommit) { 1193 | if (!name || name.trim() === '') { 1194 | throw new Error('브랜치 이름을 입력하세요.'); 1195 | } 1196 | 1197 | if (name === 'HEAD') { 1198 | throw new Error('HEAD는 브랜치 이름으로 사용할 수 없습니다.'); 1199 | } 1200 | 1201 | if (this.branches.indexOf(name) > -1) { 1202 | throw new Error('이미 존재하는 브랜치입니다.'); 1203 | } 1204 | 1205 | var startPoint = this.getCommit(startCommit || 'head') 1206 | if (!startPoint) { 1207 | throw new Error("fatal: Not a valid object name:'" + startCommit + "'") 1208 | } 1209 | startPoint.tags.push(name); 1210 | this.renderTags(); 1211 | return this; 1212 | }, 1213 | 1214 | tag: function(name) { 1215 | this.branch('[' + name + ']'); 1216 | }, 1217 | 1218 | deleteBranch: function(name) { 1219 | var branchIndex, 1220 | commit; 1221 | 1222 | if (!name || name.trim() === '') { 1223 | throw new Error('브랜치 이름을 입력하세요.'); 1224 | } 1225 | 1226 | if (name === this.currentBranch) { 1227 | throw new Error('Cannot delete the currently checked-out branch.'); 1228 | } 1229 | 1230 | branchIndex = this.branches.indexOf(name); 1231 | 1232 | if (branchIndex === -1) { 1233 | throw new Error('That branch doesn\'t exist.'); 1234 | } 1235 | 1236 | this.branches.splice(branchIndex, 1); 1237 | commit = this.getCommit(name); 1238 | delete this.logs[name] 1239 | branchIndex = commit.tags.indexOf(name); 1240 | 1241 | if (branchIndex > -1) { 1242 | commit.tags.splice(branchIndex, 1); 1243 | } 1244 | 1245 | this.renderTags(); 1246 | }, 1247 | 1248 | checkout: function(ref) { 1249 | var commit = this.getCommit(ref); 1250 | 1251 | if (!commit) { 1252 | throw new Error('Cannot find commit: ' + ref); 1253 | } 1254 | 1255 | var previousHead = this.getCircle('HEAD'), 1256 | newHead = this.getCircle(commit.id); 1257 | 1258 | if (previousHead && !previousHead.empty()) { 1259 | previousHead.classed('checked-out', false); 1260 | } 1261 | 1262 | var isBranch = this.branches.indexOf(ref) !== -1 1263 | this._setCurrentBranch(isBranch ? ref : null); 1264 | this.moveTag('HEAD', commit.id); 1265 | this.renderTags(); 1266 | 1267 | newHead.classed('checked-out', true); 1268 | 1269 | return this; 1270 | }, 1271 | 1272 | reset: function(ref) { 1273 | var commit = this.getCommit(ref); 1274 | 1275 | if (!commit) { 1276 | throw new Error('Cannot find ref: ' + ref); 1277 | } 1278 | 1279 | if (this.currentBranch) { 1280 | this.moveTag(this.currentBranch, commit.id); 1281 | this.checkout(this.currentBranch); 1282 | } else { 1283 | this.checkout(commit.id); 1284 | } 1285 | 1286 | return this; 1287 | }, 1288 | 1289 | revert: function(refs, mainline) { 1290 | refs.forEach(function(ref) { 1291 | if (!this.getCommit(ref)) { 1292 | throw new Error("fatal: bad revision '" + ref + "'") 1293 | return 1294 | } 1295 | }, this) 1296 | 1297 | if (mainline) { 1298 | if (mainline > 2 || mainline < 1) { 1299 | throw new Error("Commit " + refs[0] + " does not have parent " + mainline) 1300 | return 1301 | } 1302 | var nonMergeRefs = refs.filter(function(ref) { 1303 | var commit = this.getCommit(ref) 1304 | return !commit.parent || !commit.parent2 1305 | }, this) 1306 | 1307 | if (nonMergeRefs.length) { 1308 | throw new Error('mainline specified but ' + nonMergeRefs[0] + ' is not a merge') 1309 | } 1310 | } else { 1311 | var mergeRefs = refs.filter(function(ref) { 1312 | var commit = this.getCommit(ref) 1313 | return commit.parent && commit.parent2 1314 | }, this) 1315 | 1316 | if (mergeRefs.length) { 1317 | throw new Error('cannot revert merge commit ' + mergeRefs[0] + ' without specifying a mainline with -m') 1318 | } 1319 | } 1320 | 1321 | if (!mainline) { 1322 | refs.forEach(function(ref) { 1323 | var commit = this.getCommit(ref) 1324 | var message = commit.message || "" 1325 | this.lock() 1326 | this.flashProperty([commit.id], 'reverted', function() { 1327 | this.commit({revertSource: [commit.id]}, "Revert " + commit.id) 1328 | var reflogMessage = "revert: " + message 1329 | this.addReflogEntry( 1330 | 'HEAD', this.getCommit('HEAD').id, reflogMessage 1331 | ) 1332 | if (this.currentBranch) { 1333 | this.addReflogEntry( 1334 | this.currentBranch, this.getCommit('HEAD').id, reflogMessage 1335 | ) 1336 | } 1337 | this.unlock() 1338 | }) 1339 | }, this) 1340 | } else { 1341 | refs.forEach(function(ref) { 1342 | var commit = this.getCommit(ref) 1343 | var message = commit.message || "" 1344 | var revertSource = this.getNonMainlineCommits(commit.id, mainline) 1345 | 1346 | this.lock() 1347 | this.flashProperty(revertSource, 'reverted', function() { 1348 | this.commit({revertSource: revertSource}, "Revert " + commit.id) 1349 | var reflogMessage = "revert: " + message 1350 | this.addReflogEntry( 1351 | 'HEAD', this.getCommit('HEAD').id, reflogMessage 1352 | ) 1353 | if (this.currentBranch) { 1354 | this.addReflogEntry( 1355 | this.currentBranch, this.getCommit('HEAD').id, reflogMessage 1356 | ) 1357 | } 1358 | this.unlock() 1359 | }) 1360 | }, this) 1361 | } 1362 | }, 1363 | 1364 | fastForward: function(ref) { 1365 | var targetCommit = this.getCommit(ref); 1366 | 1367 | if (this.currentBranch) { 1368 | this.moveTag(this.currentBranch, targetCommit.id); 1369 | this.checkout(this.currentBranch); 1370 | } else { 1371 | this.checkout(targetCommit.id); 1372 | } 1373 | }, 1374 | 1375 | isAncestorOf: function(search, start) { 1376 | var startCommit = this.getCommit(start), 1377 | searchCommit = this.getCommit(search) 1378 | 1379 | if (startCommit === searchCommit) { 1380 | return true 1381 | } else { 1382 | var ancestorOnFirstParent = startCommit.parent && this.isAncestorOf(searchCommit.id, startCommit.parent) 1383 | var ancestorOnSecondParent = startCommit.parent2 && this.isAncestorOf(searchCommit.id, startCommit.parent2) 1384 | return ancestorOnFirstParent || ancestorOnSecondParent 1385 | } 1386 | }, 1387 | 1388 | merge: function(ref, noFF) { 1389 | var mergeTarget = this.getCommit(ref), 1390 | currentCommit = this.getCommit('HEAD'); 1391 | if (!mergeTarget) { 1392 | throw new Error('Cannot find ref: ' + ref); 1393 | } 1394 | 1395 | 1396 | if (currentCommit.id === mergeTarget.id) { 1397 | throw new Error('Already up-to-date.'); 1398 | } else if (currentCommit.parent2 === mergeTarget.id) { 1399 | throw new Error('Already up-to-date.'); 1400 | } else if (this.isAncestorOf(mergeTarget, currentCommit)) { 1401 | throw new Error('Already up-to-date.'); 1402 | } else if (noFF === true) { 1403 | var branchStartCommit = this.getCommit(mergeTarget.parent); 1404 | while (branchStartCommit.parent !== currentCommit.id) { 1405 | branchStartCommit = this.getCommit(branchStartCommit.parent); 1406 | } 1407 | 1408 | branchStartCommit.isNoFFBranch = true; 1409 | 1410 | this.commit({ 1411 | parent2: mergeTarget.id, 1412 | isNoFFCommit: true 1413 | }, 'Merge'); 1414 | } else if (this.isAncestorOf(currentCommit.id, mergeTarget.id)) { 1415 | this.fastForward(mergeTarget); 1416 | return 'Fast-Forward'; 1417 | } else { 1418 | this.commit({ 1419 | parent2: mergeTarget.id 1420 | }, 'Merge'); 1421 | } 1422 | }, 1423 | 1424 | rebase: function(ref) { 1425 | var targetCommit = this.getCommit(ref) 1426 | if (!targetCommit) { 1427 | throw new Error("Cannot find commit " + ref) // TODO: better message 1428 | } 1429 | 1430 | this.branch('ORIG_HEAD') 1431 | var origHeadCommit = this.getCommit('ORIG_HEAD') 1432 | var origBranch = this.currentBranch 1433 | var origRef = origBranch || origHeadCommit.id 1434 | 1435 | this.checkout(targetCommit.id) 1436 | this.addReflogEntry( 1437 | 'HEAD', targetCommit.id, 'rebase: checkout ' + ref 1438 | ) 1439 | 1440 | var ancestorsFromTarget = this.getAncestorSet(ref) 1441 | var ancestorsFromBase = this.getAncestorSet(origHeadCommit.id) 1442 | var uniqueAncestors = getUniqueSetItems(ancestorsFromTarget, ancestorsFromBase)[1] 1443 | var commitsToCopy = Object.keys(uniqueAncestors).concat(origHeadCommit.id) 1444 | .sort(function(key1, key2) { 1445 | console.log(key1, uniqueAncestors[key1], key2, uniqueAncestors[key2]) 1446 | return uniqueAncestors[key2] - uniqueAncestors[key1] 1447 | }) 1448 | 1449 | this.lock() 1450 | setTimeout(function() { 1451 | this.flashProperty(commitsToCopy, 'rebased', function() { 1452 | commitsToCopy.forEach(function(ref) { 1453 | this.commit({rebased: true, rebaseSource: ref}, this.getCommit(ref).message) 1454 | }, this) 1455 | var newHeadCommit = this.getCommit('HEAD') 1456 | setTimeout(function() { 1457 | this.deleteBranch('ORIG_HEAD') 1458 | if (origBranch) { 1459 | this.moveTag(origBranch, newHeadCommit.id) 1460 | this.reset(origBranch) 1461 | this._setCurrentBranch(origBranch) 1462 | this.addReflogEntry( 1463 | 'HEAD', targetCommit.id, 'rebase finished: returning to resf/heads/' + origBranch 1464 | ) 1465 | this.addReflogEntry( 1466 | origBranch, newHeadCommit.id, 'rebase finished: refs/heads/' + 1467 | origBranch + ' onto ' + targetCommit.id 1468 | ) 1469 | } 1470 | this.unsetProperty(commitsToCopy, 'rebased') 1471 | this.unlock() 1472 | }.bind(this), 1000) 1473 | }) 1474 | }.bind(this), 1000) 1475 | } 1476 | }; 1477 | 1478 | return HistoryView; 1479 | }); 1480 | -------------------------------------------------------------------------------- /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 | }); 56 | -------------------------------------------------------------------------------- /js/vendor/yargs-parser.js: -------------------------------------------------------------------------------- 1 | !function(n){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=n();else if("function"==typeof define&&define.amd)define([],n);else{var t;"undefined"!=typeof window?t=window:"undefined"!=typeof global?t=global:"undefined"!=typeof self&&(t=self),t.yargsParser=n()}}(function(){return function n(t,e,r){function o(c,u){if(!e[c]){if(!t[c]){var a="function"==typeof require&&require;if(!u&&a)return a(c,!0);if(i)return i(c,!0);var s=new Error("Cannot find module '"+c+"'");throw s.code="MODULE_NOT_FOUND",s}var f=e[c]={exports:{}};t[c][0].call(f.exports,function(n){var e=t[c][1][n];return o(e?e:n)},f,f.exports,n,t,e,r)}return e[c].exports}for(var i="function"==typeof require&&require,c=0;c=0;r--){var o=n[r];"."===o?n.splice(r,1):".."===o?(n.splice(r,1),e++):e&&(n.splice(r,1),e--)}if(t)for(;e--;e)n.unshift("..");return n}function r(n,t){if(n.filter)return n.filter(t);for(var e=[],r=0;r=-1&&!o;i--){var c=i>=0?arguments[i]:n.cwd();if("string"!=typeof c)throw new TypeError("Arguments to path.resolve must be strings");c&&(e=c+"/"+e,o="/"===c.charAt(0))}return e=t(r(e.split("/"),function(n){return!!n}),!o).join("/"),(o?"/":"")+e||"."},e.normalize=function(n){var o=e.isAbsolute(n),i="/"===c(n,-1);return n=t(r(n.split("/"),function(n){return!!n}),!o).join("/"),n||o||(n="."),n&&i&&(n+="/"),(o?"/":"")+n},e.isAbsolute=function(n){return"/"===n.charAt(0)},e.join=function(){var n=Array.prototype.slice.call(arguments,0);return e.normalize(r(n,function(n,t){if("string"!=typeof n)throw new TypeError("Arguments to path.join must be strings");return n}).join("/"))},e.relative=function(n,t){function r(n){for(var t=0;t=0&&""===n[e];e--);return t>e?[]:n.slice(t,e-t+1)}n=e.resolve(n).substr(1),t=e.resolve(t).substr(1);for(var o=r(n.split("/")),i=r(t.split("/")),c=Math.min(o.length,i.length),u=c,a=0;c>a;a++)if(o[a]!==i[a]){u=a;break}for(var s=[],a=u;at&&(t=n.length+t),n.substr(t,e)}}).call(this,n("_process"))},{_process:3}],3:[function(n,t,e){function r(){}var o=t.exports={};o.nextTick=function(){var n="undefined"!=typeof window&&window.setImmediate,t="undefined"!=typeof window&&window.MutationObserver,e="undefined"!=typeof window&&window.postMessage&&window.addEventListener;if(n)return function(n){return window.setImmediate(n)};var r=[];if(t){var o=document.createElement("div"),i=new MutationObserver(function(){var n=r.slice();r.length=0,n.forEach(function(n){n()})});return i.observe(o,{attributes:!0}),function(n){r.length||o.setAttribute("yes","no"),r.push(n)}}return e?(window.addEventListener("message",function(n){var t=n.source;if((t===window||null===t)&&"process-tick"===n.data&&(n.stopPropagation(),r.length>0)){var e=r.shift();e()}},!0),function(n){r.push(n),window.postMessage("process-tick","*")}):function(n){setTimeout(n,0)}}(),o.title="browser",o.browser=!0,o.env={},o.argv=[],o.on=r,o.addListener=r,o.once=r,o.off=r,o.removeListener=r,o.removeAllListeners=r,o.emit=r,o.binding=function(n){throw new Error("process.binding is not supported")},o.cwd=function(){return"/"},o.chdir=function(n){throw new Error("process.chdir is not supported")}},{}],4:[function(n,t,e){t.exports=function(n){return n&&"object"==typeof n&&"function"==typeof n.copy&&"function"==typeof n.fill&&"function"==typeof n.readUInt8}},{}],5:[function(n,t,e){(function(t,r){function o(n,t){var r={seen:[],stylize:c};return arguments.length>=3&&(r.depth=arguments[2]),arguments.length>=4&&(r.colors=arguments[3]),y(t)?r.showHidden=t:t&&e._extend(r,t),j(r.showHidden)&&(r.showHidden=!1),j(r.depth)&&(r.depth=2),j(r.colors)&&(r.colors=!1),j(r.customInspect)&&(r.customInspect=!0),r.colors&&(r.stylize=i),a(r,n,r.depth)}function i(n,t){var e=o.styles[t];return e?"["+o.colors[e][0]+"m"+n+"["+o.colors[e][1]+"m":n}function c(n,t){return n}function u(n){var t={};return n.forEach(function(n,e){t[n]=!0}),t}function a(n,t,r){if(n.customInspect&&t&&z(t.inspect)&&t.inspect!==e.inspect&&(!t.constructor||t.constructor.prototype!==t)){var o=t.inspect(r,n);return m(o)||(o=a(n,o,r)),o}var i=s(n,t);if(i)return i;var c=Object.keys(t),y=u(c);if(n.showHidden&&(c=Object.getOwnPropertyNames(t)),A(t)&&(c.indexOf("message")>=0||c.indexOf("description")>=0))return f(t);if(0===c.length){if(z(t)){var d=t.name?": "+t.name:"";return n.stylize("[Function"+d+"]","special")}if(E(t))return n.stylize(RegExp.prototype.toString.call(t),"regexp");if(x(t))return n.stylize(Date.prototype.toString.call(t),"date");if(A(t))return f(t)}var v="",b=!1,w=["{","}"];if(h(t)&&(b=!0,w=["[","]"]),z(t)){var j=t.name?": "+t.name:"";v=" [Function"+j+"]"}if(E(t)&&(v=" "+RegExp.prototype.toString.call(t)),x(t)&&(v=" "+Date.prototype.toUTCString.call(t)),A(t)&&(v=" "+f(t)),0===c.length&&(!b||0==t.length))return w[0]+v+w[1];if(0>r)return E(t)?n.stylize(RegExp.prototype.toString.call(t),"regexp"):n.stylize("[Object]","special");n.seen.push(t);var O;return O=b?l(n,t,r,y,c):c.map(function(e){return p(n,t,r,y,e,b)}),n.seen.pop(),g(O,v,w)}function s(n,t){if(j(t))return n.stylize("undefined","undefined");if(m(t)){var e="'"+JSON.stringify(t).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return n.stylize(e,"string")}return b(t)?n.stylize(""+t,"number"):y(t)?n.stylize(""+t,"boolean"):d(t)?n.stylize("null","null"):void 0}function f(n){return"["+Error.prototype.toString.call(n)+"]"}function l(n,t,e,r,o){for(var i=[],c=0,u=t.length;u>c;++c)$(t,String(c))?i.push(p(n,t,e,r,String(c),!0)):i.push("");return o.forEach(function(o){o.match(/^\d+$/)||i.push(p(n,t,e,r,o,!0))}),i}function p(n,t,e,r,o,i){var c,u,s;if(s=Object.getOwnPropertyDescriptor(t,o)||{value:t[o]},s.get?u=s.set?n.stylize("[Getter/Setter]","special"):n.stylize("[Getter]","special"):s.set&&(u=n.stylize("[Setter]","special")),$(r,o)||(c="["+o+"]"),u||(n.seen.indexOf(s.value)<0?(u=d(e)?a(n,s.value,null):a(n,s.value,e-1),u.indexOf("\n")>-1&&(u=i?u.split("\n").map(function(n){return" "+n}).join("\n").substr(2):"\n"+u.split("\n").map(function(n){return" "+n}).join("\n"))):u=n.stylize("[Circular]","special")),j(c)){if(i&&o.match(/^\d+$/))return u;c=JSON.stringify(""+o),c.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(c=c.substr(1,c.length-2),c=n.stylize(c,"name")):(c=c.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),c=n.stylize(c,"string"))}return c+": "+u}function g(n,t,e){var r=0,o=n.reduce(function(n,t){return r++,t.indexOf("\n")>=0&&r++,n+t.replace(/\u001b\[\d\d?m/g,"").length+1},0);return o>60?e[0]+(""===t?"":t+"\n ")+" "+n.join(",\n ")+" "+e[1]:e[0]+t+" "+n.join(", ")+" "+e[1]}function h(n){return Array.isArray(n)}function y(n){return"boolean"==typeof n}function d(n){return null===n}function v(n){return null==n}function b(n){return"number"==typeof n}function m(n){return"string"==typeof n}function w(n){return"symbol"==typeof n}function j(n){return void 0===n}function E(n){return O(n)&&"[object RegExp]"===_(n)}function O(n){return"object"==typeof n&&null!==n}function x(n){return O(n)&&"[object Date]"===_(n)}function A(n){return O(n)&&("[object Error]"===_(n)||n instanceof Error)}function z(n){return"function"==typeof n}function S(n){return null===n||"boolean"==typeof n||"number"==typeof n||"string"==typeof n||"symbol"==typeof n||"undefined"==typeof n}function _(n){return Object.prototype.toString.call(n)}function k(n){return 10>n?"0"+n.toString(10):n.toString(10)}function N(){var n=new Date,t=[k(n.getHours()),k(n.getMinutes()),k(n.getSeconds())].join(":");return[n.getDate(),F[n.getMonth()],t].join(" ")}function $(n,t){return Object.prototype.hasOwnProperty.call(n,t)}var D=/%[sdj%]/g;e.format=function(n){if(!m(n)){for(var t=[],e=0;e=i)return n;switch(n){case"%s":return String(r[e++]);case"%d":return Number(r[e++]);case"%j":try{return JSON.stringify(r[e++])}catch(t){return"[Circular]"}default:return n}}),u=r[e];i>e;u=r[++e])c+=d(u)||!O(u)?" "+u:" "+o(u);return c},e.deprecate=function(n,o){function i(){if(!c){if(t.throwDeprecation)throw new Error(o);t.traceDeprecation?console.trace(o):console.error(o),c=!0}return n.apply(this,arguments)}if(j(r.process))return function(){return e.deprecate(n,o).apply(this,arguments)};if(t.noDeprecation===!0)return n;var c=!1;return i};var C,B={};e.debuglog=function(n){if(j(C)&&(C=t.env.NODE_DEBUG||""),n=n.toUpperCase(),!B[n])if(new RegExp("\\b"+n+"\\b","i").test(C)){var r=t.pid;B[n]=function(){var t=e.format.apply(e,arguments);console.error("%s %d: %s",n,r,t)}}else B[n]=function(){};return B[n]},e.inspect=o,o.colors={bold:[1,22],italic:[3,23],underline:[4,24],inverse:[7,27],white:[37,39],grey:[90,39],black:[30,39],blue:[34,39],cyan:[36,39],green:[32,39],magenta:[35,39],red:[31,39],yellow:[33,39]},o.styles={special:"cyan",number:"yellow","boolean":"yellow",undefined:"grey","null":"bold",string:"green",date:"magenta",regexp:"red"},e.isArray=h,e.isBoolean=y,e.isNull=d,e.isNullOrUndefined=v,e.isNumber=b,e.isString=m,e.isSymbol=w,e.isUndefined=j,e.isRegExp=E,e.isObject=O,e.isDate=x,e.isError=A,e.isFunction=z,e.isPrimitive=S,e.isBuffer=n("./support/isBuffer");var F=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];e.log=function(){console.log("%s - %s",N(),e.format.apply(e,arguments))},e.inherits=n("inherits"),e._extend=function(n,t){if(!t||!O(t))return n;for(var e=Object.keys(t),r=e.length;r--;)n[e[r]]=t[e[r]];return n}}).call(this,n("_process"),"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./support/isBuffer":4,_process:3,inherits:1}],6:[function(n,t,e){(function(e){function r(t,r){function c(n,t,e){var r=E(t,L.nargs);e.length-(n+1)o;o++)g(t,e[o]);return n+r}function p(n,t,e){for(var r=n+1,o=n+1;o1&&N["dot-notation"]&&(L.aliases[o[0]]||[]).forEach(function(n){n=n.split(".");var t=[].concat(o);t.shift(),n=n.concat(t),w(M,n,r)}),E(n,L.normalize)&&!E(n,L.arrays)){var c=[n].concat(L.aliases[n]||[]);c.forEach(function(n){M.__defineSetter__(n,function(n){t=s.normalize(n)}),M.__defineGetter__(n,function(){return"string"==typeof t?s.normalize(t):t})})}}function h(t){var r={};b(r,L.aliases,$),Object.keys(L.configs).forEach(function(o){var i=t[o]||r[o];if(i)try{var c=null,u=s.resolve(e.cwd(),i);if("function"==typeof L.configs[o]){try{c=L.configs[o](u)}catch(a){c=a}if(c instanceof Error)return void(I=c)}else c=n(u);y(c)}catch(f){t[o]&&(I=Error(F("Invalid JSON config file: %s",i)))}})}function y(n,t){Object.keys(n).forEach(function(e){var r=n[e],o=t?t+"."+e:e;"[object Object]"===Object.prototype.toString.call(r)?y(r,o):m(M,o.split("."))&&!L.defaulted[o]||g(o,r)})}function d(){"undefined"!=typeof D&&D.forEach(function(n){y(n)})}function v(n,t){if("undefined"!=typeof C){var r="string"==typeof C?C:"";Object.keys(e.env).forEach(function(o){if(""===r||0===o.lastIndexOf(r,0)){var i=o.split("__").map(function(n,t){return 0===t&&(n=n.substring(r.length)),a(n)});!(t&&L.configs[i.join(".")]||!t)||m(n,i)&&!L.defaulted[i.join(".")]||g(i.join("."),e.env[o])}})}}function b(n,t,e){Object.keys(e).forEach(function(r){m(n,r.split("."))||(w(n,r.split("."),e[r]),(t[r]||[]).forEach(function(t){m(n,t.split("."))||w(n,t.split("."),e[r])}))})}function m(n,t){var e=n;N["dot-notation"]||(t=[t.join(".")]),t.slice(0,-1).forEach(function(n){e=e[n]||{}});var r=t[t.length-1];return"object"!=typeof e?!1:r in e}function w(n,t,e){var r=n;N["dot-notation"]||(t=[t.join(".")]),t.slice(0,-1).forEach(function(n){void 0===r[n]&&(r[n]={}),r=r[n]});var o=t[t.length-1];e===i?r[o]=i(r[o]):void 0===r[o]&&E(o,L.arrays)?r[o]=Array.isArray(e)?e:[e]:void 0===r[o]||E(o,L.bools)?r[o]=e:Array.isArray(r[o])?r[o].push(e):r[o]=[r[o],e]}function j(){Array.prototype.slice.call(arguments).forEach(function(n){Object.keys(n||{}).forEach(function(n){L.aliases[n]||(L.aliases[n]=[].concat(k[n]||[]),L.aliases[n].concat(n).forEach(function(t){if(/-/.test(t)&&N["camel-case-expansion"]){var e=a(t);L.aliases[n].push(e),B[e]=!0}}),L.aliases[n].forEach(function(t){L.aliases[t]=[n].concat(L.aliases[n].filter(function(n){return t!==n}))}))})})}function E(n,t){var e=!1,r=[].concat(L.aliases[n]||[],n);return r.forEach(function(n){t[n]&&(e=t[n])}),e}function O(n){[].concat(L.aliases[n]||[],n).forEach(function(n){L.defaulted[n]=!0})}function x(n){[].concat(L.aliases[n]||[],n).forEach(function(n){delete L.defaulted[n]})}function A(n){var t={"boolean":!0,string:"",number:void 0,array:[]};return t[n]}function z(n,t){var e="boolean";return t.strings&&t.strings[n]?e="string":t.numbers&&t.numbers[n]?e="number":t.arrays&&t.arrays[n]&&(e="array"),e}function S(n){return N["parse-numbers"]?"number"==typeof n?!0:/^0x[0-9a-f]+$/i.test(n)?!0:/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(n):!1}function _(n){return void 0===n}r||(r={}),t=f(t);var k=o(r.alias||{}),N=u({},{"short-option-groups":!0,"camel-case-expansion":!0,"dot-notation":!0,"parse-numbers":!0,"boolean-negation":!0},r.configuration),$=r["default"]||{},D=r.configObjects||[],C=r.envPrefix,B={},F=r.__||function(n){return l.format.apply(l,Array.prototype.slice.call(arguments))},I=null,L={aliases:{},arrays:{},bools:{},strings:{},numbers:{},counts:{},normalize:{},configs:{},defaulted:{},nargs:{}};[].concat(r.array).filter(Boolean).forEach(function(n){L.arrays[n]=!0}),[].concat(r["boolean"]).filter(Boolean).forEach(function(n){L.bools[n]=!0}),[].concat(r.string).filter(Boolean).forEach(function(n){L.strings[n]=!0}),[].concat(r.number).filter(Boolean).forEach(function(n){L.numbers[n]=!0}),[].concat(r.count).filter(Boolean).forEach(function(n){L.counts[n]=!0}),[].concat(r.normalize).filter(Boolean).forEach(function(n){L.normalize[n]=!0}),Object.keys(r.narg||{}).forEach(function(n){L.nargs[n]=r.narg[n]}),Array.isArray(r.config)||"string"==typeof r.config?[].concat(r.config).filter(Boolean).forEach(function(n){L.configs[n]=!0}):Object.keys(r.config||{}).forEach(function(n){L.configs[n]=r.config[n]}),j(r.key,k,r["default"],L.arrays),Object.keys($).forEach(function(n){(L.aliases[n]||[]).forEach(function(t){$[t]=$[n]})});var M={_:[]};Object.keys(L.bools).forEach(function(n){g(n,n in $?$[n]:!1),O(n)});var U=[];-1!==t.indexOf("--")&&(U=t.slice(t.indexOf("--")+1),t=t.slice(0,t.indexOf("--")));for(var P=0;PP+1?(t.splice(P+1,0,R[2]),P=p(P,R[1],t)):g(R[1],R[2]);else if(Z.match(/^--no-.+/)&&N["boolean-negation"])J=Z.match(/^--no-(.+)/)[1],g(J,!1);else if(Z.match(/^--.+/)||!N["short-option-groups"]&&Z.match(/^-.+/))J=Z.match(/^--?(.+)/)[1],E(J,L.nargs)?P=c(P,J,t):E(J,L.arrays)&&t.length>P+1?P=p(P,J,t):(H=t[P+1],void 0===H||H.match(/^-/)||E(J,L.bools)||E(J,L.counts)?/^(true|false)$/.test(H)?(g(J,H),P++):g(J,A(z(J,L))):(g(J,H),P++));else if(Z.match(/^-.\..+=/))R=Z.match(/^-([^=]+)=([\s\S]*)$/),g(R[1],R[2]);else if(Z.match(/^-.\..+/))H=t[P+1],J=Z.match(/^-(.\..+)/)[1],void 0===H||H.match(/^-/)||E(J,L.bools)||E(J,L.counts)?g(J,A(z(J,L))):(g(J,H),P++);else if(Z.match(/^-[^-]+/)){T=Z.slice(1,-1).split(""),G=!1;for(var W=0;WP+1?(t.splice(P+1,0,q),P=p(P,J,t)):g(J,q),G=!0;break}if("-"!==H){if(/[A-Za-z]/.test(T[W])&&/-?\d+(\.\d*)?(e-?\d+)?$/.test(H)){g(T[W],H),G=!0;break}if(T[W+1]&&T[W+1].match(/\W/)){g(T[W],H),G=!0;break}g(T[W],A(z(T[W],L)))}else g(T[W],H)}J=Z.slice(-1)[0],G||"-"===J||(E(J,L.nargs)?P=c(P,J,t):E(J,L.arrays)&&t.length>P+1?P=p(P,J,t):(H=t[P+1],void 0===H||/^(-|--)[^-]/.test(H)||E(J,L.bools)||E(J,L.counts)?/^(true|false)$/.test(H)?(g(J,H),P++):g(J,A(z(J,L))):(g(J,H),P++)))}else M._.push(L.strings._||!S(Z)?Z:Number(Z))}return v(M,!0),h(M),d(),v(M,!1),b(M,L.aliases,$),Object.keys(L.counts).forEach(function(n){g(n,$[n])}),U.forEach(function(n){M._.push(n)}),{argv:M,error:I,aliases:L.aliases,newAliases:B,configuration:N}}function o(n){var t=[],e=!0,r={};for(Object.keys(n).forEach(function(e){t.push([].concat(n[e],e))});e;){e=!1;for(var o=0;o-1&&n%1==0&&t>n}function o(n,t,e){var r=n[t];E.call(n,t)&&f(r,e)&&(void 0!==e||t in n)||(n[t]=e)}function i(n){return function(t){return null==t?void 0:t[n]}}function c(n,t,e,r){e||(e={});for(var i=-1,c=t.length;++i1?e[o-1]:void 0,c=o>2?e[2]:void 0;for(i="function"==typeof i?(o--,i):void 0,c&&a(e[0],e[1],c)&&(i=3>o?void 0:i,o=1),t=Object(t);++r-1&&n%1==0&&v>=n}function h(n){var t=typeof n;return!!n&&("object"==t||"function"==t)}var y=n("lodash.keys"),d=n("lodash.rest"),v=9007199254740991,b="[object Function]",m="[object GeneratorFunction]",w=/^(?:0|[1-9]\d*)$/,j=Object.prototype,E=j.hasOwnProperty,O=j.toString,x=j.propertyIsEnumerable,A=!x.call({valueOf:1},"valueOf"),z=i("length"),S=u(function(n,t){if(A||s(t)||l(t))return void c(t,y(t),n);for(var e in t)E.call(t,e)&&o(n,e,t[e])});t.exports=S},{"lodash.keys":10,"lodash.rest":11}],10:[function(n,t,e){function r(n,t){for(var e=-1,r=Array(n);++e-1&&n%1==0&&t>n}function i(n,t){return S.call(n,t)||"object"==typeof n&&t in n&&null===a(n)}function c(n){return $(Object(n))}function u(n){return function(t){return null==t?void 0:t[n]}}function a(n){return N(Object(n))}function s(n){var t=n?n.length:void 0;return y(t)&&(C(n)||b(n)||l(n))?r(t,String):null}function f(n){var t=n&&n.constructor,e="function"==typeof t&&t.prototype||z;return n===e}function l(n){return g(n)&&S.call(n,"callee")&&(!k.call(n,"callee")||_.call(n)==j)}function p(n){return null!=n&&y(D(n))&&!h(n)}function g(n){return v(n)&&p(n)}function h(n){var t=d(n)?_.call(n):"";return t==E||t==O}function y(n){return"number"==typeof n&&n>-1&&n%1==0&&w>=n}function d(n){var t=typeof n;return!!n&&("object"==t||"function"==t)}function v(n){return!!n&&"object"==typeof n}function b(n){return"string"==typeof n||!C(n)&&v(n)&&_.call(n)==x}function m(n){var t=f(n);if(!t&&!p(n))return c(n);var e=s(n),r=!!e,u=e||[],a=u.length;for(var l in n)!i(n,l)||r&&("length"==l||o(l,a))||t&&"constructor"==l||u.push(l);return u}var w=9007199254740991,j="[object Arguments]",E="[object Function]",O="[object GeneratorFunction]",x="[object String]",A=/^(?:0|[1-9]\d*)$/,z=Object.prototype,S=z.hasOwnProperty,_=z.toString,k=z.propertyIsEnumerable,N=Object.getPrototypeOf,$=Object.keys,D=u("length"),C=Array.isArray;t.exports=m},{}],11:[function(n,t,e){function r(n,t,e){var r=e.length;switch(r){case 0:return n.call(t);case 1:return n.call(t,e[0]);case 2:return n.call(t,e[0],e[1]);case 3:return n.call(t,e[0],e[1],e[2])}return n.apply(t,e)}function o(n,t){if("function"!=typeof n)throw new TypeError(l);return t=A(void 0===t?n.length-1:s(t),0),function(){for(var e=arguments,o=-1,i=A(e.length-t,0),c=Array(i);++on?-1:1;return t*g}var e=n%1;return n===n?e?n-e:n:0}function f(n){if("number"==typeof n)return n;if(a(n))return h;if(c(n)){var t=i(n.valueOf)?n.valueOf():n;n=c(t)?t+"":t}if("string"!=typeof n)return 0===n?n:+n;n=n.replace(b,"");var e=w.test(n);return e||j.test(n)?E(n.slice(2),e?2:8):m.test(n)?h:+n}var l="Expected a function",p=1/0,g=1.7976931348623157e308,h=NaN,y="[object Function]",d="[object GeneratorFunction]",v="[object Symbol]",b=/^\s+|\s+$/g,m=/^[-+]0x[0-9a-f]+$/i,w=/^0b[01]+$/i,j=/^0o[0-7]+$/i,E=parseInt,O=Object.prototype,x=O.toString,A=Math.max;t.exports=o},{}]},{},[6])(6)}); -------------------------------------------------------------------------------- /memtest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Explain Git with D3 6 | 7 | 8 | 9 | 10 | 11 |

Memtest Page

12 |

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

13 | 14 |

explain git memtest

15 |
16 |

17 | Create and destroy many git history views and control boxes to find memory leaks. 18 |

19 |
20 |

Start Test

21 | 45 |

Back to Home

46 | 47 | 48 | 49 | --------------------------------------------------------------------------------