(refs #73)Add Wiki conflict detection and some fix.

This commit is contained in:
takezoe
2013-10-04 03:48:51 +09:00
parent ed713d80a9
commit f4f2bf34fc
3 changed files with 72 additions and 44 deletions

View File

@@ -1,13 +1,14 @@
package app package app
import service._ import service._
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator, JGitUtil, StringUtil} import util._
import util.Directory._ import util.Directory._
import util.ControlUtil._ import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.api.errors.PatchApplyException
import org.scalatra.FlashMapSupport import org.scalatra.FlashMapSupport
import service.WikiService.WikiPageInfo
import scala.Some
class WikiController extends WikiControllerBase class WikiController extends WikiControllerBase
with WikiService with RepositoryService with AccountService with ActivityService with WikiService with RepositoryService with AccountService with ActivityService
@@ -17,20 +18,22 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
self: WikiService with RepositoryService with ActivityService self: WikiService with RepositoryService with ActivityService
with CollaboratorsAuthenticator with ReferrerAuthenticator => with CollaboratorsAuthenticator with ReferrerAuthenticator =>
case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String) case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String)
val newForm = mapping( val newForm = mapping(
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename, unique))), "pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename, unique))),
"content" -> trim(label("Content" , text(required))), "content" -> trim(label("Content" , text(required, conflictForNew))),
"message" -> trim(label("Message" , optional(text()))), "message" -> trim(label("Message" , optional(text()))),
"currentPageName" -> trim(label("Current page name" , text())) "currentPageName" -> trim(label("Current page name" , text())),
"id" -> trim(label("Latest commit id" , text()))
)(WikiPageEditForm.apply) )(WikiPageEditForm.apply)
val editForm = mapping( val editForm = mapping(
"pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename))), "pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename))),
"content" -> trim(label("Content" , text(required))), "content" -> trim(label("Content" , text(required, conflictForEdit))),
"message" -> trim(label("Message" , optional(text()))), "message" -> trim(label("Message" , optional(text()))),
"currentPageName" -> trim(label("Current page name" , text(required))) "currentPageName" -> trim(label("Current page name" , text(required))),
"id" -> trim(label("Latest commit id" , text(required)))
)(WikiPageEditForm.apply) )(WikiPageEditForm.apply)
get("/:owner/:repository/wiki")(referrersOnly { repository => get("/:owner/:repository/wiki")(referrersOnly { repository =>
@@ -60,45 +63,43 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository => get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository =>
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
val Array(from, to) = params("commitId").split("\\.\\.\\.")
defining(params("commitId").split("\\.\\.\\.")){ case Array(from, to) => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => wiki.html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true), repository,
wiki.html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true), repository, hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
}
} }
}) })
get("/:owner/:repository/wiki/_compare/:commitId")(referrersOnly { repository => get("/:owner/:repository/wiki/_compare/:commitId")(referrersOnly { repository =>
defining(params("commitId").split("\\.\\.\\.")){ case Array(from, to) => val Array(from, to) = params("commitId").split("\\.\\.\\.")
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
wiki.html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true), repository, using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info")) wiki.html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true), repository,
} hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info"))
} }
}) })
get("/:owner/:repository/wiki/:page/_revert/:commitId")(collaboratorsOnly { repository => get("/:owner/:repository/wiki/:page/_revert/:commitId")(collaboratorsOnly { repository =>
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
val Array(from, to) = params("commitId").split("\\.\\.\\.")
defining(params("commitId").split("\\.\\.\\.")){ case Array(from, to) => if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, Some(pageName))){
if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, Some(pageName))){ redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}")
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}") } else {
} else { flash += "info" -> "This patch was not able to be reversed."
flash += "info" -> "This patch was not able to be reversed." redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_compare/${from}...${to}")
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_compare/${from}...${to}")
}
} }
}) })
get("/:owner/:repository/wiki/_revert/:commitId")(collaboratorsOnly { repository => get("/:owner/:repository/wiki/_revert/:commitId")(collaboratorsOnly { repository =>
defining(params("commitId").split("\\.\\.\\.")){ case Array(from, to) => val Array(from, to) = params("commitId").split("\\.\\.\\.")
if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, None)){
redirect(s"/${repository.owner}/${repository.name}/wiki/}") if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, None)){
} else { redirect(s"/${repository.owner}/${repository.name}/wiki/}")
flash += "info" -> "This patch was not able to be reversed." } else {
redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}") flash += "info" -> "This patch was not able to be reversed."
} redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}")
} }
}) })
@@ -110,7 +111,7 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) => post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) =>
defining(context.loginAccount.get){ loginAccount => defining(context.loginAccount.get){ loginAccount =>
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName, saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
form.content, loginAccount, form.message.getOrElse("")).map { commitId => form.content, loginAccount, form.message.getOrElse(""), Some(form.id)).map { commitId =>
updateLastActivityDate(repository.owner, repository.name) updateLastActivityDate(repository.owner, repository.name)
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId) recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
} }
@@ -125,7 +126,7 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) => post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) =>
defining(context.loginAccount.get){ loginAccount => defining(context.loginAccount.get){ loginAccount =>
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName, saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
form.content, loginAccount, form.message.getOrElse("")) form.content, loginAccount, form.message.getOrElse(""), None)
updateLastActivityDate(repository.owner, repository.name) updateLastActivityDate(repository.owner, repository.name)
recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName) recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
@@ -160,9 +161,16 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
}) })
get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository => get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository =>
getFileContent(repository.owner, repository.name, multiParams("splat").head).map { content => val path = multiParams("splat").head
contentType = "application/octet-stream"
content getFileContent(repository.owner, repository.name, path).map { bytes =>
val mimeType = FileUtil.getMimeType(path)
contentType = if(mimeType == "application/octet-stream" && FileUtil.isText(bytes)){
"text/plain"
} else {
mimeType
}
bytes
} getOrElse NotFound } getOrElse NotFound
}) })
@@ -182,5 +190,22 @@ trait WikiControllerBase extends ControllerBase with FlashMapSupport {
} }
} }
private def conflictForNew: Constraint = new Constraint(){
override def validate(name: String, value: String): Option[String] = {
optionIf(targetWikiPage.nonEmpty){
Some("Someone has created the wiki since you started. Please reload this page and re-apply your changes.")
}
}
}
private def conflictForEdit: Constraint = new Constraint(){
override def validate(name: String, value: String): Option[String] = {
optionIf(targetWikiPage.map(_.id != params("id")).getOrElse(true)){
Some("Someone has edited the wiki since you started. Please reload this page and re-apply your changes.")
}
}
}
private def targetWikiPage = getWikiPage(params("owner"), params("repository"), params("pageName"))
} }

View File

@@ -19,8 +19,9 @@ object WikiService {
* @param content the page content * @param content the page content
* @param committer the last committer * @param committer the last committer
* @param time the last modified time * @param time the last modified time
* @param id the latest commit id
*/ */
case class WikiPageInfo(name: String, content: String, committer: String, time: Date) case class WikiPageInfo(name: String, content: String, committer: String, time: Date, id: String)
/** /**
* The model for wiki page history. * The model for wiki page history.
@@ -43,7 +44,7 @@ trait WikiService {
if(!dir.exists){ if(!dir.exists){
try { try {
JGitUtil.initRepository(dir) JGitUtil.initRepository(dir)
saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit") saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None)
} finally { } finally {
// once delete cloned repository because initial cloned repository does not have 'branch.master.merge' // once delete cloned repository because initial cloned repository does not have 'branch.master.merge'
FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner, repository)) FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner, repository))
@@ -59,7 +60,7 @@ trait WikiService {
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
optionIf(!JGitUtil.isEmpty(git)){ optionIf(!JGitUtil.isEmpty(git)){
JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file => JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file =>
WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time) WikiPageInfo(file.name, new String(git.getRepository.open(file.id).getBytes, "UTF-8"), file.committer, file.time, file.commitId)
} }
} }
} }
@@ -148,7 +149,7 @@ trait WikiService {
* Save the wiki page. * Save the wiki page.
*/ */
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String, def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
content: String, committer: model.Account, message: String): Option[String] = { content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = {
LockUtil.lock(s"${owner}/${repository}/wiki"){ LockUtil.lock(s"${owner}/${repository}/wiki"){
defining(Directory.getWikiWorkDir(owner, repository)){ workDir => defining(Directory.getWikiWorkDir(owner, repository)){ workDir =>
@@ -158,6 +159,7 @@ trait WikiService {
// write as file // write as file
using(Git.open(workDir)){ git => using(Git.open(workDir)){ git =>
defining(new File(workDir, newPageName + ".md")){ file => defining(new File(workDir, newPageName + ".md")){ file =>
// new page
val created = !file.exists val created = !file.exists
// created or updated // created or updated

View File

@@ -26,6 +26,7 @@
@helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, "width: 900px; height: 400px;", "") @helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, "width: 900px; height: 400px;", "")
<input type="text" name="message" value="" style="width: 900px;" placeholder="Write a small message here explaining this change. (Optional)"/> <input type="text" name="message" value="" style="width: 900px;" placeholder="Write a small message here explaining this change. (Optional)"/>
<input type="hidden" name="currentPageName" value="@pageName"/> <input type="hidden" name="currentPageName" value="@pageName"/>
<input type="hidden" name="id" value="@page.map(_.id)"/>
<input type="submit" value="Save" class="btn btn-success"> <input type="submit" value="Save" class="btn btn-success">
</form> </form>
} }