diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala index f3d1105..5b3b3e2 100644 --- a/src/main/scala/app/RepositoryViewerController.scala +++ b/src/main/scala/app/RepositoryViewerController.scala @@ -15,7 +15,7 @@ import java.util.zip.{ZipEntry, ZipOutputStream} import jp.sf.amateras.scalatra.forms._ import org.eclipse.jgit.dircache.DirCache -import org.eclipse.jgit.revwalk.RevWalk +import org.eclipse.jgit.revwalk.{RevCommit, RevWalk} class RepositoryViewerController extends RepositoryViewerControllerBase with RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator @@ -26,14 +26,40 @@ trait RepositoryViewerControllerBase extends ControllerBase { self: RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator => - case class EditorForm(content: String, message: Option[String], charset: String) + case class EditorForm( + branch: String, + path: String, + content: String, + message: Option[String], + charset: String, + newFileName: String, + oldFileName: Option[String] + ) + + case class DeleteForm( + branch: String, + path: String, + message: Option[String], + fileName: String + ) val editorForm = mapping( - "content" -> trim(label("Content", text())), - "message" -> trim(label("Messgae", optional(text()))), - "charset" -> trim(label("Charset", text())) + "branch" -> trim(label("Branch", text(required))), + "path" -> trim(label("Path", text())), + "content" -> trim(label("Content", text(required))), + "message" -> trim(label("Message", optional(text()))), + "charset" -> trim(label("Charset", text(required))), + "newFileName" -> trim(label("Filename", text(required))), + "oldFileName" -> trim(label("Old filename", optional(text()))) )(EditorForm.apply) + val deleteForm = mapping( + "branch" -> trim(label("Branch", text(required))), + "path" -> trim(label("Path", text())), + "message" -> trim(label("Message", optional(text()))), + "fileName" -> trim(label("Filename", text(required))) + )(DeleteForm.apply) + /** * Returns converted HTML from Markdown for preview. */ @@ -82,96 +108,68 @@ } }) - /** - * Displays the file content of the specified branch or commit. - */ + get("/:owner/:repository/new/*")(collaboratorsOnly { repository => + val (branch, path) = splitPath(repository, multiParams("splat").head) + repo.html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList, + None, JGitUtil.ContentInfo("text", None, Some("UTF-8"))) + }) + get("/:owner/:repository/edit/*")(collaboratorsOnly { repository => - val (id, path) = splitPath(repository, multiParams("splat").head) + val (branch, path) = splitPath(repository, multiParams("splat").head) using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) + val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch)) - @scala.annotation.tailrec - def getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match { - case true if(walk.getPathString == path) => Some(walk.getObjectId(0)) - case true => getPathObjectId(path, walk) - case false => None - } - - using(new TreeWalk(git.getRepository)){ treeWalk => - treeWalk.addTree(revCommit.getTree) - treeWalk.setRecursive(true) - getPathObjectId(path, treeWalk) - } map { objectId => - // Viewer - val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize) - val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other" - val bytes = if(viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None - - val content = if(viewer == "other"){ - if(bytes.isDefined && FileUtil.isText(bytes.get)){ - // text - JGitUtil.ContentInfo("text", Some(StringUtil.convertFromByteArray(bytes.get)), Some(StringUtil.detectEncoding(bytes.get))) - } else { - // binary - JGitUtil.ContentInfo("binary", None, None) - } - } else { - // image or large - JGitUtil.ContentInfo(viewer, None, None) - } - - repo.html.editor(id, repository, path.split("/").toList, content, new JGitUtil.CommitInfo(revCommit)) + getPathObjectId(git, path, revCommit).map { objectId => + val paths = path.split("/") + repo.html.editor(branch, repository, paths.take(paths.size - 1).toList, Some(paths.last), + JGitUtil.getContentInfo(git, path, objectId)) } getOrElse NotFound } }) - post("/:owner/:repository/edit/*", editorForm)(collaboratorsOnly { (form, repository) => - val (id, path) = splitPath(repository, multiParams("splat").head) + get("/:owner/:repository/remove/*")(collaboratorsOnly { repository => + val (branch, path) = splitPath(repository, multiParams("splat").head) + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch)) - LockUtil.lock(s"${repository.owner}/${repository.name}"){ - using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - val loginAccount = context.loginAccount.get - val builder = DirCache.newInCore.builder() - val inserter = git.getRepository.newObjectInserter() - val headName = s"refs/heads/${id}" - val headTip = git.getRepository.resolve(s"refs/heads/${id}") - - JGitUtil.processTree(git, headTip){ (treePath, tree) => - if(treePath != path){ - builder.add(JGitUtil.createDirCacheEntry(treePath, tree.getEntryFileMode, tree.getEntryObjectId)) - } - } - - builder.add(JGitUtil.createDirCacheEntry(path, FileMode.REGULAR_FILE, - inserter.insert(Constants.OBJ_BLOB, form.content.getBytes(form.charset)))) - builder.finish() - - val commitId = JGitUtil.createNewCommit(git, inserter, headTip, builder.getDirCache.writeTree(inserter), - loginAccount.fullName, loginAccount.mailAddress, form.message.getOrElse(s"Update ${path.split("/").last}")) - - inserter.flush() - inserter.release() - - // update refs - val refUpdate = git.getRepository.updateRef(headName) - refUpdate.setNewObjectId(commitId) - refUpdate.setForceUpdate(false) - refUpdate.setRefLogIdent(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)) - //refUpdate.setRefLogMessage("merged", true) - refUpdate.update() - - // record activity - recordPushActivity(repository.owner, repository.name, loginAccount.userName, id, - List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId)))) - - // TODO invoke hook - - redirect(s"/${repository.owner}/${repository.name}/blob/${id}/${path}") - } + getPathObjectId(git, path, revCommit).map { objectId => + val paths = path.split("/") + repo.html.delete(branch, repository, paths.take(paths.size - 1).toList, paths.last, + JGitUtil.getContentInfo(git, path, objectId)) + } getOrElse NotFound } }) + post("/:owner/:repository/create", editorForm)(collaboratorsOnly { (form, repository) => + commitFile(repository, form.branch, form.path, Some(form.newFileName), None, form.content, form.charset, + form.message.getOrElse(s"Create ${form.newFileName}")) + + redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${ + if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}" + }") + }) + + post("/:owner/:repository/update", editorForm)(collaboratorsOnly { (form, repository) => + commitFile(repository, form.branch, form.path, Some(form.newFileName), form.oldFileName, form.content, form.charset, + if(form.oldFileName.exists(_ == form.newFileName)){ + form.message.getOrElse(s"Update ${form.newFileName}") + } else { + form.message.getOrElse(s"Rename ${form.oldFileName.get} to ${form.newFileName}") + }) + + redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${ + if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}" + }") + }) + + post("/:owner/:repository/remove", deleteForm)(collaboratorsOnly { (form, repository) => + commitFile(repository, form.branch, form.path, None, Some(form.fileName), "", "", + form.message.getOrElse(s"Delete ${form.fileName}")) + + redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}${if(form.path.length == 0) "" else form.path}") + }) + /** * Displays the file content of the specified branch or commit. */ @@ -181,19 +179,7 @@ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) - - @scala.annotation.tailrec - def getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match { - case true if(walk.getPathString == path) => Some(walk.getObjectId(0)) - case true => getPathObjectId(path, walk) - case false => None - } - - using(new TreeWalk(git.getRepository)){ treeWalk => - treeWalk.addTree(revCommit.getTree) - treeWalk.setRecursive(true) - getPathObjectId(path, treeWalk) - } map { objectId => + getPathObjectId(git, path, revCommit).map { objectId => if(raw){ // Download defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes => @@ -201,26 +187,8 @@ bytes } } else { - // Viewer - val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize) - val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other" - val bytes = if(viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None - - val content = if(viewer == "other"){ - if(bytes.isDefined && FileUtil.isText(bytes.get)){ - // text - JGitUtil.ContentInfo("text", Some(StringUtil.convertFromByteArray(bytes.get)), Some(StringUtil.detectEncoding(bytes.get))) - } else { - // binary - JGitUtil.ContentInfo("binary", None, None) - } - } else { - // image or large - JGitUtil.ContentInfo(viewer, None, None) - } - - repo.html.blob(id, repository, path.split("/").toList, content, new JGitUtil.CommitInfo(revCommit), - hasWritePermission(repository.owner, repository.name, context.loginAccount)) + repo.html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId), + new JGitUtil.CommitInfo(revCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount)) } } getOrElse NotFound } @@ -395,4 +363,70 @@ } } + private def commitFile(repository: service.RepositoryService.RepositoryInfo, + branch: String, path: String, newFileName: Option[String], oldFileName: Option[String], + content: String, charset: String, message: String) = { + + val newPath = newFileName.map { newFileName => if(path.length == 0) newFileName else s"${path}/${newFileName}" } + val oldPath = oldFileName.map { oldFileName => if(path.length == 0) oldFileName else s"${path}/${oldFileName}" } + + LockUtil.lock(s"${repository.owner}/${repository.name}"){ + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + val loginAccount = context.loginAccount.get + val builder = DirCache.newInCore.builder() + val inserter = git.getRepository.newObjectInserter() + val headName = s"refs/heads/${branch}" + val headTip = git.getRepository.resolve(s"refs/heads/${branch}") + + JGitUtil.processTree(git, headTip){ (path, tree) => + if(path != newPath && !oldPath.exists(_ == path)){ + builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) + } + } + + newPath.foreach { newPath => + builder.add(JGitUtil.createDirCacheEntry(newPath, FileMode.REGULAR_FILE, + inserter.insert(Constants.OBJ_BLOB, content.getBytes(charset)))) + builder.finish() + } + + val commitId = JGitUtil.createNewCommit(git, inserter, headTip, builder.getDirCache.writeTree(inserter), + loginAccount.fullName, loginAccount.mailAddress, message) + + inserter.flush() + inserter.release() + + // update refs + val refUpdate = git.getRepository.updateRef(headName) + refUpdate.setNewObjectId(commitId) + refUpdate.setForceUpdate(false) + refUpdate.setRefLogIdent(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)) + //refUpdate.setRefLogMessage("merged", true) + refUpdate.update() + + // record activity + recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch, + List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId)))) + + // TODO invoke hook + + } + } + } + + private def getPathObjectId(git: Git, path: String, revCommit: RevCommit): Option[ObjectId] = { + @scala.annotation.tailrec + def _getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match { + case true if(walk.getPathString == path) => Some(walk.getObjectId(0)) + case true => _getPathObjectId(path, walk) + case false => None + } + + using(new TreeWalk(git.getRepository)){ treeWalk => + treeWalk.addTree(revCommit.getTree) + treeWalk.setRecursive(true) + _getPathObjectId(path, treeWalk) + } + } + } diff --git a/src/main/scala/util/JGitUtil.scala b/src/main/scala/util/JGitUtil.scala index 62887a7..97e8c41 100644 --- a/src/main/scala/util/JGitUtil.scala +++ b/src/main/scala/util/JGitUtil.scala @@ -550,6 +550,26 @@ } } + def getContentInfo(git: Git, path: String, objectId: ObjectId): ContentInfo = { + // Viewer + val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize) + val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other" + val bytes = if(viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None + + if(viewer == "other"){ + if(bytes.isDefined && FileUtil.isText(bytes.get)){ + // text + ContentInfo("text", Some(StringUtil.convertFromByteArray(bytes.get)), Some(StringUtil.detectEncoding(bytes.get))) + } else { + // binary + ContentInfo("binary", None, None) + } + } else { + // image or large + ContentInfo(viewer, None, None) + } + } + /** * Get object content of the given object id as byte array from the Git repository. * diff --git a/src/main/twirl/repo/blob.scala.html b/src/main/twirl/repo/blob.scala.html index c239343..b13846b 100644 --- a/src/main/twirl/repo/blob.scala.html +++ b/src/main/twirl/repo/blob.scala.html @@ -35,7 +35,10 @@ } Raw History - + @if(hasWritePermission){ + Delete + } +