Yi EungJun 2013-10-13
Fix server-side diff rendering.
* Remove unnecssary codes.
* Fix some bugs.
@b675b611139e01719941f1d03c140a5392bd3f72
app/controllers/CodeHistoryApp.java
--- app/controllers/CodeHistoryApp.java
+++ app/controllers/CodeHistoryApp.java
@@ -109,7 +109,7 @@
             List<Commit> commits = repository.getHistory(page, HISTORY_ITEM_LIMIT, branch, path);
 
             if (commits == null) {
-                return notFound(ErrorViews.NotFound.render("error.notfound", project, null));
+                return notFound(ErrorViews.NotFound.render("error.notfound", project));
             }
 
             return ok(history.render(project, commits, page, branch, path));
@@ -156,11 +156,11 @@
         Commit parentCommit = repository.getParentCommitOf(commitId);
 
         if (fileDiffs == null) {
-            return notFound(ErrorViews.NotFound.render("error.notfound", project, null));
+            return notFound(ErrorViews.NotFound.render("error.notfound", project));
         }
 
         if (patch == null) {
-            return notFound(ErrorViews.NotFound.render("error.notfound", project, null));
+            return notFound(ErrorViews.NotFound.render("error.notfound", project));
         }
 
         List<CommitComment> comments = CommitComment.find.where().eq("commitId",
app/controllers/IssueApp.java
--- app/controllers/IssueApp.java
+++ app/controllers/IssueApp.java
@@ -205,7 +205,7 @@
 
         Project project = Project.findByOwnerAndProjectName(ownerName, projectName);
         if (project == null) {
-            return notFound(ErrorViews.NotFound.render("error.notfound", project, null));
+            return notFound(ErrorViews.NotFound.render("error.notfound", project));
         }
 
         int updatedItems = 0;
app/controllers/PullRequestApp.java
--- app/controllers/PullRequestApp.java
+++ app/controllers/PullRequestApp.java
@@ -730,18 +730,18 @@
             return forbidden(ErrorViews.Forbidden.render("error.forbidden", project));
         }
 
-        String patch = RepositoryService.getRepository(project).getPatch(commitId);
+        List<FileDiff> fileDiffs = RepositoryService.getRepository(project).getDiff(commitId);
         Commit commit = RepositoryService.getRepository(project).getCommit(commitId);
         Commit parentCommit = RepositoryService.getRepository(project).getParentCommitOf(commitId);
 
-        if (patch == null) {
-            return notFound(ErrorViews.NotFound.render("error.notfound", project, null));
+        if (fileDiffs == null) {
+            return notFound(ErrorViews.NotFound.render("error.notfound", project));
         }
 
         List<CommitComment> comments = CommitComment.find.where().eq("commitId",
                 commitId).eq("project.id", project.id).findList();
 
-        return ok(diff.render(pullRequest, commit, parentCommit, patch, comments));
+        return ok(diff.render(pullRequest, commit, parentCommit, fileDiffs, comments));
     }
 
 
app/playRepository/DiffLine.java
--- app/playRepository/DiffLine.java
+++ app/playRepository/DiffLine.java
@@ -5,11 +5,14 @@
     public final Integer numA;
     public final Integer numB;
     public final String content;
+    public FileDiff file;
 
-    public DiffLine(DiffLineType type, Integer lineNumA, Integer lineNumB, String content) {
+    public DiffLine(FileDiff file, DiffLineType type, Integer lineNumA, Integer lineNumB,
+                    String content) {
+        this.file = file;
         this.kind = type;
         this.numA = lineNumA;
         this.numB = lineNumB;
         this.content = content;
     }
-}
+}
(No newline at end of file)
app/playRepository/FileDiff.java
--- app/playRepository/FileDiff.java
+++ app/playRepository/FileDiff.java
@@ -54,18 +54,18 @@
 
 			while (aCur < aEnd || bCur < bEnd) {
 				if (aCur < curEdit.getBeginA() || endIdx + 1 < curIdx) {
-                    hunk.lines.add(new DiffLine(DiffLineType.CONTEXT, aCur, bCur,
+                    hunk.lines.add(new DiffLine(this, DiffLineType.CONTEXT, aCur, bCur,
                             a.getString(aCur)));
 					isEndOfLineMissing = checkEndOfLineMissing(a, aCur);
 					aCur++;
 					bCur++;
 				} else if (aCur < curEdit.getEndA()) {
-                    hunk.lines.add(new DiffLine(DiffLineType.REMOVE, aCur, bCur,
+                    hunk.lines.add(new DiffLine(this, DiffLineType.REMOVE, aCur, bCur,
                             a.getString(aCur)));
                     isEndOfLineMissing = checkEndOfLineMissing(a, aCur);
 					aCur++;
 				} else if (bCur < curEdit.getEndB()) {
-                    hunk.lines.add(new DiffLine(DiffLineType.ADD, aCur, bCur,
+                    hunk.lines.add(new DiffLine(this, DiffLineType.ADD, aCur, bCur,
                             b.getString(bCur)));
                     isEndOfLineMissing = checkEndOfLineMissing(a, aCur);
 					bCur++;
app/playRepository/GitRepository.java
--- app/playRepository/GitRepository.java
+++ app/playRepository/GitRepository.java
@@ -1509,8 +1509,8 @@
 
         for (DiffEntry diff : formatter.scan(treeParserA, treeParserB)) {
             FileDiff fileDiff = new FileDiff();
-            fileDiff.commitA = commitA.getName();
-            fileDiff.commitB = commitB.getName();
+            fileDiff.commitA = commitA != null ? commitA.getName() : null;
+            fileDiff.commitB = commitB != null ? commitB.getName() : null;
 
             fileDiff.changeType = diff.getChangeType();
 
app/utils/TemplateHelper.scala
--- app/utils/TemplateHelper.scala
+++ app/utils/TemplateHelper.scala
@@ -7,6 +7,17 @@
 import java.security.MessageDigest
 import views.html._
 import java.net.URI
+import name.fraser.neil.plaintext.DiffMatchPatch
+import DiffMatchPatch.Diff
+import DiffMatchPatch.Operation._
+import playRepository.DiffLine
+import playRepository.DiffLineType
+import models.CodeComment
+import scala.collection.JavaConversions._
+import org.apache.commons.lang3.StringEscapeUtils.escapeHtml4
+import views.html.partial_diff_comment_on_line
+import views.html.partial_diff_line
+import name.fraser.neil.plaintext.DiffMatchPatch
 
 object TemplateHelper {
 
@@ -90,4 +101,92 @@
       case (_) => defaultURI
     }
   }
+
+  object DiffRenderer {
+
+    def removedWord(word: String) = "<span class='remove'>" + word + "</span>"
+
+    def addedWord(word: String) = "<span class='add'>" + word + "</span>"
+
+    def mergeList(a: List[String], b: List[String]) = {
+        a.zip(b).map(v => v._1 + v._2)
+    }
+
+    def wordDiffLinesInHtml(diffList: List[Diff]): List[String] =
+      diffList match {
+        case Nil => List("", "")
+        case head :: tail => mergeList(wordDiffLineInHtml(head), wordDiffLinesInHtml(tail))
+      }
+
+    def wordDiffLineInHtml(diff: Diff) =
+      diff.operation match {
+        case DELETE => List(removedWord(diff.text), "")
+        case INSERT => List("", addedWord(diff.text))
+        case _ => List(diff.text, diff.text)
+      }
+
+    /*
+    def writeHtmlLine(klass: String, indicator: String, numA: Integer, numB: Integer, html: String, commentsOnLine: List[_ <: CodeComment]) = {
+      partial_diff_line_html(klass, indicator, numA, numB, html) + (if(commentsOnLine != null) partial_diff_comment_on_line(commentsOnLine).body else "")
+    }
+
+    def renderWordDiff(lineA: DiffLine, lineB: DiffLine, comments: Map[String, List[_ <: CodeComment]]) = {
+      val lines = wordDiffLinesInHtml((new DiffMatchPatch()).diffMain(lineA.content, lineB.content).toList)
+      writeHtmlLine(lineA.kind.toString.toLowerCase, "-", null, lineA.numA + 1, lines(0), commentsOrEmpty(comments, commentKey(lineA.file.pathA, "remove", lineA.numA + 1))) + writeHtmlLine(lineB.kind.toString.toLowerCase, "+", lineB.numB + 1, null, lines(1), commentsOrEmpty(comments, commentKey(lineB.file.pathB, "add", lineB.numB + 1)))
+    }
+    */
+
+    /* Not implemented yet */
+    def renderWordDiff(lineA: DiffLine, lineB: DiffLine, comments: Map[String, List[CodeComment]]) =
+      renderLine(lineA, comments) + renderLine(lineB, comments)
+
+    def renderTwoLines(lineA: DiffLine, lineB: DiffLine, comments: Map[String, List[CodeComment]]) =
+      (lineA.kind, lineB.kind) match {
+        case (DiffLineType.REMOVE, DiffLineType.ADD) => renderWordDiff(lineA, lineB, comments)
+        case _ => renderLine(lineA, comments) + renderLine(lineB, comments)
+      }
+
+    def commentKey(path: String, side: String, lineNum: Integer) =
+      path + ":" + side + ":" + lineNum
+
+    def commentsOrEmpty(comments: Map[String, List[CodeComment]], key: String) =
+      if (comments.contains(key)) comments(key) else Nil
+
+    def commentsOnAddLine(line: DiffLine, comments: Map[String, List[CodeComment]]) =
+      commentsOrEmpty(comments, commentKey(line.file.pathB, "add", line.numB + 1))
+
+    def commentsOnRemoveLine(line: DiffLine, comments: Map[String, List[CodeComment]]) =
+      commentsOrEmpty(comments, commentKey(line.file.pathA, "remove", line.numA + 1))
+
+    def commentsOnContextLine(line: DiffLine, comments: Map[String, List[CodeComment]]) =
+      commentsOrEmpty(comments, commentKey(line.file.pathB, "context", line.numB + 1))
+
+    def indicator(line: DiffLine) =
+      line.kind match {
+        case DiffLineType.ADD => "+"
+        case DiffLineType.REMOVE => "-"
+        case _ => " "
+      }
+
+    def renderLine(line: DiffLine, num: Integer, numA: Integer, numB: Integer, commentsOnLine: List[CodeComment]) =
+      partial_diff_line(line.kind.toString.toLowerCase, indicator(line), num, numA, numB, line.content) +
+      partial_diff_comment_on_line(commentsOnLine).body.trim
+
+    def renderLine(line: DiffLine, comments: Map[String, List[CodeComment]]): String =
+      line.kind match {
+        case DiffLineType.ADD =>
+          renderLine(line, line.numB + 1, null, line.numB + 1, commentsOnAddLine(line, comments))
+        case DiffLineType.REMOVE =>
+          renderLine(line, line.numA + 1, line.numA + 1, null, commentsOnRemoveLine(line, comments))
+        case _ =>
+          renderLine(line, line.numB + 1, line.numA + 1, line.numB + 1, commentsOnContextLine(line, comments))
+      }
+
+    def renderLines(lines: List[DiffLine], comments: Map[String, List[CodeComment]]): String =
+      lines match {
+        case Nil => ""
+        case first::Nil => renderLine(first, comments)
+        case first::second::tail => renderTwoLines(first, second, comments) + renderLines(tail, comments)
+      }
+  }
 }
app/views/code/diff.scala.html
--- app/views/code/diff.scala.html
+++ app/views/code/diff.scala.html
@@ -1,4 +1,4 @@
-@(project: Project, commit:playRepository.Commit, parentCommit:playRepository.Commit, patch: String, comments:List[CommitComment], selectedBranch:String, diff: List[playRepository.FileDiff])
+@(project: Project, commit:playRepository.Commit, parentCommit:playRepository.Commit, comments:List[CommitComment], selectedBranch:String, diff: List[playRepository.FileDiff])
 
 @import playRepository.RepositoryService
 @import java.net.URLEncoder
@@ -90,8 +90,9 @@
             }
         </p>
         <pre class="commitMsg">@commit.getMessage</pre>
-
+        <div class="diff-body">
         @views.html.partial_diff(diff, comments)
+        </div>
 
         <div id="compare" class="modal hide compare-wrap" tabindex="-1" role="dialog">
             <h4 class="path">
@@ -107,6 +108,7 @@
 
         @** Comment **@
         <div class="board-comment-wrap">
+            @defining(comments.filter(v => v.line == null)) { comments =>
             @if(comments.size > 0) {
             <ul class="comments">
                 @for(comment <- comments){
@@ -140,6 +142,7 @@
                 }
             </ul>
             }
+            }
             
             @common.commentForm(project, ResourceType.COMMIT_COMMENT, routes.CodeHistoryApp.newComment(project.owner, project.name, commit.getId).toString())
         </div>
@@ -159,16 +162,6 @@
 </div>
 
 @common.markdown(project)
-
-<script type="text/x-jquery-tmpl" id="comment-icon-template">
-    <i class="yobicon-comments"></i>
-</script>
-<script type="text/x-jquery-tmpl" id="linenum-column-template">
-    <td class="linenum"></td>
-</script>
-<script type="text/x-jquery-tmpl" id="comment-button-template">
-    <button class="ybtn medium btn-thread"></button>
-</script>
 
 <link rel="stylesheet" type="text/css" media="screen" href="@routes.Assets.at("javascripts/lib/mergely/codemirror.css")">
 <link rel="stylesheet" type="text/css" media="screen" href="@routes.Assets.at("javascripts/lib/mergely/mergely.css")">
app/views/error/notfound.scala.html
--- app/views/error/notfound.scala.html
+++ app/views/error/notfound.scala.html
@@ -33,4 +33,4 @@
         <a href="@getReturnURL(targetType)" class="ybtn ybtn-primary">@Messages("button.list")</a>
     </div>
 </div>
-}
(No newline at end of file)
+}
app/views/git/diff.scala.html
--- app/views/git/diff.scala.html
+++ app/views/git/diff.scala.html
@@ -1,4 +1,4 @@
-@(pull: PullRequest, commit:playRepository.Commit, parentCommit:playRepository.Commit, patch: String, comments:List[CommitComment])
+@(pull: PullRequest, commit:playRepository.Commit, parentCommit:playRepository.Commit, diff: List[playRepository.FileDiff], comments:List[CommitComment])
 
 @import playRepository.RepositoryService
 @import java.net.URLEncoder
@@ -73,9 +73,8 @@
             }
         </p>
         <pre class="commitMsg">@commit.getMessage</pre>
-
-        <div class="diff-wrap">
-            <div id="commit" class="diff-body show-comments">@patch</div>
+        <div class="diff-body">
+        @views.html.partial_diff(diff, comments)
         </div>
         <div id="compare" class="modal hide compare-wrap" tabindex="-1" role="dialog">
             <h4 class="path">
@@ -92,6 +91,7 @@
 
     @** Comment **@
     <div class="board-comment-wrap">
+        @defining(comments.filter(v => v.line == null)) { comments =>
         @if(comments.size > 0) {
         <ul class="comments">
             @for(comment <- comments){
@@ -125,6 +125,7 @@
             }
         </ul>
         }
+        }
         
         @common.commentForm(pull.fromProject, ResourceType.COMMIT_COMMENT, routes.CodeHistoryApp.newComment(pull.fromProject.owner, pull.fromProject.name, commit.getId).toString())
     </div>
app/views/git/view.scala.html
--- app/views/git/view.scala.html
+++ app/views/git/view.scala.html
@@ -393,7 +393,7 @@
             </div>
 
             <div id="__changes" class="tab-pane @if(activeTab == "changes"){ active }">
-                <div id="pull-request-changes" class="diff-body show-comments">
+                <div class="diff-body">
                     @views.html.partial_diff(pull.getDiff, comments)
                 </div>
             </div>
@@ -431,6 +431,12 @@
 		    event.preventDefault();
 			$("#outdatedCommits").toggle(); 
 		})
+
+        $yobi.loadModule("code.Diff", {
+            "welDiff": $("#pull-request-changes"),
+            "sAttachmentAction": "@routes.AttachmentApp.uploadFile",
+            "bCommentable": "isProjectResourceCreatable(UserApp.currentUser, project, ResourceType.PULL_REQUEST_COMMENT)"
+        });
     });
 </script>
 }
app/views/partial_diff.scala.html
--- app/views/partial_diff.scala.html
+++ app/views/partial_diff.scala.html
@@ -1,127 +1,69 @@
 @(fileDiffs: java.util.List[playRepository.FileDiff], comments:java.util.List[_ <: CodeComment])
 
 @import playRepository.DiffLineType
+@import playRepository.DiffLine
+@import playRepository.FileDiff
 @import org.eclipse.jgit.diff.DiffEntry
+@import org.eclipse.jgit.diff.RawText
+@import utils.TemplateHelper.DiffRenderer._
+@import utils.TemplateHelper._
+@import scala.collection.immutable.Map
+@import scala.collection.immutable.List
 @import scala.collection.JavaConversions._
 
-@render(fileDiffs, comments.toList.groupBy((comment: CodeComment) => commentKey(comment.path, comment.side, comment.line)))
-
-@writeLine(klass: String, prefix: String, numA: Integer, numB: Integer, content: String, commentsOnLine: scala.collection.immutable.List[_ <: CodeComment]) = {
-    <tr class="@klass" data-line="@Option(numA).getOrElse(numB)" data-type="@klass"><td class="linenum"><i class="icon-comment"></i>@Option(numA).getOrElse("")</td><td class="linenum">@Option(numB).getOrElse("")</td><td><span>@prefix</span>@content</td></tr>
-    @if(commentsOnLine != null) { @writeCommentsOnLine(commentsOnLine) }
-}
-
-@writeCommentsOnLine(comments: scala.collection.immutable.List[CodeComment]) = {
-    <tr class="comments board-comment-wrap">
-        <td colspan=3>
-            <ul class="comments">
-                @for(comment: CodeComment <- comments) {
-                <li id="comment-@comment.id" data-path="@comment.path" data-side="@comment.side" data-line="@comment.line" class="comment">
-                    <div class="comment-avatar">
-                        <a href="@routes.UserApp.userInfo(comment.authorLoginId)" class="avatar-wrap" data-toggle="tooltip" data-placement="top" title="@comment.authorName">
-                            <img src="@User.findByLoginId(comment.authorLoginId).avatarUrl" width="32" height="32" alt="@comment.authorLoginId">
-                        </a>
-                    </div>
-                    <div class="media-body">
-                        <div class="meta-info">
-                            <span class="comment_author pull-left">
-                                <i class="yobicon-comment"></i>
-                                <a href="@routes.UserApp.userInfo(comment.authorLoginId)" data-toggle="tooltip" data-placement="top" title="@comment.authorName">
-                                    <strong>@comment.authorLoginId </strong>
-                                </a>
-                            </span>
-                            <span class="ago"><a href="#comment-@comment.id">@utils.TemplateHelper.agoString(utils.JodaDateUtil.ago(comment.createdDate))</a></span>
-                            @if(utils.AccessControl.isAllowed(UserApp.currentUser(), comment.asResource(), Operation.DELETE)){
-                            <span class="edit pull-right">
-                                <!-- FIXME: Delete comment? pull request하고 commit comment 구분해야 함 -->
-                                <button class="btn-transparent pull-right close" data-request-method="delete" data-request-uri="@routes.CommentApp.delete(comment.asResource.getType.resource, comment.asResource.getId)"><i class="yobicon-trash"></i></button>
-                            </span>
-                            }
-                        </div>
-
-                        <div class="comment-body markdown-wrap markdown-before" markdown="true">@comment.contents</div>
-
-                        <div class="attachments" resourceType="@comment.asResource.getType" resourceId="@comment.id"></div>
-                    </div>
-                </li>
-                }
-                <button class="nbtn medium btn-thread">댓글창 열기</button>
-                <button class="nbtn medium btn-thread" style="display: none;">댓글창 닫기</button>
-            </ul>
-        </td>
-    </tr>
-}
-
-@commentKey(path: String, side: String, lineNum: Integer) = @{ path + ":" + side + ":" + lineNum }
-
-@commentsOrNull(comments: Map[String, scala.collection.immutable.List[_ <: CodeComment]], key: String) = @{
-    if(comments.contains(key)) {
-        comments(key)
-    } else {
-        null
-    }
-}
-
-@renderFileDiff(diff: playRepository.FileDiff, comments:scala.collection.immutable.Map[String, scala.collection.immutable.List[_ <: CodeComment]]) = {
+@renderDiffLines(diff: playRepository.FileDiff, comments: Map[String, List[_ <: CodeComment]]) = {
+  @if(diff.getHunks.size > 0){
     @for(hunk <- diff.getHunks) {
-        <tr class="range"><td>...</td><td>...</td><td>-@(hunk.beginA + 1),@(hunk.endA - hunk.beginA) +@(hunk.beginB + 1),@(hunk.endB - hunk.beginB)</td></tr>
-        @for(line <- hunk.lines){
-            @line.kind match {
-            case DiffLineType.ADD => { @writeLine(line.kind.toString.toLowerCase, "+", null, line.numB + 1, line.content, commentsOrNull(comments, commentKey(diff.pathB, "add", line.numB + 1))) }
-            case DiffLineType.REMOVE => { @writeLine(line.kind.toString.toLowerCase, "-", line.numA + 1, null, line.content, commentsOrNull(comments, commentKey(diff.pathA, "remove", line.numA + 1))) }
-            case _ => { @writeLine(line.kind.toString.toLowerCase, " ", line.numA + 1, line.numB + 1, line.content, commentsOrNull(comments, commentKey(diff.pathA, "base", line.numA + 1))) }
-            }
-        }
+      <tr class="range"><td>...</td><td>...</td><td>-@(hunk.beginA + 1),@(hunk.endA - hunk.beginA) +@(hunk.beginB + 1),@(hunk.endB - hunk.beginB)</td></tr>
+      @Html(renderLines(hunk.lines.toList, comments))
     }
+  } else {
+    <tr><td colspan=3>@Messages("code.noChanges")</td></tr>
+  }
 }
 
-@render(fileDiffs: java.util.List[playRepository.FileDiff], comments:scala.collection.immutable.Map[String, scala.collection.immutable.List[_ <: CodeComment]]) = {
-@for(diff <- fileDiffs) {
+@renderRawFile(path: String, rawText: RawText, kind: String, prefix: String, comments: Map[String, List[_ <: CodeComment]]) = {
+  @for(i <- 0 until rawText.size) {
+    @partial_diff_line(kind, prefix, null, i + 1, rawText.getString(i))
+    @partial_diff_comment_on_line(commentsOrEmpty(comments, path + ":" + kind + ":" + (i + 1)))
+  }
+}
+
+@renderAddedLines(rawText: RawText, path: String, comments:Map[String, List[_ <: CodeComment]]) = {
+  @renderRawFile(path, rawText, "add", "+", comments)
+}
+
+@renderRemovedLines(rawText: RawText, path: String, comments:Map[String, List[_ <: CodeComment]]) = {
+  @renderRawFile(path, rawText, "remove", "-", comments)
+}
+
+@renderFile(path: String, fileHeader: String, renderedLines: Html) = {
+  <table class="diff-body show-comments" data-path="@path"><tbody>
+  <tr class="file" id="@path.substring(1)">
+      <td class="linenum"></td><td class="linenum"></td><td><span>@fileHeader</span></td>
+  </tr>
+  @renderedLines
+  </tbody></table>
+}
+
+@defining(comments.toList.groupBy((comment: CodeComment) => commentKey(comment.path, comment.side, comment.line))) { comments =>
+  @for(diff <- fileDiffs) {
     @diff.changeType match {
-    case DiffEntry.ChangeType.MODIFY => {
-        <table class="diff-body show-comments" data-path="@diff.pathB"><tbody>
-        <tr class="file">
-            <td colspan=3>@diff.pathB</td>
-        </tr>
-        @renderFileDiff(diff, comments)
-        </tbody></table>
+      case DiffEntry.ChangeType.MODIFY => {
+        @renderFile(diff.pathB, diff.pathB, renderDiffLines(diff, comments))
+      }
+      case DiffEntry.ChangeType.ADD => {
+        @renderFile(diff.pathB, Messages("code.addedPath", diff.pathB), renderAddedLines(diff.b, diff.pathB, comments))
+      }
+      case DiffEntry.ChangeType.DELETE => {
+        @renderFile(diff.pathA, Messages("code.deletedPath", diff.pathA), renderRemovedLines(diff.a, diff.pathA, comments))
+      }
+      case DiffEntry.ChangeType.RENAME => {
+        @renderFile(diff.pathB, Messages("code.renamedPath", diff.pathA, diff.pathB), renderDiffLines(diff, comments))
+      }
+      case DiffEntry.ChangeType.COPY => {
+        @renderFile(diff.pathB, Messages("code.copiedPath", diff.pathA, diff.pathA), renderAddedLines(diff.b, diff.pathB, comments))
+      }
     }
-
-    case DiffEntry.ChangeType.ADD => {
-        <table class="diff-body show-comments" data-path="@diff.pathB"><tbody>
-        <tr class="file">
-            <td colspan=3>@diff.pathB (Added)</td>
-        </tr>
-        @for(i <- 0 until diff.b.size) {
-            @writeLine("add", "+", null, i + 1, diff.b.getString(i), commentsOrNull(comments, diff.pathB + ":add:" + (i + 1)))
-        }
-        </tbody></table>
-    }
-    case DiffEntry.ChangeType.DELETE => {
-        <table class="diff-body show-comments" data-path="@diff.pathA"><tbody>
-        <tr class="file">
-            <td colspan=3>@diff.pathA (Deleted)</td>
-        </tr>
-        @for(i <- 0 until diff.a.size) {
-            @writeLine("remove", "-", null, i + 1, diff.a.getString(i), commentsOrNull(comments, diff.pathA + ":add:" + (i + 1)))
-        }
-        </tbody></table>
-    }
-    case DiffEntry.ChangeType.RENAME => {
-        <table class="diff-body show-comments" data-path="@diff.pathB"><tbody>
-        <tr class="file">
-            <td colspan=3>@diff.pathB -> @diff.pathB</td>
-        </tr>
-        @renderFileDiff(diff, comments)
-        </tbody></table>
-    }
-    case DiffEntry.ChangeType.COPY => {
-        <table class="diff-body show-comments" data-path="@diff.pathB"><tbody>
-        <tr class="file">
-            <td colspan=3>Copy @diff.pathA to @diff.pathB</td>
-        </tr>
-        </tbody></table>
-    }
-    }
-}
+  }
 }
 
app/views/partial_diff_comment_on_line.scala.html (added)
+++ app/views/partial_diff_comment_on_line.scala.html
@@ -0,0 +1,44 @@
+@(comments: List[CodeComment])
+
+@import utils.TemplateHelper._
+@import utils.TemplateHelper.DiffRenderer._
+
+@if(!comments.isEmpty){
+<tr class="comments board-comment-wrap" data-commit-id="@comments(0).commitId">
+    <td colspan=3>
+        <ul class="comments">
+            @for(comment: CodeComment <- comments) {
+            <li id="comment-@comment.id" data-path="@comment.path" data-side="@comment.side" data-line="@comment.line" class="comment">
+                <div class="comment-avatar">
+                    <a href="@routes.UserApp.userInfo(comment.authorLoginId)" class="avatar-wrap" data-toggle="tooltip" data-placement="top" title="@comment.authorName">
+                        <img src="@User.findByLoginId(comment.authorLoginId).avatarUrl" width="32" height="32" alt="@comment.authorLoginId">
+                    </a>
+                </div>
+                <div class="media-body">
+                    <div class="meta-info">
+                        <span class="comment_author pull-left">
+                            <i class="yobicon-comment"></i>
+                            <a href="@routes.UserApp.userInfo(comment.authorLoginId)" data-toggle="tooltip" data-placement="top" title="@comment.authorName">
+                                <strong>@comment.authorLoginId </strong>
+                            </a>
+                        </span>
+                        <span class="ago"><a href="#comment-@comment.id">@utils.TemplateHelper.agoString(utils.JodaDateUtil.ago(comment.createdDate))</a></span>
+                        @if(utils.AccessControl.isAllowed(UserApp.currentUser(), comment.asResource(), Operation.DELETE)){
+                        <span class="edit pull-right">
+                            <button class="btn-transparent pull-right close" data-request-method="delete" data-request-uri="@routes.CommentApp.delete(comment.asResource.getType.resource, comment.asResource.getId)"><i class="yobicon-trash"></i></button>
+                        </span>
+                        }
+                    </div>
+
+                    <div class="comment-body markdown-wrap markdown-before" markdown="true">@comment.contents</div>
+
+                    <div class="attachments" resourceType="@comment.asResource.getType" resourceId="@comment.id"></div>
+                </div>
+            </li>
+            }
+            <button class="nbtn medium btn-thread open-comment-box">@Messages("code.openCommentBox")</button>
+            <button class="nbtn medium btn-thread close-comment-box" style="display: none;">@Messages("code.closeCommentBox")</button>
+        </ul>
+    </td>
+</tr>
+}
 
app/views/partial_diff_line.scala.html (added)
+++ app/views/partial_diff_line.scala.html
@@ -0,0 +1,3 @@
+@(klass: String, prefix: String, num: Integer, numA: Integer, numB: Integer, content: String)
+
+<tr class="@klass" data-line="@num" data-type="@klass"><td class="linenum"><i class="icon-comment"></i>@Option(numA).getOrElse("")</td><td class="linenum">@Option(numB).getOrElse("")</td><td class="code"><span>@prefix</span>@content</td></tr>
conf/messages.en
--- conf/messages.en
+++ conf/messages.en
@@ -62,6 +62,10 @@
 button.user.makeAccountUnlock.false = Lock
 button.user.makeAccountUnlock.true = Unlock
 button.yes = Yes
+code.addedPath = {0} (Added)
+code.deletedPath = {0} (Deleted)
+code.renamedPath = {1} (Renamed from {0})
+code.copiedPath = {1} (Copied from {0})
 code.author = Author
 code.closeCommentBox = Close Comment Box
 code.commitDate = Commit Date
@@ -72,6 +76,7 @@
 code.files = Files
 code.history = History
 code.newer = Newer
+code.noChanges = No changes
 code.nohead = <div class="alert alert-block"><h4>The repository is empty!</h4></div>
 code.nohead.clone = Create new local repository by cloning the repository created on {0}, and push README.md file.
 code.nohead.init = Or, create new local repository by yourself and add a remote that indicate the {0} repository and push README.md file.
conf/messages.ko
--- conf/messages.ko
+++ conf/messages.ko
@@ -62,6 +62,10 @@
 button.user.makeAccountUnlock.false = 계정잠그기
 button.user.makeAccountUnlock.true = 잠김해제
 button.yes = 예
+code.addedPath = {0} (추가됨)
+code.deletedPath = {0} (삭제됨)
+code.renamedPath = {1} ({0}로부터 이름 변경됨)
+code.copiedPath = {1} ({0}로부터 복사됨)
 code.author = 작성자
 code.closeCommentBox = 댓글창 닫기
 code.commitDate = 커밋한 날짜
@@ -72,6 +76,7 @@
 code.files = 파일
 code.history = 변경이력
 code.newer = 이전
+code.noChanges = 변경 없음
 code.nohead = <div class="alert alert-block"><h4>저장소가 비어있습니다!</h4></div>
 code.nohead.clone = {0}에 생성된 Git 저장소를 clone 받아서 로컬에 Git 저장소를 만들고 README.md 파일을 {0}에 올릴 수 있습니다.
 code.nohead.init = 또는, 로컬에서 Git 저장소를 만들고 remote로 {0}에 생성된 Git 저장소를 직접 추가하고 README.md 파일을 {0}에 올릴 수 있습니다.
public/javascripts/service/yobi.code.Diff.js
--- public/javascripts/service/yobi.code.Diff.js
+++ public/javascripts/service/yobi.code.Diff.js
@@ -48,9 +48,6 @@
             // 미니맵
             htVar.sQueryMiniMap = htOptions.sQueryMiniMap || "li.comment";
             htVar.sTplMiniMapLink = '<a href="#${id}" style="top:${top}px; height:${height}px;"></a>';
-
-            htVar.sCommitA = htOptions.sCommitA;
-            htVar.sCommitB = htOptions.sCommitB;
         }
 
         /**
@@ -65,14 +62,9 @@
                 .append(welHidden.clone().attr('name', 'line'))
                 .append(welHidden.clone().attr('name', 'side'))
                 .append(welHidden.clone().attr('name', 'commitA'))
-                .append(welHidden.clone().attr('name', 'commitB'));
+                .append(welHidden.clone().attr('name', 'commitB'))
+                .append(welHidden.clone().attr('name', 'commitId'));
             htElement.welComments = $('ul.comments');
-
-            if (htVar.bCommentable) {
-                htElement.welIcon = $('#comment-icon-template').tmpl();
-            }
-            htElement.welEmptyLineNumColumn = $('#linenum-column-template').tmpl();
-            htElement.welEmptyCommentButton = $('#comment-button-template').tmpl();
 
             // 지켜보기
             htElement.welBtnWatch = $('#watch-button');
@@ -114,7 +106,7 @@
             $(window).on("resize", _initMiniMap);
             $(window).on("scroll", _updateMiniMapCurr);
             $(window).on("resize", _resizeMergely);
-            $('tr .linenum:first-child').click(_onClickLineNumA);
+            $('div.diff-body[data-outdated!="true"] tr .linenum:first-child').click(_onClickLineNumA);
 
             _attachCommentBoxToggleEvent();
         }
@@ -254,53 +246,6 @@
             }
         }
 
-        /**
-         * welTr에 줄 번호를 붙인다.
-         *
-         * @param {Object} welTr
-         * @param {Number} nLineA
-         * @param {Number} nLineB
-         */
-        function _prependLineNumberOnLine(welTr, nLineA, nLineB) {
-            var welLineNumA =
-                htElement.welEmptyLineNumColumn.clone().text(nLineA).addClass("linenum-from");
-            var welLineNumB =
-                htElement.welEmptyLineNumColumn.clone().text(nLineB).addClass("linenum-to");
-
-            welTr.append(welLineNumA);
-            welTr.append(welLineNumB);
-
-            if (htVar.bCommentable
-                    && (!isNaN(parseInt(nLineA)) || !isNaN(parseInt(nLineB)))) {
-                _prependCommentIcon(welLineNumA, welTr);
-                welLineNumA.click(_onClickLineNumA);
-            }
-        }
-
-        /**
-         * welPrependTo에, welHoverOn에 마우스 호버시 보여질 댓글 아이콘을
-         * 붙인다.
-         *
-         * @param {Object} welPrependTo
-         * @param {Object} welHoverOn
-         */
-        function _prependCommentIcon(welPrependTo, welHoverOn) {
-            var welIcon = htElement.welIcon.clone()
-            welIcon.prependTo(welPrependTo);
-
-            welHoverOn.hover(function() {
-                welIcon.css('visibility', 'visible');
-            }, function() {
-                welIcon.css('visibility', 'hidden');
-            });
-
-            welPrependTo.hover(function() {
-                welIcon.css('opacity', '1.0');
-            }, function() {
-                welIcon.css('opacity', '0.6');
-            });
-        }
-
         function _attachCommentBoxToggleEvent() {
             if (htVar.bCommentable) {
                 var welCloseButton = $('.close-comment-box');
@@ -336,6 +281,7 @@
             htElement.welEmptyCommentForm.find('[name=side]').removeAttr('value');
             htElement.welEmptyCommentForm.find('[name=commitA]').removeAttr('value');
             htElement.welEmptyCommentForm.find('[name=commitB]').removeAttr('value');
+            htElement.welEmptyCommentForm.find('[name=commitId]').removeAttr('value');
             $('.code-browse-wrap').append(htElement.welEmptyCommentForm);
             _updateMiniMap();
         }
@@ -379,7 +325,10 @@
             var welCommentTr;
             var nLine = parseInt(welTr.data('line'));
             var sType = welTr.data('type');
-            var sPath = welTr.closest('table').data('path');
+            var sCommitId;
+            var sPath;
+            var sCommitA = welTr.closest('div.diff-body').data('commitA');
+            var sCommitB = welTr.closest('div.diff-body').data('commitB');
 
             if (isNaN(nLine)) {
                 nLine = parseInt(welTr.prev().data('line'));
@@ -388,6 +337,14 @@
 
             if (isNaN(nLine)) {
                 return;
+            }
+
+            if (sType == 'remove') {
+                sPath = welTr.closest('table').data('path-a');
+                sCommitId = sCommitA;
+            } else {
+                sPath = welTr.closest('table').data('path-b');
+                sCommitId = sCommitB;
             }
 
             if (htElement.welCommentTr) {
@@ -401,8 +358,10 @@
             welCommentTr.find('[name=path]').attr('value', sPath);
             welCommentTr.find('[name=line]').attr('value', nLine);
             welCommentTr.find('[name=side]').attr('value', sType);
-            welCommentTr.find('[name=commitA]').attr('value', htVar.sCommitA);
-            welCommentTr.find('[name=commitB]').attr('value', htVar.sCommitB);
+
+            welCommentTr.find('[name=commitA]').attr('value', sCommitA);
+            welCommentTr.find('[name=commitB]').attr('value', sCommitB);
+            welCommentTr.find('[name=commitId]').attr('value', sCommitId);
 
             welTr.after(htElement.welCommentTr);
             _updateMiniMap();
Add a comment
List