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