9a82274847ddea16a2693e3d76aeca2586ee03b8
[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 "base64",
27 "github-api",
28 "plugins/github/loginbox",
29 "plugins/github/modalbox",
30 "plugins/github/commentbox",
31 "Markdown.Converter",
32 ], function($, Base64, GithubAPI, LoginBox, ModalBox, CommentBox) {
33 var GithubUser = function(login, password, repo, branch) {
34 this.login = login;
35 this.password = password;
36 this.repo = repo;
37 this.auth = "Basic " + Base64.encode(login + ':' + password);
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 ModalBox.open(
96 "Sign in error",
97 "Please enter your GitHub username, password, repository and branch.",
98 true
99 );
100 return false;
101 } else {
102 return true;
103 }
104 },
105
106 _tryLoginFromCredentials: function(infos) {
107 if(this._checkLoginInfos(infos)) {
108 var isok = this._tryLogin(infos.login, infos.password, infos.repo, infos.branch);
109 if(isok === true) {
110 this.activate(this.user, this.origin);
111 } else {
112 if(isok == "error:login") {
113 ModalBox.open(
114 "Sign in error",
115 "The username, password, repo or branch you entered is incorrect.",
116 true
117 );
118 } else if(isok == "error:sha") {
119 ModalBox.open(
120 "Base commit not found",
121 "The provided Github repository must contain the base commit '" + UI.origin.sha + "'",
122 true
123 );
124 } else if(isok == "error:profile") {
125 ModalBox.open(
126 "Incomplete Github profile",
127 "Please set your public name and email in your " +
128 "<a href='https://github.com/settings/profile'>GitHub profile</a>." +
129 "<br/><br/>Your public profile informations are used to sign-off your commits.",
130 true
131 );
132 }
133 }
134 }
135 },
136
137 _tryLoginFromLocalSession: function() {
138 if(localStorage.user) {
139 var session = JSON.parse(localStorage.user);
140 var isok = this._tryLogin(
141 session.login,
142 Base64.decode(session.password),
143 session.repo,
144 session.branch
145 );
146 if(isok === true) {
147 this.activate(this.user, this.origin);
148 } else {
149 console.debug("Github plugin: Session found but authentification failed");
150 localStorage.clear();
151 }
152 } else {
153 console.debug("Github plugin: No session found");
154 }
155 },
156
157 _tryLogin: function(login, password, repo, branch) {
158 var tmpUser = new GithubUser(login, password, repo, branch);
159 if(!GithubAPI.login(tmpUser)) {
160 return "error:login";
161 }
162 if(!tmpUser.infos.name || !tmpUser.infos.email) {
163 return "error:profile";
164 }
165 var commit = GithubAPI.getCommit(tmpUser, this.origin.sha);
166 if(!commit || !commit.sha) {
167 return "error:sha";
168 }
169 this.user = tmpUser;
170 return true;
171 },
172
173 _saveSession: function(user) {
174 localStorage.user = JSON.stringify({
175 login: user.login,
176 password: Base64.encode(user.password),
177 repo: user.repo,
178 branch: user.branch,
179 });
180 // check local storage synchro with branch
181 if(localStorage.base != this.origin.sha) {
182 console.log("Base changed: cleaned cache");
183 localStorage.requests = "[]";
184 localStorage.base = this.origin.sha;
185 }
186 },
187
188 /* html decoration */
189
190 // Attach edit button on each comment
191 _attachCommentBoxes: function() {
192 $("textarea.baseComment").each(function() {
193 $(this).commentbox();
194
195 var isNew = false;
196 if(!$(this).val()) {
197 isNew = true;
198 $(this).nextAll(".info:first").find(".noComment").hide()
199 $(this).nextAll(".info:first").before(
200 $("<div/>")
201 .hide()
202 .addClass("comment")
203 .append(
204 $("<div/>").addClass("nitdoc")
205 )
206 )
207 }
208
209 $(this).nextAll(".info:first").prepend(
210 $("<a/>")
211 .addClass("nitdoc-github-editComment")
212 .css("cursor", "pointer")
213 .text((isNew ? "add" : "edit") + " comment")
214 .click($.proxy(GithubUI._openCommentBox, GithubUI, null, $(this)))
215 .after(" for ")
216 )
217
218 $(this).bind("commentbox_commit", function(event, data) {
219 GithubUI._saveChanges(data);
220 $(this).commentbox("close");
221 GithubUI._reloadComments();
222 })
223 .bind("commentbox_preview", function(event, data) {
224 var converter = new Markdown.Converter()
225 var html = converter.makeHtml(data.value);
226 ModalBox.open("Preview", html, false);
227 })
228 .bind("commentbox_open", function(event, data) {
229 GithubUI.openedComments++;
230 $(this).nextAll(".comment").hide();
231 })
232 .bind("commentbox_close", function(event, data) {
233 GithubUI.openedComments--;
234 $(this).nextAll(".comment").show();
235 });
236 });
237 },
238
239 // reload comments from saved pull request
240 _reloadComments: function() {
241 if(!localStorage.requests){ return; }
242 $("p.pullRequest").remove();
243 var converter = new Markdown.Converter();
244 var requests = JSON.parse(localStorage.requests);
245 // Look for modified comments in page
246 for(i in requests) {
247 if(!requests[i]) { continue; }
248 var request = requests[i];
249 $("textarea[data-comment-location=\"" + request.location + "\"]").each(function () {
250 if(request.isClosed) {
251 var oldComment = Base64.decode(request.oldComment);
252 var htmlComment = converter.makeHtml(oldComment);
253 $(this).val(oldComment);
254 if(!$(this).val()) {
255 $(this).nextAll("div.comment:first").hide();
256 } else {
257 $(this).nextAll("div.comment:first").show();
258 }
259 $(this).nextAll("div.comment").find("div.nitdoc").empty().html(htmlComment);
260 $(this).nextAll("p.info").find("a.nitdoc-github-editComment").show();
261 } else {
262 var newComment = Base64.decode(request.comment);
263 var htmlComment = converter.makeHtml(newComment);
264 $(this).val(newComment);
265 if(!$(this).val()) {
266 $(this).nextAll("div.comment:first").hide();
267 } else {
268 $(this).nextAll("div.comment:first").show();
269 }
270 $(this).nextAll("div.comment").find("div.nitdoc").empty().html(htmlComment);
271 GithubUI._addPullRequestLink($(this), request);
272 $(this).nextAll("p.info").find("a.nitdoc-github-editComment").hide();
273 }
274 });
275 }
276 },
277
278 _addPullRequestLink: function(baseArea, request) {
279 baseArea.nextAll("p.info").before(
280 $("<p/>")
281 .addClass("pullRequest inheritance")
282 .text("comment modified in ")
283 .append(
284 $("<a/>")
285 .attr({
286 href: request.request.html_url,
287 title: "Review on GitHub"
288 })
289 .text("pull request #" + request.request.number)
290 )
291 .append(" ")
292 .append(
293 $("<a/>")
294 .data("pullrequest-number", request.request.number)
295 .addClass("nitdoc-github-update")
296 .text("update")
297 .click($.proxy(GithubUI._doUpdateRequest, GithubUI, null, baseArea, request))
298 )
299 .append(" ")
300 .append(
301 $("<a/>")
302 .data("pullrequest-number", request.request.number)
303 .addClass("nitdoc-github-cancel")
304 .text("cancel")
305 .click($.proxy(GithubUI._doCancelRequest, GithubUI, null, baseArea, request))
306 )
307 );
308 },
309
310 /* github calls */
311
312 _saveChanges: function(edit) {
313 // if pull request update close existing pull request for the comment
314 if(edit.requestID) {
315 this._closePullRequest(edit.requestID);
316 }
317 edit.oldContent = this._getFileContent(edit.location.path);
318 edit.newContent = this._mergeComment(edit.oldContent, edit.newComment, edit.location);
319 edit.request = this._pushChanges(edit)
320 if(!edit.request) {
321 ModalBox.open("Unable to commit changes!", response, true);
322 return;
323 }
324 this._saveRequest(edit);
325 },
326
327 // save pull request in local storage
328 _saveRequest: function(edit) {
329 var requests = {};
330 if(localStorage.requests) {requests = JSON.parse(localStorage.requests)}
331 requests[edit.request.number] = {
332 request: edit.request,
333 location: edit.location.origin,
334 comment: Base64.encode(edit.newComment),
335 oldComment: Base64.encode(edit.oldComment)
336 };
337 localStorage.requests = JSON.stringify(requests);
338 },
339
340 /*
341 Creating a new pull request with the new comment take 5 steps:
342 1. get the base tree from latest commit
343
344 2. create a new blob with updated file content
345 3. post a new tree from base tree and blob
346 4. post the new commit with new tree
347 5. create the pull request
348 */
349 _pushChanges: function(edit) {
350 var baseTree = GithubAPI.getTree(this.user, this.origin.sha);
351 if(!baseTree.sha) {
352 ModalBox.open("Unable to locate base tree!", baseTree.status + ": " + baseTree.statusText, true);
353 return false;
354 }
355 console.log("Base tree: " + baseTree.url);
356 var newBlob = GithubAPI.createBlob(this.user, edit.newContent);
357 if(!newBlob.sha) {
358 ModalBox.open("Unable to create new blob!", newBlob.status + ": " + newBlob.statusText, true);
359 return false;
360 }
361 console.log("New blob: " + newBlob.url);
362 var newTree = GithubAPI.createTree(this.user, baseTree, edit.location.path, newBlob);
363 if(!newTree.sha) {
364 ModalBox.open("Unable to create new tree!", newTree.status + ": " + newTree.statusText, true);
365 return false;
366 }
367 console.log("New tree: " + newTree.url);
368 var newCommit = GithubAPI.createCommit(this.user, edit.message, baseTree.sha, newTree);
369 if(!newCommit.sha) {
370 ModalBox.open("Unable to create new commit!", newCommit.status + ": " + newCommit.statusText, true);
371 return false;
372 }
373 console.log("New commit: " + newCommit.url);
374 var pullRequest = GithubAPI.createPullRequest(this.user, edit.title, "Pull request from Nitdoc", this.origin, newCommit.sha);
375 if(!pullRequest.number) {
376 ModalBox.open("Unable to create pull request!", pullRequest.status + ": " + pullRequest.statusText, true);
377 return false;
378 }
379 console.log("New pull request: " + pullRequest.url);
380 return pullRequest;
381 },
382
383 // close previously opened pull request
384 _closePullRequest: function(number) {
385 var requests = JSON.parse(localStorage.requests);
386 if(!requests[number]) {
387 ModalBox.open("Unable to close pull request!", "Pull request " + number + "not found", true);
388 return false;
389 }
390 // close pull request
391 var res = GithubAPI.updatePullRequest(this.user, "Closed from Nitdoc", "", "closed", requests[number].request);
392 if(!res.id) {
393 ModalBox.open("Unable to close pull request!", res.status + ": " + res.statusText, true);
394 return false;
395 }
396 // update in localstorage
397 requests[number].isClosed = true;
398 localStorage.requests = JSON.stringify(requests);
399 },
400
401 /* internals */
402
403 _parseUpstream: function(upstream) {
404 var parts = upstream.split(":");
405 return {
406 user: parts[0],
407 repo: parts[1],
408 branch: parts[2],
409 sha: basesha1
410 };
411 },
412
413 _getFileContent: function(githubUrl) {
414 var origFile = GithubAPI.getFile(this.user, githubUrl);
415 if(!origFile.content) {
416 ModalBox.open("Unable to locate source file!", origFile.status + ": " + origFile.statusText, true);
417 return;
418 }
419 var base64Content = origFile.content.substring(0, origFile.content.length - 1)
420 return Base64.decode(base64Content);
421 },
422
423 _mergeComment: function(fileContent, comment, location) {
424 // replace comment in file content
425 var res = new String();
426 var lines = fileContent.split("\n");
427 // copy lines fron 0 to lstart
428 for(var i = 0; i < location.lstart - 1; i++) {
429 res += lines[i] + "\n";
430 }
431 // set comment
432 if(comment && comment != "") {
433 var commentLines = comment.split("\n");
434 for(var i = 0; i < commentLines.length; i++) {
435 var line = commentLines[i];
436 var tab = location.tabpos > 1 ? "\t" : "";
437 res += tab + (line.length > 0 ? "# " : "#") + line + "\n";
438 }
439 }
440 // copy lines fron lend to end
441 for(var i = location.lend - 1; i < lines.length; i++) {
442 res += lines[i];
443 if(i < lines.length - 1) { res += "\n"; }
444 }
445 return res;
446 },
447
448 /* events */
449
450 _openCommentBox: function(event, baseArea) {
451 baseArea.commentbox("open", this.user);
452 },
453
454 _doCancelRequest: function(event, baseArea, request) {
455 this._closePullRequest(request.request.number);
456 this._reloadComments();
457 },
458
459 _doUpdateRequest: function(event, baseArea, request) {
460 baseArea.commentbox("open", this.user, request.request.number);
461 }
462 }
463
464 // Get github plugin data
465 var upstream = $("body").attr("data-github-upstream");
466 var basesha1 = $("body").attr("data-github-base-sha1");
467 if(upstream && basesha1) {
468 GithubUI.init(upstream, basesha1);
469 }
470 });