diff --git a/README.md b/README.md index f50c7d0..68c305a 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,15 @@ -GitBucket +GitBucket [![Gitter chat](https://badges.gitter.im/takezoe/gitbucket.png)](https://gitter.im/takezoe/gitbucket) [![Build Status](https://buildhive.cloudbees.com/job/takezoe/job/gitbucket/badge/icon)](https://buildhive.cloudbees.com/job/takezoe/job/gitbucket/) ========= GitBucket is the easily installable Github clone written with Scala. + +Features +-------- The current version of GitBucket provides a basic features below: - Public / Private Git repository (http access only) -- Repository viewer (some advanced features such as online file editing are not implemented) +- Repository viewer and online file editing - Repository search (Code and Issues) - Wiki - Issues @@ -20,7 +23,6 @@ Following features are not implemented, but we will make them in the future release! -- File editing in repository viewer - Comment for the changeset - Network graph - Statistics @@ -78,6 +80,12 @@ Release Notes -------- +### 1.13 - 29 Apr 2014 +- Direct file editing in the repository viewer using AceEditor +- File attachment for issues +- Atom feed of user activity +- Fix some bugs + ### 1.12 - 29 Mar 2014 - SSH repository access is available - Allow users can create and management their groups diff --git a/project/build.scala b/project/build.scala index 9d2e16d..51030c6 100644 --- a/project/build.scala +++ b/project/build.scala @@ -37,16 +37,14 @@ "org.apache.commons" % "commons-compress" % "1.5", "org.apache.commons" % "commons-email" % "1.3.1", "org.apache.httpcomponents" % "httpclient" % "4.3", - "org.apache.sshd" % "apache-sshd" % "0.10.0", + "org.apache.sshd" % "apache-sshd" % "0.11.0", "com.typesafe.slick" %% "slick" % "1.0.1", "com.novell.ldap" % "jldap" % "2009-10-07", "com.h2database" % "h2" % "1.3.173", "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime", "org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided", "org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"), - "junit" % "junit" % "4.11" % "test", - "org.asciidoctor" % "asciidoctor-java-integration" % "0.1.4", - "net.sourceforge.htmlcleaner" % "htmlcleaner" % "2.7" + "junit" % "junit" % "4.11" % "test" ), EclipseKeys.withSource := true, javacOptions in compile ++= Seq("-target", "6", "-source", "6"), diff --git a/src/main/resources/update/1_13.sql b/src/main/resources/update/1_13.sql new file mode 100644 index 0000000..ed26f65 --- /dev/null +++ b/src/main/resources/update/1_13.sql @@ -0,0 +1 @@ +DROP TABLE COMMIT_LOG; \ No newline at end of file diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala index 21c213d..90fe415 100644 --- a/src/main/scala/ScalatraBootstrap.scala +++ b/src/main/scala/ScalatraBootstrap.scala @@ -1,6 +1,6 @@ import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter} import app._ -import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider +//import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider import org.scalatra._ import javax.servlet._ import java.util.EnumSet @@ -28,7 +28,6 @@ context.mount(new IssuesController, "/*") context.mount(new PullRequestsController, "/*") context.mount(new RepositorySettingsController, "/*") - context.mount(new ValidationJavaScriptProvider, "/assets/common/js/*") // Create GITBUCKET_HOME directory if it does not exist val dir = new java.io.File(_root_.util.Directory.GitBucketHome) diff --git a/src/main/scala/app/AccountController.scala b/src/main/scala/app/AccountController.scala index d0a0a89..72244dc 100644 --- a/src/main/scala/app/AccountController.scala +++ b/src/main/scala/app/AccountController.scala @@ -112,13 +112,19 @@ val members = getGroupMembers(account.userName) _root_.account.html.repositories(account, if(account.isGroupAccount) Nil else getGroupsByUserName(userName), - getVisibleRepositories(context.loginAccount, baseUrl, Some(userName)), + getVisibleRepositories(context.loginAccount, context.baseUrl, Some(userName)), context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })) } } } getOrElse NotFound } + get("/:userName.atom") { + val userName = params("userName") + contentType = "application/atom+xml; type=feed" + helper.xml.feed(getActivitiesByUser(userName, true)) + } + get("/:userName/_avatar"){ val userName = params("userName") getAccountByUserName(userName).flatMap(_.image).map { image => @@ -286,7 +292,7 @@ */ post("/new", newRepositoryForm)(usersOnly { form => LockUtil.lock(s"${form.owner}/${form.name}/create"){ - if(getRepository(form.owner, form.name, baseUrl).isEmpty){ + if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){ val ownerAccount = getAccountByUserName(form.owner).get val loginAccount = context.loginAccount.get val loginUserName = loginAccount.userName @@ -385,20 +391,6 @@ getWikiRepositoryDir(repository.owner, repository.name), getWikiRepositoryDir(loginUserName, repository.name)) - // insert commit id - using(Git.open(getRepositoryDir(loginUserName, repository.name))){ git => - JGitUtil.getRepositoryInfo(loginUserName, repository.name, baseUrl).branchList.foreach { branch => - JGitUtil.getCommitLog(git, branch) match { - case Right((commits, _)) => commits.foreach { commit => - if(!existsCommitId(loginUserName, repository.name, commit.id)){ - insertCommitId(loginUserName, repository.name, commit.id) - } - } - case Left(_) => ??? - } - } - } - // Record activity recordForkActivity(repository.owner, repository.name, loginUserName) // redirect to the repository diff --git a/src/main/scala/app/ControllerBase.scala b/src/main/scala/app/ControllerBase.scala index d7d7171..6abacc7 100644 --- a/src/main/scala/app/ControllerBase.scala +++ b/src/main/scala/app/ControllerBase.scala @@ -11,8 +11,7 @@ import org.apache.commons.io.FileUtils import model.Account import service.{SystemSettingsService, AccountService} -import javax.servlet.http.{HttpServletResponse, HttpSession, HttpServletRequest} -import java.text.SimpleDateFormat +import javax.servlet.http.{HttpServletResponse, HttpServletRequest} import javax.servlet.{FilterChain, ServletResponse, ServletRequest} import org.scalatra.i18n._ @@ -139,9 +138,10 @@ */ case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){ - lazy val path = settings.baseUrl.getOrElse(request.getServletContext.getContextPath) - - lazy val currentPath = request.getRequestURI.substring(request.getContextPath.length) + val path = settings.baseUrl.getOrElse(request.getContextPath) + val currentPath = request.getRequestURI.substring(request.getContextPath.length) + val baseUrl = settings.baseUrl(request) + val host = new java.net.URL(baseUrl).getHost /** * Get object from cache. @@ -163,7 +163,7 @@ /** * Base trait for controllers which manages account information. */ -trait AccountManagementControllerBase extends ControllerBase with FileUploadControllerBase { +trait AccountManagementControllerBase extends ControllerBase { self: AccountService => protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit = @@ -174,9 +174,9 @@ } } else { fileId.map { fileId => - val filename = "avatar." + FileUtil.getExtension(getUploadedFilename(fileId).get) + val filename = "avatar." + FileUtil.getExtension(session.getAndRemove(Keys.Session.Upload(fileId)).get) FileUtils.moveFile( - getTemporaryFile(fileId), + new java.io.File(getTemporaryDir(session.getId), fileId), new java.io.File(getUserUploadDir(userName), filename) ) updateAvatarImage(userName, Some(filename)) @@ -196,28 +196,3 @@ } } - -/** - * Base trait for controllers which needs file uploading feature. - */ -trait FileUploadControllerBase { - - def generateFileId: String = - new SimpleDateFormat("yyyyMMddHHmmSSsss").format(new java.util.Date(System.currentTimeMillis)) - - def TemporaryDir(implicit session: HttpSession): java.io.File = - new java.io.File(GitBucketHome, s"tmp/_upload/${session.getId}") - - def getTemporaryFile(fileId: String)(implicit session: HttpSession): java.io.File = - new java.io.File(TemporaryDir, fileId) - - // def removeTemporaryFile(fileId: String)(implicit session: HttpSession): Unit = - // getTemporaryFile(fileId).delete() - - def removeTemporaryFiles()(implicit session: HttpSession): Unit = - FileUtils.deleteDirectory(TemporaryDir) - - def getUploadedFilename(fileId: String)(implicit session: HttpSession): Option[String] = - session.getAndRemove[String](Keys.Session.Upload(fileId)) - -} \ No newline at end of file diff --git a/src/main/scala/app/DashboardController.scala b/src/main/scala/app/DashboardController.scala index 871dd34..8fe73bf 100644 --- a/src/main/scala/app/DashboardController.scala +++ b/src/main/scala/app/DashboardController.scala @@ -49,7 +49,7 @@ ) val userName = context.loginAccount.get.userName - val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name) + val repositories = getUserRepositories(userName, context.baseUrl).map(repo => repo.owner -> repo.name) val filterUser = Map(filter -> userName) val page = IssueSearchCondition.page(request) // @@ -80,7 +80,7 @@ }.copy(repo = repository)) val userName = context.loginAccount.get.userName - val repositories = getUserRepositories(userName, baseUrl).map(repo => repo.owner -> repo.name) + val repositories = getUserRepositories(userName, context.baseUrl).map(repo => repo.owner -> repo.name) val filterUser = Map(filter -> userName) val page = IssueSearchCondition.page(request) diff --git a/src/main/scala/app/FileUploadController.scala b/src/main/scala/app/FileUploadController.scala index 9950b48..ad8ea28 100644 --- a/src/main/scala/app/FileUploadController.scala +++ b/src/main/scala/app/FileUploadController.scala @@ -1,31 +1,42 @@ package app -import _root_.util.{Keys, FileUtil} +import util.{Keys, FileUtil} import util.ControlUtil._ +import util.Directory._ import org.scalatra._ -import org.scalatra.servlet.{MultipartConfig, FileUploadSupport} +import org.scalatra.servlet.{MultipartConfig, FileUploadSupport, FileItem} import org.apache.commons.io.FileUtils /** * Provides Ajax based file upload functionality. * - * This servlet saves uploaded file as temporary file and returns the unique id. - * You can get uploaded file using [[app.FileUploadControllerBase#getTemporaryFile()]] with this id. + * This servlet saves uploaded file. */ -class FileUploadController extends ScalatraServlet with FileUploadSupport with FileUploadControllerBase { +class FileUploadController extends ScalatraServlet with FileUploadSupport { configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024))) post("/image"){ - fileParams.get("file") match { - case Some(file) if(FileUtil.isImage(file.name)) => defining(generateFileId){ fileId => - FileUtils.writeByteArrayToFile(getTemporaryFile(fileId), file.get) - session += Keys.Session.Upload(fileId) -> file.name - Ok(fileId) - } - case None => BadRequest + execute { (file, fileId) => + FileUtils.writeByteArrayToFile(new java.io.File(getTemporaryDir(session.getId), fileId), file.get) + session += Keys.Session.Upload(fileId) -> file.name } } -} + post("/image/:owner/:repository"){ + execute { (file, fileId) => + FileUtils.writeByteArrayToFile(new java.io.File(getAttachedDir(params("owner"), params("repository")), fileId), file.get) + } + } + private def execute(f: (FileItem, String) => Unit) = fileParams.get("file") match { + case Some(file) if(FileUtil.isImage(file.name)) => + defining(FileUtil.generateFileId){ fileId => + f(file, fileId) + + Ok(fileId) + } + case _ => BadRequest + } + +} diff --git a/src/main/scala/app/IndexController.scala b/src/main/scala/app/IndexController.scala index 426ec63..6cc95b6 100644 --- a/src/main/scala/app/IndexController.scala +++ b/src/main/scala/app/IndexController.scala @@ -21,8 +21,8 @@ val loginAccount = context.loginAccount html.index(getRecentActivities(), - getVisibleRepositories(loginAccount, baseUrl), - loginAccount.map{ account => getUserRepositories(account.userName, baseUrl) }.getOrElse(Nil) + getVisibleRepositories(loginAccount, context.baseUrl), + loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl) }.getOrElse(Nil) ) } @@ -46,6 +46,11 @@ redirect("/") } + get("/activities.atom"){ + contentType = "application/atom+xml; type=feed" + helper.xml.feed(getRecentActivities()) + } + /** * Set account information into HttpSession and redirect. */ diff --git a/src/main/scala/app/IssuesController.scala b/src/main/scala/app/IssuesController.scala index b8f1682..8251e88 100644 --- a/src/main/scala/app/IssuesController.scala +++ b/src/main/scala/app/IssuesController.scala @@ -118,7 +118,7 @@ // notifications Notifier().toNotify(repository, issueId, form.content.getOrElse("")){ - Notifier.msgIssue(s"${baseUrl}/${owner}/${name}/issues/${issueId}") + Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") } redirect(s"/${owner}/${name}/issues/${issueId}") @@ -273,6 +273,15 @@ } }) + get("/:owner/:repository/_attached/:file")(referrersOnly { repository => + defining(new java.io.File(Directory.getAttachedDir(repository.owner, repository.name), params("file"))){ file => + if(file.exists) { + contentType = FileUtil.getMimeType(file) + file + } else NotFound + } + }) + val assignedUserName = (key: String) => params.get(key) filter (_.trim != "") val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt) @@ -342,13 +351,13 @@ case f => content foreach { f.toNotify(repository, issueId, _){ - Notifier.msgComment(s"${baseUrl}/${owner}/${name}/${ + Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${ if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}") } } action foreach { f.toNotify(repository, issueId, _){ - Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/issues/${issueId}") + Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") } } } diff --git a/src/main/scala/app/PullRequestsController.scala b/src/main/scala/app/PullRequestsController.scala index 47c0614..0f08602 100644 --- a/src/main/scala/app/PullRequestsController.scala +++ b/src/main/scala/app/PullRequestsController.scala @@ -100,7 +100,7 @@ pulls.html.mergeguide( checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId), pullreq, - s"${baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git") + s"${context.baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git") } } getOrElse NotFound }) @@ -111,7 +111,7 @@ val userName = context.loginAccount.get.userName if(repository.repository.defaultBranch != branchName){ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - git.branchDelete().setBranchNames(branchName).call() + git.branchDelete().setForce(true).setBranchNames(branchName).call() recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName) } } @@ -177,14 +177,8 @@ val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom, pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo) - commits.flatten.foreach { commit => - if(!existsCommitId(owner, name, commit.id)){ - insertCommitId(owner, name, commit.id) - } - } - // close issue by content of pull request - val defaultBranch = getRepository(owner, name, baseUrl).get.repository.defaultBranch + val defaultBranch = getRepository(owner, name, context.baseUrl).get.repository.defaultBranch if(pullreq.branch == defaultBranch){ commits.flatten.foreach { commit => closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name) @@ -207,7 +201,7 @@ // notifications Notifier().toNotify(repository, issueId, "merge"){ - Notifier.msgStatus(s"${baseUrl}/${owner}/${name}/pull/${issueId}") + Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}") } redirect(s"/${owner}/${name}/pull/${issueId}") @@ -220,7 +214,7 @@ get("/:owner/:repository/compare")(referrersOnly { forkedRepository => (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match { case (Some(originUserName), Some(originRepositoryName)) => { - getRepository(originUserName, originRepositoryName, baseUrl).map { originRepository => + getRepository(originUserName, originRepositoryName, context.baseUrl).map { originRepository => using( Git.open(getRepositoryDir(originUserName, originRepositoryName)), Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name)) @@ -257,7 +251,7 @@ getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2) } }; - originRepository <- getRepository(originOwner, originRepositoryName, baseUrl) + originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl) ) yield { using( Git.open(getRepositoryDir(originRepository.owner, originRepository.name)), @@ -266,7 +260,7 @@ val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2 val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2 - val forkedId = getForkedCommitId(oldGit, newGit, + val forkedId = JGitUtil.getForkedCommitId(oldGit, newGit, originRepository.owner, originRepository.name, originBranch, forkedRepository.owner, forkedRepository.name, forkedBranch) @@ -309,7 +303,7 @@ getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2) } }; - originRepository <- getRepository(originOwner, originRepositoryName, baseUrl) + originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl) ) yield { using( Git.open(getRepositoryDir(originRepository.owner, originRepository.name)), @@ -362,7 +356,7 @@ // notifications Notifier().toNotify(repository, issueId, form.content.getOrElse("")){ - Notifier.msgPullRequest(s"${baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}") + Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}") } redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") @@ -438,24 +432,8 @@ (defaultOwner, value) } - /** - * Extracts all repository names from [[service.RepositoryService.RepositoryTreeNode]] as flat list. - */ - private def getRepositoryNames(node: RepositoryTreeNode): List[String] = - node.owner :: node.children.map { child => getRepositoryNames(child) }.flatten - - /** - * Returns the identifier of the root commit (or latest merge commit) of the specified branch. - */ - private def getForkedCommitId(oldGit: Git, newGit: Git, userName: String, repositoryName: String, branch: String, - requestUserName: String, requestRepositoryName: String, requestBranch: String): String = - JGitUtil.getCommitLogs(newGit, requestBranch, true){ commit => - existsCommitId(userName, repositoryName, commit.getName) && JGitUtil.getBranchesOfCommit(oldGit, commit.getName).contains(branch) - }.head.id - private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String, - requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = { - + requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = using( Git.open(getRepositoryDir(userName, repositoryName)), Git.open(getRepositoryDir(requestUserName, requestRepositoryName)) @@ -473,7 +451,6 @@ (commits, diffs) } - } private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) = defining(repository.owner, repository.name){ case (owner, repoName) => diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala index c38eb01..710b34c 100644 --- a/src/main/scala/app/RepositoryViewerController.scala +++ b/src/main/scala/app/RepositoryViewerController.scala @@ -1,8 +1,9 @@ package app +import _root_.util.JGitUtil.CommitInfo import util.Directory._ import util.Implicits._ -import util.ControlUtil._ +import _root_.util.ControlUtil._ import _root_.util._ import service._ import org.scalatra._ @@ -12,17 +13,53 @@ import org.apache.commons.io.FileUtils import org.eclipse.jgit.treewalk._ import java.util.zip.{ZipEntry, ZipOutputStream} -import scala.Some +import jp.sf.amateras.scalatra.forms._ +import org.eclipse.jgit.dircache.DirCache +import org.eclipse.jgit.revwalk.{RevCommit, RevWalk} -class RepositoryViewerController extends RepositoryViewerControllerBase +class RepositoryViewerController extends RepositoryViewerControllerBase with RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator /** * The repository viewer. */ -trait RepositoryViewerControllerBase extends ControllerBase { +trait RepositoryViewerControllerBase extends ControllerBase { self: RepositoryService with AccountService with ActivityService with ReferrerAuthenticator with CollaboratorsAuthenticator => + 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( + "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. */ @@ -72,6 +109,68 @@ } }) + 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 (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)) + + 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 + } + }) + + 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)) + + 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. */ @@ -81,19 +180,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 => @@ -101,25 +188,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", bytes.map(StringUtil.convertFromByteArray)) - } else { - // binary - JGitUtil.ContentInfo("binary", None) - } - } else { - // image or large - JGitUtil.ContentInfo(viewer, None) - } - - repo.html.blob(id, repository, path.split("/").toList, content, new JGitUtil.CommitInfo(revCommit)) + 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 } @@ -165,7 +235,7 @@ val userName = context.loginAccount.get.userName if(repository.repository.defaultBranch != branchName){ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - git.branchDelete().setBranchNames(branchName).call() + git.branchDelete().setForce(true).setBranchNames(branchName).call() recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName) } } @@ -235,13 +305,13 @@ getRepository( repository.repository.originUserName.getOrElse(repository.owner), repository.repository.originRepositoryName.getOrElse(repository.name), - baseUrl), + context.baseUrl), getForkedRepositories( repository.repository.originUserName.getOrElse(repository.owner), repository.repository.originRepositoryName.getOrElse(repository.name)), repository) }) - + private def splitPath(repository: service.RepositoryService.RepositoryInfo, path: String): (String, String) = { val id = repository.branchList.collectFirst { case branch if(path == branch || path.startsWith(branch + "/")) => branch @@ -257,7 +327,7 @@ /** * Provides HTML of the file list. - * + * * @param repository the repository information * @param revstr the branch name or commit id(optional) * @param path the directory path (optional) @@ -265,7 +335,7 @@ */ private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = { if(repository.commitCount == 0){ - repo.html.guide(repository) + repo.html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount)) } else { using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => //val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head) @@ -287,11 +357,77 @@ repo.html.files(revision, repository, if(path == ".") Nil else path.split("/").toList, // current path new JGitUtil.CommitInfo(revCommit), // latest commit - files, readme) + files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount)) } } getOrElse NotFound } } } - + + 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(!newPath.exists(_ == path) && !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/model/Activity.scala b/src/main/scala/model/Activity.scala index 9c2ae02..b9d5bb5 100644 --- a/src/main/scala/model/Activity.scala +++ b/src/main/scala/model/Activity.scala @@ -13,12 +13,6 @@ def autoInc = userName ~ repositoryName ~ activityUserName ~ activityType ~ message ~ additionalInfo.? ~ activityDate returning activityId } -object CommitLog extends Table[(String, String, String)]("COMMIT_LOG") with BasicTemplate { - def commitId = column[String]("COMMIT_ID") - def * = userName ~ repositoryName ~ commitId - def byPrimaryKey(userName: String, repositoryName: String, commitId: String) = byRepository(userName, repositoryName) && (this.commitId is commitId.bind) -} - case class Activity( activityId: Int, userName: String, diff --git a/src/main/scala/service/ActivityService.scala b/src/main/scala/service/ActivityService.scala index fce0b09..64fbb4e 100644 --- a/src/main/scala/service/ActivityService.scala +++ b/src/main/scala/service/ActivityService.scala @@ -153,19 +153,6 @@ Some(message), currentDate) - def insertCommitId(userName: String, repositoryName: String, commitId: String) = { - CommitLog insert (userName, repositoryName, commitId) - } - - def insertAllCommitIds(userName: String, repositoryName: String, commitIds: List[String]) = - CommitLog insertAll (commitIds.map(commitId => (userName, repositoryName, commitId)): _*) - - def getAllCommitIds(userName: String, repositoryName: String): List[String] = - Query(CommitLog).filter(_.byRepository(userName, repositoryName)).map(_.commitId).list - - def existsCommitId(userName: String, repositoryName: String, commitId: String): Boolean = - Query(CommitLog).filter(_.byPrimaryKey(userName, repositoryName, commitId)).firstOption.isDefined - - private def cut(value: String, length: Int): String = + private def cut(value: String, length: Int): String = if(value.length > length) value.substring(0, length) + "..." else value } diff --git a/src/main/scala/service/PullRequestService.scala b/src/main/scala/service/PullRequestService.scala index 7d11f08..07f7145 100644 --- a/src/main/scala/service/PullRequestService.scala +++ b/src/main/scala/service/PullRequestService.scala @@ -3,7 +3,6 @@ import scala.slick.driver.H2Driver.simple._ import Database.threadLocalSession import model._ -import util.ControlUtil._ trait PullRequestService { self: IssuesService => import PullRequestService._ @@ -15,8 +14,10 @@ } } - def updateCommitIdTo(owner: String, repository: String, issueId: Int, commitIdTo: String): Unit = - Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)).map(_.commitIdTo).update(commitIdTo) + def updateCommitId(owner: String, repository: String, issueId: Int, commitIdTo: String, commitIdFrom: String): Unit = + Query(PullRequests).filter(_.byPrimaryKey(owner, repository, issueId)) + .map(pr => pr.commitIdTo ~ pr.commitIdFrom) + .update((commitIdTo, commitIdFrom)) def getPullRequestCountGroupByUser(closed: Boolean, owner: String, repository: Option[String]): List[PullRequestCount] = Query(PullRequests) diff --git a/src/main/scala/service/RepositoryService.scala b/src/main/scala/service/RepositoryService.scala index 38e7606..774017e 100644 --- a/src/main/scala/service/RepositoryService.scala +++ b/src/main/scala/service/RepositoryService.scala @@ -52,7 +52,6 @@ val issueComments = Query(IssueComments).filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueLabels = Query(IssueLabels ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val collaborators = Query(Collaborators).filter(_.byRepository(oldUserName, oldRepositoryName)).list - val commitLog = Query(CommitLog ).filter(_.byRepository(oldUserName, oldRepositoryName)).list val activities = Query(Activities ).filter(_.byRepository(oldUserName, oldRepositoryName)).list Repositories.filter { t => @@ -78,7 +77,6 @@ Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Collaborators .insertAll(collaborators .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) - CommitLog .insertAll(commitLog .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*) Activities .insertAll(activities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) // Update activity messages @@ -102,7 +100,6 @@ def deleteRepository(userName: String, repositoryName: String): Unit = { Activities .filter(_.byRepository(userName, repositoryName)).delete - CommitLog .filter(_.byRepository(userName, repositoryName)).delete Collaborators .filter(_.byRepository(userName, repositoryName)).delete IssueLabels .filter(_.byRepository(userName, repositoryName)).delete Labels .filter(_.byRepository(userName, repositoryName)).delete diff --git a/src/main/scala/service/SystemSettingsService.scala b/src/main/scala/service/SystemSettingsService.scala index ba425a3..fd72b60 100644 --- a/src/main/scala/service/SystemSettingsService.scala +++ b/src/main/scala/service/SystemSettingsService.scala @@ -7,11 +7,7 @@ trait SystemSettingsService { - def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl.getOrElse { - defining(request.getRequestURL.toString){ url => - url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length)) - } - }.replaceFirst("/$", "") + def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl(request) def saveSystemSettings(settings: SystemSettings): Unit = { defining(new java.util.Properties()){ props => @@ -110,7 +106,13 @@ sshPort: Option[Int], smtp: Option[Smtp], ldapAuthentication: Boolean, - ldap: Option[Ldap]) + ldap: Option[Ldap]){ + def baseUrl(request: HttpServletRequest): String = baseUrl.getOrElse { + defining(request.getRequestURL.toString){ url => + url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length)) + } + }.replaceFirst("/$", "") + } case class Ldap( host: String, diff --git a/src/main/scala/service/WikiService.scala b/src/main/scala/service/WikiService.scala index 870fe20..20fa764 100644 --- a/src/main/scala/service/WikiService.scala +++ b/src/main/scala/service/WikiService.scala @@ -175,17 +175,9 @@ 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)) - } - } + JGitUtil.processTree(git, headId){ (path, tree) => + if(revertInfo.find(x => x.filePath == path).isEmpty){ + builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) } } @@ -226,22 +218,14 @@ var removed = false 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.getContentFromId(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false) - } - } + JGitUtil.processTree(git, headId){ (path, tree) => + 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.getContentFromId(git, tree.getEntryObjectId, true).map(new String(_, "UTF-8") != content).getOrElse(false) } } } @@ -262,7 +246,7 @@ message }) - Some(newHeadId) + Some(newHeadId.getName) } else None } } @@ -280,25 +264,16 @@ val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}") var removed = false - 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 - } - } + JGitUtil.processTree(git, headId){ (path, tree) => + if(path != pageName + ".md"){ + builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) + } else { + removed = true } - - if(removed){ - builder.finish() - JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message) - } + } + if(removed){ + builder.finish() + JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message) } } } diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/AutoUpdateListener.scala index 8cafb60..9ffed83 100644 --- a/src/main/scala/servlet/AutoUpdateListener.scala +++ b/src/main/scala/servlet/AutoUpdateListener.scala @@ -50,6 +50,7 @@ * The history of versions. A head of this sequence is the current BitBucket version. */ val versions = Seq( + Version(1, 13), Version(1, 12), Version(1, 11), Version(1, 10), diff --git a/src/main/scala/servlet/BasicAuthenticationFilter.scala b/src/main/scala/servlet/BasicAuthenticationFilter.scala index 16c516c..c335cbc 100644 --- a/src/main/scala/servlet/BasicAuthenticationFilter.scala +++ b/src/main/scala/servlet/BasicAuthenticationFilter.scala @@ -3,6 +3,7 @@ import javax.servlet._ import javax.servlet.http._ import service.{SystemSettingsService, AccountService, RepositoryService} +import model.Account import org.slf4j.LoggerFactory import util.Implicits._ import util.ControlUtil._ @@ -38,9 +39,12 @@ request.getHeader("Authorization") match { case null => requireAuth(response) case auth => decodeAuthHeader(auth).split(":") match { - case Array(username, password) if(isWritableUser(username, password, repository)) => { - request.setAttribute(Keys.Request.UserName, username) - chain.doFilter(req, wrappedResponse) + case Array(username, password) => getWritableUser(username, password, repository) match { + case Some(account) => { + request.setAttribute(Keys.Request.UserName, account.userName) + chain.doFilter(req, wrappedResponse) + } + case None => requireAuth(response) } case _ => requireAuth(response) } @@ -61,10 +65,10 @@ } } - private def isWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Boolean = + private def getWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo): Option[Account] = authenticate(loadSystemSettings(), username, password) match { - case Some(account) => hasWritePermission(repository.owner, repository.name, Some(account)) - case None => false + case x @ Some(account) if(hasWritePermission(repository.owner, repository.name, x)) => x + case _ => None } private def requireAuth(response: HttpServletResponse): Unit = { diff --git a/src/main/scala/servlet/GitRepositoryServlet.scala b/src/main/scala/servlet/GitRepositoryServlet.scala index d02cb2f..0e43432 100644 --- a/src/main/scala/servlet/GitRepositoryServlet.scala +++ b/src/main/scala/servlet/GitRepositoryServlet.scala @@ -81,7 +81,9 @@ logger.debug("repository:" + owner + "/" + repository) if(!repository.endsWith(".wiki")){ - receivePack.setPostReceiveHook(new CommitLogHook(owner, repository, pusher, baseUrl(request))) + val hook = new CommitLogHook(owner, repository, pusher, baseUrl(request)) + receivePack.setPreReceiveHook(hook) + receivePack.setPostReceiveHook(hook) } receivePack } @@ -90,11 +92,25 @@ import scala.collection.JavaConverters._ -class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String) extends PostReceiveHook +class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String) extends PostReceiveHook with PreReceiveHook with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService { private val logger = LoggerFactory.getLogger(classOf[CommitLogHook]) - + private var existIds: Seq[String] = Nil + + def onPreReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = { + try { + using(Git.open(Directory.getRepositoryDir(owner, repository))) { git => + existIds = JGitUtil.getAllCommitIds(git) + } + } catch { + case ex: Exception => { + logger.error(ex.toString, ex) + throw ex + } + } + } + def onPostReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = { try { using(Git.open(Directory.getRepositoryDir(owner, repository))) { git => @@ -117,30 +133,15 @@ countIssue(IssueSearchCondition(state = "closed"), Map.empty, false, owner -> repository) // Extract new commit and apply issue comment - val newCommits = if(commits.size > 1000){ - val existIds = getAllCommitIds(owner, repository) - commits.flatMap { commit => - if(!existIds.contains(commit.id)){ - if(issueCount > 0) { - createIssueComment(commit) - } - Some(commit) - } else None - } - } else { - commits.flatMap { commit => - if(!existsCommitId(owner, repository, commit.id)){ - if(issueCount > 0) { - createIssueComment(commit) - } - Some(commit) - } else None - } + val newCommits = commits.flatMap { commit => + if (!existIds.contains(commit.id)) { + if (issueCount > 0) { + createIssueComment(commit) + } + Some(commit) + } else None } - // batch insert all new commit id - insertAllCommitIds(owner, repository, newCommits.map(_.id)) - // record activity if(refName(1) == "heads"){ command.getType match { @@ -217,14 +218,18 @@ private def updatePullRequests(branch: String) = getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq => if(getRepository(pullreq.userName, pullreq.repositoryName, baseUrl).isDefined){ - using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName))){ git => - git.fetch + using(Git.open(Directory.getRepositoryDir(pullreq.userName, pullreq.repositoryName)), + Git.open(Directory.getRepositoryDir(pullreq.requestUserName, pullreq.requestRepositoryName))){ (oldGit, newGit) => + oldGit.fetch .setRemote(Directory.getRepositoryDir(owner, repository).toURI.toString) .setRefSpecs(new RefSpec(s"refs/heads/${branch}:refs/pull/${pullreq.issueId}/head").setForceUpdate(true)) .call - val commitIdTo = git.getRepository.resolve(s"refs/pull/${pullreq.issueId}/head").getName - updateCommitIdTo(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo) + val commitIdTo = oldGit.getRepository.resolve(s"refs/pull/${pullreq.issueId}/head").getName + val commitIdFrom = JGitUtil.getForkedCommitId(oldGit, newGit, + pullreq.userName, pullreq.repositoryName, pullreq.branch, + pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch) + updateCommitId(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo, commitIdFrom) } } } diff --git a/src/main/scala/servlet/SessionCleanupListener.scala b/src/main/scala/servlet/SessionCleanupListener.scala index 87ce1d1..ee4ea7b 100644 --- a/src/main/scala/servlet/SessionCleanupListener.scala +++ b/src/main/scala/servlet/SessionCleanupListener.scala @@ -1,15 +1,16 @@ package servlet import javax.servlet.http.{HttpSessionEvent, HttpSessionListener} -import app.FileUploadControllerBase +import org.apache.commons.io.FileUtils +import util.Directory._ /** * Removes session associated temporary files when session is destroyed. */ -class SessionCleanupListener extends HttpSessionListener with FileUploadControllerBase { +class SessionCleanupListener extends HttpSessionListener { def sessionCreated(se: HttpSessionEvent): Unit = {} - def sessionDestroyed(se: HttpSessionEvent): Unit = removeTemporaryFiles()(se.getSession) + def sessionDestroyed(se: HttpSessionEvent): Unit = FileUtils.deleteDirectory(getTemporaryDir(se.getSession.getId)) } diff --git a/src/main/scala/ssh/GitCommand.scala b/src/main/scala/ssh/GitCommand.scala index 9f014f0..ffc9ccc 100644 --- a/src/main/scala/ssh/GitCommand.scala +++ b/src/main/scala/ssh/GitCommand.scala @@ -106,7 +106,9 @@ val repository = git.getRepository val receive = new ReceivePack(repository) if(!repoName.endsWith(".wiki")){ - receive.setPostReceiveHook(new CommitLogHook(owner, repoName, user, baseUrl)) + val hook = new CommitLogHook(owner, repoName, user, baseUrl) + receive.setPreReceiveHook(hook) + receive.setPostReceiveHook(hook) } receive.receive(in, out, err) } diff --git a/src/main/scala/util/ControlUtil.scala b/src/main/scala/util/ControlUtil.scala index 0b0f712..c231fb0 100644 --- a/src/main/scala/util/ControlUtil.scala +++ b/src/main/scala/util/ControlUtil.scala @@ -37,15 +37,4 @@ def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T = try f(treeWalk) finally treeWalk.release() - -// def withTmpRefSpec[T](ref: RefSpec, git: Git)(f: RefSpec => T): T = { -// try { -// f(ref) -// } finally { -// val refUpdate = git.getRepository.updateRef(ref.getDestination) -// refUpdate.setForceUpdate(true) -// refUpdate.delete() -// } -// } - } diff --git a/src/main/scala/util/Directory.scala b/src/main/scala/util/Directory.scala index d37577a..1d15f1a 100644 --- a/src/main/scala/util/Directory.scala +++ b/src/main/scala/util/Directory.scala @@ -29,24 +29,10 @@ }).getAbsolutePath val GitBucketConf = new File(GitBucketHome, "gitbucket.conf") - + val RepositoryHome = s"${GitBucketHome}/repositories" val DatabaseHome = s"${GitBucketHome}/data" - - /** - * Repository names of the specified user. - */ - def getRepositories(owner: String): List[String] = - defining(new File(s"${RepositoryHome}/${owner}")){ dir => - if(dir.exists){ - dir.listFiles.filter { file => - file.isDirectory && !file.getName.endsWith(".wiki.git") - }.map(_.getName.replaceFirst("\\.git$", "")).toList - } else { - Nil - } - } /** * Substance directory of the repository. @@ -55,11 +41,23 @@ new File(s"${RepositoryHome}/${owner}/${repository}.git") /** + * Directory for files which are attached to issue. + */ + def getAttachedDir(owner: String, repository: String): File = + new File(s"${RepositoryHome}/${owner}/${repository}/issues") + + /** * Directory for uploaded files by the specified user. */ def getUserUploadDir(userName: String): File = new File(s"${GitBucketHome}/data/${userName}/files") /** + * Root of temporary directories for the upload file. + */ + def getTemporaryDir(sessionId: String): File = + new File(s"${GitBucketHome}/tmp/_upload/${sessionId}") + + /** * Root of temporary directories for the specified repository. */ def getTemporaryDir(owner: String, repository: String): File = diff --git a/src/main/scala/util/FileUtil.scala b/src/main/scala/util/FileUtil.scala index e4c052f..46218ee 100644 --- a/src/main/scala/util/FileUtil.scala +++ b/src/main/scala/util/FileUtil.scala @@ -4,9 +4,14 @@ import java.net.URLConnection import java.io.File import util.ControlUtil._ +import scala.util.Random +import eu.medsea.mimeutil.{MimeUtil2, MimeType} object FileUtil { - + + private val mimeUtil = new MimeUtil2() + mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector") + def getMimeType(name: String): String = defining(URLConnection.getFileNameMap()){ fileNameMap => fileNameMap.getContentTypeFor(name) match { @@ -15,6 +20,16 @@ } } + /** + * Returns mime type detected by file content. + * + * @param file File object + * @return mime type String + */ + def getMimeType(file: File): String = { + MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString + } + def getContentType(name: String, bytes: Array[Byte]): String = { defining(getMimeType(name)){ mimeType => if(mimeType == "application/octet-stream" && isText(bytes)){ @@ -26,32 +41,12 @@ } def isImage(name: String): Boolean = getMimeType(name).startsWith("image/") - + def isLarge(size: Long): Boolean = (size > 1024 * 1000) - + def isText(content: Array[Byte]): Boolean = !content.contains(0) -// def createZipFile(dest: File, dir: File): Unit = { -// def addDirectoryToZip(out: ZipArchiveOutputStream, dir: File, path: String): Unit = { -// dir.listFiles.map { file => -// if(file.isFile){ -// out.putArchiveEntry(new ZipArchiveEntry(path + "/" + file.getName)) -// out.write(FileUtils.readFileToByteArray(file)) -// out.closeArchiveEntry -// } else if(file.isDirectory){ -// addDirectoryToZip(out, file, path + "/" + file.getName) -// } -// } -// } -// -// using(new ZipArchiveOutputStream(dest)){ out => -// addDirectoryToZip(out, dir, dir.getName) -// } -// } - - def getFileName(path: String): String = defining(path.lastIndexOf('/')){ i => - if(i >= 0) path.substring(i + 1) else path - } + def generateFileId: String = System.currentTimeMillis + Random.alphanumeric.take(10).mkString def getExtension(name: String): String = name.lastIndexOf('.') match { diff --git a/src/main/scala/util/JGitUtil.scala b/src/main/scala/util/JGitUtil.scala index 5ab4bf7..9f48c39 100644 --- a/src/main/scala/util/JGitUtil.scala +++ b/src/main/scala/util/JGitUtil.scala @@ -92,8 +92,9 @@ * * @param viewType "image", "large" or "other" * @param content the string content + * @param charset the character encoding */ - case class ContentInfo(viewType: String, content: Option[String]) + case class ContentInfo(viewType: String, content: Option[String], charset: Option[String]) /** * The tag data. @@ -137,7 +138,7 @@ using(Git.open(getRepositoryDir(owner, repository))){ git => try { // get commit count - val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10000).sum + val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10001).sum RepositoryInfo( owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", @@ -480,7 +481,7 @@ } def createNewCommit(git: Git, inserter: ObjectInserter, headId: AnyObjectId, treeId: AnyObjectId, - fullName: String, mailAddress: String, message: String): String = { + fullName: String, mailAddress: String, message: String): ObjectId = { val newCommit = new CommitBuilder() newCommit.setCommitter(new PersonIdent(fullName, mailAddress)) newCommit.setAuthor(new PersonIdent(fullName, mailAddress)) @@ -498,7 +499,7 @@ refUpdate.setNewObjectId(newHeadId) refUpdate.update() - newHeadId.getName + newHeadId } /** @@ -549,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. * @@ -570,4 +591,42 @@ case e: MissingObjectException => None } + /** + * Returns all commit id in the specified repository. + */ + def getAllCommitIds(git: Git): Seq[String] = if(isEmpty(git)) { + Nil + } else { + val existIds = new scala.collection.mutable.ListBuffer[String]() + val i = git.log.all.call.iterator + while(i.hasNext){ + existIds += i.next.name + } + existIds.toSeq + } + + def processTree(git: Git, id: ObjectId)(f: (String, CanonicalTreeParser) => Unit) = { + using(new RevWalk(git.getRepository)){ revWalk => + using(new TreeWalk(git.getRepository)){ treeWalk => + val index = treeWalk.addTree(revWalk.parseTree(id)) + treeWalk.setRecursive(true) + while(treeWalk.next){ + f(treeWalk.getPathString, treeWalk.getTree(index, classOf[CanonicalTreeParser])) + } + } + } + } + + /** + * Returns the identifier of the root commit (or latest merge commit) of the specified branch. + */ + def getForkedCommitId(oldGit: Git, newGit: Git, + userName: String, repositoryName: String, branch: String, + requestUserName: String, requestRepositoryName: String, requestBranch: String): String = + defining(getAllCommitIds(oldGit)){ existIds => + getCommitLogs(newGit, requestBranch, true) { commit => + existIds.contains(commit.name) && getBranchesOfCommit(oldGit, commit.getName).contains(branch) + }.head.id + } + } diff --git a/src/main/scala/view/Asciidoc.scala b/src/main/scala/view/Asciidoc.scala deleted file mode 100644 index 5c131c4..0000000 --- a/src/main/scala/view/Asciidoc.scala +++ /dev/null @@ -1,62 +0,0 @@ -package view - -import org.asciidoctor.Asciidoctor -import org.asciidoctor.AttributesBuilder -import org.asciidoctor.OptionsBuilder -import org.asciidoctor.SafeMode -import org.htmlcleaner.HtmlCleaner -import org.htmlcleaner.HtmlNode -import org.htmlcleaner.SimpleHtmlSerializer -import org.htmlcleaner.TagNode -import org.htmlcleaner.TagNodeVisitor - -object Asciidoc { - - private[this] lazy val asciidoctor = Asciidoctor.Factory.create() - - /** - * Converts Markdown of Wiki pages to HTML. - */ - def toHtml(filePath: List[String], asciidoc: String, branch: String, repository: service.RepositoryService.RepositoryInfo, - enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): String = { - - val options = OptionsBuilder.options() - options.safe(SafeMode.SECURE) - val attributes = AttributesBuilder.attributes() - attributes.showTitle(true) - options.attributes(attributes.get()) - val rendered = asciidoctor.render(asciidoc, options) - - val path = filePath.reverse.tail.reverse match { - case Nil => "" - case p => p.mkString("", "/", "/") - } - val relativeUrlPrefix = s"${helpers.url(repository)}/blob/${branch}/${path}" - prefixRelativeUrls(rendered, relativeUrlPrefix) - } - - private[this] val exceptionPrefixes = Seq("#", "/", "http://", "https://") - - def prefixRelativeUrls(html: String, urlPrefix: String): String = { - val cleaner = new HtmlCleaner() - val node = cleaner.clean(html) - node.traverse(new TagNodeVisitor() { - override def visit(tagNode: TagNode, htmlNode: HtmlNode): Boolean = { - htmlNode match { - case tag: TagNode if tag.getName == "a" => - Option(tag.getAttributeByName("href")) foreach { href => - if (exceptionPrefixes.forall(p => !href.startsWith(p))) { - tag.addAttribute("href", s"${urlPrefix}${href}") - } - } - case _ => - } - // continue traversal - true - } - }) - new SimpleHtmlSerializer(cleaner.getProperties()).getAsString(node) - } - -} - diff --git a/src/main/scala/view/AvatarImageProvider.scala b/src/main/scala/view/AvatarImageProvider.scala index 1dff9ab..b41023f 100644 --- a/src/main/scala/view/AvatarImageProvider.scala +++ b/src/main/scala/view/AvatarImageProvider.scala @@ -17,7 +17,7 @@ // by user name getAccountByUserName(userName).map { account => if(account.image.isEmpty && context.settings.gravatar){ - s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}""" + s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g""" } else { s"""${context.path}/${account.userName}/_avatar""" } @@ -28,13 +28,13 @@ // by mail address getAccountByMailAddress(mailAddress).map { account => if(account.image.isEmpty && context.settings.gravatar){ - s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}""" + s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g""" } else { s"""${context.path}/${account.userName}/_avatar""" } } getOrElse { if(context.settings.gravatar){ - s"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}""" + s"""https://www.gravatar.com/avatar/${StringUtil.md5(mailAddress.toLowerCase)}?s=${size}&d=retro&r=g""" } else { s"""${context.path}/_unknown/_avatar""" } @@ -42,9 +42,9 @@ } if(tooltip){ - Html(s"""""") + Html(s"""""") } else { - Html(s"""""") + Html(s"""""") } } diff --git a/src/main/scala/view/Markdown.scala b/src/main/scala/view/Markdown.scala index f83ef10..040d8d9 100644 --- a/src/main/scala/view/Markdown.scala +++ b/src/main/scala/view/Markdown.scala @@ -96,7 +96,8 @@ ) with RepositoryService with LinkConverter with RequestCache { override protected def printImageTag(imageNode: SuperNode, url: String): Unit = - printer.print("\"").printEncoded(printChildrenToString(imageNode)).print("\"/") + printer.print("") + .print("\"").printEncoded(printChildrenToString(imageNode)).print("\"/") override protected def printLink(rendering: LinkRenderer.Rendering): Unit = { printer.print('<').print('a') diff --git a/src/main/scala/view/helpers.scala b/src/main/scala/view/helpers.scala index 774c034..e57d79e 100644 --- a/src/main/scala/view/helpers.scala +++ b/src/main/scala/view/helpers.scala @@ -16,6 +16,11 @@ def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date) /** + * Format java.util.Date to "yyyy-MM-dd'T'hh:mm:ss'Z'". + */ + def datetimeRFC3339(date: Date): String = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'").format(date).replaceAll("(\\d\\d)(\\d\\d)$","$1:$2") + + /** * Format java.util.Date to "yyyy-MM-dd". */ def date(date: Date): String = new SimpleDateFormat("yyyy-MM-dd").format(date) @@ -30,9 +35,7 @@ private[this] val renderersBySuffix: Seq[(String, (List[String], String, String, service.RepositoryService.RepositoryInfo, Boolean, Boolean, app.Context) => Html)] = Seq( ".md" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context)), - ".markdown" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context)), - ".adoc" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => asciidoc(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink)(context)), - ".asciidoc" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => asciidoc(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink)(context)) + ".markdown" -> ((filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) => markdown(fileContent, repository, enableWikiLink, enableRefsLink)(context)) ) def renderableSuffixes: Seq[String] = renderersBySuffix.map(_._1) @@ -59,10 +62,6 @@ } } - def asciidoc(filePath: List[String], value: String, branch: String, repository: service.RepositoryService.RepositoryInfo, - enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = - Html(Asciidoc.toHtml(filePath, value, branch, repository, enableWikiLink, enableRefsLink)) - /** * Returns <img> which displays the avatar icon for the given user name. * This method looks up Gravatar if avatar icon has not been configured in user settings. @@ -165,6 +164,45 @@ def isPast(date: Date): Boolean = System.currentTimeMillis > date.getTime /** + * Returns file type for AceEditor. + */ + def editorType(fileName: String): String = { + fileName.toLowerCase match { + case x if(x.endsWith(".bat")) => "batchfile" + case x if(x.endsWith(".java")) => "java" + case x if(x.endsWith(".scala")) => "scala" + case x if(x.endsWith(".js")) => "javascript" + case x if(x.endsWith(".css")) => "css" + case x if(x.endsWith(".md")) => "markdown" + case x if(x.endsWith(".html")) => "html" + case x if(x.endsWith(".xml")) => "xml" + case x if(x.endsWith(".c")) => "c_cpp" + case x if(x.endsWith(".cpp")) => "c_cpp" + case x if(x.endsWith(".coffee")) => "coffee" + case x if(x.endsWith(".ejs")) => "ejs" + case x if(x.endsWith(".hs")) => "haskell" + case x if(x.endsWith(".json")) => "json" + case x if(x.endsWith(".jsp")) => "jsp" + case x if(x.endsWith(".jsx")) => "jsx" + case x if(x.endsWith(".cl")) => "lisp" + case x if(x.endsWith(".clojure")) => "lisp" + case x if(x.endsWith(".lua")) => "lua" + case x if(x.endsWith(".php")) => "php" + case x if(x.endsWith(".py")) => "python" + case x if(x.endsWith(".rdoc")) => "rdoc" + case x if(x.endsWith(".rhtml")) => "rhtml" + case x if(x.endsWith(".ruby")) => "ruby" + case x if(x.endsWith(".sh")) => "sh" + case x if(x.endsWith(".sql")) => "sql" + case x if(x.endsWith(".tcl")) => "tcl" + case x if(x.endsWith(".vbs")) => "vbscript" + case x if(x.endsWith(".tcl")) => "tcl" + case x if(x.endsWith(".yml")) => "yaml" + case _ => "plain_text" + } + } + + /** * Implicit conversion to add mkHtml() to Seq[Html]. */ implicit class RichHtmlSeq(seq: Seq[Html]) { diff --git a/src/main/twirl/account/activity.scala.html b/src/main/twirl/account/activity.scala.html index 1f2cefc..ca4c3c7 100644 --- a/src/main/twirl/account/activity.scala.html +++ b/src/main/twirl/account/activity.scala.html @@ -2,5 +2,8 @@ @import context._ @import view.helpers._ @main(account, groupNames, "activity"){ +
+ activities +
@helper.html.activities(activities) } diff --git a/src/main/twirl/account/main.scala.html b/src/main/twirl/account/main.scala.html index 17e05c0..6a16f51 100644 --- a/src/main/twirl/account/main.scala.html +++ b/src/main/twirl/account/main.scala.html @@ -28,7 +28,7 @@
-