[Notice] Announcing the End of Demo Server [Read me]
whiteship 2013-09-28
referring issues from a pullrequest and a commit
@05e4a408bb86c1b81dd00f2056da1a0edbfcc89f
 
app/actors/CommitCheckActor.java (added)
+++ app/actors/CommitCheckActor.java
@@ -0,0 +1,114 @@
+package actors;
+
+import akka.actor.UntypedActor;
+import controllers.routes;
+import models.*;
+import models.enumeration.EventType;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import play.i18n.Messages;
+import playRepository.GitCommit;
+import playRepository.GitRepository;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.regex.Matcher;
+
+/**
+ * @author Keesun Baik
+ */
+public class CommitCheckActor extends UntypedActor {
+
+    @Override
+    public void onReceive(Object object) throws Exception {
+        if(!(object instanceof CommitCheckMessage)) {
+            return;
+        }
+
+        CommitCheckMessage message = (CommitCheckMessage)object;
+        List<RevCommit> commits = getCommits(message);
+        for(RevCommit commit : commits) {
+            addIssueEvent(commit, message.getProject());
+        }
+
+    }
+
+    private void addIssueEvent(RevCommit commit, Project project) {
+        GitCommit gitCommit = new GitCommit(commit);
+        String fullMessage = gitCommit.getMessage();
+        List<Issue> referredIssues = IssueEvent.findReferredIssue(fullMessage, project);
+        String newValue = getNewEventValue(gitCommit, project);
+        for(Issue issue : referredIssues) {
+            IssueEvent issueEvent = new IssueEvent();
+            issueEvent.issue = issue;
+            issueEvent.senderLoginId = gitCommit.getCommitterName();
+            issueEvent.newValue = newValue;
+            issueEvent.created = new Date();
+            issueEvent.eventType = EventType.ISSUE_REFERRED;
+            issueEvent.save();
+        }
+    }
+
+    private String getNewEventValue(GitCommit gitCommit, Project project) {
+        return Messages.get("issue.event.referred.from.commit",
+                gitCommit.getCommitterName(), gitCommit.getShortId(),
+                routes.CodeHistoryApp.show(project.owner, project.name, gitCommit.getId()));
+    }
+
+    private List<RevCommit> getCommits(CommitCheckMessage message) {
+        List<RevCommit> commits = new ArrayList<>();
+        for(ReceiveCommand command : message.getCommands()) {
+            if(isNewOrUpdateCommand(command)) {
+                commits.addAll(parseCommitsFrom(command, message.getProject()));
+            }
+        }
+        return commits;
+    }
+
+    private Collection<? extends RevCommit> parseCommitsFrom(ReceiveCommand command, Project project) {
+        Repository repository = GitRepository.buildGitRepository(project);
+        List<RevCommit> list = new ArrayList<>();
+
+        try {
+            ObjectId endRange = command.getNewId();
+            ObjectId startRange = command.getOldId();
+
+            RevWalk rw = new RevWalk(repository);
+            rw.markStart(rw.parseCommit(endRange));
+            if (startRange.equals(ObjectId.zeroId())) {
+                // maybe this is a tag or an orphan branch
+                list.add(rw.parseCommit(endRange));
+                rw.dispose();
+                return list;
+            } else {
+                rw.markUninteresting(rw.parseCommit(startRange));
+            }
+
+            Iterable<RevCommit> revlog = rw;
+            for (RevCommit rev : revlog) {
+                list.add(rev);
+            }
+            rw.dispose();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+
+        return list;
+    }
+
+    private boolean isNewOrUpdateCommand(ReceiveCommand command) {
+        List<ReceiveCommand.Type> allowdTypes = new ArrayList<>();
+        allowdTypes.add(ReceiveCommand.Type.CREATE);
+        allowdTypes.add(ReceiveCommand.Type.UPDATE);
+        allowdTypes.add(ReceiveCommand.Type.UPDATE_NONFASTFORWARD);
+        return allowdTypes.contains(command.getType());
+    }
+
+
+}
app/controllers/PullRequestApp.java
--- app/controllers/PullRequestApp.java
+++ app/controllers/PullRequestApp.java
@@ -235,7 +235,7 @@
             return redirect(routes.PullRequestApp.pullRequest(originalProject.owner, originalProject.name, sentRequest.number));
         }
 
-        pullRequest.saveWithNumber();
+        pullRequest.save();
 
         Attachment.moveAll(UserApp.currentUser().asResource(), pullRequest.asResource());
 
 
app/models/CommitCheckMessage.java (added)
+++ app/models/CommitCheckMessage.java
@@ -0,0 +1,28 @@
+package models;
+
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+import java.util.Collection;
+
+/**
+ * @author Keesun Baik
+ */
+public class CommitCheckMessage {
+
+    private Collection<ReceiveCommand> commands;
+
+    private Project project;
+
+    public CommitCheckMessage(Collection<ReceiveCommand> commands, Project project) {
+        this.commands = commands;
+        this.project = project;
+    }
+
+    public Collection<ReceiveCommand> getCommands() {
+        return commands;
+    }
+
+    public Project getProject() {
+        return project;
+    }
+}
app/models/Issue.java
--- app/models/Issue.java
+++ app/models/Issue.java
@@ -21,6 +21,7 @@
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.util.*;
+import java.util.regex.Pattern;
 
 /**
  * 이슈
@@ -47,6 +48,7 @@
 
     public static final String DEFAULT_SORTER = "createdDate";
     public static final String TO_BE_ASSIGNED = "TBA";
+    public static final Pattern ISSUE_PATTERN = Pattern.compile("#\\d+");
 
     public State state;
 
app/models/IssueEvent.java
--- app/models/IssueEvent.java
+++ app/models/IssueEvent.java
@@ -10,7 +10,10 @@
 import javax.persistence.Entity;
 import javax.persistence.Id;
 import javax.persistence.ManyToOne;
+import java.util.ArrayList;
 import java.util.Date;
+import java.util.List;
+import java.util.regex.Matcher;
 
 @Entity
 public class IssueEvent extends Model implements TimelineItem {
@@ -106,4 +109,20 @@
     public Date getDate() {
         return created;
     }
+
+    public static List<Issue> findReferredIssue(String message, Project project) {
+        Matcher m = Issue.ISSUE_PATTERN.matcher(message);
+        List<Issue> referredIssues = new ArrayList<>();
+
+        while(m.find()) {
+            String issueText = m.group();
+            String issueNumber = issueText.substring(1); // removing the leading char #
+            Issue issue = Issue.findByNumber(project, Long.parseLong(issueNumber));
+            if(issue != null) {
+                referredIssues.add(issue);
+            }
+        }
+
+        return referredIssues;
+    }
 }
app/models/PullRequest.java
--- app/models/PullRequest.java
+++ app/models/PullRequest.java
@@ -2,6 +2,8 @@
 
 import com.avaje.ebean.Page;
 import controllers.UserApp;
+import controllers.routes;
+import models.enumeration.EventType;
 import models.enumeration.Operation;
 import models.enumeration.ResourceType;
 import models.enumeration.State;
@@ -15,6 +17,7 @@
 import play.data.validation.Constraints;
 import play.db.ebean.Model;
 import play.db.ebean.Transactional;
+import play.i18n.Messages;
 import playRepository.GitRepository;
 import utils.AccessControl;
 import utils.Constants;
@@ -23,10 +26,8 @@
 
 import javax.persistence.*;
 import javax.validation.constraints.Size;
-import java.util.Date;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
+import java.util.regex.Matcher;
 
 @Entity
 public class PullRequest extends Model implements ResourceConvertible {
@@ -289,6 +290,7 @@
         this.fromBranch = newPullRequest.fromBranch;
         this.title = newPullRequest.title;
         this.body = newPullRequest.body;
+        updateIssueEvents();
         update();
     }
 
@@ -382,9 +384,11 @@
     }
 
     @Transactional
-    public void saveWithNumber() {
+    @Override
+    public void save() {
         this.number = nextPullRequestNumber(toProject);
-        this.save();
+        super.save();
+        addNewIssueEvents();
     }
 
     public static long nextPullRequestNumber(Project project) {
@@ -465,4 +469,59 @@
                 .findPagingList(ITEMS_PER_PAGE)
                 .getPage(pageNum);
     }
+
+    /**
+     * 새로운 풀리퀘가 저장될때 풀리퀘의 제목과 본문에서 참조한 이슈에 이슈 이벤트를 생성한다.
+     */
+    private void addNewIssueEvents() {
+        List<Issue> referredIsseus = IssueEvent.findReferredIssue(this.title + this.body, this.toProject);
+        String newValue = getNewEventValue();
+        for(Issue issue : referredIsseus) {
+            IssueEvent issueEvent = new IssueEvent();
+            issueEvent.issue = issue;
+            issueEvent.senderLoginId = this.contributor.loginId;
+            issueEvent.newValue = newValue;
+            issueEvent.created = new Date();
+            issueEvent.eventType = EventType.ISSUE_REFERRED;
+            issueEvent.save();
+        }
+    }
+
+    private String getNewEventValue() {
+        return Messages.get("issue.event.referred.from.pullrequest",
+                this.contributor.loginId, this.fromBranch, this.toBranch,
+                routes.PullRequestApp.pullRequest(this.toProject.owner, this.toProject.name, this.number));
+    }
+
+    /**
+     * 풀리퀘가 수정될 때 기존의 모든 이슈 이벤트를 삭제하고 새로 추가한다.
+     */
+    public void updateIssueEvents() {
+        deleteIssueEvents();
+        addNewIssueEvents();
+    }
+
+    /**
+     * 풀리퀘가 삭제될 때 관련있는 모든 이슈 이벤트를 삭제한다.
+     */
+    public void deleteIssueEvents() {
+        String newValue = getNewEventValue();
+
+        List<IssueEvent> oldEvents = IssueEvent.find.where()
+                .eq("newValue", newValue)
+                .eq("senderLoginId", this.contributor.loginId)
+                .eq("eventType", EventType.ISSUE_REFERRED)
+                .findList();
+
+        for(IssueEvent event : oldEvents) {
+            event.delete();
+        }
+    }
+
+    @Override
+    public void delete() {
+        deleteIssueEvents();
+        super.delete();
+    }
+
 }
app/models/enumeration/EventType.java
--- app/models/enumeration/EventType.java
+++ app/models/enumeration/EventType.java
@@ -15,7 +15,8 @@
     NEW_COMMENT("notification.type.new.comment", 7),
     NEW_SIMPLE_COMMENT("notification.type.new.simple.comment", 8),
     MEMBER_ENROLL_REQUEST("notification.type.member.enroll", 9),
-    PULL_REQUEST_CONFLICTS("notification.type.pull.request.conflicts", 10);
+    PULL_REQUEST_CONFLICTS("notification.type.pull.request.conflicts", 10),
+    ISSUE_REFERRED("notification.type.issue.referred", 11);
 
     private String descr;
 
app/playRepository/GitRepository.java
--- app/playRepository/GitRepository.java
+++ app/playRepository/GitRepository.java
@@ -120,9 +120,13 @@
      * @return
      * @throws IOException
      */
-    public static Repository buildGitRepository(String ownerName, String projectName) throws IOException {
-        return new RepositoryBuilder().setGitDir(
-                new File(getGitDirectory(ownerName, projectName))).build();
+    public static Repository buildGitRepository(String ownerName, String projectName) {
+        try {
+            return new RepositoryBuilder().setGitDir(
+                    new File(getGitDirectory(ownerName, projectName))).build();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
     }
 
     /**
@@ -133,7 +137,7 @@
      * @throws IOException
      * @see #buildGitRepository(String, String)
      */
-    public static Repository buildGitRepository(Project project) throws IOException {
+    public static Repository buildGitRepository(Project project) {
         return buildGitRepository(project.owner, project.name);
     }
 
app/playRepository/RepositoryService.java
--- app/playRepository/RepositoryService.java
+++ app/playRepository/RepositoryService.java
@@ -1,20 +1,28 @@
 package playRepository;
 
+import actors.CommitCheckActor;
 import actors.ConflictCheckActor;
 import akka.actor.Props;
 import controllers.ProjectApp;
 import controllers.UserApp;
+import models.CommitCheckMessage;
 import models.ConflictCheckMessage;
 import models.Project;
 import models.User;
 
+import org.apache.commons.lang.StringUtils;
 import org.codehaus.jackson.node.ObjectNode;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.NoHeadException;
 import org.eclipse.jgit.errors.AmbiguousObjectException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.revwalk.filter.CommitTimeRevFilter;
 import org.eclipse.jgit.transport.PacketLineOut;
 import org.eclipse.jgit.transport.PostReceiveHook;
 import org.eclipse.jgit.transport.ReceiveCommand;
@@ -35,15 +43,8 @@
 import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
 import java.io.*;
-import java.util.Collection;
-import java.util.Date;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.ArrayList;
-import java.util.List;
+
+import java.util.*;
 
 /**
  * 저장소 관련 서비스를 제공하는 클래스
@@ -382,6 +383,12 @@
             public void onPostReceive(ReceivePack receivePack, Collection<ReceiveCommand> commands) {
                 updateLastPushedDate();
                 conflictCheck(commands);
+                commitCheck(commands);
+            }
+
+            private void commitCheck(Collection<ReceiveCommand> commands) {
+                CommitCheckMessage message = new CommitCheckMessage(commands, project);
+                Akka.system().actorOf(new Props(CommitCheckActor.class)).tell(message, null);
             }
 
             /*
app/views/issue/view.scala.html
--- app/views/issue/view.scala.html
+++ app/views/issue/view.scala.html
@@ -29,8 +29,8 @@
 }
 
 @linkToUser(loginId: String, loginName: String, showAvatar: Boolean = true) = {
-    @loginId match { 
-    case (loginId: String) => { 
+    @loginId match {
+    case (loginId: String) => {
         @if(showAvatar){ @avatarByLoginId(loginId, loginName) }
         <a href="@routes.UserApp.userInfo(loginId)" class="usf-group" data-toggle="tooltip" data-placement="top" title="@loginName">
             <strong>@loginId</strong>
@@ -49,7 +49,7 @@
     }
 }
 @isAuthorComment(commentId: String) = @{
-    if(commentId == UserApp.currentUser().loginId) {"author"}   
+    if(commentId == UserApp.currentUser().loginId) {"author"}
 }
 
 @getissueStateIcons(state: String) = @{
@@ -101,11 +101,11 @@
             <div class="content markdown-wrap markdown-before" markdown="true">@issue.body</div>
             <div class="attachments" data-resourceType="@ResourceType.ISSUE_POST" data-resourceId="@issue.id"></div>
         </div>
-        
+
         <div class="span3 mb20">
             <div class="author-info">
                 <form id="issueUpdateForm" action="@routes.IssueApp.massUpdate(project.owner, project.name)" method="post" class="frm-wrap">
-                    <input type="hidden" name="issues[0].id" value="@issue.id" /> 
+                    <input type="hidden" name="issues[0].id" value="@issue.id" />
 
                     @**<!-- author  -->**@
                     <dl class="author">
@@ -125,7 +125,7 @@
                         </dd>
                     </dl>
                     @**<!-- // -->**@
-                    
+
                     @**<!-- state  -->**@
                     <!--
                     <dl>
@@ -144,7 +144,7 @@
                         </dd>
                     </dl>
                     -->
-                    
+
                     @**<!-- assignee  -->**@
                     <dl>
                         <dt>@Messages("issue.assignee")</dt>
@@ -191,7 +191,7 @@
                             <a href="@routes.UserApp.userInfo(issue.assignee.user.loginId)" class="usf-group">
                                 <span class="avatar-wrap smaller">
                                     <img src="@User.findByLoginId(issue.assignee.user.loginId).avatarUrl" width="20" height="20">
-                                </span>                                
+                                </span>
                                 <strong class="name">@issue.assigneeName</strong>
                                 <span class="loginid"> <strong>@{"@"}</strong>@issue.assignee.user.loginId</span>
                             </a>
@@ -240,10 +240,10 @@
                         </dd>
                     </dl>
                     @**<!-- // -->**@
-                    
+
                 </form>
             </div>
-        </div>        
+        </div>
     </div>
 
     <div class="board-footer board-actrow">
@@ -258,8 +258,9 @@
         @if(isAllowed(UserApp.currentUser(), issue.asResource(), Operation.UPDATE)) {
         <a href="@routes.IssueApp.editIssueForm(project.owner, project.name, issue.getNumber)" class="ybtn">@Messages("button.edit")</a>
         }
-        
+
         <button id="watch-button" type="button" class="ybtn" data-toggle="button" data-watching="@if(issue.getWatchers.contains(UserApp.currentUser())){true}else{false}">
+
             @if(issue.getWatchers.contains(UserApp.currentUser())) {
                 @Messages("project.unwatch")
             } else {
@@ -282,7 +283,7 @@
                     <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>
                 <div class="media-body">
                     <div class="meta-info">
                         <span class="comment_author pull-left">
@@ -297,9 +298,9 @@
                         </span>
                         }
                     </div>
-                    
+
                     <div class="comment-body markdown-wrap markdown-before" markdown="true">@comment.contents</div>
-                    
+
                     <div class="attachments pull-right" data-resourceType="@ResourceType.ISSUE_COMMENT" data-resourceId="@comment.id"></div>
                 </div>
             </li>
@@ -316,6 +317,9 @@
                             <span class="state changed"><i class="yobicon-ftpaccounts"></i> @Messages("issue.state.assigned")</span>
                             @Html(Messages(assginedMesssage(event.newValue, user), linkToUser(user.loginId, user.name), linkToUser(event.newValue,user.name, true)))
                         }
+                        case EventType.ISSUE_REFERRED => {
+                            @Html(event.newValue) by @linkToUser(user.loginId, user.name)
+                        }
                         case _ => {
                             @event.newValue by @linkToUser(user.loginId, user.name)
                         }
 
conf/evolutions/default/36.sql (added)
+++ conf/evolutions/default/36.sql
@@ -0,0 +1,9 @@
+# --- !Ups
+
+ALTER TABLE issue_event DROP CONSTRAINT IF EXISTS ck_issue_event_event_type;
+ALTER TABLE issue_event ADD constraint ck_issue_event_event_type check (event_type in ('NEW_ISSUE','NEW_POSTING','ISSUE_ASSIGNEE_CHANGED','ISSUE_STATE_CHANGED','NEW_COMMENT','NEW_PULL_REQUEST','NEW_SIMPLE_COMMENT','PULL_REQUEST_STATE_CHANGED', 'ISSUE_REFERRED'));
+
+# --- !Downs
+
+ALTER TABLE issue_event DROP CONSTRAINT IF EXISTS ck_issue_event_event_type;
+ALTER TABLE issue_event ADD CONSTRAINT ck_issue_event_event_type check (event_type in ('NEW_ISSUE','NEW_POSTING','ISSUE_ASSIGNEE_CHANGED','ISSUE_STATE_CHANGED','NEW_COMMENT','NEW_PULL_REQUEST','NEW_SIMPLE_COMMENT','PULL_REQUEST_STATE_CHANGED'));(No newline at end of file)
conf/messages.en
--- conf/messages.en
+++ conf/messages.en
@@ -20,7 +20,7 @@
 # - user
 # - userinfo
 # - validation
-# 
+#
 app.description = Make it better and simpler!
 app.name = Yobi
 app.restart.notice = The server needs to be restarted.
@@ -154,6 +154,8 @@
 issue.event.closed = {0} closed this issue
 issue.event.open = {0} reopened this issue
 issue.event.unassigned = {0} unassinged this issue
+issue.event.referred.from.pullrequest = {0} referred this issue in a pullrequest, <a href="{3}">from {1} to {2}</a>.
+issue.event.referred.from.commit = {0} referred this issue in a commit, <a href="{2}">{1}</a>.
 issue.is.empty = There is no Issue.
 issue.list.all = All Issues
 issue.list.assignedToMe = Assigned to me
@@ -228,6 +230,7 @@
 issue.update.milestone = Update milestone
 issue.update.state = Update state
 issue.watch.start = You will receive notifications of this issue
+<<<<<<< HEAD
 label = Label
 label.add = Add Label
 label.addNewCategory = Add new category
conf/messages.ko
--- conf/messages.ko
+++ conf/messages.ko
@@ -20,7 +20,7 @@
 # - user
 # - userinfo
 # - validation
-# 
+#
 app.description = Make it better and simpler!
 app.name = Yobi
 app.restart.notice = 서버를 재시작해야합니다.
@@ -154,6 +154,8 @@
 issue.event.closed = {0}님이 이 이슈를 닫았습니다.
 issue.event.open = {0}님이 이 이슈를 다시 열었습니다.
 issue.event.unassigned = {0}님이 이 이슈의 담당자를 "없음"으로 설정하였습니다.
+issue.event.referred.from.pullrequest = {0}님이 <a href="{3}">{1}에서 {2}로 보내는 코드</a>에서 이 이슈를 언급했습니다.
+issue.event.referred.from.commit = {0}님이 <a href="{2}">{1}</a>에서 이 이슈를 언급했습니다.
 issue.is.empty = 등록된 이슈가 없습니다.
 issue.list.all = 전체 이슈
 issue.list.assignedToMe = 나에게 할당된 이슈
@@ -228,6 +230,7 @@
 issue.update.milestone = 마일스톤 변경
 issue.update.state = 상태 변경
 issue.watch.start = 이제 이 이슈에 관한 알림을 받습니다
+<<<<<<< HEAD
 label = 라벨
 label.add = 라벨 추가
 label.addNewCategory = 새 분류 추가
 
test/models/PullRequestTest.java (added)
+++ test/models/PullRequestTest.java
@@ -0,0 +1,33 @@
+package models;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+/**
+ * @author Keesun Baik
+ */
+public class PullRequestTest {
+
+    @Test
+    public void addIssueEvent() {
+        // Given
+        Pattern issuePattern = Pattern.compile("#\\d+");
+
+        // When
+        Matcher m = issuePattern.matcher("blah blah #12, sdl #13 sldkfjsd");
+
+        // Then
+        List<String> numberTexts = new ArrayList<>();
+        while(m.find()) {
+            numberTexts.add(m.group());
+        }
+        assertThat(numberTexts.size()).isEqualTo(2);
+    }
+
+}
Add a comment
List