[Notice] Announcing the End of Demo Server [Read me]
박완순 2013-10-30
Merge pull request #172 from whiteship/yobi refs/heads/pullrequest-tab
@2d9caf40f1e535a09fb38a98d12ba25e3ea9c550
app/assets/stylesheets/less/_page.less
--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
@@ -3183,16 +3183,16 @@
           margin-bottom: 3px;
         }
 
-        > div {
-            display: table-cell;
-            vertical-align: middle;
-        }
-        .num {
+        .board-id {
             width: 35px;
             color: #B2B2B2;
             vertical-align: middle;
             font-size: 12px;
+            float:left;
+            line-height: 20px;
+            padding: 10px 0;
         }
+
         .attach-wrap {
             width: 35px;
             text-align: center;
@@ -3200,28 +3200,45 @@
                 margin-top: 5px;
             }
         }
+
+        .author-avatar-space{
+            width:38px;
+            height:38px;
+            float:left;
+            padding-right: 10px;
+            
+            .img-rounded {
+                .inline-block;
+                vertical-align:top;
+                width:100%;
+                height:100%;
+            }
+        }
+        
         .contents {
-            width: 80%;/*720px;*/
-            padding-right: 20px;
+            margin-left: 100px;
 
             label {
                 display:block;
                 margin:0;padding:0;
             }
+
             .title {
                 margin-bottom: 5px;
                 font-weight: bold;
                 font-size: 15px;
+                
                 .label-notice {
                     background:@secondary;
                 }
+
                 a {
-                   overflow:hidden;
-                    text-overflow:clip; /*ellipsis;*/
-                    -webkit-line-clamp:4;
-                    -webkit-box-orient:vertical;
+                    text-overflow: ellipsis; 
+                    white-space: nowrap; 
+                    overflow: hidden;
                 }
             }
+            
             .infos {
                 line-height: 14px;
                 font-size: 11px;
@@ -3238,54 +3255,6 @@
                     .num { color: @secondary; font-weight:bold; }
                     .icon {  margin-right: 3px; color : #51aacc; } 
                 }
-            }
-        }
-        .author-avatar-space{
-          width:32px;
-          padding-right: 10px;
-          .img-rounded {
-            .inline-block;
-            width:40px; height:40px;
-          }
-        }
-        .right-panel {
-            width: 160px;
-            padding-right: 10px;
-            .state {
-                float:right;
-                margin-top:7px;
-                margin-right:13px;
-                font-weight:bold;
-                font-size:11px;
-                color:#bbb; width:38px;
-                &.open {
-                    color:@blue;
-                }
-             }
-            .avatar-wrap {
-              float: right;
-            }
-            .empty-avatar-space{
-              width:32px;
-              display:inline-block;
-              float:right
-            }
-            .comment-wrap {
-                float: right;
-                margin-top: 7px;
-                margin-right:15px;
-
-                .ico {
-                    vertical-align: bottom;
-                    margin-right: 5px;
-                }
-                .num {
-                    color: #4489A4;
-                }
-            }
-            .img-rounded {
-                .inline-block;
-                width:32px; height:32px;
             }
         }
     }
@@ -4608,8 +4577,12 @@
         width: 40px;
     }
     .filename { float:left; line-height:30px; }
+    
+    .hide {
+        display:none;
+    }
 }
 
 div.diff-body[data-outdated="true"] tr:hover .icon-comment {
     visibility: hidden;
-}
+}
(No newline at end of file)
app/controllers/CodeHistoryApp.java
--- app/controllers/CodeHistoryApp.java
+++ app/controllers/CodeHistoryApp.java
@@ -12,6 +12,7 @@
 import models.enumeration.Operation;
 
 import models.enumeration.ResourceType;
+import org.apache.commons.lang.StringUtils;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.NoHeadException;
 import org.tmatesoft.svn.core.SVNException;
@@ -154,7 +155,10 @@
         Commit commit = repository.getCommit(commitId);
         Commit parentCommit = repository.getParentCommitOf(commitId);
         List<CommitComment> comments = CommitComment.find.where().eq("commitId",
-                commitId).eq("project.id", project.id).findList();
+                commitId).eq("project.id", project.id).order("createdDate").findList();
+
+        String selectedBranch = StringUtils.defaultIfBlank(request().getQueryString("branch"), "HEAD");
+        String path = StringUtils.defaultIfBlank(request().getQueryString("path"), "");
 
         if(project.vcs.equals(RepositoryService.VCS_SUBVERSION)) {
             String patch = repository.getPatch(commitId);
@@ -163,9 +167,7 @@
                 return notFound(ErrorViews.NotFound.render("error.notfound", project));
             }
 
-            String selectedBranch = request().getQueryString("branch");
-
-            return ok(svnDiff.render(project, commit, parentCommit, patch, comments, selectedBranch));
+            return ok(svnDiff.render(project, commit, parentCommit, patch, comments, selectedBranch, path));
         } else {
             List<FileDiff> fileDiffs = repository.getDiff(commitId);
 
@@ -173,9 +175,7 @@
                 return notFound(ErrorViews.NotFound.render("error.notfound", project));
             }
 
-            String selectedBranch = request().getQueryString("branch");
-
-            return ok(diff.render(project, commit, parentCommit, comments, selectedBranch, fileDiffs));
+            return ok(diff.render(project, commit, parentCommit, comments, selectedBranch, fileDiffs, path));
         }
     }
 
app/controllers/PullRequestApp.java
--- app/controllers/PullRequestApp.java
+++ app/controllers/PullRequestApp.java
@@ -534,13 +534,13 @@
 
         Call call = routes.PullRequestApp.pullRequest(userName, projectName, pullRequestNumber);
 
-        addNotification(pullRequest, call);
+        addNotification(pullRequest, call, State.OPEN, State.CLOSED);
 
         return redirect(call);
     }
 
-    private static void addNotification(PullRequest pullRequest, Call call) {
-        NotificationEvent notiEvent = NotificationEvent.addPullRequestUpdate(call, request(), pullRequest, State.OPEN, State.CLOSED);
+    private static void addNotification(PullRequest pullRequest, Call call, State from, State to) {
+        NotificationEvent notiEvent = NotificationEvent.addPullRequestUpdate(call, request(), pullRequest, from, to);
         PullRequestEvent.addEvent(notiEvent, pullRequest);
     }
 
@@ -567,7 +567,7 @@
 
         Call call = routes.PullRequestApp.pullRequest(userName, projectName, pullRequestNumber);
 
-        addNotification(pullRequest, call);
+        addNotification(pullRequest, call, State.OPEN, State.REJECTED);
 
         return redirect(call);
     }
@@ -595,7 +595,7 @@
 
         Call call = routes.PullRequestApp.pullRequest(userName, projectName, pullRequestNumber);
 
-        addNotification(pullRequest, call);
+        addNotification(pullRequest, call, State.REJECTED, State.OPEN);
 
         return redirect(call);
     }
@@ -775,7 +775,7 @@
         }
 
         List<CommitComment> comments = CommitComment.find.where().eq("commitId",
-                commitId).eq("project.id", project.id).findList();
+                commitId).eq("project.id", project.id).order("createdDate").findList();
 
         return ok(diff.render(pullRequest, commit, parentCommit, fileDiffs, comments));
     }
app/models/Attachment.java
--- app/models/Attachment.java
+++ app/models/Attachment.java
@@ -12,6 +12,7 @@
 
 import javax.persistence.*;
 
+import models.resource.GlobalResource;
 import models.resource.Resource;
 import models.resource.ResourceConvertible;
 
@@ -143,10 +144,10 @@
      * @return
      */
     public void moveTo(Resource to) {
-        if(to.getProject() != null) {
-            projectId = to.getProject().id;
-        } else {
+        if (to instanceof GlobalResource) {
             projectId = null;
+        } else {
+            projectId = to.getProject().id;
         }
         containerType = to.getType();
         containerId = to.getId();
@@ -223,9 +224,9 @@
     public boolean store(File file, String name, Resource container) throws IOException, NoSuchAlgorithmException {
         // Store the file as its SHA1 hash in filesystem, and record its
         // metadata - projectId, containerType, containerId, size and hash - in Database.
-
-        Project project = container.getProject();
-        this.projectId = project == null ? 0L : project.id;
+        if (!(container instanceof GlobalResource)) {
+            this.projectId = container.getProject().id;
+        }
         this.containerType = container.getType();
         this.containerId = container.getId();
 
@@ -325,50 +326,70 @@
      */
     @Override
     public Resource asResource() {
-        return new Resource() {
-            @Override
-            public String getId() {
-                return id.toString();
-            }
-
-            @Override
-            public Project getProject() {
-                if (projectId != null) {
-                    return Project.find.byId(projectId);
-                } else {
-                    return null;
+        if (projectId == null) {
+            return new GlobalResource() {
+                @Override
+                public String getId() {
+                    return id.toString();
                 }
-            }
 
-            @Override
-            public ResourceType getType() {
-                return ResourceType.ATTACHMENT;
-            }
+                @Override
+                public ResourceType getType() {
+                    return ResourceType.ATTACHMENT;
+                }
 
-            @Override
-            public Resource getContainer() {
-                return new Resource() {
-
-                    @Override
-                    public String getId() {
-                        return containerId;
-                    }
-
-                    @Override
-                    public Project getProject() {
-                        if (projectId != null) {
-                            return Project.find.byId(projectId);
-                        } else {
-                            return null;
+                @Override
+                public Resource getContainer() {
+                    return  new GlobalResource() {
+                        @Override
+                        public String getId() {
+                            return containerId;
                         }
-                    }
 
-                    @Override
-                    public ResourceType getType() {
-                        return containerType;
-                    }
-                };
-            }
-        };
+                        @Override
+                        public ResourceType getType() {
+                            return containerType;
+                        }
+                    };
+                }
+            };
+        } else {
+            return new Resource() {
+                @Override
+                public String getId() {
+                    return id.toString();
+                }
+
+                @Override
+                public Project getProject() {
+                    return Project.find.byId(projectId);
+                }
+
+                @Override
+                public ResourceType getType() {
+                    return ResourceType.ATTACHMENT;
+                }
+
+                @Override
+                public Resource getContainer() {
+                    return new Resource() {
+                        @Override
+                        public String getId() {
+                            return containerId;
+                        }
+
+                        @Override
+                        public Project getProject() {
+                            return Project.find.byId(projectId);
+                        }
+
+                        @Override
+                        public ResourceType getType() {
+                            return containerType;
+                        }
+                    };
+                }
+            };
+        }
     }
 }
app/models/Label.java
--- app/models/Label.java
+++ app/models/Label.java
@@ -1,6 +1,7 @@
 package models;
 
 import models.enumeration.ResourceType;
+import models.resource.GlobalResource;
 import models.resource.Resource;
 import models.resource.ResourceConvertible;
 import play.data.validation.Constraints.Required;
@@ -98,15 +99,10 @@
      */
     @Override
     public Resource asResource() {
-        return new Resource() {
+        return new GlobalResource() {
             @Override
             public String getId() {
                 return id.toString();
-            }
-
-            @Override
-            public Project getProject() {
-                return null;
             }
 
             @Override
app/models/NotificationEvent.java
--- app/models/NotificationEvent.java
+++ app/models/NotificationEvent.java
@@ -4,6 +4,7 @@
 import models.enumeration.RequestState;
 import models.enumeration.ResourceType;
 import models.enumeration.State;
+import models.resource.GlobalResource;
 import models.resource.Resource;
 import org.apache.commons.lang3.StringUtils;
 import org.joda.time.DateTime;
@@ -186,7 +187,11 @@
         default:
             Resource resource = getResource();
             if (resource != null) {
-                return resource.getProject();
+                if (resource instanceof GlobalResource) {
+                    return null;
+                } else {
+                    return resource.getProject();
+                }
             } else {
                 return null;
             }
app/models/NullUser.java
--- app/models/NullUser.java
+++ app/models/NullUser.java
@@ -2,6 +2,7 @@
 
 import controllers.UserApp;
 import models.enumeration.ResourceType;
+import models.resource.GlobalResource;
 import models.resource.Resource;
 import play.i18n.Messages;
 
@@ -31,15 +32,10 @@
 
     @Override
     public Resource asResource() {
-        return new Resource() {
+        return new GlobalResource() {
             @Override
             public String getId() {
                 return id.toString();
-            }
-
-            @Override
-            public Project getProject() {
-                return null;
             }
 
             @Override
app/models/Project.java
--- app/models/Project.java
+++ app/models/Project.java
@@ -9,6 +9,7 @@
 import models.enumeration.RequestState;
 import models.enumeration.ResourceType;
 import models.enumeration.RoleType;
+import models.resource.GlobalResource;
 import models.resource.Resource;
 
 import org.apache.commons.lang3.StringUtils;
@@ -485,16 +486,11 @@
      */
     @Override
     public Resource asResource() {
-        return new Resource() {
+        return new GlobalResource() {
 
             @Override
             public String getId() {
                 return id.toString();
-            }
-
-            @Override
-            public Project getProject() {
-                return null;
             }
 
             @Override
@@ -753,6 +749,14 @@
             label.delete();
         }
 
+        for(Issue issue : issues) {
+            issue.delete();
+        }
+
+        for(Posting posting : posts) {
+            posting.delete();
+        }
+
         super.delete();
     }
 
app/models/PullRequestComment.java
--- app/models/PullRequestComment.java
+++ app/models/PullRequestComment.java
@@ -79,7 +79,7 @@
 
             @Override
             public Project getProject() {
-                return null;
+                return pullRequest.asResource().getProject();
             }
 
             @Override
app/models/User.java
--- app/models/User.java
+++ app/models/User.java
@@ -7,6 +7,7 @@
 
 import controllers.UserApp;
 import models.enumeration.*;
+import models.resource.GlobalResource;
 import models.resource.Resource;
 import models.resource.ResourceConvertible;
 import models.support.FinderTemplate;
@@ -319,15 +320,10 @@
      */
     @Override
     public Resource asResource() {
-        return new Resource() {
+        return new GlobalResource() {
             @Override
             public String getId() {
                 return id.toString();
-            }
-
-            @Override
-            public Project getProject() {
-                return null;
             }
 
             @Override
@@ -338,15 +334,10 @@
     }
 
     public Resource avatarAsResource() {
-        return new Resource() {
+        return new GlobalResource() {
             @Override
             public String getId() {
                 return id.toString();
-            }
-
-            @Override
-            public Project getProject() {
-                return null;
             }
 
             @Override
 
app/models/resource/GlobalResource.java (added)
+++ app/models/resource/GlobalResource.java
@@ -0,0 +1,12 @@
+package models.resource;
+
+import models.Project;
+import models.enumeration.ResourceType;
+import models.resource.Resource;
+
+abstract public class GlobalResource extends Resource {
+    @Override
+    public Project getProject() {
+        throw new UnsupportedOperationException();
+    }
+}
app/playRepository/GitRepository.java
--- app/playRepository/GitRepository.java
+++ app/playRepository/GitRepository.java
@@ -483,9 +483,13 @@
 
     /**
      * Git 저장소 디렉토리를 삭제한다.
+     * 변경전 {@code repository.close()}를 통해 open된 repository의 리소스를 반환하고
+     * repository 내부에서 사용하는 {@code Cache}를 초기화하여 packFile의 참조를 제거한다.
      */
     @Override
     public void delete() {
+        repository.close();
+        WindowCache.reconfigure(new WindowCacheConfig());
         FileUtil.rm_rf(repository.getDirectory());
     }
 
app/utils/AccessControl.java
--- app/utils/AccessControl.java
+++ app/utils/AccessControl.java
@@ -4,6 +4,7 @@
 import models.enumeration.Operation;
 import models.enumeration.ResourceType;
 
+import models.resource.GlobalResource;
 import models.resource.Resource;
 
 public class AccessControl {
@@ -84,7 +85,8 @@
      * @param operation
      * @return
      */
-    private static boolean isGlobalResourceAllowed(User user, Resource resource, Operation operation) {
+    private static boolean isGlobalResourceAllowed(User user, GlobalResource resource,
+                                                   Operation operation) {
         // Temporary attachments are allowed only for the user who uploads them.
         if (resource.getType() == ResourceType.ATTACHMENT
                 && resource.getContainer().getType() == ResourceType.USER) {
@@ -195,16 +197,21 @@
      * @return {@code user}가 {@code resource}에 {@code operation}을
      *         하는 것이 허용되는지의 여부
      */
-    public static boolean isAllowed(User user, Resource resource, Operation operation) {
+    public static boolean isAllowed(User user, Resource resource, Operation operation)
+            throws IllegalStateException {
         if (user.isSiteManager()) {
             return true;
         }
 
-        Project project = resource.getProject();
-
-        if (project == null) {
-            return isGlobalResourceAllowed(user, resource, operation);
+        if (resource instanceof GlobalResource) {
+            return isGlobalResourceAllowed(user, (GlobalResource) resource, operation);
         } else {
+            Project project = resource.getProject();
+
+            if (project == null) {
+                throw new IllegalStateException("A project resource lost its project");
+            }
+
             return isProjectResourceAllowed(user, project, resource, operation);
         }
     }
app/views/board/partial_list.scala.html
--- app/views/board/partial_list.scala.html
+++ app/views/board/partial_list.scala.html
@@ -1,6 +1,6 @@
 @(post:models.Posting, project:Project)
 <li class="board">
-    <div class="num">
+    <div class="board-id">
         <a href="@routes.BoardApp.post(project.owner, project.name, post.getNumber)">@post.getNumber</a>
     </div>
     <div class="author-avatar-space">
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, 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],path:String)
 
 @import playRepository.RepositoryService
 @import java.net.URLEncoder
@@ -14,7 +14,7 @@
     <div class="code-browse-wrap">
         <div id="branches" class="btn-group branches pull-right" data-name="branch" data-activate="manual">
             <button class="btn dropdown-toggle large" data-toggle="dropdown">
-                <span class="d-label">@if(selectedBranch){ @selectedBranch } else { HEAD }</span>
+                <span class="d-label">@selectedBranch</span>
                 <span class="d-caret"><span class="caret"></span></span>
             </button>
             <ul class="dropdown-menu">
@@ -129,7 +129,7 @@
 
     <button id="watch-button" type="button" class="ybtn @if(commit.getWatchers(project).contains(UserApp.currentUser())) { active ybtn-watching }" data-toggle="button">@Messages("notification.watch")</button>
 
-    <a href="javascript: history.back();" class="ybtn pull-right">@Messages("button.list")</a>
+    <a href="@routes.CodeHistoryApp.history(project.owner, project.name, selectedBranch, path)" class="ybtn pull-right">@Messages("button.list")</a>
 
     <div id="minimap" class="minimap-outer">
         <div class="minimap-wrap">
app/views/code/history.scala.html
--- app/views/code/history.scala.html
+++ app/views/code/history.scala.html
@@ -22,9 +22,13 @@
         routes.CodeHistoryApp.historyUntilHead(project.owner, project.name)
     }
 }
-@getShowCommitURL(project:Project, commit:playRepository.Commit) = @{
+@getShowCommitURL(project:Project, commit:playRepository.Commit, path:String) = @{
     if(selectedBranch){
-        routes.CodeHistoryApp.show(project.owner, project.name, commit.getId()) + "?branch=" + URLEncoder.encode(selectedBranch, "UTF-8")
+        var queryString = "?branch=" + URLEncoder.encode(selectedBranch, "UTF-8");
+        if(path != null) {
+            queryString += "&path=" + path;
+        }
+        routes.CodeHistoryApp.show(project.owner, project.name, commit.getId()) + queryString;
     } else {
         routes.CodeHistoryApp.show(project.owner, project.name, commit.getId())
     }
@@ -105,15 +109,15 @@
                     </thead>
                     <tbody class="tbody">
                         @for(commit <- history.iterator()) {
-                        @defining(getShowCommitURL(project, commit)){ showCommitURL =>
+                        @defining(getShowCommitURL(project, commit, path)){ showCommitURL =>
                         <tr>
                             <td class="commit-id">
-                                <a href="@showCommitURL#@path" title="@Messages("code.showCommit")">
+                                <a href="@showCommitURL" title="@Messages("code.showCommit")">
                                     @commit.getShortId()
                                 </a>
                             </td>
                             <td class="messages">
-                                @defining(CommitComment.count(project, commit.getId, if(path != null){"/"+path}else{null})){ numOfComment =>
+                                @defining(CommitComment.count(project, commit.getId, path)){ numOfComment =>
                                 @if(numOfComment > 0) {
                                 <span class="number-of-comments"><i class="yobicon-comments"></i> @numOfComment</span>
                                 }
@@ -123,7 +127,7 @@
                                 @if(commitMsg.startsWith("Merge pull request")){
                                     @defining(commitMsg.split(" ")(3)) { pullRequestIdPart =>
                                         @defining(pullRequestIdPart.replace("#", "").toLong) { pullRequestId =>
-                                        <a href="@showCommitURL#@path">
+                                        <a href="@showCommitURL">
                                             @defining(commitMsg.indexOf(" #")) { indexOfPullRequestStart =>
                                                 @commitMsg.split("\n")(0).substring(0, indexOfPullRequestStart)
                                             }
@@ -131,7 +135,7 @@
                                         <a href="@routes.PullRequestApp.pullRequest(project.owner, project.name, pullRequestId)" class="secondary-txt">
                                             #@pullRequestId
                                         </a>
-                                        <a href="@showCommitURL#@path">
+                                        <a href="@showCommitURL">
                                             @defining(commitMsg.indexOf(commitMsg.split(" ")(3)) + commitMsg.split(" ")(3).length) { indexOfPullRequestIdEnd =>
                                             @commitMsg.split("\n")(0).substring(indexOfPullRequestIdEnd)
                                             }
@@ -139,7 +143,7 @@
                                         }
                                     }
                                 } else {
-                                    <a href="@showCommitURL#@path">@commitMsg.split("\n")(0)</a>
+                                    <a href="@showCommitURL">@commitMsg.split("\n")(0)</a>
                                 }
 
                                 @if(commitMsg.split("\n").length > 1){
app/views/code/svnDiff.scala.html
--- app/views/code/svnDiff.scala.html
+++ app/views/code/svnDiff.scala.html
@@ -1,4 +1,4 @@
-@(project: Project, commit:playRepository.Commit, parentCommit:playRepository.Commit, patch: String, comments:List[CommitComment], selectedBranch:String)
+@(project: Project, commit:playRepository.Commit, parentCommit:playRepository.Commit, patch: String, comments:List[CommitComment], selectedBranch:String, path:String)
 
 @import playRepository.RepositoryService
 @import java.net.URLEncoder
@@ -91,7 +91,7 @@
         </p>
         <pre class="commitMsg">@commit.getMessage</pre>
         <div class="diff-wrap">
-            <div id="commit" class="diff-body show-comments">@patch</div>
+            <div id="commit" data-commit-origin="true" class="diff-body show-comments hide">@patch</div>
         </div>
         <div id="compare" class="modal hide compare-wrap" tabindex="-1" role="dialog">
             <h4 class="path">
@@ -148,7 +148,7 @@
 
     <button id="watch-button" type="button" class="ybtn @if(commit.getWatchers(project).contains(UserApp.currentUser())) { active }" data-toggle="button">@Messages("notification.watch")</button>
 
-    <a href="javascript: history.back();" class="ybtn pull-right">@Messages("button.list")</a>
+    <a href="@routes.CodeHistoryApp.history(project.owner, project.name, selectedBranch, path)" class="ybtn pull-right">@Messages("button.list")</a>
 
     <div id="minimap" class="minimap-outer">
         <div class="minimap-wrap">
app/views/common/commentForm.scala.html
--- app/views/common/commentForm.scala.html
+++ app/views/common/commentForm.scala.html
@@ -16,11 +16,32 @@
                 }
                 @** end of fileUploader **@
                 <div class="right-txt">
-                    <button class="ybtn ybtn-success">@Messages("button.comment.new")</button>
+                    <button type="submit" class="ybtn ybtn-success">@Messages("button.comment.new")</button>
                 </div>
             </div>
         </div>
     </form>
+    <script type="text/javascript">
+        $(document).ready(function(){
+            var welForm = $("#comment-form");
+            var welBtnSubmit = welForm.find("button[type=submit]");
+            
+            welForm.on("submit", function(){
+                // set submit button as disabled
+                welBtnSubmit.attr("disabled", "disabled").addClass("ybtn-disabled");
+                
+                // restore button if cancel submitting with ESC key
+                if(!welForm.data("cancelerAttached")){
+                    welForm.data("cancelerAttached", true);
+                    $(window).on("keydown", function(weEvt){
+                        if(weEvt.keyCode === 27){
+                            welBtnSubmit.removeAttr("disabled").removeClass("ybtn-disabled");
+                        }
+                    });
+                }
+            });
+        });
+    </script>
 
 } else {
 
app/views/git/edit.scala.html
--- app/views/git/edit.scala.html
+++ app/views/git/edit.scala.html
@@ -105,7 +105,7 @@
             @helper.inputText(form("title"), 'class->"text title", 'maxlength -> "250", 'tabindex -> 1, 'placeholder->"Title")
             @helper.textarea(form("body"), 'markdown -> true, 'class->"text content", 'tabindex -> 2)
 
-            @common.fileUploader(ResourceType.PULL_REQUEST, null)
+            @common.fileUploader(ResourceType.PULL_REQUEST, pull.id)
 
             <div class="actions">
                 <button type="submit" class="ybtn ybtn-info">@Messages("button.save")</button>
app/views/git/list.scala.html
--- app/views/git/list.scala.html
+++ app/views/git/list.scala.html
@@ -1,4 +1,5 @@
 @(project: Project, page: com.avaje.ebean.Page[PullRequest], requestType: String)
+@import utils.AccessControl
 
 @projectLayout(Messages("menu.pullRequest"), project, utils.MenuType.PULL_REQUEST) {
 <div class="page">
@@ -11,10 +12,20 @@
             @Messages("pullRequest")
         </a>
         }
+        
+        @** 이 프로젝트가 복사본이 아니며, 현재 사용자가 복사본을 갖고 있고, 코드보내기 권한이 있는 경우 **@
+        @defining(Project.findByOwnerAndOriginalProject(UserApp.currentUser().loginId, project)){ myFork =>
+        @if(!project.isFork && myFork != null && AccessControl.isProjectResourceCreatable(UserApp.currentUser(), project, ResourceType.PULL_REQUEST)){
+        <a href="@routes.PullRequestApp.newPullRequestForm(myFork.owner, myFork.name)" class="ybtn ybtn-success">
+            <i class="yobicon-split yobicon-large"></i>
+            @Messages("pullRequest.toHere")
+        </a>
+        }
+        }
     </div>
 
     @if(project.hasForks() || project.isFork()) {
-    <ul class="nav nav-tabs cb">
+    <ul class="nav nav-tabs">
         @if(project.hasForks()){
             <li @if(requestType.equals("opened")){class="active"}>
                 <a href="@routes.PullRequestApp.pullRequests(project.owner, project.name)">
app/views/git/partial_diff.scala.html
--- app/views/git/partial_diff.scala.html
+++ app/views/git/partial_diff.scala.html
@@ -25,121 +25,120 @@
     }
 }
 
-            @helper.inputText(form("title"), 'class->"text title", 'maxlength -> "250", 'tabindex -> 1)
-            @helper.textarea(form("body"), 'markdown -> true, 'class->"text content", 'tabindex -> 2)
+@helper.inputText(form("title"), 'class->"text title", 'maxlength -> "250", 'tabindex -> 1)
+@helper.textarea(form("body"), 'markdown -> true, 'class->"text content", 'tabindex -> 2)
 
-            @common.fileUploader(ResourceType.PULL_REQUEST, null)
+@common.fileUploader(ResourceType.PULL_REQUEST, null)
+@common.markdown(project)
 
-			@common.markdown(project)
-			
-			<div class="actions">
-                <button type="submit" class="ybtn ybtn-success">@Messages("button.save")</button>
-                <a href="javascript:history.back();" class="ybtn">@Messages("button.cancel")</a>
+<div class="actions">
+    <button type="submit" class="ybtn ybtn-success">@Messages("button.save")</button>
+    <a href="javascript:history.back();" class="ybtn">@Messages("button.cancel")</a>
+</div>
+
+<div id="spin" style="position: absolute; top:50%; left:50%"></div>
+
+<div id="diff">
+@if(result != null) {
+    @if(result.getGitCommits().isEmpty()) {
+        <h4>@Messages("pullRequest.diff.noChanges")</h4>
+    } else {
+        <div style="margin-top:20px;">
+            @if(result.getGitConflicts() == null){
+            <div class="alert alert-success">
+                <h5>@Messages("pullRequest.is.safe")</h5>
             </div>
-			
-			<div id="spin" style="position: absolute; top:50%; left:50%"></div>
-			
-            <div id="diff">
-            @if(result != null) {
-			@if(result.getGitCommits().isEmpty()) {
-			<h4>@Messages("pullRequest.diff.noChanges")</h4>
-			} else {
-			<div style="margin-top:20px;">
-				@if(result.getGitConflicts() == null){
-				<div class="alert alert-success">
-					<h5>@Messages("pullRequest.is.safe")</h5>
-				</div>
-				} else {
-				<div class="alert alert-error">
-					<h5>@Messages("pullRequest.is.not.safe")</h5>
-				</div>
-				}
-			</div>
-
-		<ul class="nav nav-tabs nm">
+            } else {
+            <div class="alert alert-error">
+                <h5>@Messages("pullRequest.is.not.safe")</h5>
+            </div>
+            }
+        </div>
+    
+        <ul class="nav nav-tabs nm">
             <li class="active"><a href="#__commits" data-toggle="tab">@Messages("pullRequest.menu.commit")</a></li>
             <li><a href="#__changes" data-toggle="tab">@Messages("pullRequest.menu.changes")</a></li>
         </ul>
-        <div class="tab-content">	
-			<div id="__commits" class="code-browse-wrap tab-pane active">
-				<div id="history" class="commit-wrap mt20">
-				    <table class="code-table commits">
-				        <thead class="thead">
-				        <tr>
-				            <td class="commit-id"><strong>@{"@"}</strong></td>
-				            <td class="messages"><strong>@Messages("code.commitMsg")</strong></td>
-				            <td class="date"><strong>@Messages("code.commitDate")</strong></td>
-				            <td class="author"><strong>@Messages("code.author")</strong></td>
-				        </tr>
-				        </thead>
-				        <tbody class="tbody">
-				        @for(commit <- result.getGitCommits()) {
-				        <tr>
-				            <td class="commit-id">
-				                <a href="@routes.CodeHistoryApp.show(pullRequest.fromProject.owner, pullRequest.fromProject.name, commit.getId())">
-				                    @commit.getShortId()
-				                    <i class="yobicon-right"></i>
-				                </a>
-				            </td>
-				
-				            <td class="messages">
-				                @defining(CommitComment.count(pullRequest.fromProject, commit.getId, null)){ numOfComment =>
-				                    @if(numOfComment > 0) {
-				                    <span class="number-of-comments"><i class="yobicon-comments"></i> @numOfComment</span>
-				                    }
-				                }
-			
-								@defining(commit.getMessage()){ commitMsg =>
-								<a href="@routes.CodeHistoryApp.show(pullRequest.fromProject.owner, pullRequest.fromProject.name, commit.getId())">
-								    @commitMsg.split("\n")(0)
-								</a>
-								@if(commitMsg.split("\n").length > 1){
-								<button type="button" class="more"><i class="yobicon-ellipsis-horizontal"></i></button>
-								<pre class="hidden">@commitMsg.replace(commitMsg.split("\n")(0)+"\n", "")</pre>
-								}
-								}
-							</td>
-			                <td class="date">@agoString(ago(commit.getAuthorDate()))</td>
-			                <td class="author @commit.getAuthorEmail">
-			                    @defining(User.find.where.eq("email", commit.getAuthorEmail).findUnique) { user =>
-			                    @if(user != null) {
-			                    <a href="@routes.UserApp.userInfo(user.loginId)" class="avatar-wrap">
-			                        <img src="@user.avatarUrl" alt="@user.name" width="32" height="32"/>
-			                    </a>
-			                    } else {
-			                    <div class="avatar-wrap">
-			                        <img src="@urlToPicture(commit.getAuthorEmail(), 32)" width="32" height="32"/>
-			                    </div>
-			                    }
-			                    }
-			                </td>
-			            </tr>
-			            }
-			            </tbody>
-			        </table>
-			    </div>
-			</div>
-			   
-			<div id="__changes" class="tab-pane">
-				<div class="diff-body">
-				@views.html.partial_diff(pullRequest.getDiff)
-				</div>
-			</div>
-			</div>
-			}
-			}
-			</div>
-			<input type="hidden" id="commitChanged" value="@if(result != null){ @result.commitChanged } else {false}" />
-			<script type="text/javascript">
-			$(document).ready(function() {
-				$yobi.loadModule("code.Diff", {
-		            "welDiff": $("#pull-request-changes"),
-		            "sAttachmentAction": "@routes.AttachmentApp.uploadFile",
-		            "bCommentable": false,
-		            "sTplFileURLA"    : "@routes.CodeApp.codeBrowserWithBranch(pullRequest.toProject.owner, pullRequest.toProject.name, "${commitId}", "${path}")",
-		            "sTplFileURLB"    : "@routes.CodeApp.codeBrowserWithBranch(pullRequest.fromProject.owner, pullRequest.fromProject.name, "${commitId}", "${path}")",
-		            "sTplRawURLA"     : "@routes.CodeApp.showRawFile(pullRequest.toProject.owner, pullRequest.toProject.name, "${commitId}", "${path}")",
-		            "sTplRawURLB"     : "@routes.CodeApp.showRawFile(pullRequest.fromProject.owner, pullRequest.fromProject.name, "${commitId}", "${path}")"
-		        });
-			});
-			</script>
(No newline at end of file)
+        <div class="tab-content">    
+            <div id="__commits" class="code-browse-wrap tab-pane active">
+                <div id="history" class="commit-wrap mt20">
+                    <table class="code-table commits">
+                        <thead class="thead">
+                        <tr>
+                            <td class="commit-id"><strong>@{"@"}</strong></td>
+                            <td class="messages"><strong>@Messages("code.commitMsg")</strong></td>
+                            <td class="date"><strong>@Messages("code.commitDate")</strong></td>
+                            <td class="author"><strong>@Messages("code.author")</strong></td>
+                        </tr>
+                        </thead>
+                        <tbody class="tbody">
+                        @for(commit <- result.getGitCommits()) {
+                        <tr>
+                            <td class="commit-id">
+                                <a href="@routes.CodeHistoryApp.show(pullRequest.fromProject.owner, pullRequest.fromProject.name, commit.getId())">
+                                    @commit.getShortId()
+                                    <i class="yobicon-right"></i>
+                                </a>
+                            </td>
+                
+                            <td class="messages">
+                                @defining(CommitComment.count(pullRequest.fromProject, commit.getId, null)){ numOfComment =>
+                                    @if(numOfComment > 0) {
+                                    <span class="number-of-comments"><i class="yobicon-comments"></i> @numOfComment</span>
+                                    }
+                                }
+            
+                                @defining(commit.getMessage()){ commitMsg =>
+                                <a href="@routes.CodeHistoryApp.show(pullRequest.fromProject.owner, pullRequest.fromProject.name, commit.getId())">
+                                    @commitMsg.split("\n")(0)
+                                </a>
+                                @if(commitMsg.split("\n").length > 1){
+                                <button type="button" class="more"><i class="yobicon-ellipsis-horizontal"></i></button>
+                                <pre class="hidden">@commitMsg.replace(commitMsg.split("\n")(0)+"\n", "")</pre>
+                                }
+                                }
+                            </td>
+                            <td class="date">@agoString(ago(commit.getAuthorDate()))</td>
+                            <td class="author @commit.getAuthorEmail">
+                                @defining(User.find.where.eq("email", commit.getAuthorEmail).findUnique) { user =>
+                                @if(user != null) {
+                                <a href="@routes.UserApp.userInfo(user.loginId)" class="avatar-wrap">
+                                    <img src="@user.avatarUrl" alt="@user.name" width="32" height="32"/>
+                                </a>
+                                } else {
+                                <div class="avatar-wrap">
+                                    <img src="@urlToPicture(commit.getAuthorEmail(), 32)" width="32" height="32"/>
+                                </div>
+                                }
+                                }
+                            </td>
+                        </tr>
+                        }
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+               
+            <div id="__changes" class="tab-pane">
+                <div class="diff-body">
+                @views.html.partial_diff(pullRequest.getDiff)
+                </div>
+            </div>
+        </div>
+    }
+}
+</div>
+<input type="hidden" id="commitChanged" value="@if(result != null){ @result.commitChanged } else {false}" />
+<script type="text/javascript">
+$(document).ready(function() {
+    $yobi.loadModule("code.Diff", {
+        "welDiff"     : $("#pull-request-changes"),
+        "bCommentable": false,
+        "bUseUploader": false,
+        "sTplFileURLA": "@routes.CodeApp.codeBrowserWithBranch(pullRequest.toProject.owner, pullRequest.toProject.name, "${commitId}", "${path}")",
+        "sTplFileURLB": "@routes.CodeApp.codeBrowserWithBranch(pullRequest.fromProject.owner, pullRequest.fromProject.name, "${commitId}", "${path}")",
+        "sTplRawURLA" : "@routes.CodeApp.showRawFile(pullRequest.toProject.owner, pullRequest.toProject.name, "${commitId}", "${path}")",
+        "sTplRawURLB" : "@routes.CodeApp.showRawFile(pullRequest.fromProject.owner, pullRequest.fromProject.name, "${commitId}", "${path}")"
+    });
+});
+</script>
(No newline at end of file)
conf/messages
--- conf/messages
+++ conf/messages
@@ -26,7 +26,7 @@
 app.restart.notice = The server needs to be restarted.
 app.restart.welcome = Welcome!
 app.secret.notice = This form is to reset the Secret key.<br>Submitting seed generates random Secret key based the seed.<br>Feel free to submit long and complex value. You don't need to remember it.
-app.secret.warning.title = The Secret key MUST be secret. 
+app.secret.warning.title = The Secret key MUST be secret.
 app.secret.warning.desc = If a bad guy knows the secret, He can login as any user of your site!
 app.secret.welcome = Welcome to {0}!
 app.title = Collaborative Software Development Platform
@@ -233,7 +233,7 @@
 issue.state.enrolled = Enrolled
 issue.state.finished = Finished
 issue.state.open = Open
-issue.state.rejected = Rejected
+issue.state.rejected = Postponed
 issue.state.solved = Solved
 issue.unwatch.start = Notifications of this issue has muted
 issue.update.assignee = Update assignee
@@ -459,7 +459,7 @@
 pullRequest.help.message.2 = Please select your branch that contains your code to send and original project's branch to receive your code, and explain what you have done.
 pullRequest.help.message.3 = The member of the original project can accept your code or reject.
 pullRequest.is.empty = There are no pull requests
-pullRequest.is.merging = Checking whether code is safety. Please wait for a while to complete. 
+pullRequest.is.merging = Checking whether code is safety. Please wait for a while to complete.
 pullRequest.is.not.safe = This pull request can not be merged safely, There may be some conflicts.
 pullRequest.is.safe = This pull request can be merged safely.
 pullRequest.listEmpty = No request received yet.
@@ -495,12 +495,13 @@
 pullRequest.title.required = Input title.
 pullRequest.to = To
 pullRequest.toBranch.required = Select a branch that will receives the sending code.
+pullRequest.toHere = New PullRequest
 site = Site
 site.features.codeManagement = All your codes are stored in a version controlled system safely.
 site.features.codeReview = You can review all changes on code with your team before merging. Discussion makes your code better.
 site.features.issueTracker = Yobi provides an issue tracker to make you deal with your issues more easily and clearly.
 site.features.privateRepositories = Everyone has secrets. You can keep your secret codes at your private repositories.
-site.features.unlimitedProjects = You can create repositories as many as you want. Just create it. 
+site.features.unlimitedProjects = You can create repositories as many as you want. Just create it.
 site.features.workTeam = You can make a team for your project with simple and easy team management tools of Yobi.
 site.mail.authMethod = Authentication method
 site.mail.body = Body
conf/messages.ja
--- conf/messages.ja
+++ conf/messages.ja
@@ -496,6 +496,7 @@
 pullRequest.title.required = タイトルを入力してください
 pullRequest.to = コード 受ける場所
 pullRequest.toBranch.required = コードを受けてもらいたいブランチを選んでください
+pullRequest.toHere = こちらへプルリクエスト
 site = サイト
 site.features.codeManagement = All your codes are stored in a version controlled system safely.
 site.features.codeReview = You can review all changes on code with your team before merging. Discussion makes your code better.
conf/messages.ko
--- conf/messages.ko
+++ conf/messages.ko
@@ -26,7 +26,7 @@
 app.restart.notice = 서버를 재시작해야합니다.
 app.restart.welcome = 환영합니다!
 app.secret.notice = 이 폼은 사이트 비밀값을 생성하는 폼입니다.<br>임의의 문자열을 입력하시면 그 문자열을 시드로 하여 무작위로 비밀값을 생성합니다.<br>기억할 필요가 없으므로, 길고 복잡한 값을 입력하셔도 됩니다.
-app.secret.warning.title = 사이트 비밀값은 사이트 관리자외의 누구도 알아서는 안됩니다. 
+app.secret.warning.title = 사이트 비밀값은 사이트 관리자외의 누구도 알아서는 안됩니다.
 app.secret.warning.desc = 사이트 비밀값을 알고 있는 사람은 이 사이트의 어떠한 사용자 계정으로도 로그인을 할 수 있습니다!
 app.secret.welcome = {0}가 처음으로 시작되려는 순간입니다! (두둥!)
 app.title = 협업개발 플랫폼
@@ -298,7 +298,7 @@
 notification.none = 알림 메시지가 없습니다.
 notification.pullrequest.closed = 보낸 코드가 반영됨(merged)
 notification.pullrequest.conflicts = 충돌이 발생
-notification.pullrequest.rejected = 보낸 코드 거절
+notification.pullrequest.rejected = 보낸 코드 보류
 notification.pullrequest.reopened = 코드 주고받기 다시 열림
 notification.type.issue.assignee.changed = 이슈 담당자 변경
 notification.type.issue.state.changed = 이슈 상태 변경
@@ -313,7 +313,7 @@
 notification.type.pullrequest.conflicts = 코드보내기 충돌
 notification.type.pullrequest.merged = 코드보내기 반영됨(merged)
 notification.type.pullrequest.merged.conflict = 코드보내기 충돌
-notification.type.pullrequest.merged.resolved = 코드보내기 충돌 해결  
+notification.type.pullrequest.merged.resolved = 코드보내기 충돌 해결
 notification.type.pullrequest.state.changed = 코드보내기 상태 변경
 notification.watch = 지켜보기
 notification.will.help = 프로젝트를 지켜보면 다음 이벤트가 발생할 때 알림 메시지를 받습니다.
@@ -496,6 +496,7 @@
 pullRequest.title.required = 제목을 입력하세요.
 pullRequest.to = 코드 받을 곳
 pullRequest.toBranch.required = 코드를 받을 브랜치를 선택하세요.
+pullRequest.toHere = 이 프로젝트에 코드 보내기
 site = 사이트
 site.features.codeManagement = 작성한 코드는 모두 이력이 관리되는 형태로 안전하게 서버에 보관됩니다.
 site.features.codeReview = 변경된 코드를 보면서 팀원들과 토론해보세요. 코드의 완성도를 더욱 높일 수 있습니다.
public/javascripts/common/yobi.Files.js
--- public/javascripts/common/yobi.Files.js
+++ public/javascripts/common/yobi.Files.js
@@ -292,6 +292,7 @@
      * 
      * @param {HTMLElement} elContainer
      * @param {HTMLTextareaElement} elTextarea (Optional)
+     * @param {String} sNamespace
      * @return {Wrapped Element}
      */
     function _getUploader(elContainer, elTextarea, sNamespace){
@@ -301,7 +302,7 @@
         if($(elContainer).data("isYobiUploader") || $(elTextarea).data("isYobiUploader")){
             return false;
         }
-        
+
         _initElement({
             "elContainer": elContainer, 
             "elTextarea" : elTextarea,
@@ -392,6 +393,8 @@
         htElement.welInputFile.unbind();
         htElement.welContainer.unbind();
         htElement.welTextarea.unbind();
+        htElement.welContainer.data("isYobiUploader", false);
+        htElement.welTextarea.data("isYobiUploader", false);
     }
     
     /**
public/javascripts/service/yobi.board.Write.js
--- public/javascripts/service/yobi.board.Write.js
+++ public/javascripts/service/yobi.board.Write.js
@@ -91,7 +91,7 @@
                     "elContainer"  : htElement.welUploader,
                     "elTextarea"   : htElement.welTextarea,
                     "sTplFileItem" : htVar.sTplFileItem,
-                    "sUploaderId"  : Uploader.attr("data-namespace")
+                    "sUploaderId"  : oUploader.attr("data-namespace")
                 }));
             }
 		}
public/javascripts/service/yobi.code.Diff.js
--- public/javascripts/service/yobi.code.Diff.js
+++ public/javascripts/service/yobi.code.Diff.js
@@ -24,7 +24,11 @@
             _attachEvent();
             _render();
             
-            _initFileUploader();
+            // bUseUploader 를 명시적으로 false 로 지정한 경우
+            // code.Diff 에서는 파일업로더 초기화를 실행하지 않음
+            if(htVar.bUseUploader !== false){
+                _initFileUploader();
+            }
             _initFileDownloader();
             _initToggleCommentsButton();
             _initFileViewButton();
public/javascripts/service/yobi.code.SvnDiff.js
--- public/javascripts/service/yobi.code.SvnDiff.js
+++ public/javascripts/service/yobi.code.SvnDiff.js
@@ -50,7 +50,7 @@
             htVar.sTplMiniMapLink = '<a href="#${id}" style="top:${top}px; height:${height}px;"></a>';
             
             // yobi.Attachments
-            htVar.sTplFileItem = ('#tplAttachedFile').text();
+            htVar.sTplFileItem = $('#tplAttachedFile').text();
         }
 
         /**
@@ -134,6 +134,8 @@
                     window.scrollTo(0, welTarget.offset().top);
                 }
             }
+
+            $('[data-commit-origin="true"]').removeClass("hide");
         }
 
         /**
test/utils/AccessControlTest.java
--- test/utils/AccessControlTest.java
+++ test/utils/AccessControlTest.java
@@ -5,12 +5,14 @@
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.Assert;
 
 import play.test.Helpers;
 
 import static org.fest.assertions.Assertions.assertThat;
 
 import models.enumeration.Operation;
+import models.enumeration.State;
 
 public class AccessControlTest extends ModelTest<Role>{
     @Before
@@ -74,4 +76,30 @@
         assertThat(canUpdate).isEqualTo(false);
         assertThat(canDelete).isEqualTo(false);
     }
+
+    // AccessControl.isAllowed throws IllegalStateException if the resource
+    // belongs to a project but the project is missing.
+    @Test
+    public void isAllowed_lostProject() {
+        // Given
+        User author = User.findByLoginId("nori");
+        Project projectYobi = Project.findByOwnerAndProjectName("yobi", "projectYobi");
+        Issue issue = new Issue();
+        issue.setProject(projectYobi);
+        issue.setTitle("hello");
+        issue.setBody("world");
+        issue.setAuthor(author);
+        issue.state = State.OPEN;
+        issue.save();
+
+        // When
+        issue.project = null;
+
+        // Then
+        try {
+            AccessControl.isAllowed(author, issue.asResource(), Operation.READ);
+            Assert.fail();
+        } catch (IllegalStateException e) {
+        }
+    }
 }
Add a comment
List