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 + } + diff --git a/src/main/twirl/repo/delete.scala.html b/src/main/twirl/repo/delete.scala.html new file mode 100644 index 0000000..c0c7dff --- /dev/null +++ b/src/main/twirl/repo/delete.scala.html @@ -0,0 +1,110 @@ +@(branch: String, + repository: service.RepositoryService.RepositoryInfo, + pathList: List[String], + fileName: String, + content: util.JGitUtil.ContentInfo)(implicit context: app.Context) +@import context._ +@import view.helpers._ +@html.main(s"Deleting ${path} at ${fileName} - ${repository.owner}/${repository.name}", Some(repository)) { + @html.header("code", repository) + @tab(branch, repository, "files") +
+
+ @repository.name / + @pathList.zipWithIndex.map { case (section, i) => + @section / + } + @fileName + + + +
+ + + + + +
+ @fileName +
+ View +
+
+
+ + +
+
@avatar(loginAccount.get.userName, 48)
+
+
+
+ Commit changes +
+
+ +
+
+ Cancel + +
+
+
+
+} + + + + + \ No newline at end of file diff --git a/src/main/twirl/repo/editor.scala.html b/src/main/twirl/repo/editor.scala.html index 69f88f4..2be0590 100644 --- a/src/main/twirl/repo/editor.scala.html +++ b/src/main/twirl/repo/editor.scala.html @@ -1,47 +1,32 @@ @(branch: String, repository: service.RepositoryService.RepositoryInfo, pathList: List[String], - content: util.JGitUtil.ContentInfo, - latestCommit: util.JGitUtil.CommitInfo)(implicit context: app.Context) + fileName: Option[String], + content: util.JGitUtil.ContentInfo)(implicit context: app.Context) @import context._ @import view.helpers._ -@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { +@html.main(if(fileName.isEmpty) "New File" else s"Editing ${path} at ${fileName} - ${repository.owner}/${repository.name}", Some(repository)) { @html.header("code", repository) @tab(branch, repository, "files") -
- @repository.name / - @pathList.zipWithIndex.map { case (section, i) => - @if(i == pathList.length - 1){ - @section - } else { +
+ +
+ @repository.name / + @pathList.zipWithIndex.map { case (section, i) => @section / } + + + + +
+ - + - @* - - - - *@
-
- @avatar(latestCommit, 20) - @user(latestCommit.committer, latestCommit.mailAddress, "username strong") - @datetime(latestCommit.time) - @link(latestCommit.summary, repository) -
-
- Raw - History -
-
@@ -55,14 +40,14 @@ Commit changes
- +
Cancel - +
@@ -74,7 +59,9 @@ $('#editor').text($('#initial').val()); var editor = ace.edit("editor"); editor.setTheme("ace/theme/monokai"); - editor.getSession().setMode("ace/mode/@editorType(pathList.last)"); + @if(fileName.isDefined){ + editor.getSession().setMode("ace/mode/@editorType(fileName.get)"); + } editor.on('change', function(){ $('#commit').attr('disabled', editor.getValue() == $('#initial').val()); }); @@ -82,5 +69,7 @@ $('#commit').click(function(){ $('#content').val(editor.getValue()); }); + + }) diff --git a/src/main/twirl/repo/files.scala.html b/src/main/twirl/repo/files.scala.html index 96be278..0077f8c 100644 --- a/src/main/twirl/repo/files.scala.html +++ b/src/main/twirl/repo/files.scala.html @@ -19,6 +19,8 @@ @pathList.zipWithIndex.map { case (section, i) => @section / } + + +