nitdoc: move css files to share/css/ dir
[nit.git] / share / nitdoc / js / plugins / github.js
1 /* This file is part of NIT ( http://www.nitlanguage.org ).
2
3 Licensed under the Apache License, Version 2.0 (the "License");
4 you may not use this file except in compliance with the License.
5 You may obtain a copy of the License at
6
7 http://www.apache.org/licenses/LICENSE-2.0
8
9 Unless required by applicable law or agreed to in writing, software
10 distributed under the License is distributed on an "AS IS" BASIS,
11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 See the License for the specific language governing permissions and
13 limitations under the License.
14
15 Documentation generator for the nit language.
16 Generate API documentation in HTML format from nit source code.
17 */
18
19 /*
20 * Nitdoc.Github comment edition module
21 *
22 * Allows user to modify source code comments directly from the Nitdoc
23 */
24 define([
25 "jquery",
26 "github-api",
27 "plugins/modalbox",
28 "plugins/github/loginbox",
29 "plugins/github/commentbox",
30 "utils",
31 "Markdown.Converter"
32 ], function($, GithubAPI) {
33 var GithubUser = function(login, password, repo, branch) {
34 this.login = login;
35 this.password = password;
36 this.repo = repo;
37 this.auth = "Basic " + (login + ':' + password).base64Encode();
38 this.branch = branch;
39 }
40
41 var GithubUI = {
42 init: function(upstream, basesha1) {
43 console.info("Github plugin: init GitHub module (upstream: "+ upstream +", base: " + basesha1 + ")");
44 this.origin = this._parseUpstream(upstream);
45 // Add github menu
46 $("nav.main ul").append(
47 $("<li/>")
48 .attr("id", "nitdoc-github-li")
49 .loginbox()
50 .loginbox("displayLogin")
51 .bind("loginbox_logoff", function() {
52 GithubUI.disactivate();
53 })
54 .bind("loginbox_login", function(event, infos) {
55 GithubUI._tryLoginFromCredentials(infos);
56 })
57 );
58 // check local session
59 this._tryLoginFromLocalSession();
60 },
61
62 activate: function(user, origin) {
63 this.openedComments = 0;
64 this._saveSession(user);
65 $("#nitdoc-github-li").loginbox("displayLogout", origin, user);
66 this._attachCommentBoxes();
67 this._reloadComments();
68
69 // Prevent page unload if there is comments in editing mode
70 $(window).on('beforeunload', function() {
71 if(GithubUI.openedComments > 0){
72 return "There is uncommited modified comments. Are you sure you want to leave this page?";
73 }
74 });
75 },
76
77 disactivate: function() {
78 if(this.openedComments > 0){
79 if(!confirm('There is uncommited modified comments. Are you sure you want to leave this page?')) {
80 return false;
81 }
82 }
83
84 localStorage.clear();
85 $("#nitdoc-github-li").loginbox("toggle");
86 $("#nitdoc-github-li").loginbox("displayLogin");
87 $(window).unbind('beforeunload');
88 //window.location.reload();
89 },
90
91 /* login */
92
93 _checkLoginInfos: function(infos) {
94 if(!infos.login || !infos.password || !infos.repo || !infos.branch) {
95 $("<p/>")
96 .text("Please enter your GitHub username, password, repository and branch.")
97 .modalbox({
98 title: "Sign in error",
99 isError: true
100 })
101 .modalbox("open");
102 return false;
103 } else {
104 return true;
105 }
106 },
107
108 _tryLoginFromCredentials: function(infos) {
109 if(this._checkLoginInfos(infos)) {
110 var isok = this._tryLogin(infos.login, infos.password, infos.repo, infos.branch);
111 if(isok === true) {
112 this.activate(this.user, this.origin);
113 } else {
114 if(isok == "error:login") {
115 $("<p/>")
116 .text("The username, password, repo or branch you entered is incorrect.")
117 .modalbox({
118 title: "Github sign in error",
119 isError: true
120 })
121 .modalbox("open");
122 } else if(isok == "error:sha") {
123 $("<p/>")
124 .text("The provided Github repository must contain the base commit '" + UI.origin.sha + "'.")
125 .modalbox({
126 title: "Github base commit error",
127 isError: true
128 })
129 .modalbox("open");
130 } else if(isok == "error:profile") {
131 $("<p/>")
132 .text("Please set your public name and email in your " +
133 "<a href='https://github.com/settings/profile'>GitHub profile</a>." +
134 "<br/><br/>Your public profile informations are used to sign-off your commits.")
135 .modalbox({
136 title: "Github profile error",
137 isError: true
138 })
139 .modalbox("open");
140 }
141 }
142 }
143 },
144
145 _tryLoginFromLocalSession: function() {
146 if(localStorage.user) {
147 var session = JSON.parse(localStorage.user);
148 var isok = this._tryLogin(
149 session.login,
150 session.password.base64Decode(),
151 session.repo,
152 session.branch
153 );
154 if(isok === true) {
155 this.activate(this.user, this.origin);
156 } else {
157 console.debug("Github plugin: Session found but authentification failed");
158 localStorage.clear();
159 }
160 } else {
161 console.debug("Github plugin: No session found");
162 }
163 },
164
165 _tryLogin: function(login, password, repo, branch) {
166 var tmpUser = new GithubUser(login, password, repo, branch);
167 if(!GithubAPI.login(tmpUser)) {
168 return "error:login";
169 }
170 if(!tmpUser.infos.name || !tmpUser.infos.email) {
171 return "error:profile";
172 }
173 var commit = GithubAPI.getCommit(tmpUser, this.origin.sha);
174 if(!commit || !commit.sha) {
175 return "error:sha";
176 }
177 this.user = tmpUser;
178 return true;
179 },
180
181 _saveSession: function(user) {
182 localStorage.user = JSON.stringify({
183 login: user.login,
184 password: user.password.base64Encode(),
185 repo: user.repo,
186 branch: user.branch,
187 });
188 // check local storage synchro with branch
189 if(localStorage.base != this.origin.sha) {
190 console.log("Base changed: cleaned cache");
191 localStorage.requests = "[]";
192 localStorage.base = this.origin.sha;
193 }
194 },
195
196 /* html decoration */
197
198 // Attach edit button on each comment
199 _attachCommentBoxes: function() {
200 $("textarea.baseComment").each(function() {
201 $(this).commentbox();
202
203 var isNew = false;
204 if(!$(this).val()) {
205 isNew = true;
206 $(this).nextAll(".info:first").find(".noComment").hide()
207 $(this).nextAll(".info:first").before(
208 $("<div/>")
209 .hide()
210 .addClass("comment")
211 .append(
212 $("<div/>").addClass("nitdoc")
213 )
214 )
215 }
216
217 $(this).nextAll(".info:first").prepend(
218 $("<a/>")
219 .addClass("nitdoc-github-editComment")
220 .css("cursor", "pointer")
221 .text((isNew ? "add" : "edit") + " comment")
222 .click($.proxy(GithubUI._openCommentBox, GithubUI, null, $(this)))
223 .after(" for ")
224 )
225
226 $(this).bind("commentbox_commit", function(event, data) {
227 GithubUI._saveChanges(data);
228 $(this).commentbox("close");
229 GithubUI._reloadComments();
230 })
231 .bind("commentbox_preview", function(event, data) {
232 var converter = new Markdown.Converter()
233 var html = converter.makeHtml(data.value);
234 $("<p/>")
235 .html(html)
236 .modalbox({
237 title: "Preview comment"
238 })
239 .modalbox("open");
240 })
241 .bind("commentbox_open", function(event, data) {
242 GithubUI.openedComments++;
243 $(this).nextAll(".comment").hide();
244 })
245 .bind("commentbox_close", function(event, data) {
246 GithubUI.openedComments--;
247 $(this).nextAll(".comment").show();
248 });
249 });
250 },
251
252 // reload comments from saved pull request
253 _reloadComments: function() {
254 if(!localStorage.requests){ return; }
255 $("p.pullRequest").remove();
256 var converter = new Markdown.Converter();
257 var requests = JSON.parse(localStorage.requests);
258 // Look for modified comments in page
259 for(i in requests) {
260 if(!requests[i]) { continue; }
261 var request = requests[i];
262 $("textarea[data-comment-location=\"" + request.location + "\"]").each(function () {
263 if(request.isClosed) {
264 var oldComment = request.oldComment.base64Decode();
265 var htmlComment = converter.makeHtml(oldComment);
266 $(this).val(oldComment);
267 if(!$(this).val()) {
268 $(this).nextAll("div.comment:first").hide();
269 } else {
270 $(this).nextAll("div.comment:first").show();
271 }
272 $(this).nextAll("div.comment").find("div.nitdoc").empty().html(htmlComment);
273 $(this).nextAll("p.info").find("a.nitdoc-github-editComment").show();
274 } else {
275 var newComment = request.comment.base64Decode();
276 var htmlComment = converter.makeHtml(newComment);
277 $(this).val(newComment);
278 if(!$(this).val()) {
279 $(this).nextAll("div.comment:first").hide();
280 } else {
281 $(this).nextAll("div.comment:first").show();
282 }
283 $(this).nextAll("div.comment").find("div.nitdoc").empty().html(htmlComment);
284 GithubUI._addPullRequestLink($(this), request);
285 $(this).nextAll("p.info").find("a.nitdoc-github-editComment").hide();
286 }
287 });
288 }
289 },
290
291 _addPullRequestLink: function(baseArea, request) {
292 baseArea.nextAll("p.info").before(
293 $("<p/>")
294 .addClass("pullRequest inheritance")
295 .text("comment modified in ")
296 .append(
297 $("<a/>")
298 .attr({
299 href: request.request.html_url,
300 title: "Review on GitHub"
301 })
302 .text("pull request #" + request.request.number)
303 )
304 .append(" ")
305 .append(
306 $("<a/>")
307 .data("pullrequest-number", request.request.number)
308 .addClass("nitdoc-github-update")
309 .text("update")
310 .click($.proxy(GithubUI._doUpdateRequest, GithubUI, null, baseArea, request))
311 )
312 .append(" ")
313 .append(
314 $("<a/>")
315 .data("pullrequest-number", request.request.number)
316 .addClass("nitdoc-github-cancel")
317 .text("cancel")
318 .click($.proxy(GithubUI._doCancelRequest, GithubUI, null, baseArea, request))
319 )
320 );
321 },
322
323 /* github calls */
324
325 _saveChanges: function(edit) {
326 // if pull request update close existing pull request for the comment
327 if(edit.requestID) {
328 this._closePullRequest(edit.requestID);
329 }
330 edit.oldContent = this._getFileContent(edit.location.path);
331 edit.newContent = this._mergeComment(edit.oldContent, edit.newComment, edit.location);
332 edit.request = this._pushChanges(edit)
333 if(!edit.request) {
334 $("<p/>")
335 .text("Unable to commit changes.<br/>" + response)
336 .modalbox({
337 title: "Github commit error",
338 isError: true
339 })
340 .modalbox("open");
341 return;
342 }
343 this._saveRequest(edit);
344 },
345
346 // save pull request in local storage
347 _saveRequest: function(edit) {
348 var requests = {};
349 if(localStorage.requests) {requests = JSON.parse(localStorage.requests)}
350 requests[edit.request.number] = {
351 request: edit.request,
352 location: edit.location.origin,
353 comment: edit.newComment.base64Encode(),
354 oldComment: edit.oldComment.base64Encode()
355 };
356 localStorage.requests = JSON.stringify(requests);
357 },
358
359 /*
360 Creating a new pull request with the new comment take 5 steps:
361 1. get the base tree from latest commit
362
363 2. create a new blob with updated file content
364 3. post a new tree from base tree and blob
365 4. post the new commit with new tree
366 5. create the pull request
367 */
368 _pushChanges: function(edit) {
369 var baseTree = GithubAPI.getTree(this.user, this.origin.sha);
370 if(!baseTree.sha) {
371 $("<p/>")
372 .text("Unable to locate base tree.<br/>" + baseTree.status + ": " + baseTree.statusText)
373 .modalbox({
374 title: "Github commit error",
375 isError: true
376 })
377 .modalbox("open");
378 return false;
379 }
380 console.log("Base tree: " + baseTree.url);
381 var newBlob = GithubAPI.createBlob(this.user, edit.newContent);
382 if(!newBlob.sha) {
383 $("<p/>")
384 .text("Unable to create new blob.<br/>" + newBlob.status + ": " + newBlob.statusText)
385 .modalbox({
386 title: "Github commit error",
387 isError: true
388 })
389 .modalbox("open");
390 return false;
391 }
392 console.log("New blob: " + newBlob.url);
393 var newTree = GithubAPI.createTree(this.user, baseTree, edit.location.path, newBlob);
394 if(!newTree.sha) {
395 $("<p/>")
396 .text("Unable to create new tree.<br/>" + newTree.status + ": " + newTree.statusText)
397 .modalbox({
398 title: "Github commit error",
399 isError: true
400 })
401 .modalbox("open");
402 return false;
403 }
404 console.log("New tree: " + newTree.url);
405 var newCommit = GithubAPI.createCommit(this.user, edit.message, baseTree.sha, newTree);
406 if(!newCommit.sha) {
407 $("<p/>")
408 .text("Unable to create new commit.<br/>" + newCommit.status + ": " + newCommit.statusText)
409 .modalbox({
410 title: "Github commit error",
411 isError: true
412 })
413 .modalbox("open");
414 return false;
415 }
416 console.log("New commit: " + newCommit.url);
417 var pullRequest = GithubAPI.createPullRequest(this.user, edit.title, "Pull request from Nitdoc", this.origin, newCommit.sha);
418 if(!pullRequest.number) {
419 $("<p/>")
420 .text("Unable to create pull request.<br/>" + pullRequest.status + ": " + pullRequest.statusText)
421 .modalbox({
422 title: "Github commit error",
423 isError: true
424 })
425 .modalbox("open");
426 return false;
427 }
428 console.log("New pull request: " + pullRequest.url);
429 return pullRequest;
430 },
431
432 // close previously opened pull request
433 _closePullRequest: function(number) {
434 var requests = JSON.parse(localStorage.requests);
435 if(!requests[number]) {
436 $("<p/>")
437 .text("Unable to close pull request.<br/>" + "Pull request " + number + "not found")
438 .modalbox({
439 title: "Github commit error",
440 isError: true
441 })
442 .modalbox("open");
443 return false;
444 }
445 // close pull request
446 var res = GithubAPI.updatePullRequest(this.user, "Closed from Nitdoc", "", "closed", requests[number].request);
447 if(!res.id) {
448 $("<p/>")
449 .text("Unable to close pull request.<br/>" + res.status + ": " + res.statusText)
450 .modalbox({
451 title: "Github commit error",
452 isError: true
453 })
454 .modalbox("open");
455 return false;
456 }
457 // update in localstorage
458 requests[number].isClosed = true;
459 localStorage.requests = JSON.stringify(requests);
460 },
461
462 /* internals */
463
464 _parseUpstream: function(upstream) {
465 var parts = upstream.split(":");
466 return {
467 user: parts[0],
468 repo: parts[1],
469 branch: parts[2],
470 sha: basesha1
471 };
472 },
473
474 _getFileContent: function(githubUrl) {
475 var origFile = GithubAPI.getFile(this.user, githubUrl);
476 if(!origFile.content) {
477 $("<p/>")
478 .text("Unable to locate source file.<br/>" + origFile.status + ": " + origFile.statusText)
479 .modalbox({
480 title: "Github commit error",
481 isError: true
482 })
483 .modalbox("open");
484 return;
485 }
486 var base64Content = origFile.content.substring(0, origFile.content.length - 1)
487 return base64Content.base64Decode();
488 },
489
490 _mergeComment: function(fileContent, comment, location) {
491 // replace comment in file content
492 var res = new String();
493 var lines = fileContent.split("\n");
494 // copy lines fron 0 to lstart
495 for(var i = 0; i < location.lstart - 1; i++) {
496 res += lines[i] + "\n";
497 }
498 // set comment
499 if(comment && comment != "") {
500 var commentLines = comment.split("\n");
501 for(var i = 0; i < commentLines.length; i++) {
502 var line = commentLines[i];
503 var tab = location.tabpos > 1 ? "\t" : "";
504 res += tab + (line.length > 0 ? "# " : "#") + line + "\n";
505 }
506 }
507 // copy lines fron lend to end
508 for(var i = location.lend - 1; i < lines.length; i++) {
509 res += lines[i];
510 if(i < lines.length - 1) { res += "\n"; }
511 }
512 return res;
513 },
514
515 /* events */
516
517 _openCommentBox: function(event, baseArea) {
518 baseArea.commentbox("open", this.user);
519 },
520
521 _doCancelRequest: function(event, baseArea, request) {
522 this._closePullRequest(request.request.number);
523 this._reloadComments();
524 },
525
526 _doUpdateRequest: function(event, baseArea, request) {
527 baseArea.commentbox("open", this.user, request.request.number);
528 },
529 }
530
531 // Get github plugin data
532 var upstream = $("body").attr("data-github-upstream");
533 var basesha1 = $("body").attr("data-github-base-sha1");
534 if(upstream && basesha1) {
535 GithubUI.init(upstream, basesha1);
536 }
537 });