Wansoon Park 2013-10-28
pull-request-comment: Changes the commit comment reply sorting
@65c08aa055f4a70bb8fe094077728758dc0183be
app/assets/stylesheets/less/_page.less
--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
@@ -2734,7 +2734,6 @@
 
                     .yobicon-paperclip {vertical-align:middle;margin-top: 4px;}
 
-
                 }
                 .comment-body {
                     font-size: 12px;
@@ -2749,6 +2748,7 @@
                         .border-radius(2px);
                     }
                 }
+
             }
             .attach-preview-wrap {
                 margin-top: 10px;
@@ -2762,8 +2762,32 @@
                 .meta-info { background-color:#f8f8f8; }
 
                 .attach { background-color:#f8f8f8; }
-            }        
+            }
+        }
 
+        .reply {
+            padding:0 20px;
+            .media-body {
+                border-top:0px;
+                .border-radius(0 0 0 0);
+                &:before {
+                    content:none;
+                }
+            }
+        }
+
+        .hasReply {
+            padding-bottom:0px;
+            .media-body {
+                .border-radius(3px 3px 0 0);
+            }
+        }
+
+        .last {
+            padding-bottom:10px;
+            .media-body {
+                .border-radius(0 0 3px 3px);
+            }
         }
 
         .event {
@@ -2771,7 +2795,6 @@
             padding:15px 0 15px 75px;
             font-size:14px;
             line-height:14px;
-
             .state { 
                 color:#FFF;
                 padding: 6px 0;
@@ -4585,4 +4608,4 @@
 
 div.diff-body[data-outdated="true"] tr:hover .icon-comment {
     visibility: hidden;
-}
(No newline at end of file)
+}
app/models/CommitComment.java
--- app/models/CommitComment.java
+++ app/models/CommitComment.java
@@ -14,6 +14,8 @@
     private static final long serialVersionUID = 1L;
     public static final Finder<Long, CommitComment> find = new Finder<>(Long.class, CommitComment.class);
 
+    public List<CommitComment> replies = new ArrayList<>();
+    
     public CommitComment() {
         super();
     }
@@ -80,4 +82,16 @@
         }
         return list;
     }
+
+    /**
+     * CommitComment의 groupKey를 반환한다.
+     * commitId, path, line정보를 조한한 키가 일치할 경우 동일한 내용에 대한
+     * 코멘트로 간주한다.
+     * 
+     * @return
+     */
+    public String groupKey() {
+        return new StringBuilder().append(this.commitId)
+                .append(this.path).append(this.line).toString();
+    }
 }
app/models/PullRequest.java
--- app/models/PullRequest.java
+++ app/models/PullRequest.java
@@ -11,31 +11,15 @@
 import models.resource.Resource;
 import models.resource.ResourceConvertible;
 
-import org.apache.commons.lang.ArrayUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.eclipse.jgit.api.MergeResult;
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryBuilder;
 
-import org.joda.time.DateTimeConstants;
-import org.eclipse.jgit.diff.*;
-import org.eclipse.jgit.lib.ConfigConstants;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevTree;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.treewalk.TreeWalk;
-import org.eclipse.jgit.util.io.NullOutputStream;
 import org.joda.time.Duration;
 
-import com.avaje.ebean.Ebean;
 import com.avaje.ebean.Expr;
-import com.avaje.ebean.RawSql;
-import com.avaje.ebean.RawSqlBuilder;
-import com.avaje.ebean.SqlQuery;
-import com.avaje.ebean.SqlRow;
 
 import play.data.validation.Constraints;
 import play.db.ebean.Model;
@@ -48,29 +32,14 @@
 import playRepository.GitRepository.CloneAndFetch;
 import utils.AccessControl;
 import utils.Constants;
-import utils.GravatarUtil;
 import utils.JodaDateUtil;
 import utils.WatchService;
 
 import javax.persistence.*;
 import javax.validation.constraints.Size;
 
+import java.io.IOException;
 import java.util.*;
-import java.util.regex.Matcher;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.io.IOException;
-import java.io.OutputStream;
 import java.io.File;
 
 @Entity
@@ -610,7 +579,7 @@
      * @return
      */
     @Transient
-    public List<CommitComment> getCodeComments() {
+    public List<CommitComment> getCommitComments() {
         return CommitComment.findByCommits(fromProject, pullRequestCommits);
     }
     
@@ -625,15 +594,17 @@
     
     /**
      * pull request의 모든 코멘트 정보를 가져오고 시간순으로 정렬 후 반환한다. (코멘트 + 코드코멘트 + 이벤트 )
+     * 
      * @return
      */
     @Transient
     public List<TimelineItem> getTimelineComments() {
-        List<CommitComment> codeComments = getCodeComments();
-        
+        List<CommitComment> commitComment 
+                        = computeCommitCommentReplies(getCommitComments());
+
         List<TimelineItem> timelineComments = new ArrayList<>();
         timelineComments.addAll(comments);
-        timelineComments.addAll(codeComments);
+        timelineComments.addAll(commitComment);
         timelineComments.addAll(pullRequestEvents);
 
         Collections.sort(timelineComments, new Comparator<TimelineItem>() {
@@ -641,12 +612,97 @@
             public int compare(TimelineItem o1, TimelineItem o2) {
                 return o1.getDate().compareTo(o2.getDate());
             }
-            
+
         });
-        
+
         return timelineComments;
     }
 
+    /**
+     * 전체 코멘트중 부모글과 답글 정보를 재할당한다.
+     * @param commitComments
+     * @return
+     */
+    private List<CommitComment> computeCommitCommentReplies(
+            List<CommitComment> commitComments) {
+        return reAssignReplyComments(sameTopicCommentGroups(commitComments));
+    }
+
+    /**
+     * 답글목록을 부모글의 필드로 재할당한다.
+     * 
+     * commentGroup은 등록일순으로 오름차순 정렬되어 있는 상태이며
+     * 목록의 첫번째 코멘트를 부모글로 판단한다.
+     * 
+     * @param commentGroup
+     * @return
+     */
+    private List<CommitComment> reAssignReplyComments(
+            Map<String, List<CommitComment>> commentGroup) {
+        List<CommitComment> parentCommitComments = new ArrayList<>();
+
+        for (List<CommitComment> commitComments : commentGroup.values()) {
+            CommitComment parentComment = commitComments.get(0);
+            if (hasReply(commitComments)) {
+                parentComment.replies = replies(commitComments);
+            }
+            parentCommitComments.add(parentComment);
+        }
+        return parentCommitComments;
+    }
+
+    /**
+     * 답글 목록을 반환한다.
+     * @param commitComments
+     * @return
+     */
+    private List<CommitComment> replies(List<CommitComment> commitComments) {
+        return commitComments.subList(1, commitComments.size());
+    }
+
+    /**
+     * 답글 유무를 체크한다.
+     * @param commitComments
+     * @return
+     */
+    private boolean hasReply(List<CommitComment> commitComments) {
+        return commitComments.size() > 1;
+    }
+
+    /**
+     * groupKey를 통해 같은 코멘트그룹 목록을 반환한다.
+     * (같은 커밋, 같은 파일, 같은 라인의 덧글들)
+     * @param commitComments
+     * @return
+     */
+    private Map<String, List<CommitComment>> sameTopicCommentGroups(
+            List<CommitComment> commitComments) {
+        Map<String, List<CommitComment>> commentGroup = new HashMap<>();
+        for (CommitComment commitComment : commitComments) {
+            commentGroup.put(
+                    commitComment.groupKey(),
+                    commitCommentsGroupByKey(commitComment.groupKey(),
+                            commitComments));
+        }
+        return commentGroup;
+    }
+
+    /**
+     * groupKey를 통해 같은 코멘트그룹을 반환한다.
+     * @param groupKey
+     * @param codeComments
+     * @return
+     */
+    private List<CommitComment> commitCommentsGroupByKey(String groupKey,
+            List<CommitComment> codeComments) {
+        List<CommitComment> commitCommentGroups = new ArrayList<CommitComment>();
+        for (CommitComment commitComment : codeComments) {
+            if (commitComment.groupKey().equals(groupKey)) {
+                commitCommentGroups.add(commitComment);
+            }
+        }
+        return commitCommentGroups;
+    }
 
     /**
      * 보낸 코드를 병합해보고 결과 정보를 반환한다.
app/views/git/partial_comments.scala.html
--- app/views/git/partial_comments.scala.html
+++ app/views/git/partial_comments.scala.html
@@ -122,7 +122,7 @@
 @renderCommitComment(comment: CommitComment) = {
   @defining(PullRequestCommit.getStateByCommitId(pull, comment.commitId)) { state =>
     @if(state != null) {
-        <li class="comment @isAuthorComment(comment.authorLoginId)" id="comment-@comment.id">
+        <li class="comment @isAuthorComment(comment.authorLoginId) @if(!comment.replies.isEmpty) {hasReply}" id="comment-@comment.id">
             <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">
@@ -133,7 +133,7 @@
                     <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>
+                            <strong>@comment.authorLoginId</strong>
                         </a>
                     </span>
                     <span class="ago">
@@ -155,6 +155,36 @@
                 <div class="attachments pull-right" data-resourceType="@ResourceType.COMMIT_COMMENT" data-resourceId="@comment.id"></div>
             </div>
         </li>
+        @for((reply, index) <- comment.replies.zipWithIndex) {
+        <li class="comment @isAuthorComment(reply.authorLoginId) reply @if(index == (comment.replies.size - 1)) {last}" id="comment-@reply.id">
+            <div class="media-body">
+                <div class="meta-info">
+                    <span class="comment_author pull-left">
+                        <i class="yobicon-comment"></i>
+                        <a href="@routes.UserApp.userInfo(reply.authorLoginId)" data-toggle="tooltip" data-placement="top" title="@reply.authorName">
+                            <strong>@reply.authorLoginId</strong>
+                        </a>
+                    </span>
+                    <span class="ago">
+                    @if(PullRequestCommit.State.PRIOR == state) {
+						@utils.TemplateHelper.agoString(ago(reply.createdDate)) - <strike>@Messages("pullRequest.code.replied")</strike>
+					} else {
+						@utils.TemplateHelper.agoString(ago(reply.createdDate)) - @Messages("pullRequest.code.replied")
+					}
+                    </span>
+                    @if(isAllowed(UserApp.currentUser(), reply.asResource(), Operation.DELETE)){
+                    <span class="edit pull-right">
+                        <button type="button" class="btn-transparent close" data-request-method="delete" data-request-uri="@routes.CodeHistoryApp.deleteComment(pull.fromProject.owner, pull.fromProject.name, reply.commitId, reply.id)"><i class="yobicon-trash"></i></button>
+                    </span>
+                    }
+                </div>
+
+                <div class="comment-body markdown-wrap markdown-before" markdown="true">@reply.contents</div>
+
+                <div class="attachments pull-right" data-resourceType="@ResourceType.COMMIT_COMMENT" data-resourceId="@reply.id"></div>
+            </div>
+        </li>
+        }
     }
   }
 }
app/views/git/view.scala.html
--- app/views/git/view.scala.html
+++ app/views/git/view.scala.html
@@ -95,7 +95,7 @@
             </div>
             }
             <div class="board-comment-wrap">
-                <div class="comment-header"><i class="yobicon-comments"></i><strong>@Messages("common.comment")</strong> <strong class="num">@{pull.comments.size() + pull.getCodeComments.size()}</strong></div>
+                <div class="comment-header"><i class="yobicon-comments"></i><strong>@Messages("common.comment")</strong> <strong class="num">@{pull.comments.size() + pull.getCommitComments.size()}</strong></div>
 
                 @if(pull.getTimelineComments.size > 0) {
                 <ul class="comments" id="comments">
conf/messages
--- conf/messages
+++ conf/messages
@@ -437,6 +437,7 @@
 pullRequest.body.required = Explain this pull request.
 pullRequest.cancel = Cancel
 pullRequest.code.commented = commented on {0}
+pullRequest.code.replied = replied
 pullRequest.conflict.files = Conflicted Files
 pullRequest.delete.branch = Delete Branch
 pullRequest.delete.frombranch.message = can be deleted.
conf/messages.ja
--- conf/messages.ja
+++ conf/messages.ja
@@ -438,6 +438,7 @@
 pullRequest.body.required = 説明を入力してください
 pullRequest.cancel = 取り消し
 pullRequest.code.commented = commented on {0}
+pullRequest.code.replied = replied
 pullRequest.conflict.files = コンフリクトが発生したファイル
 pullRequest.delete.branch = ブランチ削除
 pullRequest.delete.frombranch.message = ブランチを削除できます
conf/messages.ko
--- conf/messages.ko
+++ conf/messages.ko
@@ -438,6 +438,7 @@
 pullRequest.body.required = 내용을 입력하세요.
 pullRequest.cancel = 취소
 pullRequest.code.commented = {0} 에 덧글을 작성했습니다.
+pullRequest.code.replied = 답글을 작성했습니다.
 pullRequest.conflict.files = 충돌난 파일
 pullRequest.delete.branch = 브랜치 삭제
 pullRequest.delete.frombranch.message = 브랜치를 삭제할 수 있습니다.
Add a comment
List