diff --git a/src/main/java/util/PatchUtil.java b/src/main/java/util/PatchUtil.java new file mode 100644 index 0000000..a316a75 --- /dev/null +++ b/src/main/java/util/PatchUtil.java @@ -0,0 +1,93 @@ +package util; + +import org.eclipse.jgit.api.errors.PatchApplyException; +import org.eclipse.jgit.diff.RawText; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.patch.FileHeader; +import org.eclipse.jgit.patch.HunkHeader; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; + +/** + * This class helps to apply patch. Most of these code came from {@link org.eclipse.jgit.api.ApplyCommand}. + */ +public class PatchUtil { + + public static String apply(String source, String patch, FileHeader fh) + throws IOException, PatchApplyException { + RawText rt = new RawText(source.getBytes("UTF-8")); + List oldLines = new ArrayList(rt.size()); + for (int i = 0; i < rt.size(); i++) + oldLines.add(rt.getString(i)); + List newLines = new ArrayList(oldLines); + for (HunkHeader hh : fh.getHunks()) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(patch.getBytes("UTF-8"), hh.getStartOffset(), hh.getEndOffset() - hh.getStartOffset()); + RawText hrt = new RawText(out.toByteArray()); + List hunkLines = new ArrayList(hrt.size()); + for (int i = 0; i < hrt.size(); i++) + hunkLines.add(hrt.getString(i)); + int pos = 0; + for (int j = 1; j < hunkLines.size(); j++) { + String hunkLine = hunkLines.get(j); + switch (hunkLine.charAt(0)) { + case ' ': + if (!newLines.get(hh.getNewStartLine() - 1 + pos).equals( + hunkLine.substring(1))) { + throw new PatchApplyException(MessageFormat.format( + JGitText.get().patchApplyException, hh)); + } + pos++; + break; + case '-': + if (!newLines.get(hh.getNewStartLine() - 1 + pos).equals( + hunkLine.substring(1))) { + throw new PatchApplyException(MessageFormat.format( + JGitText.get().patchApplyException, hh)); + } + newLines.remove(hh.getNewStartLine() - 1 + pos); + break; + case '+': + newLines.add(hh.getNewStartLine() - 1 + pos, + hunkLine.substring(1)); + pos++; + break; + } + } + } + if (!isNoNewlineAtEndOfFile(fh)) + newLines.add(""); //$NON-NLS-1$ + if (!rt.isMissingNewlineAtEnd()) + oldLines.add(""); //$NON-NLS-1$ + if (!isChanged(oldLines, newLines)) + return null; // don't touch the file + StringBuilder sb = new StringBuilder(); + for (String l : newLines) { + // don't bother handling line endings - if it was windows, the \r is + // still there! + sb.append(l).append('\n'); + } + sb.deleteCharAt(sb.length() - 1); + return sb.toString(); + } + + private static boolean isChanged(List ol, List nl) { + if (ol.size() != nl.size()) + return true; + for (int i = 0; i < ol.size(); i++) + if (!ol.get(i).equals(nl.get(i))) + return true; + return false; + } + + private static boolean isNoNewlineAtEndOfFile(FileHeader fh) { + HunkHeader lastHunk = fh.getHunks().get(fh.getHunks().size() - 1); + RawText lhrt = new RawText(lastHunk.getBuffer()); + return lhrt.getString(lhrt.size() - 1).equals( + "\\ No newline at end of file"); //$NON-NLS-1$ + } +} diff --git a/src/main/scala/app/CreateRepositoryController.scala b/src/main/scala/app/CreateRepositoryController.scala index a3c52d0..c596b8c 100644 --- a/src/main/scala/app/CreateRepositoryController.scala +++ b/src/main/scala/app/CreateRepositoryController.scala @@ -8,7 +8,8 @@ import org.eclipse.jgit.api.Git import org.apache.commons.io._ import jp.sf.amateras.scalatra.forms._ -import org.eclipse.jgit.lib.PersonIdent +import org.eclipse.jgit.lib.{FileMode, Constants, PersonIdent} +import org.eclipse.jgit.dircache.DirCache class CreateRepositoryController extends CreateRepositoryControllerBase with RepositoryService with AccountService with WikiService with LabelsService with ActivityService @@ -73,28 +74,26 @@ JGitUtil.initRepository(gitdir) if(form.createReadme){ - FileUtil.withTmpDir(getInitRepositoryDir(form.owner, form.name)){ tmpdir => - // Clone the repository - Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call + using(Git.open(gitdir)){ git => + val builder = DirCache.newInCore.builder() + val inserter = git.getRepository.newObjectInserter() + val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") + val content = if(form.description.nonEmpty){ + form.name + "\n" + + "===============\n" + + "\n" + + form.description.get + } else { + form.name + "\n" + + "===============\n" + } - // Create README.md - FileUtils.writeStringToFile(new File(tmpdir, "README.md"), - if(form.description.nonEmpty){ - form.name + "\n" + - "===============\n" + - "\n" + - form.description.get - } else { - form.name + "\n" + - "===============\n" - }, "UTF-8") + builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE, + inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")))) + builder.finish() - val git = Git.open(tmpdir) - git.add.addFilepattern("README.md").call - git.commit - .setCommitter(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)) - .setMessage("Initial commit").call - git.push.call + JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), + loginAccount.fullName, loginAccount.mailAddress, "Initial commit") } } diff --git a/src/main/scala/app/WikiController.scala b/src/main/scala/app/WikiController.scala index 4d141de..e7eaf23 100644 --- a/src/main/scala/app/WikiController.scala +++ b/src/main/scala/app/WikiController.scala @@ -66,7 +66,7 @@ val Array(from, to) = params("commitId").split("\\.\\.\\.") 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).filter(_.newPath == pageName + ".md"), repository, hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info")) } }) @@ -96,7 +96,7 @@ 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/}") + redirect(s"/${repository.owner}/${repository.name}/wiki/") } else { flash += "info" -> "This patch was not able to be reversed." redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}") @@ -195,7 +195,7 @@ private def conflictForEdit: Constraint = new Constraint(){ override def validate(name: String, value: String): Option[String] = { - optionIf(targetWikiPage.map(_.id != params("id")).getOrElse(true)){ + optionIf(targetWikiPage.map(_.id != params("id")).getOrElse(false)){ Some("Someone has edited the wiki since you started. Please reload this page and re-apply your changes.") } } diff --git a/src/main/scala/service/WikiService.scala b/src/main/scala/service/WikiService.scala index bce71e4..4b8a05c 100644 --- a/src/main/scala/service/WikiService.scala +++ b/src/main/scala/service/WikiService.scala @@ -1,14 +1,20 @@ package service -import java.io.File import java.util.Date import org.eclipse.jgit.api.Git import org.apache.commons.io.FileUtils -import util.{StringUtil, Directory, JGitUtil, LockUtil} -import util.ControlUtil._ -import org.eclipse.jgit.treewalk.CanonicalTreeParser -import org.eclipse.jgit.diff.DiffFormatter -import org.eclipse.jgit.api.errors.PatchApplyException +import util.{PatchUtil, Directory, JGitUtil, LockUtil} +import _root_.util.ControlUtil._ +import org.eclipse.jgit.treewalk.{TreeWalk, CanonicalTreeParser} +import org.eclipse.jgit.lib._ +import org.eclipse.jgit.dircache.{DirCache, DirCacheEntry} +import org.eclipse.jgit.revwalk.RevWalk +import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter} +import java.io.ByteArrayInputStream +import org.eclipse.jgit.patch._ +import org.eclipse.jgit.api.errors.PatchFormatException +import scala.collection.JavaConverters._ + object WikiService { @@ -42,13 +48,8 @@ LockUtil.lock(s"${owner}/${repository}/wiki"){ defining(Directory.getWikiRepositoryDir(owner, repository)){ dir => if(!dir.exists){ - try { - JGitUtil.initRepository(dir) - saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None) - } finally { - // once delete cloned repository because initial cloned repository does not have 'branch.master.merge' - FileUtils.deleteDirectory(Directory.getWikiWorkDir(owner, repository)) - } + JGitUtil.initRepository(dir) + saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None) } } } @@ -99,12 +100,13 @@ */ def revertWikiPage(owner: String, repository: String, from: String, to: String, committer: model.Account, pageName: Option[String]): Boolean = { - LockUtil.lock(s"${owner}/${repository}/wiki"){ - defining(Directory.getWikiWorkDir(owner, repository)){ workDir => - // clone working copy - cloneOrPullWorkingCopy(workDir, owner, repository) - using(Git.open(workDir)){ git => + case class RevertInfo(operation: String, filePath: String, source: String) + + try { + LockUtil.lock(s"${owner}/${repository}/wiki"){ + using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => + val reader = git.getRepository.newObjectReader val oldTreeIter = new CanonicalTreeParser oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}")) @@ -112,7 +114,6 @@ val newTreeIter = new CanonicalTreeParser newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}")) - import scala.collection.JavaConverters._ val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff => pageName match { case Some(x) => diff.getNewPath == x + ".md" @@ -127,72 +128,135 @@ new String(out.toByteArray, "UTF-8") } - try { - git.apply.setPatch(new java.io.ByteArrayInputStream(patch.getBytes("UTF-8"))).call - git.add.addFilepattern(".").call - git.commit.setCommitter(committer.fullName, committer.mailAddress).setMessage(pageName match { - case Some(x) => s"Revert ${from} ... ${to} on ${x}" - case None => s"Revert ${from} ... ${to}" - }).call - git.push.call - true - } catch { - case ex: PatchApplyException => false + val p = new Patch() + p.parse(new ByteArrayInputStream(patch.getBytes("UTF-8"))) + if(!p.getErrors.isEmpty){ + throw new PatchFormatException(p.getErrors()) + } + val revertInfo = (p.getFiles.asScala.map { fh => + fh.getChangeType match { + case DiffEntry.ChangeType.MODIFY => { + val source = getWikiPage(owner, repository, fh.getNewPath.replaceFirst("\\.md$", "")).map(_.content).getOrElse("") + val applied = PatchUtil.apply(source, patch, fh) + if(applied != null){ + Seq(RevertInfo("ADD", fh.getNewPath, applied)) + } else Nil + } + case DiffEntry.ChangeType.ADD => { + val applied = PatchUtil.apply("", patch, fh) + if(applied != null){ + Seq(RevertInfo("ADD", fh.getNewPath, applied)) + } else Nil + } + case DiffEntry.ChangeType.DELETE => { + Seq(RevertInfo("DELETE", fh.getNewPath, "")) + } + case DiffEntry.ChangeType.RENAME => { + val applied = PatchUtil.apply("", patch, fh) + if(applied != null){ + Seq(RevertInfo("DELETE", fh.getOldPath, ""), RevertInfo("ADD", fh.getNewPath, applied)) + } else { + Seq(RevertInfo("DELETE", fh.getOldPath, "")) + } + } + case _ => Nil + } + }).flatten + + if(revertInfo.nonEmpty){ + val builder = DirCache.newInCore.builder() + val inserter = git.getRepository.newObjectInserter() + val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") + + using(new RevWalk(git.getRepository)){ revWalk => + using(new TreeWalk(git.getRepository)){ treeWalk => + val index = treeWalk.addTree(revWalk.parseTree(headId)) + treeWalk.setRecursive(true) + while(treeWalk.next){ + val path = treeWalk.getPathString + val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser]) + if(revertInfo.find(x => x.filePath == path).isEmpty){ + builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) + } + } + } + } + + revertInfo.filter(_.operation == "ADD").foreach { x => + builder.add(JGitUtil.createDirCacheEntry(x.filePath, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, x.source.getBytes("UTF-8")))) + } + builder.finish() + + JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress, + pageName match { + case Some(x) => s"Revert ${from} ... ${to} on ${x}" + case None => s"Revert ${from} ... ${to}" + }) } } } + true + } catch { + case e: Exception => { + e.printStackTrace() + false + } } } - /** * Save the wiki page. */ def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String, content: String, committer: model.Account, message: String, currentId: Option[String]): Option[String] = { - LockUtil.lock(s"${owner}/${repository}/wiki"){ - defining(Directory.getWikiWorkDir(owner, repository)){ workDir => - // clone working copy - cloneOrPullWorkingCopy(workDir, owner, repository) + using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => + val builder = DirCache.newInCore.builder() + val inserter = git.getRepository.newObjectInserter() + val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") + var created = true + var updated = false + var removed = false - // write as file - using(Git.open(workDir)){ git => - defining(new File(workDir, newPageName + ".md")){ file => - // new page - val created = !file.exists - - // created or updated - val added = executeIf(!file.exists || FileUtils.readFileToString(file, "UTF-8") != content){ - FileUtils.writeStringToFile(file, content, "UTF-8") - git.add.addFilepattern(file.getName).call - } - - // delete file - val deleted = executeIf(currentPageName != "" && currentPageName != newPageName){ - git.rm.addFilepattern(currentPageName + ".md").call - } - - // commit and push - optionIf(added || deleted){ - defining(git.commit.setCommitter(committer.fullName, committer.mailAddress) - .setMessage(if(message.trim.length == 0){ - if(deleted){ - s"Rename ${currentPageName} to ${newPageName}" - } else if(created){ - s"Created ${newPageName}" - } else { - s"Updated ${newPageName}" - } - } else { - message - }).call){ commit => - git.push.call - Some(commit.getName) + if(headId != null){ + using(new RevWalk(git.getRepository)){ revWalk => + using(new TreeWalk(git.getRepository)){ treeWalk => + val index = treeWalk.addTree(revWalk.parseTree(headId)) + treeWalk.setRecursive(true) + while(treeWalk.next){ + val path = treeWalk.getPathString + val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser]) + if(path == currentPageName + ".md" && currentPageName != newPageName){ + removed = true + } else if(path != newPageName + ".md"){ + builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) + } else { + created = false + updated = JGitUtil.getContent(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false) + } } } } } + + optionIf(created || updated || removed){ + builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")))) + builder.finish() + val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress, + if(message.trim.length == 0) { + if(removed){ + s"Rename ${currentPageName} to ${newPageName}" + } else if(created){ + s"Created ${newPageName}" + } else { + s"Updated ${newPageName}" + } + } else { + message + }) + + Some(newHeadId) + } } } } @@ -202,36 +266,35 @@ */ def deleteWikiPage(owner: String, repository: String, pageName: String, committer: String, mailAddress: String, message: String): Unit = { - LockUtil.lock(s"${owner}/${repository}/wiki"){ - defining(Directory.getWikiWorkDir(owner, repository)){ workDir => - // clone working copy - cloneOrPullWorkingCopy(workDir, owner, repository) + LockUtil.lock(s"${owner}/${repository}/wiki"){ + using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => + val builder = DirCache.newInCore.builder() + val inserter = git.getRepository.newObjectInserter() + val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") + var removed = false - // delete file - new File(workDir, pageName + ".md").delete + using(new RevWalk(git.getRepository)){ revWalk => + using(new TreeWalk(git.getRepository)){ treeWalk => + val index = treeWalk.addTree(revWalk.parseTree(headId)) + treeWalk.setRecursive(true) + while(treeWalk.next){ + val path = treeWalk.getPathString + val tree = treeWalk.getTree(index, classOf[CanonicalTreeParser]) + if(path != pageName + ".md"){ + builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) + } else { + removed = true + } + } + } - using(Git.open(workDir)){ git => - git.rm.addFilepattern(pageName + ".md").call - - // commit and push - git.commit.setCommitter(committer, mailAddress).setMessage(message).call - git.push.call + if(removed){ + builder.finish() + JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message) + } + } } } - } - } - - private def cloneOrPullWorkingCopy(workDir: File, owner: String, repository: String): Unit = { - if(!workDir.exists){ - Git.cloneRepository - .setURI(Directory.getWikiRepositoryDir(owner, repository).toURI.toString) - .setDirectory(workDir) - .call - .getRepository - .close - } else using(Git.open(workDir)){ git => - git.pull.call - } } } diff --git a/src/main/scala/util/Directory.scala b/src/main/scala/util/Directory.scala index 5409962..383a3be 100644 --- a/src/main/scala/util/Directory.scala +++ b/src/main/scala/util/Directory.scala @@ -57,24 +57,9 @@ new File(getTemporaryDir(owner, repository), s"download/${sessionId}") /** - * Temporary directory which is used in the repository creation. - * - * GitBucket generates initial repository contents in this directory and push them. - * This directory is removed after the repository creation. - */ - def getInitRepositoryDir(owner: String, repository: String): File = - new File(getTemporaryDir(owner, repository), "init") - - /** * Substance directory of the wiki repository. */ def getWikiRepositoryDir(owner: String, repository: String): File = new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git") - - /** - * Wiki working directory which is cloned from the wiki repository. - */ - def getWikiWorkDir(owner: String, repository: String): File = - new File(getTemporaryDir(owner, repository), "wiki") } \ No newline at end of file diff --git a/src/main/scala/util/JGitUtil.scala b/src/main/scala/util/JGitUtil.scala index c282e9a..d54f4fe 100644 --- a/src/main/scala/util/JGitUtil.scala +++ b/src/main/scala/util/JGitUtil.scala @@ -15,6 +15,7 @@ import java.util.Date import org.eclipse.jgit.api.errors.NoHeadException import service.RepositoryService +import org.eclipse.jgit.dircache.DirCacheEntry /** * Provides complex JGit operations. @@ -464,4 +465,32 @@ }.find(_._1 != null) } + def createDirCacheEntry(path: String, mode: FileMode, objectId: ObjectId): DirCacheEntry = { + val entry = new DirCacheEntry(path) + entry.setFileMode(mode) + entry.setObjectId(objectId) + entry + } + + def createNewCommit(git: Git, inserter: ObjectInserter, headId: AnyObjectId, treeId: AnyObjectId, + fullName: String, mailAddress: String, message: String): String = { + val newCommit = new CommitBuilder() + newCommit.setCommitter(new PersonIdent(fullName, mailAddress)) + newCommit.setAuthor(new PersonIdent(fullName, mailAddress)) + newCommit.setMessage(message) + if(headId != null){ + newCommit.setParentIds(List(headId).asJava) + } + newCommit.setTreeId(treeId) + + val newHeadId = inserter.insert(newCommit) + inserter.flush() + + val refUpdate = git.getRepository.updateRef(Constants.HEAD) + refUpdate.setNewObjectId(newHeadId) + refUpdate.update() + + newHeadId.getName + } + } diff --git a/src/main/scala/view/helpers.scala b/src/main/scala/view/helpers.scala index 9c74fe3..a504329 100644 --- a/src/main/scala/view/helpers.scala +++ b/src/main/scala/view/helpers.scala @@ -57,7 +57,6 @@ value } - import scala.util.matching.Regex import scala.util.matching.Regex._ implicit class RegexReplaceString(s: String) { def replaceAll(pattern: String, replacer: (Match) => String): String = { diff --git a/src/main/twirl/wiki/history.scala.html b/src/main/twirl/wiki/history.scala.html index 22d0b37..ece4df9 100644 --- a/src/main/twirl/wiki/history.scala.html +++ b/src/main/twirl/wiki/history.scala.html @@ -35,7 +35,7 @@ @commits.map { commit => - @avatar(commit.committer, 20) @commit.committer + @avatar(commit, 20) @user(commit.committer, commit.mailAddress) @datetime(commit.time): @commit.shortMessage