diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala index 8fea200..3cbd887 100644 --- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala +++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala @@ -1,6 +1,6 @@ package gitbucket.core.controller -import gitbucket.core.model.WebHook +import gitbucket.core.model.{CommitComment, CommitComments, IssueComment, WebHook} import gitbucket.core.plugin.PluginRegistry import gitbucket.core.pulls.html import gitbucket.core.service.CommitStatusService @@ -113,33 +113,89 @@ val name = repository.name getPullRequest(owner, name, issueId) map { case (issue, pullreq) => - using(Git.open(getRepositoryDir(owner, name))) { - git => - val (commits, diffs) = - getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo) - html.pullreq( - issue, - pullreq, - (commits.flatten - .map(commit => getCommitComments(owner, name, commit.id, true)) - .flatten - .toList ::: getComments(owner, name, issueId)) - .sortWith((a, b) => a.registeredDate before b.registeredDate), - getIssueLabels(owner, name, issueId), - getAssignableUserNames(owner, name), - getMilestonesWithIssueCount(owner, name), - getPriorities(owner, name), - getLabels(owner, name), - commits, - diffs, - isEditable(repository), - isManageable(repository), - hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount), - repository, - getRepository(pullreq.requestUserName, pullreq.requestRepositoryName), - flash.toMap.map(f => f._1 -> f._2.toString) - ) - } + val (commits, _) = + getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo) + + html.conversation( + issue, + pullreq, + commits.flatten, + getPullRequestComments(owner, name, issue.issueId, commits.flatten), + getIssueLabels(owner, name, issueId), + getAssignableUserNames(owner, name), + getMilestonesWithIssueCount(owner, name), + getPriorities(owner, name), + getLabels(owner, name), + isEditable(repository), + isManageable(repository), + hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount), + repository, + getRepository(pullreq.requestUserName, pullreq.requestRepositoryName), + flash.toMap.map(f => f._1 -> f._2.toString) + ) + +// html.pullreq( +// issue, +// pullreq, +// comments, +// getIssueLabels(owner, name, issueId), +// getAssignableUserNames(owner, name), +// getMilestonesWithIssueCount(owner, name), +// getPriorities(owner, name), +// getLabels(owner, name), +// commits, +// diffs, +// isEditable(repository), +// isManageable(repository), +// hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount), +// repository, +// getRepository(pullreq.requestUserName, pullreq.requestRepositoryName), +// flash.toMap.map(f => f._1 -> f._2.toString) +// ) + } + } getOrElse NotFound() + }) + + get("/:owner/:repository/pull/:id/commits")(referrersOnly { repository => + params("id").toIntOpt.flatMap { issueId => + val owner = repository.owner + val name = repository.name + getPullRequest(owner, name, issueId) map { + case (issue, pullreq) => + val (commits, _) = + getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo) + + html.commits( + issue, + pullreq, + commits, + getPullRequestComments(owner, name, issue.issueId, commits.flatten), + isManageable(repository), + repository + ) + } + } getOrElse NotFound() + }) + + get("/:owner/:repository/pull/:id/files")(referrersOnly { repository => + params("id").toIntOpt.flatMap { + issueId => + val owner = repository.owner + val name = repository.name + getPullRequest(owner, name, issueId) map { + case (issue, pullreq) => + val (commits, diffs) = + getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo) + + html.files( + issue, + pullreq, + diffs, + commits.flatten, + getPullRequestComments(owner, name, issue.issueId, commits.flatten), + isManageable(repository), + repository + ) } } getOrElse NotFound() }) diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala index 3da3594..b3b7685 100644 --- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala @@ -103,7 +103,8 @@ oldLineNumber: Option[Int], newLineNumber: Option[Int], content: String, - issueId: Option[Int] + issueId: Option[Int], + diff: Option[String] ) val uploadForm = mapping( @@ -138,7 +139,8 @@ "oldLineNumber" -> trim(label("Old line number", optional(number()))), "newLineNumber" -> trim(label("New line number", optional(number()))), "content" -> trim(label("Content", text(required))), - "issueId" -> trim(label("Issue Id", optional(number()))) + "issueId" -> trim(label("Issue Id", optional(number()))), + "diff" -> optional(text()) )(CommentForm.apply) /** @@ -562,6 +564,22 @@ form.newLineNumber, form.issueId ) + + for { + fileName <- form.fileName + diff <- form.diff + } { + saveCommitCommentDiff( + repository.owner, + repository.name, + id, + fileName, + form.oldLineNumber, + form.newLineNumber, + diff + ) + } + form.issueId match { case Some(issueId) => recordCommentPullRequestActivity( @@ -613,6 +631,22 @@ form.newLineNumber, form.issueId ) + + for { + fileName <- form.fileName + diff <- form.diff + } { + saveCommitCommentDiff( + repository.owner, + repository.name, + id, + fileName, + form.oldLineNumber, + form.newLineNumber, + diff + ) + } + val comment = getCommitComment(repository.owner, repository.name, commentId.toString).get form.issueId match { case Some(issueId) => diff --git a/src/main/scala/gitbucket/core/model/Comment.scala b/src/main/scala/gitbucket/core/model/Comment.scala index 8f79a52..8e5044c 100644 --- a/src/main/scala/gitbucket/core/model/Comment.scala +++ b/src/main/scala/gitbucket/core/model/Comment.scala @@ -1,6 +1,7 @@ package gitbucket.core.model +import java.util.Date -trait Comment { +sealed trait Comment { val commentedUserName: String val registeredDate: java.util.Date } @@ -87,3 +88,11 @@ updatedDate: java.util.Date, issueId: Option[Int] ) extends Comment + +case class CommitComments( + fileName: String, + commentedUserName: String, + registeredDate: Date, + comments: Seq[CommitComment], + diff: Option[String] +) extends Comment diff --git a/src/main/scala/gitbucket/core/service/CommitsService.scala b/src/main/scala/gitbucket/core/service/CommitsService.scala index c3142c7..237762d 100644 --- a/src/main/scala/gitbucket/core/service/CommitsService.scala +++ b/src/main/scala/gitbucket/core/service/CommitsService.scala @@ -1,9 +1,14 @@ package gitbucket.core.service +import java.io.File + import gitbucket.core.model.CommitComment import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile.profile.blockingApi._ import gitbucket.core.model.Profile.dateColumnType +import gitbucket.core.util.Directory._ +import gitbucket.core.util.StringUtil +import org.apache.commons.io.FileUtils trait CommitsService { @@ -68,4 +73,48 @@ def deleteCommitComment(commentId: Int)(implicit s: Session) = CommitComments filter (_.byPrimaryKey(commentId)) delete + + def saveCommitCommentDiff( + owner: String, + repository: String, + commitId: String, + fileName: String, + oldLine: Option[Int], + newLine: Option[Int], + diffJson: String + ): Unit = { + val dir = new java.io.File(getDiffDir(owner, repository), commitId) + if (!dir.exists) { + dir.mkdirs() + } + val file = diffFile(dir, fileName, oldLine, newLine) + FileUtils.write(file, diffJson, "UTF-8") + } + + def loadCommitCommentDiff( + owner: String, + repository: String, + commitId: String, + fileName: String, + oldLine: Option[Int], + newLine: Option[Int] + ): Option[String] = { + val dir = new java.io.File(getDiffDir(owner, repository), commitId) + val file = diffFile(dir, fileName, oldLine, newLine) + if (file.exists) { + Option(FileUtils.readFileToString(file, "UTF-8")) + } else None + } + + private def diffFile(dir: java.io.File, fileName: String, oldLine: Option[Int], newLine: Option[Int]): File = { + new File( + dir, + StringUtil.sha1( + fileName + + "_oldLine:" + oldLine.map(_.toString).getOrElse("") + + "_newLine:" + newLine.map(_.toString).getOrElse("") + ) + ) + } + } diff --git a/src/main/scala/gitbucket/core/service/PullRequestService.scala b/src/main/scala/gitbucket/core/service/PullRequestService.scala index ddcb345..0f7f50c 100644 --- a/src/main/scala/gitbucket/core/service/PullRequestService.scala +++ b/src/main/scala/gitbucket/core/service/PullRequestService.scala @@ -1,6 +1,6 @@ package gitbucket.core.service -import gitbucket.core.model.{Issue, PullRequest, CommitStatus, CommitState, CommitComment} +import gitbucket.core.model.{CommitComments => _, Session => _, _} import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile.profile.blockingApi._ import difflib.{Delta, DiffUtils} @@ -12,6 +12,7 @@ import gitbucket.core.view import gitbucket.core.view.helpers import org.eclipse.jgit.api.Git + import scala.collection.JavaConverters._ trait PullRequestService { self: IssuesService with CommitsService => @@ -314,11 +315,49 @@ helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime) } + // TODO Isolate to an another method? val diffs = JGitUtil.getDiffs(newGit, Some(oldId.getName), newId.getName, true, false) (commits, diffs) } + def getPullRequestComments(userName: String, repositoryName: String, issueId: Int, commits: Seq[CommitInfo])( + implicit s: Session + ): Seq[Comment] = { + (commits + .map(commit => getCommitComments(userName, repositoryName, commit.id, true)) + .flatten ++ getComments(userName, repositoryName, issueId)) + .groupBy { + case x: IssueComment => (Some(x.commentId), None, None, None) + case x: CommitComment if x.fileName.isEmpty => (Some(x.commentId), None, None, None) + case x: CommitComment => (None, x.fileName, x.oldLine, x.newLine) + case x => throw new MatchError(x) + } + .toSeq + .map { + // Normal comment + case ((Some(_), _, _, _), comments) => + comments.head + // Comment on a specific line of a commit + case ((None, Some(fileName), oldLine, newLine), comments) => + gitbucket.core.model.CommitComments( + fileName = fileName, + commentedUserName = comments.head.commentedUserName, + registeredDate = comments.head.registeredDate, + comments = comments.map(_.asInstanceOf[CommitComment]), + diff = loadCommitCommentDiff( + userName, + repositoryName, + comments.head.asInstanceOf[CommitComment].commitId, + fileName, + oldLine, + newLine + ) + ) + } + .sortWith(_.registeredDate before _.registeredDate) + } + } object PullRequestService { diff --git a/src/main/scala/gitbucket/core/util/Directory.scala b/src/main/scala/gitbucket/core/util/Directory.scala index 6e8a0b8..416b7a5 100644 --- a/src/main/scala/gitbucket/core/util/Directory.scala +++ b/src/main/scala/gitbucket/core/util/Directory.scala @@ -68,6 +68,12 @@ new File(getRepositoryFilesDir(owner, repository), "lfs") /** + * Directory for files which store diff fragment + */ + def getDiffDir(owner: String, repository: String): File = + new File(getRepositoryFilesDir(owner, repository), "diff") + + /** * Directory for uploaded files by the specified user. */ def getUserUploadDir(userName: String): File = diff --git a/src/main/scala/gitbucket/core/view/helpers.scala b/src/main/scala/gitbucket/core/view/helpers.scala index c5f1a40..848981a 100644 --- a/src/main/scala/gitbucket/core/view/helpers.scala +++ b/src/main/scala/gitbucket/core/view/helpers.scala @@ -3,6 +3,7 @@ import java.text.SimpleDateFormat import java.util.{Date, Locale, TimeZone} +import com.nimbusds.jose.util.JSONObjectUtils import gitbucket.core.controller.Context import gitbucket.core.model.CommitState import gitbucket.core.plugin.{PluginRegistry, RenderRequest} @@ -462,4 +463,44 @@ */ def readableSize(size: Option[Long]): String = FileUtil.readableSize(size.getOrElse(0)) + /** + * Make HTML fragment of the partial diff for a comment on a line of diff. + * + * @param jsonString JSON string which is stored in COMMIT_COMMENT table. + * @return HTML fragment of diff + */ + def diff(jsonString: String): Html = { + import org.json4s._ + import org.json4s.jackson.JsonMethods._ + implicit val formats = DefaultFormats + + val diff = parse(jsonString).extract[Seq[CommentDiffLine]] + + val sb = new StringBuilder() + sb.append("
""") + line.oldLine.foreach { oldLine => + sb.append(oldLine) + } + sb.append(" | ") + sb.append(s"""""") + line.newLine.foreach { newLine => + sb.append(newLine) + } + sb.append(" | ") + + sb.append(s"""""") + sb.append(StringUtil.escapeHtml(line.text)) + sb.append(" | ") + sb.append("
---|
+ + @issue.title + #@issue.issueId + + + + + +
+@pullreq.userName:@pullreq.branch
from@pullreq.requestUserName + :@pullreq.requestBranch
+ @gitbucket.core.helper.html.datetimeago(comment.registeredDate) + + }.getOrElse { + Closed + + @helpers.user(issue.openedUserName, styleClass = "username strong") + wants to merge @commits.size @helpers.plural(commits.size, "commit") + into@pullreq.userName:@pullreq.branch
from@pullreq.requestUserName + :@pullreq.requestBranch
+ + } + } else { + Open + + @helpers.user(issue.openedUserName, styleClass = "username strong") + wants to merge @commits.size @helpers.plural(commits.size, "commit") + into@pullreq.userName:@pullreq.branch
from@pullreq.requestUserName + :@pullreq.requestBranch
+ + } ++- Conversation
+ - Commits
+ - Files Changed
+
+