1e932adeccec268b75f553411377dec0de22b2ce
1 $
(document
).ready(function() {
6 // check cookie at startup
7 sessionCookie
= new SessionCookie("nitdoc_github_session");
8 var session
= sessionCookie
.getSession();
11 githubAPI
= new GitHubAPI(session
.user
, session
.password
, session
.repo
)
13 console
.log("Session started from cookie (head: "+ $
("body").attr("data-github-head") +", head: "+ $
("body").attr("data-github-base") +")");
16 console
.log("No cookie found");
20 // Check if a comment is editing
21 window
.onbeforeunload
= function() {
22 if(ui
.openedComments
> 0){
23 return 'Are you sure you want to leave this page?';
29 function GitHubAPI(login
, password
, repo
) {
31 this.password
= password
;
33 this.auth
= "Basic " + Base64
.encode(login
+ ':' + password
);
37 // try to login to github API
38 this.tryLogin
= function() {
41 beforeSend
: function (xhr
) {
42 xhr
.setRequestHeader ("Authorization", githubAPI
.auth
);
45 url
: "https://api.github.com/repos/" + this.login
+ "/" + this.repo
,
55 this.getUserInfos
= function() {
58 beforeSend
: function (xhr
) {
59 xhr
.setRequestHeader ("Authorization", githubAPI
.auth
);
62 url
: "https://api.github.com/users/" + this.login
,
65 success
: function(response
) {
68 error
: function(response
) {
75 this.getSignedOff
= function() {
76 var infos
= this.getUserInfos();
77 return infos
.name
+ " <" + infos
.email
+ ">";
82 this.getFile
= function(path
, branch
){
86 url
: "https://api.github.com/repos/" + this.login
+ "/" + this.repo
+ "/contents/" + path
,
92 success
: function(response
) {
95 error
: function(response
) {
104 // get the latest commit on `branchName`
105 this.getLastCommit
= function(branchName
) {
108 beforeSend
: function (xhr
) {
109 xhr
.setRequestHeader ("Authorization", githubAPI
.auth
);
112 url
: "https://api.github.com/repos/" + this.login
+ "/" + this.repo
+ "/git/refs/heads/" + branchName
,
115 success
: function(response
) {
116 res
= response
.object
;
118 error
: function(response
) {
125 // get the base tree for commit
126 this.getTree
= function(sha
) {
129 beforeSend
: function (xhr
) {
130 xhr
.setRequestHeader ("Authorization", githubAPI
.auth
);
133 url
: "https://api.github.com/repos/" + this.login
+ "/" + this.repo
+ "/git/trees/" + sha
+ "?recursive=1",
136 success
: function(response
) {
139 error
: function(response
) {
147 this.createBlob
= function(content
) {
150 beforeSend
: function (xhr
) {
151 xhr
.setRequestHeader ("Authorization", githubAPI
.auth
);
154 url
: "https://api.github.com/repos/" + this.login
+ "/" + this.repo
+ "/git/blobs",
157 data
: JSON
.stringify({
158 content
: Base64
.encode(content
),
161 success
: function(response
) {
164 error
: function(response
) {
171 // create a new tree from a base tree
172 this.createTree
= function(baseTree
, path
, blob
) {
175 beforeSend
: function (xhr
) {
176 xhr
.setRequestHeader ("Authorization", githubAPI
.auth
);
179 url
: "https://api.github.com/repos/" + this.login
+ "/" + this.repo
+ "/git/trees",
180 data
: JSON
.stringify({
181 base_tree
: baseTree
.sha
,
184 mode
: 100644, // file (blob)
191 success
: function(response
) {
194 error
: function(response
) {
201 // create a new commit
202 this.createCommit
= function(message
, parentCommit
, tree
) {
205 beforeSend
: function (xhr
) {
206 xhr
.setRequestHeader ("Authorization", githubAPI
.auth
);
209 url
: "https://api.github.com/repos/" + this.login
+ "/" + this.repo
+ "/git/commits",
210 data
: JSON
.stringify({
212 parents
: parentCommit
,
217 success
: function(response
) {
220 error
: function(response
) {
227 // create a pull request
228 this.createPullRequest
= function(title
, body
, base
, head
) {
231 beforeSend
: function (xhr
) {
232 xhr
.setRequestHeader ("Authorization", githubAPI
.auth
);
235 url
: "https://api.github.com/repos/" + this.login
+ "/" + this.repo
+ "/pulls",
236 data
: JSON
.stringify({
244 success
: function(response
) {
247 error
: function(response
) {
254 // update a pull request
255 this.updatePullRequest
= function(title
, body
, state
, number
) {
258 beforeSend
: function (xhr
) {
259 xhr
.setRequestHeader ("Authorization", githubAPI
.auth
);
262 url
: "https://api.github.com/repos/" + this.login
+ "/" + this.repo
+ "/pulls/" + number
,
263 data
: JSON
.stringify({
270 success
: function(response
) {
273 error
: function(response
) {
282 /* GitHub cookie management */
284 function SessionCookie(cookieName
) {
285 this.cookieName
= cookieName
287 this.setSession
= function (user
, password
, repo
) {
288 var value
= Base64
.encode(JSON
.stringify({
293 var exdate
= new Date();
294 exdate
.setDate(exdate
.getDate() + 1);
295 document
.cookie
= this.cookieName
+ "=" + value
+ "; expires=" + exdate
.toUTCString();
298 this.delSession
= function() {
299 document
.cookie
= this.cookieName
+ '=; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
302 this.getCookieDatas
= function() {
303 var c_name
= this.cookieName
;
304 var c_value
= document
.cookie
;
305 var c_start
= c_value
.indexOf(" " + c_name
+ "=");
306 if (c_start
== -1) { c_start
= c_value
.indexOf(c_name
+ "="); }
310 c_start
= c_value
.indexOf("=", c_start
) + 1;
311 var c_end
= c_value
.indexOf(";", c_start
);
312 if (c_end
== -1) { c_end
= c_value
.length
; }
313 c_value
= unescape(c_value
.substring(c_start
,c_end
));
318 this.getSession
= function() {
319 var cookie
= this.getCookieDatas();
321 return JSON
.parse(Base64
.decode(cookie
));
328 /* GitHub login box */
330 function LoginBox() {
332 $
("nav.main ul").append(
333 $
("<li id='liGitHub'></li>")
335 $
("<a class='btn' id='logGitHub'><img id='imgGitHub' src='resources/icons/github-icon.png' alt='GitHub'/></a>")
336 .click(function() { ui
.loginBox
.toggle() })
339 " <div id='loginBox' style='display: none;'>" +
340 " <div class='arrow'> </div>" +
341 " <h3>Github Sign In</h3>" +
342 " <div id='signedIn' style='display: none'>" +
343 " <label id='logginMessage'>Hello " +
344 " <a id='githubAccount'><strong id='nickName'></strong></a>!" +
346 " <label for='github-repo'>Repo</label>" +
347 " <input id='github-repo' disabled='disabled' type='text'/>" +
348 " <label for='github-head'>Head</label>" +
349 " <input id='github-head' disabled='disabled' type='text'/>" +
350 " <label for='github-base'>Base</label>" +
351 " <input id='github-base' disabled='disabled' type='text'/>" +
352 " <button class='signIn github'><img src='resources/icons/github-icon.png'/>Sign Off</button>" +
354 " <div id='signedOff'>" +
356 " <label for='loginGit'>Username</label>" +
357 " <input id='loginGit' type='text'/>" +
358 " <label for='passwordGit'>Password</label>" +
359 " <input id='passwordGit' type='password'/>" +
360 " <label for='repositoryGit'>Repository</label>" +
361 " <input id='repositoryGit' type='text'/>" +
362 " <button class='signIn github'><img src='resources/icons/github-icon.png'/>Sign In</button>" +
370 // Login with github user or logout current session
371 $
("#loginBox .signIn").click(function(){
372 if($
('#signedIn').is(':hidden')){
373 if(!$
('#loginGit').val() ||
!$
('#passwordGit').val() ||
!$
('#repositoryGit').val()) {
374 ui
.openModalBox("Login incorrect!", "Please enter your username, password and repository.", true);
376 githubAPI
= new GitHubAPI($
('#loginGit').val(), $
('#passwordGit').val(), $
('#repositoryGit').val());
377 if(githubAPI
.tryLogin()) {
378 // open session and set cookie
379 sessionCookie
.setSession(githubAPI
.login
, githubAPI
.password
, githubAPI
.repo
);
383 ui
.openModalBox("Login incorrect!", "Your login information was incorrect!", true);
388 ui
.loginBox
.toggle();
393 this.toggle
= function() {
394 if ($
('#loginBox').is(':hidden')) {
395 $
('#loginBox').show();
396 if (!$
('#loginGit').is(':hidden')) { $
('#loginGit').focus(); }
398 $
('#loginBox').hide();
403 /* Comment edition UI */
405 function GitHubUI() {
406 this.loginBox
= new LoginBox();
407 this.openedComments
= 0;
409 this.init
= function() {
410 $
("body").append("<div id='modal'></div>");
411 $
('body').append('<div id="fade"></div>');
414 this.disactivate
= function() {
415 // close session and purge cookie
416 sessionCookie
.delSession();
417 localStorage
.clear();
418 window
.location
.reload();
421 this.activate
= function() {
422 // get lastest commit
423 var latest
= githubAPI
.getLastCommit($
("body").attr("data-github-head"));
424 if(!latest ||
!latest
.sha
) {
425 this.openModalBox("Head branch not found!", latest
.status
+ ": " + latest
.statusText
, true)
428 if(localStorage
.latestCommit
!= latest
.sha
) {
429 console
.log("Latest commit changed: cleaned cache");
430 localStorage
.requests
= "[]";
431 localStorage
.latestCommit
= latest
.sha
;
433 console
.log("Latest commit sha: " + localStorage
.latestCommit
);
436 $
('#signedOff').hide();
437 $
('#signedIn').show();
438 $
("#imgGitHub").attr("src", "resources/icons/github-icon-w.png");
439 $
("#liGitHub").addClass("current");
442 $
('#nickName').text(githubAPI
.login
);
443 $
('#githubAccount').attr("href", "https://github.com/" + githubAPI
.login
);
444 $
('#github-repo').val(githubAPI
.repo
);
445 $
('#github-base').val($
("body").attr("data-github-base"));
446 $
('#github-head').val($
("body").attr("data-github-head"));
448 // Activate edit mode
450 // Add hidden <pre> to empty commits
451 $
("span.noComment").each(function() {
452 $
(this).addClass("editComment");
453 var baseComment
= $
(this).parent().prev();
454 var location
= ui
.parseLocation(baseComment
.attr("data-comment-location"));
455 location
.lend
= location
.lstart
;
456 var locString
= "../" + location
.path
+ ":" + location
.lstart
+ "," + location
.tabpos
+ "--" + location
.lend
+ ",0";
457 baseComment
.attr("data-comment-location", locString
);
458 $
(this).html("<a class='editComment noComment'>add comment</a> for ");
460 $
('.description div.comment').each(function() {
461 var p
= $
(this).next();
462 p
.prepend("<span class='editComment'><a class='editComment'>edit comment</a> for </span>")
464 $
('a.editComment').each(function() {
465 $
(this).css("cursor", "pointer")
466 $
(this).click(function() {
467 $
(this).parent().hide();
468 if(!$
(this).hasClass("noComment")) {
469 $
(this).parent().parent().prev().hide();
470 ui
.openCommentBox($
(this).parent().parent().prev().prev());
472 ui
.openCommentBox($
(this).parent().parent().prev());
477 // load comment from current branch
478 this.reloadComments();
481 this.openModalBox
= function(title
, msg
, isError
) {
485 .append($
('<a class="close"><img src="resources/icons/close.png" class="btnClose" title="Close" alt="Close"/></a>').click(function() {ui
.closeModalBox()}))
486 .append("<h3>" + title
+ "</h3>")
487 .append("<div>" + msg
+ "</div>")
489 $
("<div class='buttonArea'>")
490 .append($
("<button>Ok</button>").click(function() {ui
.closeModalBox()}))
494 .css("margin-top", -($
('#modal').outerHeight() / 2) + "px")
496 .css("margin-left", -($
('#modal').outerWidth() / 2) + "px");
498 $
("#modal h3").addClass("error");
502 this.closeModalBox
= function() {
503 $
('#fade , #modal').hide();
506 this.openCommentBox
= function(baseArea
) {
507 this.openedComments
+= 1;
508 // get text and format it
511 var commentLines
= baseArea
.text().split('\n');
512 for (var i
= 0; i
< commentLines
.length
; i
++) {
513 formated
+= commentLines
[i];
514 if(i
< commentLines
.length
- 2){ formated
+= "\n"; }
516 len
= commentLines
.length
- 1;
518 // create comment box
519 var tarea
= $
("<textarea rows='" + len
+ "'>" + formated
+ "</textarea>");
520 var width
= width
= baseArea
.parent().innerWidth() - 13;
521 tarea
.css("width", width
+ "px");
522 tarea
.css("display", "block");
523 tarea
.keyup(function(event
) {
524 $
(event
.target
).css("height", (event
.target
.value
.split(/\r|
\n/).length
* 16) + "px");
525 var baseComment
= $
(event
.target
).parents("div.description").find("textarea.baseComment").text();
526 if ($
(event
.target
).val() != baseComment
) {
527 $
(event
.target
).parent().find("button.commit").removeAttr("disabled");
529 $
(event
.target
).parent().find("button.commit").attr("disabled", "disabled");
532 tarea
.keydown(function(event
) {
533 if(event
.keyCode
== 13){
534 $
(event
.target
).css("height", ($
(event
.target
).outerHeight() + 6) + "px");
537 var commentBox
= $
("<div class='commentBox'></div>")
538 .attr("data-comment-namespace", baseArea
.attr("data-comment-namespace"))
539 .attr("data-comment-location", baseArea
.attr("data-comment-location"))
542 $
("<a class='preview'>preview</a>")
544 var converter
= new Markdown
.Converter()
545 var html
= converter
.makeHtml(tarea
.val());
546 ui
.openModalBox("Preview", html
, false);
550 $
("<button class='commit'>Commit</button>")
552 ui
.openCommitBox($
(this).parent());
556 $
("<button class='cancel'>Cancel</button>")
557 .click(function() {ui
.closeCommentBox($
(this).parent())})
559 if(!baseArea
.text()) {
560 commentBox
.addClass("newComment");
562 baseArea
.after(commentBox
);
563 tarea
.trigger("keyup");
566 this.closeCommentBox
= function(commentBox
) {
567 this.openedComments
-= 1;
568 var target
= commentBox
.next();
569 if(!commentBox
.hasClass("newComment")) {
571 target
= target
.next();
573 target
.find("span.editComment").show();
577 this.openCommitBox
= function(commentBox
) {
581 .append($
('<a class="close"><img src="resources/icons/close.png" class="btnClose" title="Close" alt="Close"/></a>').click(function() {ui
.closeModalBox()}))
582 .append("<h3>Commit changes</h3><br/>")
583 .append("<label for='message'>Message:</label><br/>")
584 .append("<textarea id='message'>Wikidoc: " + (commentBox
.hasClass("newComment") ?
"added" : "modified") + " comment for " + commentBox
.attr("data-comment-namespace") + "</textarea><br/>")
585 .append("<input id='signOff' type='checkbox' value='Signed-off-by: " + githubAPI
.getSignedOff() + "'/>")
586 .change(function(e
) {
587 if ($
(e
.target
).is(':checked')) {
588 $
("#commitBtn").removeAttr("disabled");
590 $
("#commitBtn").attr("disabled", "disabled");
593 .append("<label for='signOff'> Signed-off-by: " + githubAPI
.getSignedOff() + "</label>")
595 $
("<div class='buttonArea'>")
597 $
("<button id='commitBtn' disabled='disabled' class='github'><img src='resources/icons/github-icon.png'/>Commit</button>")
598 .mousedown(function() {
599 $
(this).text("Commiting...");
601 .mouseup(function() {
602 ui
.commit($
(this).parent().parent(), commentBox
)
608 .css("margin-top", -($
('#modal').outerHeight() / 2) + "px")
610 .css("margin-left", -($
('#modal').outerWidth() / 2) + "px");
614 this.commit
= function(commitBox
, commentBox
) {
615 // get comments datas
616 var location
= this.parseLocation(commentBox
.attr("data-comment-location"));
617 var comment
= commentBox
.find("textarea").val();
619 // get file content from github
620 var origFile
= githubAPI
.getFile(location
.path
, $
('#github-head').val());
621 if(!origFile
.content
) {
622 this.openModalBox("Unable to locate source file!", origFile
.status
+ ": " + origFile
.statusText
);
625 var base64Content
= origFile
.content
.substring(0, origFile
.content
.length
- 1)
626 var fileContent
= Base64
.decode(base64Content
);
629 var newContent
= this.mergeComment(fileContent
, comment
, location
);
630 var message
= commitBox
.find("#message").val() + "\n\n" + commitBox
.find("#signOff").val();
631 var response
= this.pushComment($
('#github-base').val(), $
('#github-head').val(), location
.path
, newContent
, message
)
637 // save pull request in cookie
639 if(!!localStorage
.requests
) {requests
= JSON
.parse(localStorage
.requests
)}
640 requests
[requests
.length
] = {
642 location
: commentBox
.attr("data-comment-location"),
643 comment
: Base64
.encode(comment
)
645 localStorage
.requests
= JSON
.stringify(requests
);
648 this.closeCommentBox(commentBox
);
650 this.reloadComments();
654 Creating a new pull request with the new comment take 5 steps:
655 1. get the base tree from latest commit
656 2. create a new blob with updated file content
657 3. post a new tree from base tree and blob
658 4. post the new commit with new tree
659 5. create the pull request
661 this.pushComment
= function(base
, branch
, path
, content
, message
) {
662 var baseTree
= githubAPI
.getTree(localStorage
.latestCommit
);
664 this.openModalBox("Unable to locate base tree!", baseTree
.status
+ ": " + baseTree
.statusText
, true);
667 console
.log("Base tree: " + baseTree
.url
);
668 var newBlob
= githubAPI
.createBlob(content
);
670 this.openModalBox("Unable to create new blob!", newBlob
.status
+ ": " + newBlob
.statusText
, true);
673 console
.log("New blob: " + newBlob
.url
);
674 var newTree
= githubAPI
.createTree(baseTree
, path
, newBlob
);
676 this.openModalBox("Unable to create new tree!", newTree
.status
+ ": " + newTree
.statusText
, true);
679 console
.log("New tree: " + newTree
.url
);
680 var newCommit
= githubAPI
.createCommit(message
, localStorage
.latestCommit
, newTree
);
682 this.openModalBox("Unable to create new commit!", newCommit
.status
+ ": " + newCommit
.statusText
, true);
685 console
.log("New commit: " + newCommit
.url
);
686 var pullRequest
= githubAPI
.createPullRequest(message
.split("\n\n")[0], message
, base
, newCommit
.sha
);
687 if(!pullRequest
.number
) {
688 this.openModalBox("Unable to create pull request!", pullRequest
.status
+ ": " + pullRequest
.statusText
, true);
691 console
.log("New pull request: " + pullRequest
.url
);
695 this.reloadComments
= function() {
696 if(!localStorage
.requests
){ return; }
697 var requests
= JSON
.parse(localStorage
.requests
);
698 var converter
= new Markdown
.Converter();
699 // Look for modified comments in page
701 var request
= requests
[i];
702 $
("textarea[data-comment-location=\"" + request
.location
+ "\"]").each(function () {
703 var div
= $
(this).next();
704 if(request
.isClosed
) {
705 if(div
.is("div.comment.newComment")) {
706 // hide empty comment
708 div
.next().find("span.noComment").show();
710 } else if(div
.is("div.comment.locked")) {
712 div
.removeClass("locked");
713 div
.css("cursor", "pointer")
714 div
.click(function() {
715 ui
.openCommentBox(div
.prev());
720 // create div for the new coment
721 if(!div
.is("div.comment")) {
722 $
(this).after("<div class='comment newComment'></div>");
723 div
= $
(this).next();
725 // lock modified comment
726 if(!div
.hasClass("locked")) {
727 // convert modified comment to markdown
729 div
.append(converter
.makeHtml(Base64
.decode(request
.comment
)));
731 div
.css("cursor", "auto");
733 div
.addClass("locked");
735 $
("<p class='locked inheritance'>")
736 .text("comment modified in ")
737 .append("<a href='"+ request
.request
.html_url
+"' target='_blank' title='Review on GitHub'>pull request #"+ request
.request
.number
+"</a>")
740 $
("<a data-pullrequest-number='"+ request
.request
.number
+"' class='cancel'>cancel</a>")
742 ui
.closePullRequest($
(this).attr("data-pullrequest-number"));
747 // hide "add comment" link
748 if(div
.hasClass("newComment")) {
749 div
.next().next().find("span.noComment").hide();
757 this.closePullRequest
= function(number
) {
758 // close pull request
759 var res
= githubAPI
.updatePullRequest("Canceled from Wikidoc", "", "closed", number
);
761 this.openModalBox("Unable to close pull request!", res
.status
+ ": " + res
.statusText
, true);
764 // remove from localstorage
765 var requests
= JSON
.parse(localStorage
.requests
);
767 if(requests
[i].request
.number
== number
) {
768 requests
[i].isClosed
= true;
771 localStorage
.requests
= JSON
.stringify(requests
);
777 // Extract infos from string location "../lib/standard/collection/array.nit:457,1--458,0"
778 this.parseLocation
= function(location
) {
779 var parts
= location
.split(":");
780 var loc
= new Object();
781 loc
.path
= parts
[0].substr(3, parts
[0].length
);
782 loc
.lstart
= parseInt(parts
[1].split("--")[0].split(",")[0]);
783 loc
.tabpos
= parseInt(parts
[1].split("--")[0].split(",")[1]);
784 loc
.lend
= parseInt(parts
[1].split("--")[1].split(",")[0]);
788 // Meld modified comment into file content
789 this.mergeComment
= function(fileContent
, comment
, location
) {
790 // replace comment in file content
791 var res
= new String();
792 var lines
= fileContent
.split("\n");
793 // copy lines fron 0 to lstart
794 for(var i
= 0; i
< location
.lstart
- 1; i
++) {
795 res
+= lines
[i] + "\n";
798 if(comment
&& comment
!= "") {
799 var commentLines
= comment
.split("\n");
800 for(var i
= 0; i
< commentLines
.length
; i
++) {
801 var line
= commentLines
[i];
802 var tab
= location
.tabpos
> 1 ?
"\t" : "";
803 res
+= tab
+ (line
.length
> 0 ?
"# " : "#") + line
+ "\n";
806 // copy lines fron lend to end
807 for(var i
= location
.lend
- 1; i
< lines
.length
; i
++) {
809 if(i
< lines
.length
- 1) { res
+= "\n"; }