diff --git a/project/build.scala b/project/build.scala index 7d41538..4626000 100644 --- a/project/build.scala +++ b/project/build.scala @@ -8,7 +8,7 @@ import sbtassembly.AssemblyKeys._ object MyBuild extends Build { - val Organization = "jp.sf.amateras" + val Organization = "gitbucket" val Name = "gitbucket" val Version = "0.0.1" val ScalaVersion = "2.11.2" @@ -66,6 +66,7 @@ "com.typesafe" % "config" % "1.2.1", "com.typesafe.play" %% "twirl-compiler" % "1.0.2" ), + play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._", EclipseKeys.withSource := true, javacOptions in compile ++= Seq("-target", "7", "-source", "7"), testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"), diff --git a/src/main/java/gitbucket/core/util/PatchUtil.java b/src/main/java/gitbucket/core/util/PatchUtil.java new file mode 100644 index 0000000..3cc64ce --- /dev/null +++ b/src/main/java/gitbucket/core/util/PatchUtil.java @@ -0,0 +1,93 @@ +package gitbucket.core.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/java/util/PatchUtil.java b/src/main/java/util/PatchUtil.java deleted file mode 100644 index a316a75..0000000 --- a/src/main/java/util/PatchUtil.java +++ /dev/null @@ -1,93 +0,0 @@ -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/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala index cfa37e6..81002e3 100644 --- a/src/main/scala/ScalatraBootstrap.scala +++ b/src/main/scala/ScalatraBootstrap.scala @@ -1,6 +1,8 @@ -import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter} -import app._ -import plugin.PluginRegistry + +import gitbucket.core.controller._ +import gitbucket.core.plugin.PluginRegistry +import gitbucket.core.servlet.{TransactionFilter, BasicAuthenticationFilter} +import gitbucket.core.util.Directory //import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider import org.scalatra._ @@ -38,7 +40,7 @@ context.mount(new RepositorySettingsController, "/*") // Create GITBUCKET_HOME directory if it does not exist - val dir = new java.io.File(_root_.util.Directory.GitBucketHome) + val dir = new java.io.File(Directory.GitBucketHome) if(!dir.exists){ dir.mkdirs() } diff --git a/src/main/scala/app/AccountController.scala b/src/main/scala/app/AccountController.scala deleted file mode 100644 index 8311654..0000000 --- a/src/main/scala/app/AccountController.scala +++ /dev/null @@ -1,468 +0,0 @@ -package app - -import service._ -import util._ -import util.StringUtil._ -import util.Directory._ -import util.ControlUtil._ -import util.Implicits._ -import ssh.SshUtil -import jp.sf.amateras.scalatra.forms._ -import org.apache.commons.io.FileUtils -import org.scalatra.i18n.Messages -import org.eclipse.jgit.api.Git -import org.eclipse.jgit.lib.{FileMode, Constants} -import org.eclipse.jgit.dircache.DirCache -import model.GroupMember - -class AccountController extends AccountControllerBase - with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService - with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator - -trait AccountControllerBase extends AccountManagementControllerBase { - self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService - with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator => - - case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String, - url: Option[String], fileId: Option[String]) - - case class AccountEditForm(password: Option[String], fullName: String, mailAddress: String, - url: Option[String], fileId: Option[String], clearImage: Boolean) - - case class SshKeyForm(title: String, publicKey: String) - - val newForm = mapping( - "userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))), - "password" -> trim(label("Password" , text(required, maxlength(20)))), - "fullName" -> trim(label("Full Name" , text(required, maxlength(100)))), - "mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))), - "url" -> trim(label("URL" , optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" , optional(text()))) - )(AccountNewForm.apply) - - val editForm = mapping( - "password" -> trim(label("Password" , optional(text(maxlength(20))))), - "fullName" -> trim(label("Full Name" , text(required, maxlength(100)))), - "mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))), - "url" -> trim(label("URL" , optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" , optional(text()))), - "clearImage" -> trim(label("Clear image" , boolean())) - )(AccountEditForm.apply) - - val sshKeyForm = mapping( - "title" -> trim(label("Title", text(required, maxlength(100)))), - "publicKey" -> trim(label("Key" , text(required, validPublicKey))) - )(SshKeyForm.apply) - - case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String) - case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String, clearImage: Boolean) - - val newGroupForm = mapping( - "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))), - "members" -> trim(label("Members" ,text(required, members))) - )(NewGroupForm.apply) - - val editGroupForm = mapping( - "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))), - "members" -> trim(label("Members" ,text(required, members))), - "clearImage" -> trim(label("Clear image" ,boolean())) - )(EditGroupForm.apply) - - case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) - case class ForkRepositoryForm(owner: String, name: String) - - val newRepositoryForm = mapping( - "owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))), - "name" -> trim(label("Repository name", text(required, maxlength(40), identifier, uniqueRepository))), - "description" -> trim(label("Description" , optional(text()))), - "isPrivate" -> trim(label("Repository Type", boolean())), - "createReadme" -> trim(label("Create README" , boolean())) - )(RepositoryCreationForm.apply) - - val forkRepositoryForm = mapping( - "owner" -> trim(label("Repository owner", text(required))), - "name" -> trim(label("Repository name", text(required))) - )(ForkRepositoryForm.apply) - - case class AccountForm(accountName: String) - - val accountForm = mapping( - "account" -> trim(label("Group/User name", text(required, validAccountName))) - )(AccountForm.apply) - - /** - * Displays user information. - */ - get("/:userName") { - val userName = params("userName") - getAccountByUserName(userName).map { account => - params.getOrElse("tab", "repositories") match { - // Public Activity - case "activity" => - _root_.account.html.activity(account, - if(account.isGroupAccount) Nil else getGroupsByUserName(userName), - getActivitiesByUser(userName, true)) - - // Members - case "members" if(account.isGroupAccount) => { - val members = getGroupMembers(account.userName) - _root_.account.html.members(account, members.map(_.userName), - context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })) - } - - // Repositories - case _ => { - val members = getGroupMembers(account.userName) - _root_.account.html.repositories(account, - if(account.isGroupAccount) Nil else getGroupsByUserName(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 => - RawData(FileUtil.getMimeType(image), new java.io.File(getUserUploadDir(userName), image)) - } getOrElse { - contentType = "image/png" - Thread.currentThread.getContextClassLoader.getResourceAsStream("noimage.png") - } - } - - get("/:userName/_edit")(oneselfOnly { - val userName = params("userName") - getAccountByUserName(userName).map { x => - account.html.edit(x, flash.get("info")) - } getOrElse NotFound - }) - - post("/:userName/_edit", editForm)(oneselfOnly { form => - val userName = params("userName") - getAccountByUserName(userName).map { account => - updateAccount(account.copy( - password = form.password.map(sha1).getOrElse(account.password), - fullName = form.fullName, - mailAddress = form.mailAddress, - url = form.url)) - - updateImage(userName, form.fileId, form.clearImage) - flash += "info" -> "Account information has been updated." - redirect(s"/${userName}/_edit") - - } getOrElse NotFound - }) - - get("/:userName/_delete")(oneselfOnly { - val userName = params("userName") - - getAccountByUserName(userName, true).foreach { account => - // Remove repositories - getRepositoryNamesOfUser(userName).foreach { repositoryName => - deleteRepository(userName, repositoryName) - FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName)) - FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName)) - FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName)) - } - // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY - removeUserRelatedData(userName) - - updateAccount(account.copy(isRemoved = true)) - } - - session.invalidate - redirect("/") - }) - - get("/:userName/_ssh")(oneselfOnly { - val userName = params("userName") - getAccountByUserName(userName).map { x => - account.html.ssh(x, getPublicKeys(x.userName)) - } getOrElse NotFound - }) - - post("/:userName/_ssh", sshKeyForm)(oneselfOnly { form => - val userName = params("userName") - addPublicKey(userName, form.title, form.publicKey) - redirect(s"/${userName}/_ssh") - }) - - get("/:userName/_ssh/delete/:id")(oneselfOnly { - val userName = params("userName") - val sshKeyId = params("id").toInt - deletePublicKey(userName, sshKeyId) - redirect(s"/${userName}/_ssh") - }) - - get("/register"){ - if(context.settings.allowAccountRegistration){ - if(context.loginAccount.isDefined){ - redirect("/") - } else { - account.html.register() - } - } else NotFound - } - - post("/register", newForm){ form => - if(context.settings.allowAccountRegistration){ - createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url) - updateImage(form.userName, form.fileId, false) - redirect("/signin") - } else NotFound - } - - get("/groups/new")(usersOnly { - account.html.group(None, List(GroupMember("", context.loginAccount.get.userName, true))) - }) - - post("/groups/new", newGroupForm)(usersOnly { form => - createGroup(form.groupName, form.url) - updateGroupMembers(form.groupName, form.members.split(",").map { - _.split(":") match { - case Array(userName, isManager) => (userName, isManager.toBoolean) - } - }.toList) - updateImage(form.groupName, form.fileId, false) - redirect(s"/${form.groupName}") - }) - - get("/:groupName/_editgroup")(managersOnly { - defining(params("groupName")){ groupName => - account.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName)) - } - }) - - get("/:groupName/_deletegroup")(managersOnly { - defining(params("groupName")){ groupName => - // Remove from GROUP_MEMBER - updateGroupMembers(groupName, Nil) - // Remove repositories - getRepositoryNamesOfUser(groupName).foreach { repositoryName => - deleteRepository(groupName, repositoryName) - FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName)) - FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName)) - FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName)) - } - } - redirect("/") - }) - - post("/:groupName/_editgroup", editGroupForm)(managersOnly { form => - defining(params("groupName"), form.members.split(",").map { - _.split(":") match { - case Array(userName, isManager) => (userName, isManager.toBoolean) - } - }.toList){ case (groupName, members) => - getAccountByUserName(groupName, true).map { account => - updateGroup(groupName, form.url, false) - - // Update GROUP_MEMBER - updateGroupMembers(form.groupName, members) - // Update COLLABORATOR for group repositories - getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => - removeCollaborators(form.groupName, repositoryName) - members.foreach { case (userName, isManager) => - addCollaborator(form.groupName, repositoryName, userName) - } - } - - updateImage(form.groupName, form.fileId, form.clearImage) - redirect(s"/${form.groupName}") - - } getOrElse NotFound - } - }) - - /** - * Show the new repository form. - */ - get("/new")(usersOnly { - account.html.newrepo(getGroupsByUserName(context.loginAccount.get.userName), context.settings.isCreateRepoOptionPublic) - }) - - /** - * Create new repository. - */ - post("/new", newRepositoryForm)(usersOnly { form => - LockUtil.lock(s"${form.owner}/${form.name}"){ - if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){ - val ownerAccount = getAccountByUserName(form.owner).get - val loginAccount = context.loginAccount.get - val loginUserName = loginAccount.userName - - // Insert to the database at first - createRepository(form.name, form.owner, form.description, form.isPrivate) - - // Add collaborators for group repository - if(ownerAccount.isGroupAccount){ - getGroupMembers(form.owner).foreach { member => - addCollaborator(form.owner, form.name, member.userName) - } - } - - // Insert default labels - insertDefaultLabels(form.owner, form.name) - - // Create the actual repository - val gitdir = getRepositoryDir(form.owner, form.name) - JGitUtil.initRepository(gitdir) - - if(form.createReadme){ - 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" - } - - builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE, - inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")))) - builder.finish() - - JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), - Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit") - } - } - - // Create Wiki repository - createWikiRepository(loginAccount, form.owner, form.name) - - // Record activity - recordCreateRepositoryActivity(form.owner, form.name, loginUserName) - } - - // redirect to the repository - redirect(s"/${form.owner}/${form.name}") - } - }) - - get("/:owner/:repository/fork")(readableUsersOnly { repository => - val loginAccount = context.loginAccount.get - val loginUserName = loginAccount.userName - val groups = getGroupsByUserName(loginUserName) - groups match { - case _: List[String] => - val managerPermissions = groups.map { group => - val members = getGroupMembers(group) - context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }) - } - _root_.helper.html.forkrepository( - repository, - (groups zip managerPermissions).toMap - ) - case _ => redirect(s"/${loginUserName}") - } - }) - - post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) => - val loginAccount = context.loginAccount.get - val loginUserName = loginAccount.userName - val accountName = form.accountName - - LockUtil.lock(s"${accountName}/${repository.name}"){ - if(getRepository(accountName, repository.name, baseUrl).isDefined || - (accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){ - // redirect to the repository if repository already exists - redirect(s"/${accountName}/${repository.name}") - } else { - // Insert to the database at first - val originUserName = repository.repository.originUserName.getOrElse(repository.owner) - val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name) - - createRepository( - repositoryName = repository.name, - userName = accountName, - description = repository.repository.description, - isPrivate = repository.repository.isPrivate, - originRepositoryName = Some(originRepositoryName), - originUserName = Some(originUserName), - parentRepositoryName = Some(repository.name), - parentUserName = Some(repository.owner) - ) - - // Insert default labels - insertDefaultLabels(accountName, repository.name) - - // clone repository actually - JGitUtil.cloneRepository( - getRepositoryDir(repository.owner, repository.name), - getRepositoryDir(accountName, repository.name)) - - // Create Wiki repository - JGitUtil.cloneRepository( - getWikiRepositoryDir(repository.owner, repository.name), - getWikiRepositoryDir(accountName, repository.name)) - - // Record activity - recordForkActivity(repository.owner, repository.name, loginUserName, accountName) - // redirect to the repository - redirect(s"/${accountName}/${repository.name}") - } - } - }) - - private def insertDefaultLabels(userName: String, repositoryName: String): Unit = { - createLabel(userName, repositoryName, "bug", "fc2929") - createLabel(userName, repositoryName, "duplicate", "cccccc") - createLabel(userName, repositoryName, "enhancement", "84b6eb") - createLabel(userName, repositoryName, "invalid", "e6e6e6") - createLabel(userName, repositoryName, "question", "cc317c") - createLabel(userName, repositoryName, "wontfix", "ffffff") - } - - private def existsAccount: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = - if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None - } - - private def uniqueRepository: Constraint = new Constraint(){ - override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = - params.get("owner").flatMap { userName => - getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.") - } - } - - private def members: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = { - if(value.split(",").exists { - _.split(":") match { case Array(userName, isManager) => isManager.toBoolean } - }) None else Some("Must select one manager at least.") - } - } - - private def validPublicKey: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = SshUtil.str2PublicKey(value) match { - case Some(_) => None - case None => Some("Key is invalid.") - } - } - - private def validAccountName: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = { - getAccountByUserName(value) match { - case Some(_) => None - case None => Some("Invalid Group/User Account.") - } - } - } -} diff --git a/src/main/scala/app/AnonymousAccessController.scala b/src/main/scala/app/AnonymousAccessController.scala deleted file mode 100644 index 35481ab..0000000 --- a/src/main/scala/app/AnonymousAccessController.scala +++ /dev/null @@ -1,14 +0,0 @@ -package app - -class AnonymousAccessController extends AnonymousAccessControllerBase - -trait AnonymousAccessControllerBase extends ControllerBase { - get(!context.settings.allowAnonymousAccess, context.loginAccount.isEmpty) { - if(!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") && - !context.currentPath.startsWith("/register")) { - Unauthorized() - } else { - pass() - } - } -} diff --git a/src/main/scala/app/ControllerBase.scala b/src/main/scala/app/ControllerBase.scala deleted file mode 100644 index 8a184f0..0000000 --- a/src/main/scala/app/ControllerBase.scala +++ /dev/null @@ -1,213 +0,0 @@ -package app - -import _root_.util.Directory._ -import _root_.util.Implicits._ -import _root_.util.ControlUtil._ -import _root_.util.{StringUtil, FileUtil, Validations, Keys} -import org.scalatra._ -import org.scalatra.json._ -import org.json4s._ -import jp.sf.amateras.scalatra.forms._ -import org.apache.commons.io.FileUtils -import model._ -import service.{SystemSettingsService, AccountService} -import javax.servlet.http.{HttpServletResponse, HttpServletRequest} -import javax.servlet.{FilterChain, ServletResponse, ServletRequest} -import org.scalatra.i18n._ - -/** - * Provides generic features for controller implementations. - */ -abstract class ControllerBase extends ScalatraFilter - with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations - with SystemSettingsService { - - implicit val jsonFormats = DefaultFormats - -// TODO Scala 2.11 -// // Don't set content type via Accept header. -// override def format(implicit request: HttpServletRequest) = "" - - override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = try { - val httpRequest = request.asInstanceOf[HttpServletRequest] - val httpResponse = response.asInstanceOf[HttpServletResponse] - val context = request.getServletContext.getContextPath - val path = httpRequest.getRequestURI.substring(context.length) - - if(path.startsWith("/console/")){ - val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account] - val baseUrl = this.baseUrl(httpRequest) - if(account == null){ - // Redirect to login form - httpResponse.sendRedirect(baseUrl + "/signin?redirect=" + StringUtil.urlEncode(path)) - } else if(account.isAdmin){ - // H2 Console (administrators only) - chain.doFilter(request, response) - } else { - // Redirect to dashboard - httpResponse.sendRedirect(baseUrl + "/") - } - } else if(path.startsWith("/git/")){ - // Git repository - chain.doFilter(request, response) - } else { - // Scalatra actions - super.doFilter(request, response, chain) - } - } finally { - contextCache.remove(); - } - - private val contextCache = new java.lang.ThreadLocal[Context]() - - /** - * Returns the context object for the request. - */ - implicit def context: Context = { - contextCache.get match { - case null => { - val context = Context(loadSystemSettings(), LoginAccount, request) - contextCache.set(context) - context - } - case context => context - } - } - - private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount) - - def ajaxGet(path : String)(action : => Any) : Route = - super.get(path){ - request.setAttribute(Keys.Request.Ajax, "true") - action - } - - override def ajaxGet[T](path : String, form : ValueType[T])(action : T => Any) : Route = - super.ajaxGet(path, form){ form => - request.setAttribute(Keys.Request.Ajax, "true") - action(form) - } - - def ajaxPost(path : String)(action : => Any) : Route = - super.post(path){ - request.setAttribute(Keys.Request.Ajax, "true") - action - } - - override def ajaxPost[T](path : String, form : ValueType[T])(action : T => Any) : Route = - super.ajaxPost(path, form){ form => - request.setAttribute(Keys.Request.Ajax, "true") - action(form) - } - - protected def NotFound() = - if(request.hasAttribute(Keys.Request.Ajax)){ - org.scalatra.NotFound() - } else { - org.scalatra.NotFound(html.error("Not Found")) - } - - protected def Unauthorized()(implicit context: app.Context) = - if(request.hasAttribute(Keys.Request.Ajax)){ - org.scalatra.Unauthorized() - } else { - if(context.loginAccount.isDefined){ - org.scalatra.Unauthorized(redirect("/")) - } else { - if(request.getMethod.toUpperCase == "POST"){ - org.scalatra.Unauthorized(redirect("/signin")) - } else { - org.scalatra.Unauthorized(redirect("/signin?redirect=" + StringUtil.urlEncode( - defining(request.getQueryString){ queryString => - request.getRequestURI.substring(request.getContextPath.length) + (if(queryString != null) "?" + queryString else "") - } - ))) - } - } - } - - // TODO Scala 2.11 - override def url(path: String, params: Iterable[(String, Any)] = Iterable.empty, - includeContextPath: Boolean = true, includeServletPath: Boolean = true, - absolutize: Boolean = true, withSessionId: Boolean = true) - (implicit request: HttpServletRequest, response: HttpServletResponse): String = - if (path.startsWith("http")) path - else baseUrl + super.url(path, params, false, false, false) - - /** - * Use this method to response the raw data against XSS. - */ - protected def RawData[T](contentType: String, rawData: T): T = { - if(contentType.split(";").head.trim.toLowerCase.startsWith("text/html")){ - this.contentType = "text/plain" - } else { - this.contentType = contentType - } - response.addHeader("X-Content-Type-Options", "nosniff") - rawData - } -} - -/** - * Context object for the current request. - */ -case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){ - - 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. - * - * If object has not been cached with the specified key then retrieves by given action. - * Cached object are available during a request. - */ - def cache[A](key: String)(action: => A): A = - defining(Keys.Request.Cache(key)){ cacheKey => - Option(request.getAttribute(cacheKey).asInstanceOf[A]).getOrElse { - val newObject = action - request.setAttribute(cacheKey, newObject) - newObject - } - } - -} - -/** - * Base trait for controllers which manages account information. - */ -trait AccountManagementControllerBase extends ControllerBase { - self: AccountService => - - protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit = - if(clearImage){ - getAccountByUserName(userName).flatMap(_.image).map { image => - new java.io.File(getUserUploadDir(userName), image).delete() - updateAvatarImage(userName, None) - } - } else { - fileId.map { fileId => - val filename = "avatar." + FileUtil.getExtension(session.getAndRemove(Keys.Session.Upload(fileId)).get) - FileUtils.moveFile( - new java.io.File(getTemporaryDir(session.getId), fileId), - new java.io.File(getUserUploadDir(userName), filename) - ) - updateAvatarImage(userName, Some(filename)) - } - } - - protected def uniqueUserName: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = - getAccountByUserName(value, true).map { _ => "User already exists." } - } - - protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){ - override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = - getAccountByMailAddress(value, true) - .filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) } - .map { _ => "Mail address is already registered." } - } - -} diff --git a/src/main/scala/app/DashboardController.scala b/src/main/scala/app/DashboardController.scala deleted file mode 100644 index 7c44529..0000000 --- a/src/main/scala/app/DashboardController.scala +++ /dev/null @@ -1,137 +0,0 @@ -package app - -import service._ -import util.{StringUtil, UsersAuthenticator, Keys} -import util.Implicits._ -import service.IssuesService.IssueSearchCondition - -class DashboardController extends DashboardControllerBase - with IssuesService with PullRequestService with RepositoryService with AccountService - with UsersAuthenticator - -trait DashboardControllerBase extends ControllerBase { - self: IssuesService with PullRequestService with RepositoryService with AccountService - with UsersAuthenticator => - - get("/dashboard/issues")(usersOnly { - val q = request.getParameter("q") - val account = context.loginAccount.get - Option(q).map { q => - val condition = IssueSearchCondition(q, Map[String, Int]()) - q match { - case q if(q.contains("is:pr")) => redirect(s"/dashboard/pulls?q=${StringUtil.urlEncode(q)}") - case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/issues/created_by${condition.toURL}") - case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/issues/assigned${condition.toURL}") - case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/issues/mentioned${condition.toURL}") - case _ => searchIssues("created_by") - } - } getOrElse { - searchIssues("created_by") - } - }) - - get("/dashboard/issues/assigned")(usersOnly { - searchIssues("assigned") - }) - - get("/dashboard/issues/created_by")(usersOnly { - searchIssues("created_by") - }) - - get("/dashboard/issues/mentioned")(usersOnly { - searchIssues("mentioned") - }) - - get("/dashboard/pulls")(usersOnly { - val q = request.getParameter("q") - val account = context.loginAccount.get - Option(q).map { q => - val condition = IssueSearchCondition(q, Map[String, Int]()) - q match { - case q if(q.contains("is:issue")) => redirect(s"/dashboard/issues?q=${StringUtil.urlEncode(q)}") - case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/pulls/created_by${condition.toURL}") - case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/pulls/assigned${condition.toURL}") - case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/pulls/mentioned${condition.toURL}") - case _ => searchPullRequests("created_by") - } - } getOrElse { - searchPullRequests("created_by") - } - }) - - get("/dashboard/pulls/created_by")(usersOnly { - searchPullRequests("created_by") - }) - - get("/dashboard/pulls/assigned")(usersOnly { - searchPullRequests("assigned") - }) - - get("/dashboard/pulls/mentioned")(usersOnly { - searchPullRequests("mentioned") - }) - - private def getOrCreateCondition(key: String, filter: String, userName: String) = { - val condition = session.putAndGet(key, if(request.hasQueryString){ - val q = request.getParameter("q") - if(q == null){ - IssueSearchCondition(request) - } else { - IssueSearchCondition(q, Map[String, Int]()) - } - } else session.getAs[IssueSearchCondition](key).getOrElse(IssueSearchCondition())) - - filter match { - case "assigned" => condition.copy(assigned = Some(userName), author = None , mentioned = None) - case "mentioned" => condition.copy(assigned = None , author = None , mentioned = Some(userName)) - case _ => condition.copy(assigned = None , author = Some(userName), mentioned = None) - } - } - - private def searchIssues(filter: String) = { - import IssuesService._ - - val userName = context.loginAccount.get.userName - val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName) - val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name) - val page = IssueSearchCondition.page(request) - - dashboard.html.issues( - searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*), - page, - countIssue(condition.copy(state = "open" ), false, userRepos: _*), - countIssue(condition.copy(state = "closed"), false, userRepos: _*), - filter match { - case "assigned" => condition.copy(assigned = Some(userName)) - case "mentioned" => condition.copy(mentioned = Some(userName)) - case _ => condition.copy(author = Some(userName)) - }, - filter, - getGroupNames(userName)) - } - - private def searchPullRequests(filter: String) = { - import IssuesService._ - import PullRequestService._ - - val userName = context.loginAccount.get.userName - val condition = getOrCreateCondition(Keys.Session.DashboardPulls, filter, userName) - val allRepos = getAllRepositories(userName) - val page = IssueSearchCondition.page(request) - - dashboard.html.pulls( - searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*), - page, - countIssue(condition.copy(state = "open" ), true, allRepos: _*), - countIssue(condition.copy(state = "closed"), true, allRepos: _*), - filter match { - case "assigned" => condition.copy(assigned = Some(userName)) - case "mentioned" => condition.copy(mentioned = Some(userName)) - case _ => condition.copy(author = Some(userName)) - }, - filter, - getGroupNames(userName)) - } - - -} diff --git a/src/main/scala/app/FileUploadController.scala b/src/main/scala/app/FileUploadController.scala deleted file mode 100644 index 9798c0a..0000000 --- a/src/main/scala/app/FileUploadController.scala +++ /dev/null @@ -1,44 +0,0 @@ -package app - -import util.{Keys, FileUtil} -import util.ControlUtil._ -import util.Directory._ -import org.scalatra._ -import org.scalatra.servlet.{MultipartConfig, FileUploadSupport, FileItem} -import org.apache.commons.io.FileUtils - -/** - * Provides Ajax based file upload functionality. - * - * This servlet saves uploaded file. - */ -class FileUploadController extends ScalatraServlet with FileUploadSupport { - - configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024))) - - post("/image"){ - 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 + "." + FileUtil.getExtension(file.getName)), 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 deleted file mode 100644 index 9a2bc37..0000000 --- a/src/main/scala/app/IndexController.scala +++ /dev/null @@ -1,106 +0,0 @@ -package app - -import util._ -import util.Implicits._ -import service._ -import jp.sf.amateras.scalatra.forms._ - -class IndexController extends IndexControllerBase - with RepositoryService with ActivityService with AccountService with UsersAuthenticator - -trait IndexControllerBase extends ControllerBase { - self: RepositoryService with ActivityService with AccountService with UsersAuthenticator => - - case class SignInForm(userName: String, password: String) - - val form = mapping( - "userName" -> trim(label("Username", text(required))), - "password" -> trim(label("Password", text(required))) - )(SignInForm.apply) - - get("/"){ - val loginAccount = context.loginAccount - if(loginAccount.isEmpty) { - html.index(getRecentActivities(), - getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true), - loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil) - ) - } else { - val loginUserName = loginAccount.get.userName - val loginUserGroups = getGroupsByUserName(loginUserName) - var visibleOwnerSet : Set[String] = Set(loginUserName) - - visibleOwnerSet ++= loginUserGroups - - html.index(getRecentActivitiesByOwners(visibleOwnerSet), - getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true), - loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil) - ) - } - } - - get("/signin"){ - val redirect = params.get("redirect") - if(redirect.isDefined && redirect.get.startsWith("/")){ - flash += Keys.Flash.Redirect -> redirect.get - } - html.signin() - } - - post("/signin", form){ form => - authenticate(context.settings, form.userName, form.password) match { - case Some(account) => signin(account) - case None => redirect("/signin") - } - } - - get("/signout"){ - session.invalidate - redirect("/") - } - - get("/activities.atom"){ - contentType = "application/atom+xml; type=feed" - helper.xml.feed(getRecentActivities()) - } - - /** - * Set account information into HttpSession and redirect. - */ - private def signin(account: model.Account) = { - session.setAttribute(Keys.Session.LoginAccount, account) - updateLastLoginDate(account.userName) - - if(LDAPUtil.isDummyMailAddress(account)) { - redirect("/" + account.userName + "/_edit") - } - - flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl => - if(redirectUrl.stripSuffix("/") == request.getContextPath){ - redirect("/") - } else { - redirect(redirectUrl) - } - }.getOrElse { - redirect("/") - } - } - - /** - * JSON API for collaborator completion. - */ - get("/_user/proposals")(usersOnly { - contentType = formats("json") - org.json4s.jackson.Serialization.write( - Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray) - ) - }) - - /** - * JSON APU for checking user existence. - */ - post("/_user/existence")(usersOnly { - getAccountByUserName(params("userName")).isDefined - }) - -} diff --git a/src/main/scala/app/IssuesController.scala b/src/main/scala/app/IssuesController.scala deleted file mode 100644 index b52b223..0000000 --- a/src/main/scala/app/IssuesController.scala +++ /dev/null @@ -1,420 +0,0 @@ -package app - -import jp.sf.amateras.scalatra.forms._ - -import service._ -import IssuesService._ -import util._ -import util.Implicits._ -import util.ControlUtil._ -import org.scalatra.Ok -import model.Issue - -class IssuesController extends IssuesControllerBase - with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService - with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator - -trait IssuesControllerBase extends ControllerBase { - self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService - with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator => - - case class IssueCreateForm(title: String, content: Option[String], - assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String]) - case class CommentForm(issueId: Int, content: String) - case class IssueStateForm(issueId: Int, content: Option[String]) - - val issueCreateForm = mapping( - "title" -> trim(label("Title", text(required))), - "content" -> trim(optional(text())), - "assignedUserName" -> trim(optional(text())), - "milestoneId" -> trim(optional(number())), - "labelNames" -> trim(optional(text())) - )(IssueCreateForm.apply) - - val issueTitleEditForm = mapping( - "title" -> trim(label("Title", text(required))) - )(x => x) - val issueEditForm = mapping( - "content" -> trim(optional(text())) - )(x => x) - - val commentForm = mapping( - "issueId" -> label("Issue Id", number()), - "content" -> trim(label("Comment", text(required))) - )(CommentForm.apply) - - val issueStateForm = mapping( - "issueId" -> label("Issue Id", number()), - "content" -> trim(optional(text())) - )(IssueStateForm.apply) - - get("/:owner/:repository/issues")(referrersOnly { repository => - val q = request.getParameter("q") - if(Option(q).exists(_.contains("is:pr"))){ - redirect(s"/${repository.owner}/${repository.name}/pulls?q=" + StringUtil.urlEncode(q)) - } else { - searchIssues(repository) - } - }) - - get("/:owner/:repository/issues/:id")(referrersOnly { repository => - defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) => - getIssue(owner, name, issueId) map { - issues.html.issue( - _, - getComments(owner, name, issueId.toInt), - getIssueLabels(owner, name, issueId.toInt), - (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, - getMilestonesWithIssueCount(owner, name), - getLabels(owner, name), - hasWritePermission(owner, name, context.loginAccount), - repository) - } getOrElse NotFound - } - }) - - get("/:owner/:repository/issues/new")(readableUsersOnly { repository => - defining(repository.owner, repository.name){ case (owner, name) => - issues.html.create( - (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, - getMilestones(owner, name), - getLabels(owner, name), - hasWritePermission(owner, name, context.loginAccount), - repository) - } - }) - - post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) => - defining(repository.owner, repository.name){ case (owner, name) => - val writable = hasWritePermission(owner, name, context.loginAccount) - val userName = context.loginAccount.get.userName - - // insert issue - val issueId = createIssue(owner, name, userName, form.title, form.content, - if(writable) form.assignedUserName else None, - if(writable) form.milestoneId else None) - - // insert labels - if(writable){ - form.labelNames.map { value => - val labels = getLabels(owner, name) - value.split(",").foreach { labelName => - labels.find(_.labelName == labelName).map { label => - registerIssueLabel(owner, name, issueId, label.labelId) - } - } - } - } - - // record activity - recordCreateIssueActivity(owner, name, userName, issueId, form.title) - - // extract references and create refer comment - getIssue(owner, name, issueId.toString).foreach { issue => - createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) - } - - // notifications - Notifier().toNotify(repository, issueId, form.content.getOrElse("")){ - Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") - } - - redirect(s"/${owner}/${name}/issues/${issueId}") - } - }) - - ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) => - defining(repository.owner, repository.name){ case (owner, name) => - getIssue(owner, name, params("id")).map { issue => - if(isEditable(owner, name, issue.openedUserName)){ - // update issue - updateIssue(owner, name, issue.issueId, title, issue.content) - // extract references and create refer comment - createReferComment(owner, name, issue.copy(title = title), title) - - redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") - } else Unauthorized - } getOrElse NotFound - } - }) - - ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) => - defining(repository.owner, repository.name){ case (owner, name) => - getIssue(owner, name, params("id")).map { issue => - if(isEditable(owner, name, issue.openedUserName)){ - // update issue - updateIssue(owner, name, issue.issueId, issue.title, content) - // extract references and create refer comment - createReferComment(owner, name, issue, content.getOrElse("")) - - redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") - } else Unauthorized - } getOrElse NotFound - } - }) - - post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => - handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) => - redirect(s"/${repository.owner}/${repository.name}/${ - if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") - } getOrElse NotFound - }) - - post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) => - handleComment(form.issueId, form.content, repository)() map { case (issue, id) => - redirect(s"/${repository.owner}/${repository.name}/${ - if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") - } getOrElse NotFound - }) - - ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => - defining(repository.owner, repository.name){ case (owner, name) => - getComment(owner, name, params("id")).map { comment => - if(isEditable(owner, name, comment.commentedUserName)){ - updateComment(comment.commentId, form.content) - redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}") - } else Unauthorized - } getOrElse NotFound - } - }) - - ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository => - defining(repository.owner, repository.name){ case (owner, name) => - getComment(owner, name, params("id")).map { comment => - if(isEditable(owner, name, comment.commentedUserName)){ - Ok(deleteComment(comment.commentId)) - } else Unauthorized - } getOrElse NotFound - } - }) - - ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository => - getIssue(repository.owner, repository.name, params("id")) map { x => - if(isEditable(x.userName, x.repositoryName, x.openedUserName)){ - params.get("dataType") collect { - case t if t == "html" => issues.html.editissue( - x.content, x.issueId, x.userName, x.repositoryName) - } getOrElse { - contentType = formats("json") - org.json4s.jackson.Serialization.write( - Map("title" -> x.title, - "content" -> view.Markdown.toHtml(x.content getOrElse "No description given.", - repository, false, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName)) - )) - } - } else Unauthorized - } getOrElse NotFound - }) - - ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository => - getComment(repository.owner, repository.name, params("id")) map { x => - if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){ - params.get("dataType") collect { - case t if t == "html" => issues.html.editcomment( - x.content, x.commentId, x.userName, x.repositoryName) - } getOrElse { - contentType = formats("json") - org.json4s.jackson.Serialization.write( - Map("content" -> view.Markdown.toHtml(x.content, - repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName)) - )) - } - } else Unauthorized - } getOrElse NotFound - }) - - ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository => - defining(params("id").toInt){ issueId => - registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) - issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) - } - }) - - ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository => - defining(params("id").toInt){ issueId => - deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) - issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) - } - }) - - ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository => - updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName")) - Ok("updated") - }) - - ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository => - updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId")) - milestoneId("milestoneId").map { milestoneId => - getMilestonesWithIssueCount(repository.owner, repository.name) - .find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) => - issues.milestones.html.progress(openCount + closeCount, closeCount) - } getOrElse NotFound - } getOrElse Ok() - }) - - post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => - defining(params.get("value")){ action => - action match { - case Some("open") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("reopen")) } - case Some("close") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("close")) } - case _ => // TODO BadRequest - } - } - }) - - post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository => - params("value").toIntOpt.map{ labelId => - executeBatch(repository) { issueId => - getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse { - registerIssueLabel(repository.owner, repository.name, issueId, labelId) - } - } - } getOrElse NotFound - }) - - post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository => - defining(assignedUserName("value")){ value => - executeBatch(repository) { - updateAssignedUserName(repository.owner, repository.name, _, value) - } - } - }) - - post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository => - defining(milestoneId("value")){ value => - executeBatch(repository) { - updateMilestoneId(repository.owner, repository.name, _, value) - } - } - }) - - get("/:owner/:repository/_attached/:file")(referrersOnly { repository => - (Directory.getAttachedDir(repository.owner, repository.name) match { - case dir if(dir.exists && dir.isDirectory) => - dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file => - RawData(FileUtil.getMimeType(file.getName), file) - } - case _ => None - }) getOrElse NotFound - }) - - val assignedUserName = (key: String) => params.get(key) filter (_.trim != "") - val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt) - - private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean = - hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName - - private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = { - params("checked").split(',') map(_.toInt) foreach execute - params("from") match { - case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues") - case "pulls" => redirect(s"/${repository.owner}/${repository.name}/pulls") - } - } - - private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = { - StringUtil.extractIssueId(message).foreach { issueId => - if(getIssue(owner, repository, issueId).isDefined){ - createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, - fromIssue.issueId + ":" + fromIssue.title, "refer") - } - } - } - - /** - * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]] - */ - private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo) - (getAction: model.Issue => Option[String] = - p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = { - - defining(repository.owner, repository.name){ case (owner, name) => - val userName = context.loginAccount.get.userName - - getIssue(owner, name, issueId.toString) flatMap { issue => - val (action, recordActivity) = - getAction(issue) - .collect { - case "close" if(!issue.closed) => true -> - (Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _)) - case "reopen" if(issue.closed) => false -> - (Some("reopen") -> Some(recordReopenIssueActivity _)) - } - .map { case (closed, t) => - updateClosed(owner, name, issueId, closed) - t - } - .getOrElse(None -> None) - - val commentId = (content, action) match { - case (None, None) => None - case (None, Some(action)) => Some(createComment(owner, name, userName, issueId, action.capitalize, action)) - case (Some(content), _) => Some(createComment(owner, name, userName, issueId, content, action.map(_+ "_comment").getOrElse("comment"))) - } - - // record comment activity if comment is entered - content foreach { - (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _) - (owner, name, userName, issueId, _) - } - recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) ) - - // extract references and create refer comment - content.map { content => - createReferComment(owner, name, issue, content) - } - - // notifications - Notifier() match { - case f => - content foreach { - f.toNotify(repository, issueId, _){ - Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${ - if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId.get}") - } - } - action foreach { - f.toNotify(repository, issueId, _){ - Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") - } - } - } - - commentId.map( issue -> _ ) - } - } - } - - private def searchIssues(repository: RepositoryService.RepositoryInfo) = { - defining(repository.owner, repository.name){ case (owner, repoName) => - val page = IssueSearchCondition.page(request) - val sessionKey = Keys.Session.Issues(owner, repoName) - - // retrieve search condition - val condition = session.putAndGet(sessionKey, - if(request.hasQueryString){ - val q = request.getParameter("q") - if(q == null || q.trim.isEmpty){ - IssueSearchCondition(request) - } else { - IssueSearchCondition(q, getMilestones(owner, repoName).map(x => (x.title, x.milestoneId)).toMap) - } - } else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) - ) - - issues.html.list( - "issues", - searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), - page, - (getCollaborators(owner, repoName) :+ owner).sorted, - getMilestones(owner, repoName), - getLabels(owner, repoName), - countIssue(condition.copy(state = "open" ), false, owner -> repoName), - countIssue(condition.copy(state = "closed"), false, owner -> repoName), - condition, - repository, - hasWritePermission(owner, repoName, context.loginAccount)) - } - } - -} diff --git a/src/main/scala/app/LabelsController.scala b/src/main/scala/app/LabelsController.scala deleted file mode 100644 index 7cd42bf..0000000 --- a/src/main/scala/app/LabelsController.scala +++ /dev/null @@ -1,82 +0,0 @@ -package app - -import jp.sf.amateras.scalatra.forms._ -import service._ -import util.{ReferrerAuthenticator, CollaboratorsAuthenticator} -import util.Implicits._ -import org.scalatra.i18n.Messages -import org.scalatra.Ok - -class LabelsController extends LabelsControllerBase - with LabelsService with IssuesService with RepositoryService with AccountService -with ReferrerAuthenticator with CollaboratorsAuthenticator - -trait LabelsControllerBase extends ControllerBase { - self: LabelsService with IssuesService with RepositoryService - with ReferrerAuthenticator with CollaboratorsAuthenticator => - - case class LabelForm(labelName: String, color: String) - - val labelForm = mapping( - "labelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))), - "labelColor" -> trim(label("Color", text(required, color))) - )(LabelForm.apply) - - get("/:owner/:repository/issues/labels")(referrersOnly { repository => - issues.labels.html.list( - getLabels(repository.owner, repository.name), - countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), - repository, - hasWritePermission(repository.owner, repository.name, context.loginAccount)) - }) - - ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository => - issues.labels.html.edit(None, repository) - }) - - ajaxPost("/:owner/:repository/issues/labels/new", labelForm)(collaboratorsOnly { (form, repository) => - val labelId = createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1)) - issues.labels.html.label( - getLabel(repository.owner, repository.name, labelId).get, - // TODO futility - countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), - repository, - hasWritePermission(repository.owner, repository.name, context.loginAccount)) - }) - - ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository => - getLabel(repository.owner, repository.name, params("labelId").toInt).map { label => - issues.labels.html.edit(Some(label), repository) - } getOrElse NotFound() - }) - - ajaxPost("/:owner/:repository/issues/labels/:labelId/edit", labelForm)(collaboratorsOnly { (form, repository) => - updateLabel(repository.owner, repository.name, params("labelId").toInt, form.labelName, form.color.substring(1)) - issues.labels.html.label( - getLabel(repository.owner, repository.name, params("labelId").toInt).get, - // TODO futility - countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), - repository, - hasWritePermission(repository.owner, repository.name, context.loginAccount)) - }) - - ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository => - deleteLabel(repository.owner, repository.name, params("labelId").toInt) - Ok() - }) - - /** - * Constraint for the identifier such as user name, repository name or page name. - */ - private def labelName: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = - if(value.contains(',')){ - Some(s"${name} contains invalid character.") - } else if(value.startsWith("_") || value.startsWith("-")){ - Some(s"${name} starts with invalid character.") - } else { - None - } - } - -} diff --git a/src/main/scala/app/MilestonesController.scala b/src/main/scala/app/MilestonesController.scala deleted file mode 100644 index 234ca07..0000000 --- a/src/main/scala/app/MilestonesController.scala +++ /dev/null @@ -1,84 +0,0 @@ -package app - -import jp.sf.amateras.scalatra.forms._ - -import service._ -import util.{CollaboratorsAuthenticator, ReferrerAuthenticator} -import util.Implicits._ - -class MilestonesController extends MilestonesControllerBase - with MilestonesService with RepositoryService with AccountService - with ReferrerAuthenticator with CollaboratorsAuthenticator - -trait MilestonesControllerBase extends ControllerBase { - self: MilestonesService with RepositoryService - with ReferrerAuthenticator with CollaboratorsAuthenticator => - - case class MilestoneForm(title: String, description: Option[String], dueDate: Option[java.util.Date]) - - val milestoneForm = mapping( - "title" -> trim(label("Title", text(required, maxlength(100)))), - "description" -> trim(label("Description", optional(text()))), - "dueDate" -> trim(label("Due Date", optional(date()))) - )(MilestoneForm.apply) - - get("/:owner/:repository/issues/milestones")(referrersOnly { repository => - issues.milestones.html.list( - params.getOrElse("state", "open"), - getMilestonesWithIssueCount(repository.owner, repository.name), - repository, - hasWritePermission(repository.owner, repository.name, context.loginAccount)) - }) - - get("/:owner/:repository/issues/milestones/new")(collaboratorsOnly { - issues.milestones.html.edit(None, _) - }) - - post("/:owner/:repository/issues/milestones/new", milestoneForm)(collaboratorsOnly { (form, repository) => - createMilestone(repository.owner, repository.name, form.title, form.description, form.dueDate) - redirect(s"/${repository.owner}/${repository.name}/issues/milestones") - }) - - get("/:owner/:repository/issues/milestones/:milestoneId/edit")(collaboratorsOnly { repository => - params("milestoneId").toIntOpt.map{ milestoneId => - issues.milestones.html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository) - } getOrElse NotFound - }) - - post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(collaboratorsOnly { (form, repository) => - params("milestoneId").toIntOpt.flatMap{ milestoneId => - getMilestone(repository.owner, repository.name, milestoneId).map { milestone => - updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate)) - redirect(s"/${repository.owner}/${repository.name}/issues/milestones") - } - } getOrElse NotFound - }) - - get("/:owner/:repository/issues/milestones/:milestoneId/close")(collaboratorsOnly { repository => - params("milestoneId").toIntOpt.flatMap{ milestoneId => - getMilestone(repository.owner, repository.name, milestoneId).map { milestone => - closeMilestone(milestone) - redirect(s"/${repository.owner}/${repository.name}/issues/milestones") - } - } getOrElse NotFound - }) - - get("/:owner/:repository/issues/milestones/:milestoneId/open")(collaboratorsOnly { repository => - params("milestoneId").toIntOpt.flatMap{ milestoneId => - getMilestone(repository.owner, repository.name, milestoneId).map { milestone => - openMilestone(milestone) - redirect(s"/${repository.owner}/${repository.name}/issues/milestones") - } - } getOrElse NotFound - }) - - get("/:owner/:repository/issues/milestones/:milestoneId/delete")(collaboratorsOnly { repository => - params("milestoneId").toIntOpt.flatMap{ milestoneId => - getMilestone(repository.owner, repository.name, milestoneId).map { milestone => - deleteMilestone(repository.owner, repository.name, milestone.milestoneId) - redirect(s"/${repository.owner}/${repository.name}/issues/milestones") - } - } getOrElse NotFound - }) - -} diff --git a/src/main/scala/app/PullRequestsController.scala b/src/main/scala/app/PullRequestsController.scala deleted file mode 100644 index 84987b1..0000000 --- a/src/main/scala/app/PullRequestsController.scala +++ /dev/null @@ -1,486 +0,0 @@ -package app - -import util._ -import util.Directory._ -import util.Implicits._ -import util.ControlUtil._ -import service._ -import org.eclipse.jgit.api.Git -import jp.sf.amateras.scalatra.forms._ -import org.eclipse.jgit.transport.RefSpec -import scala.collection.JavaConverters._ -import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent} -import service.IssuesService._ -import service.PullRequestService._ -import org.slf4j.LoggerFactory -import org.eclipse.jgit.merge.MergeStrategy -import org.eclipse.jgit.errors.NoMergeBaseException -import service.WebHookService.WebHookPayload -import util.JGitUtil.DiffInfo -import util.JGitUtil.CommitInfo - - -class PullRequestsController extends PullRequestsControllerBase - with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService - with CommitsService with ActivityService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator - -trait PullRequestsControllerBase extends ControllerBase { - self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService - with CommitsService with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator => - - private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase]) - - val pullRequestForm = mapping( - "title" -> trim(label("Title" , text(required, maxlength(100)))), - "content" -> trim(label("Content", optional(text()))), - "targetUserName" -> trim(text(required, maxlength(100))), - "targetBranch" -> trim(text(required, maxlength(100))), - "requestUserName" -> trim(text(required, maxlength(100))), - "requestRepositoryName" -> trim(text(required, maxlength(100))), - "requestBranch" -> trim(text(required, maxlength(100))), - "commitIdFrom" -> trim(text(required, maxlength(40))), - "commitIdTo" -> trim(text(required, maxlength(40))) - )(PullRequestForm.apply) - - val mergeForm = mapping( - "message" -> trim(label("Message", text(required))) - )(MergeForm.apply) - - case class PullRequestForm( - title: String, - content: Option[String], - targetUserName: String, - targetBranch: String, - requestUserName: String, - requestRepositoryName: String, - requestBranch: String, - commitIdFrom: String, - commitIdTo: String) - - case class MergeForm(message: String) - - get("/:owner/:repository/pulls")(referrersOnly { repository => - val q = request.getParameter("q") - if(Option(q).exists(_.contains("is:issue"))){ - redirect(s"/${repository.owner}/${repository.name}/issues?q=" + StringUtil.urlEncode(q)) - } else { - searchPullRequests(None, repository) - } - }) - - get("/:owner/:repository/pull/:id")(referrersOnly { repository => - params("id").toIntOpt.flatMap{ issueId => - val owner = repository.owner - val name = repository.name - getPullRequest(owner, name, issueId) map { case(issue, pullreq) => - using(Git.open(getRepositoryDir(owner, name))){ git => - val (commits, diffs) = - getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo) - - pulls.html.pullreq( - issue, pullreq, - (commits.flatten.map(commit => getCommitComments(owner, name, commit.id, true)).flatten.toList ::: getComments(owner, name, issueId)) - .sortWith((a, b) => a.registeredDate before b.registeredDate), - getIssueLabels(owner, name, issueId), - (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, - getMilestonesWithIssueCount(owner, name), - getLabels(owner, name), - commits, - diffs, - hasWritePermission(owner, name, context.loginAccount), - repository) - } - } - } getOrElse NotFound - }) - - ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository => - params("id").toIntOpt.flatMap{ issueId => - val owner = repository.owner - val name = repository.name - getPullRequest(owner, name, issueId) map { case(issue, pullreq) => - pulls.html.mergeguide( - checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId), - pullreq, - s"${context.baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git") - } - } getOrElse NotFound - }) - - get("/:owner/:repository/pull/:id/delete/*")(collaboratorsOnly { repository => - params("id").toIntOpt.map { issueId => - val branchName = multiParams("splat").head - val userName = context.loginAccount.get.userName - if(repository.repository.defaultBranch != branchName){ - using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - git.branchDelete().setForce(true).setBranchNames(branchName).call() - recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName) - } - } - createComment(repository.owner, repository.name, userName, issueId, branchName, "delete_branch") - redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") - } getOrElse NotFound - }) - - post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) => - params("id").toIntOpt.flatMap { issueId => - val owner = repository.owner - val name = repository.name - LockUtil.lock(s"${owner}/${name}"){ - getPullRequest(owner, name, issueId).map { case (issue, pullreq) => - using(Git.open(getRepositoryDir(owner, name))) { git => - // mark issue as merged and close. - val loginAccount = context.loginAccount.get - createComment(owner, name, loginAccount.userName, issueId, form.message, "merge") - createComment(owner, name, loginAccount.userName, issueId, "Close", "close") - updateClosed(owner, name, issueId, true) - - // record activity - recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message) - - // merge - val mergeBaseRefName = s"refs/heads/${pullreq.branch}" - val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true) - val mergeBaseTip = git.getRepository.resolve(mergeBaseRefName) - val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head") - val conflicted = try { - !merger.merge(mergeBaseTip, mergeTip) - } catch { - case e: NoMergeBaseException => true - } - if (conflicted) { - throw new RuntimeException("This pull request can't merge automatically.") - } - - // creates merge commit - val mergeCommit = new CommitBuilder() - mergeCommit.setTreeId(merger.getResultTreeId) - mergeCommit.setParentIds(Array[ObjectId](mergeBaseTip, mergeTip): _*) - val personIdent = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress) - mergeCommit.setAuthor(personIdent) - mergeCommit.setCommitter(personIdent) - mergeCommit.setMessage(s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" + - form.message) - - // insertObject and got mergeCommit Object Id - val inserter = git.getRepository.newObjectInserter - val mergeCommitId = inserter.insert(mergeCommit) - inserter.flush() - inserter.release() - - // update refs - val refUpdate = git.getRepository.updateRef(mergeBaseRefName) - refUpdate.setNewObjectId(mergeCommitId) - refUpdate.setForceUpdate(false) - refUpdate.setRefLogIdent(personIdent) - refUpdate.setRefLogMessage("merged", true) - refUpdate.update() - - val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom, - pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo) - - // close issue by content of pull request - 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) - } - issue.content match { - case Some(content) => closeIssuesFromMessage(content, loginAccount.userName, owner, name) - case _ => - } - closeIssuesFromMessage(form.message, loginAccount.userName, owner, name) - } - // call web hook - getWebHookURLs(owner, name) match { - case webHookURLs if(webHookURLs.nonEmpty) => - for(ownerAccount <- getAccountByUserName(owner)){ - callWebHook(owner, name, webHookURLs, - WebHookPayload(git, loginAccount, mergeBaseRefName, repository, commits.flatten.toList, ownerAccount)) - } - case _ => - } - - // notifications - Notifier().toNotify(repository, issueId, "merge"){ - Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}") - } - - redirect(s"/${owner}/${name}/pull/${issueId}") - } - } - } - } getOrElse NotFound - }) - - get("/:owner/:repository/compare")(referrersOnly { forkedRepository => - (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match { - case (Some(originUserName), Some(originRepositoryName)) => { - getRepository(originUserName, originRepositoryName, context.baseUrl).map { originRepository => - using( - Git.open(getRepositoryDir(originUserName, originRepositoryName)), - Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name)) - ){ (oldGit, newGit) => - val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2 - val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2 - - redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}") - } - } getOrElse NotFound - } - case _ => { - using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git => - JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) => - redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}") - } getOrElse { - redirect(s"/${forkedRepository.owner}/${forkedRepository.name}") - } - } - } - } - }) - - get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository => - val Seq(origin, forked) = multiParams("splat") - val (originOwner, originId) = parseCompareIdentifie(origin, forkedRepository.owner) - val (forkedOwner, forkedId) = parseCompareIdentifie(forked, forkedRepository.owner) - - (for( - originRepositoryName <- if(originOwner == forkedOwner){ - Some(forkedRepository.name) - } else { - forkedRepository.repository.originRepositoryName.orElse { - getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2) - } - }; - originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl) - ) yield { - using( - Git.open(getRepositoryDir(originRepository.owner, originRepository.name)), - Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name)) - ){ case (oldGit, newGit) => - val (oldId, newId) = - if(originRepository.branchList.contains(originId) && forkedRepository.branchList.contains(forkedId)){ - // Branch name - val rootId = JGitUtil.getForkedCommitId(oldGit, newGit, - originRepository.owner, originRepository.name, originId, - forkedRepository.owner, forkedRepository.name, forkedId) - - (oldGit.getRepository.resolve(rootId), newGit.getRepository.resolve(forkedId)) - } else { - // Commit id - (oldGit.getRepository.resolve(originId), newGit.getRepository.resolve(forkedId)) - } - - val (commits, diffs) = getRequestCompareInfo( - originRepository.owner, originRepository.name, oldId.getName, - forkedRepository.owner, forkedRepository.name, newId.getName) - - pulls.html.compare( - commits, - diffs, - (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match { - case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName) - case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name) - }, - commits.flatten.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)).flatten.toList, - originId, - forkedId, - oldId.getName, - newId.getName, - forkedRepository, - originRepository, - forkedRepository, - hasWritePermission(forkedRepository.owner, forkedRepository.name, context.loginAccount)) - } - }) getOrElse NotFound - }) - - ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { forkedRepository => - val Seq(origin, forked) = multiParams("splat") - val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner) - val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner) - - (for( - originRepositoryName <- if(originOwner == forkedOwner){ - Some(forkedRepository.name) - } else { - forkedRepository.repository.originRepositoryName.orElse { - getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2) - } - }; - originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl) - ) yield { - using( - Git.open(getRepositoryDir(originRepository.owner, originRepository.name)), - Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name)) - ){ case (oldGit, newGit) => - val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2 - val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2 - - pulls.html.mergecheck( - checkConflict(originRepository.owner, originRepository.name, originBranch, - forkedRepository.owner, forkedRepository.name, forkedBranch)) - } - }) getOrElse NotFound - }) - - post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) => - val loginUserName = context.loginAccount.get.userName - - val issueId = createIssue( - owner = repository.owner, - repository = repository.name, - loginUser = loginUserName, - title = form.title, - content = form.content, - assignedUserName = None, - milestoneId = None, - isPullRequest = true) - - createPullRequest( - originUserName = repository.owner, - originRepositoryName = repository.name, - issueId = issueId, - originBranch = form.targetBranch, - requestUserName = form.requestUserName, - requestRepositoryName = form.requestRepositoryName, - requestBranch = form.requestBranch, - commitIdFrom = form.commitIdFrom, - commitIdTo = form.commitIdTo) - - // fetch requested branch - using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - git.fetch - .setRemote(getRepositoryDir(form.requestUserName, form.requestRepositoryName).toURI.toString) - .setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head")) - .call - } - - // record activity - recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title) - - // notifications - Notifier().toNotify(repository, issueId, form.content.getOrElse("")){ - Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}") - } - - redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") - }) - - /** - * Checks whether conflict will be caused in merging. Returns true if conflict will be caused. - */ - private def checkConflict(userName: String, repositoryName: String, branch: String, - requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = { - LockUtil.lock(s"${userName}/${repositoryName}"){ - using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git => - val remoteRefName = s"refs/heads/${branch}" - val tmpRefName = s"refs/merge-check/${userName}/${branch}" - val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true) - try { - // fetch objects from origin repository branch - git.fetch - .setRemote(getRepositoryDir(userName, repositoryName).toURI.toString) - .setRefSpecs(refSpec) - .call - - // merge conflict check - val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true) - val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${requestBranch}") - val mergeTip = git.getRepository.resolve(tmpRefName) - try { - !merger.merge(mergeBaseTip, mergeTip) - } catch { - case e: NoMergeBaseException => true - } - } finally { - val refUpdate = git.getRepository.updateRef(refSpec.getDestination) - refUpdate.setForceUpdate(true) - refUpdate.delete() - } - } - } - } - - /** - * Checks whether conflict will be caused in merging within pull request. Returns true if conflict will be caused. - */ - private def checkConflictInPullRequest(userName: String, repositoryName: String, branch: String, - requestUserName: String, requestRepositoryName: String, requestBranch: String, - issueId: Int): Boolean = { - LockUtil.lock(s"${userName}/${repositoryName}") { - using(Git.open(getRepositoryDir(userName, repositoryName))) { git => - // merge - val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true) - val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${branch}") - val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head") - try { - !merger.merge(mergeBaseTip, mergeTip) - } catch { - case e: NoMergeBaseException => true - } - } - } - } - - /** - * Parses branch identifier and extracts owner and branch name as tuple. - * - * - "owner:branch" to ("owner", "branch") - * - "branch" to ("defaultOwner", "branch") - */ - private def parseCompareIdentifie(value: String, defaultOwner: String): (String, String) = - if(value.contains(':')){ - val array = value.split(":") - (array(0), array(1)) - } else { - (defaultOwner, value) - } - - private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String, - requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = - using( - Git.open(getRepositoryDir(userName, repositoryName)), - Git.open(getRepositoryDir(requestUserName, requestRepositoryName)) - ){ (oldGit, newGit) => - val oldId = oldGit.getRepository.resolve(branch) - val newId = newGit.getRepository.resolve(requestCommitId) - - val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit => - new CommitInfo(revCommit) - }.toList.splitWith { (commit1, commit2) => - view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime) - } - - val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true) - - (commits, diffs) - } - - private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) = - defining(repository.owner, repository.name){ case (owner, repoName) => - val page = IssueSearchCondition.page(request) - val sessionKey = Keys.Session.Pulls(owner, repoName) - - // retrieve search condition - val condition = session.putAndGet(sessionKey, - if(request.hasQueryString) IssueSearchCondition(request) - else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) - ) - - issues.html.list( - "pulls", - searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), - page, - (getCollaborators(owner, repoName) :+ owner).sorted, - getMilestones(owner, repoName), - getLabels(owner, repoName), - countIssue(condition.copy(state = "open" ), true, owner -> repoName), - countIssue(condition.copy(state = "closed"), true, owner -> repoName), - condition, - repository, - hasWritePermission(owner, repoName, context.loginAccount)) - } - -} diff --git a/src/main/scala/app/RepositorySettingsController.scala b/src/main/scala/app/RepositorySettingsController.scala deleted file mode 100644 index cce95bb..0000000 --- a/src/main/scala/app/RepositorySettingsController.scala +++ /dev/null @@ -1,274 +0,0 @@ -package app - -import service._ -import util.Directory._ -import util.Implicits._ -import util.{LockUtil, UsersAuthenticator, OwnerAuthenticator} -import jp.sf.amateras.scalatra.forms._ -import org.apache.commons.io.FileUtils -import org.scalatra.i18n.Messages -import service.WebHookService.WebHookPayload -import util.JGitUtil.CommitInfo -import util.ControlUtil._ -import org.eclipse.jgit.api.Git -import org.eclipse.jgit.lib.Constants - -class RepositorySettingsController extends RepositorySettingsControllerBase - with RepositoryService with AccountService with WebHookService - with OwnerAuthenticator with UsersAuthenticator - -trait RepositorySettingsControllerBase extends ControllerBase { - self: RepositoryService with AccountService with WebHookService - with OwnerAuthenticator with UsersAuthenticator => - - // for repository options - case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean) - - val optionsForm = mapping( - "repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))), - "description" -> trim(label("Description" , optional(text()))), - "defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))), - "isPrivate" -> trim(label("Repository Type", boolean())) - )(OptionsForm.apply) - - // for collaborator addition - case class CollaboratorForm(userName: String) - - val collaboratorForm = mapping( - "userName" -> trim(label("Username", text(required, collaborator))) - )(CollaboratorForm.apply) - - // for web hook url addition - case class WebHookForm(url: String) - - val webHookForm = mapping( - "url" -> trim(label("url", text(required, webHook))) - )(WebHookForm.apply) - - // for transfer ownership - case class TransferOwnerShipForm(newOwner: String) - - val transferForm = mapping( - "newOwner" -> trim(label("New owner", text(required, transferUser))) - )(TransferOwnerShipForm.apply) - - /** - * Redirect to the Options page. - */ - get("/:owner/:repository/settings")(ownerOnly { repository => - redirect(s"/${repository.owner}/${repository.name}/settings/options") - }) - - /** - * Display the Options page. - */ - get("/:owner/:repository/settings/options")(ownerOnly { - settings.html.options(_, flash.get("info")) - }) - - /** - * Save the repository options. - */ - post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) => - val defaultBranch = if(repository.branchList.isEmpty) "master" else form.defaultBranch - saveRepositoryOptions( - repository.owner, - repository.name, - form.description, - defaultBranch, - repository.repository.parentUserName.map { _ => - repository.repository.isPrivate - } getOrElse form.isPrivate - ) - // Change repository name - if(repository.name != form.repositoryName){ - // Update database - renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName) - // Move git repository - defining(getRepositoryDir(repository.owner, repository.name)){ dir => - FileUtils.moveDirectory(dir, getRepositoryDir(repository.owner, form.repositoryName)) - } - // Move wiki repository - defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir => - FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName)) - } - } - // Change repository HEAD - using(Git.open(getRepositoryDir(repository.owner, form.repositoryName))) { git => - git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + defaultBranch) - } - flash += "info" -> "Repository settings has been updated." - redirect(s"/${repository.owner}/${form.repositoryName}/settings/options") - }) - - /** - * Display the Collaborators page. - */ - get("/:owner/:repository/settings/collaborators")(ownerOnly { repository => - settings.html.collaborators( - getCollaborators(repository.owner, repository.name), - getAccountByUserName(repository.owner).get.isGroupAccount, - repository) - }) - - /** - * Add the collaborator. - */ - post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) => - if(!getAccountByUserName(repository.owner).get.isGroupAccount){ - addCollaborator(repository.owner, repository.name, form.userName) - } - redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") - }) - - /** - * Add the collaborator. - */ - get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository => - if(!getAccountByUserName(repository.owner).get.isGroupAccount){ - removeCollaborator(repository.owner, repository.name, params("name")) - } - redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") - }) - - /** - * Display the web hook page. - */ - get("/:owner/:repository/settings/hooks")(ownerOnly { repository => - settings.html.hooks(getWebHookURLs(repository.owner, repository.name), flash.get("url"), repository, flash.get("info")) - }) - - /** - * Add the web hook URL. - */ - post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) => - addWebHookURL(repository.owner, repository.name, form.url) - redirect(s"/${repository.owner}/${repository.name}/settings/hooks") - }) - - /** - * Delete the web hook URL. - */ - get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository => - deleteWebHookURL(repository.owner, repository.name, params("url")) - redirect(s"/${repository.owner}/${repository.name}/settings/hooks") - }) - - /** - * Send the test request to registered web hook URLs. - */ - post("/:owner/:repository/settings/hooks/test", webHookForm)(ownerOnly { (form, repository) => - using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - import scala.collection.JavaConverters._ - val commits = git.log - .add(git.getRepository.resolve(repository.repository.defaultBranch)) - .setMaxCount(3) - .call.iterator.asScala.map(new CommitInfo(_)) - - getAccountByUserName(repository.owner).foreach { ownerAccount => - callWebHook(repository.owner, repository.name, - List(model.WebHook(repository.owner, repository.name, form.url)), - WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount) - ) - } - flash += "url" -> form.url - flash += "info" -> "Test payload deployed!" - } - redirect(s"/${repository.owner}/${repository.name}/settings/hooks") - }) - - /** - * Display the danger zone. - */ - get("/:owner/:repository/settings/danger")(ownerOnly { - settings.html.danger(_) - }) - - /** - * Transfer repository ownership. - */ - post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) => - // Change repository owner - if(repository.owner != form.newOwner){ - LockUtil.lock(s"${repository.owner}/${repository.name}"){ - // Update database - renameRepository(repository.owner, repository.name, form.newOwner, repository.name) - // Move git repository - defining(getRepositoryDir(repository.owner, repository.name)){ dir => - FileUtils.moveDirectory(dir, getRepositoryDir(form.newOwner, repository.name)) - } - // Move wiki repository - defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir => - FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name)) - } - } - } - redirect(s"/${form.newOwner}/${repository.name}") - }) - - /** - * Delete the repository. - */ - post("/:owner/:repository/settings/delete")(ownerOnly { repository => - LockUtil.lock(s"${repository.owner}/${repository.name}"){ - deleteRepository(repository.owner, repository.name) - - FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name)) - FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name)) - FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name)) - } - redirect(s"/${repository.owner}") - }) - - /** - * Provides duplication check for web hook url. - */ - private def webHook: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = - getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.") - } - - /** - * Provides Constraint to validate the collaborator name. - */ - private def collaborator: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = - getAccountByUserName(value) match { - case None => Some("User does not exist.") - case Some(x) if(x.isGroupAccount) - => Some("User does not exist.") - case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName)) - => Some("User can access this repository already.") - case _ => None - } - } - - /** - * Duplicate check for the rename repository name. - */ - private def renameRepositoryName: Constraint = new Constraint(){ - override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = - params.get("repository").filter(_ != value).flatMap { _ => - params.get("owner").flatMap { userName => - getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.") - } - } - } - - /** - * Provides Constraint to validate the repository transfer user. - */ - private def transferUser: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = - getAccountByUserName(value) match { - case None => Some("User does not exist.") - case Some(x) => if(x.userName == params("owner")){ - Some("This is current repository owner.") - } else { - params.get("repository").flatMap { repositoryName => - getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map{ _ => "User already has same repository." } - } - } - } - } -} \ No newline at end of file diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala deleted file mode 100644 index 8faf1ad..0000000 --- a/src/main/scala/app/RepositoryViewerController.scala +++ /dev/null @@ -1,564 +0,0 @@ -package app - -import _root_.util.JGitUtil.CommitInfo -import util.Directory._ -import util.Implicits._ -import _root_.util.ControlUtil._ -import _root_.util._ -import service._ -import org.scalatra._ -import java.io.File - -import org.eclipse.jgit.api.{ArchiveCommand, Git} -import org.eclipse.jgit.archive.{TgzFormat, ZipFormat} -import org.eclipse.jgit.lib._ -import org.apache.commons.io.FileUtils -import org.eclipse.jgit.treewalk._ -import jp.sf.amateras.scalatra.forms._ -import org.eclipse.jgit.dircache.DirCache -import org.eclipse.jgit.revwalk.RevCommit -import service.WebHookService.WebHookPayload - -class RepositoryViewerController extends RepositoryViewerControllerBase - with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService - with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService - - -/** - * The repository viewer. - */ -trait RepositoryViewerControllerBase extends ControllerBase { - self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService - with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService => - - ArchiveCommand.registerFormat("zip", new ZipFormat) - ArchiveCommand.registerFormat("tar.gz", new TgzFormat) - - case class EditorForm( - branch: String, - path: String, - content: String, - message: Option[String], - charset: String, - lineSeparator: String, - newFileName: String, - oldFileName: Option[String] - ) - - case class DeleteForm( - branch: String, - path: String, - message: Option[String], - fileName: String - ) - - case class CommentForm( - fileName: Option[String], - oldLineNumber: Option[Int], - newLineNumber: Option[Int], - content: String, - issueId: Option[Int] - ) - - 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))), - "lineSeparator" -> trim(label("Line Separator", 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) - - val commentForm = mapping( - "fileName" -> trim(label("Filename", optional(text()))), - "oldLineNumber" -> trim(label("Old line number", optional(number()))), - "newLineNumber" -> trim(label("New line number", optional(number()))), - "content" -> trim(label("Content", text(required))), - "issueId" -> trim(label("Issue Id", optional(number()))) - )(CommentForm.apply) - - /** - * Returns converted HTML from Markdown for preview. - */ - post("/:owner/:repository/_preview")(referrersOnly { repository => - contentType = "text/html" - view.helpers.markdown(params("content"), repository, - params("enableWikiLink").toBoolean, - params("enableRefsLink").toBoolean, - params("enableTaskList").toBoolean, - hasWritePermission(repository.owner, repository.name, context.loginAccount)) - }) - - /** - * Displays the file list of the repository root and the default branch. - */ - get("/:owner/:repository")(referrersOnly { - fileList(_) - }) - - /** - * Displays the file list of the specified path and branch. - */ - get("/:owner/:repository/tree/*")(referrersOnly { repository => - val (id, path) = splitPath(repository, multiParams("splat").head) - if(path.isEmpty){ - fileList(repository, id) - } else { - fileList(repository, id, path) - } - }) - - /** - * Displays the commit list of the specified resource. - */ - get("/:owner/:repository/commits/*")(referrersOnly { repository => - val (branchName, path) = splitPath(repository, multiParams("splat").head) - val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1) - - using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - JGitUtil.getCommitLog(git, branchName, page, 30, path) match { - case Right((logs, hasNext)) => - repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository, - logs.splitWith{ (commit1, commit2) => - view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime) - }, page, hasNext, hasWritePermission(repository.owner, repository.name, context.loginAccount)) - case Left(_) => NotFound - } - } - }) - - 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, - StringUtil.convertLineSeparator(form.content, form.lineSeparator), 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, - StringUtil.convertLineSeparator(form.content, form.lineSeparator), 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. - */ - get("/:owner/:repository/blob/*")(referrersOnly { repository => - val (id, path) = splitPath(repository, multiParams("splat").head) - val raw = params.get("raw").getOrElse("false").toBoolean - - using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) - val lastModifiedCommit = JGitUtil.getLastModifiedCommit(git, revCommit, path) - getPathObjectId(git, path, revCommit).map { objectId => - if(raw){ - // Download - defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes => - RawData(FileUtil.getContentType(path, bytes), bytes) - } - } else { - repo.html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId), - new JGitUtil.CommitInfo(lastModifiedCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount)) - } - } getOrElse NotFound - } - }) - - /** - * Displays details of the specified commit. - */ - get("/:owner/:repository/commit/:id")(referrersOnly { repository => - val id = params("id") - - using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))){ revCommit => - JGitUtil.getDiffs(git, id) match { case (diffs, oldCommitId) => - repo.html.commit(id, new JGitUtil.CommitInfo(revCommit), - JGitUtil.getBranchesOfCommit(git, revCommit.getName), - JGitUtil.getTagsOfCommit(git, revCommit.getName), - getCommitComments(repository.owner, repository.name, id, false), - repository, diffs, oldCommitId, hasWritePermission(repository.owner, repository.name, context.loginAccount)) - } - } - } - }) - - post("/:owner/:repository/commit/:id/comment/new", commentForm)(readableUsersOnly { (form, repository) => - val id = params("id") - createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName, form.content, - form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined) - form.issueId match { - case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content) - case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content) - } - redirect(s"/${repository.owner}/${repository.name}/commit/${id}") - }) - - ajaxGet("/:owner/:repository/commit/:id/comment/_form")(readableUsersOnly { repository => - val id = params("id") - val fileName = params.get("fileName") - val oldLineNumber = params.get("oldLineNumber") map (_.toInt) - val newLineNumber = params.get("newLineNumber") map (_.toInt) - val issueId = params.get("issueId") map (_.toInt) - repo.html.commentform( - commitId = id, - fileName, oldLineNumber, newLineNumber, issueId, - hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount), - repository = repository - ) - }) - - ajaxPost("/:owner/:repository/commit/:id/comment/_data/new", commentForm)(readableUsersOnly { (form, repository) => - val id = params("id") - val commentId = createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName, - form.content, form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined) - form.issueId match { - case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content) - case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content) - } - helper.html.commitcomment(getCommitComment(repository.owner, repository.name, commentId.toString).get, - hasWritePermission(repository.owner, repository.name, context.loginAccount), repository) - }) - - ajaxGet("/:owner/:repository/commit_comments/_data/:id")(readableUsersOnly { repository => - getCommitComment(repository.owner, repository.name, params("id")) map { x => - if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){ - params.get("dataType") collect { - case t if t == "html" => repo.html.editcomment( - x.content, x.commentId, x.userName, x.repositoryName) - } getOrElse { - contentType = formats("json") - org.json4s.jackson.Serialization.write( - Map("content" -> view.Markdown.toHtml(x.content, - repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName)) - )) - } - } else Unauthorized - } getOrElse NotFound - }) - - ajaxPost("/:owner/:repository/commit_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => - defining(repository.owner, repository.name){ case (owner, name) => - getCommitComment(owner, name, params("id")).map { comment => - if(isEditable(owner, name, comment.commentedUserName)){ - updateCommitComment(comment.commentId, form.content) - redirect(s"/${owner}/${name}/commit_comments/_data/${comment.commentId}") - } else Unauthorized - } getOrElse NotFound - } - }) - - ajaxPost("/:owner/:repository/commit_comments/delete/:id")(readableUsersOnly { repository => - defining(repository.owner, repository.name){ case (owner, name) => - getCommitComment(owner, name, params("id")).map { comment => - if(isEditable(owner, name, comment.commentedUserName)){ - Ok(deleteCommitComment(comment.commentId)) - } else Unauthorized - } getOrElse NotFound - } - }) - - /** - * Displays branches. - */ - get("/:owner/:repository/branches")(referrersOnly { repository => - val branches = JGitUtil.getBranches(repository.owner, repository.name, repository.repository.defaultBranch) - .sortBy(br => (br.mergeInfo.isEmpty, br.commitTime)) - .map(br => br -> getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId)) - .reverse - repo.html.branches(branches, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository) - }) - - /** - * Creates a branch. - */ - post("/:owner/:repository/branches")(collaboratorsOnly { repository => - val newBranchName = params.getOrElse("new", halt(400)) - val fromBranchName = params.getOrElse("from", halt(400)) - using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - JGitUtil.createBranch(git, fromBranchName, newBranchName) - } match { - case Right(message) => - flash += "info" -> message - redirect(s"/${repository.owner}/${repository.name}/tree/${StringUtil.urlEncode(newBranchName).replace("%2F", "/")}") - case Left(message) => - flash += "error" -> message - redirect(s"/${repository.owner}/${repository.name}/tree/${fromBranchName}") - } - }) - - /** - * Deletes branch. - */ - get("/:owner/:repository/delete/*")(collaboratorsOnly { repository => - val branchName = multiParams("splat").head - val userName = context.loginAccount.get.userName - if(repository.repository.defaultBranch != branchName){ - using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - git.branchDelete().setForce(true).setBranchNames(branchName).call() - recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName) - } - } - redirect(s"/${repository.owner}/${repository.name}/branches") - }) - - /** - * Displays tags. - */ - get("/:owner/:repository/tags")(referrersOnly { - repo.html.tags(_) - }) - - /** - * Download repository contents as an archive. - */ - get("/:owner/:repository/archive/*")(referrersOnly { repository => - multiParams("splat").head match { - case name if name.endsWith(".zip") => - archiveRepository(name, ".zip", repository) - case name if name.endsWith(".tar.gz") => - archiveRepository(name, ".tar.gz", repository) - case _ => BadRequest - } - }) - - get("/:owner/:repository/network/members")(referrersOnly { repository => - repo.html.forked( - getRepository( - repository.repository.originUserName.getOrElse(repository.owner), - repository.repository.originRepositoryName.getOrElse(repository.name), - 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 - } orElse repository.tags.collectFirst { - case tag if(path == tag.name || path.startsWith(tag.name + "/")) => tag.name - } getOrElse path.split("/")(0) - - (id, path.substring(id.length).stripPrefix("/")) - } - - - private val readmeFiles = view.helpers.renderableSuffixes.map(suffix => s"readme${suffix}") ++ Seq("readme.txt", "readme") - - /** - * 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) - * @return HTML of the file list - */ - private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = { - if(repository.commitCount == 0){ - repo.html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount)) - } else { - using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - // get specified commit - JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) => - defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit => - val lastModifiedCommit = if(path == ".") revCommit else JGitUtil.getLastModifiedCommit(git, revCommit, path) - // get files - val files = JGitUtil.getFileList(git, revision, path) - val parentPath = if (path == ".") Nil else path.split("/").toList - // process README.md or README.markdown - val readme = files.find { file => - readmeFiles.contains(file.name.toLowerCase) - }.map { file => - val path = (file.name :: parentPath.reverse).reverse - path -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId( - Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get) - } - - repo.html.files(revision, repository, - if(path == ".") Nil else path.split("/").toList, // current path - context.loginAccount match { - case None => List() - case account: Option[model.Account] => getGroupsByUserName(account.get.userName) - }, // groups of current user - new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit - files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount), - flash.get("info"), flash.get("error")) - } - } 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(headName) - - 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), - headName, 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() - - // update pull request - updatePullRequests(repository.owner, repository.name, branch) - - // record activity - recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch, - List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId)))) - - // close issue by commit message - closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name) - - // call web hook - val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId)) - getWebHookURLs(repository.owner, repository.name) match { - case webHookURLs if(webHookURLs.nonEmpty) => - for(ownerAccount <- getAccountByUserName(repository.owner)){ - callWebHook(repository.owner, repository.name, webHookURLs, - WebHookPayload(git, loginAccount, headName, repository, List(commit), ownerAccount)) - } - case _ => - } - } - } - } - - 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) - } - } - - private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): Unit = { - val revision = name.stripSuffix(suffix) - val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId) - if(workDir.exists) { - FileUtils.deleteDirectory(workDir) - } - workDir.mkdirs - - val filename = repository.name + "-" + - (if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + suffix - - using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision)) - - contentType = "application/octet-stream" - response.setHeader("Content-Disposition", s"attachment; filename=${filename}") - response.setBufferSize(1024 * 1024); - - git.archive - .setFormat(suffix.tail) - .setTree(revCommit.getTree) - .setOutputStream(response.getOutputStream) - .call() - - Unit - } - } - - private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean = - hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName -} diff --git a/src/main/scala/app/SearchController.scala b/src/main/scala/app/SearchController.scala deleted file mode 100644 index dc71b0b..0000000 --- a/src/main/scala/app/SearchController.scala +++ /dev/null @@ -1,50 +0,0 @@ -package app - -import util._ -import ControlUtil._ -import Implicits._ -import service._ -import jp.sf.amateras.scalatra.forms._ - -class SearchController extends SearchControllerBase - with RepositoryService with AccountService with ActivityService with RepositorySearchService with IssuesService with ReferrerAuthenticator - -trait SearchControllerBase extends ControllerBase { self: RepositoryService - with ActivityService with RepositorySearchService with ReferrerAuthenticator => - - val searchForm = mapping( - "query" -> trim(text(required)), - "owner" -> trim(text(required)), - "repository" -> trim(text(required)) - )(SearchForm.apply) - - case class SearchForm(query: String, owner: String, repository: String) - - post("/search", searchForm){ form => - redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}") - } - - get("/:owner/:repository/search")(referrersOnly { repository => - defining(params("q").trim, params.getOrElse("type", "code")){ case (query, target) => - val page = try { - val i = params.getOrElse("page", "1").toInt - if(i <= 0) 1 else i - } catch { - case e: NumberFormatException => 1 - } - - target.toLowerCase match { - case "issue" => search.html.issues( - searchIssues(repository.owner, repository.name, query), - countFiles(repository.owner, repository.name, query), - query, page, repository) - - case _ => search.html.code( - searchFiles(repository.owner, repository.name, query), - countIssues(repository.owner, repository.name, query), - query, page, repository) - } - } - }) - -} diff --git a/src/main/scala/app/SystemSettingsController.scala b/src/main/scala/app/SystemSettingsController.scala deleted file mode 100644 index efa952a..0000000 --- a/src/main/scala/app/SystemSettingsController.scala +++ /dev/null @@ -1,84 +0,0 @@ -package app - -import service.{AccountService, SystemSettingsService} -import SystemSettingsService._ -import util.AdminAuthenticator -import jp.sf.amateras.scalatra.forms._ -import ssh.SshServer - -class SystemSettingsController extends SystemSettingsControllerBase - with AccountService with AdminAuthenticator - -trait SystemSettingsControllerBase extends ControllerBase { - self: AccountService with AdminAuthenticator => - - private val form = mapping( - "baseUrl" -> trim(label("Base URL", optional(text()))), - "information" -> trim(label("Information", optional(text()))), - "allowAccountRegistration" -> trim(label("Account registration", boolean())), - "allowAnonymousAccess" -> trim(label("Anonymous access", boolean())), - "isCreateRepoOptionPublic" -> trim(label("Default option to create a new repository", boolean())), - "gravatar" -> trim(label("Gravatar", boolean())), - "notification" -> trim(label("Notification", boolean())), - "ssh" -> trim(label("SSH access", boolean())), - "sshPort" -> trim(label("SSH port", optional(number()))), - "smtp" -> optionalIfNotChecked("notification", mapping( - "host" -> trim(label("SMTP Host", text(required))), - "port" -> trim(label("SMTP Port", optional(number()))), - "user" -> trim(label("SMTP User", optional(text()))), - "password" -> trim(label("SMTP Password", optional(text()))), - "ssl" -> trim(label("Enable SSL", optional(boolean()))), - "fromAddress" -> trim(label("FROM Address", optional(text()))), - "fromName" -> trim(label("FROM Name", optional(text()))) - )(Smtp.apply)), - "ldapAuthentication" -> trim(label("LDAP", boolean())), - "ldap" -> optionalIfNotChecked("ldapAuthentication", mapping( - "host" -> trim(label("LDAP host", text(required))), - "port" -> trim(label("LDAP port", optional(number()))), - "bindDN" -> trim(label("Bind DN", optional(text()))), - "bindPassword" -> trim(label("Bind Password", optional(text()))), - "baseDN" -> trim(label("Base DN", text(required))), - "userNameAttribute" -> trim(label("User name attribute", text(required))), - "additionalFilterCondition"-> trim(label("Additional filter condition", optional(text()))), - "fullNameAttribute" -> trim(label("Full name attribute", optional(text()))), - "mailAttribute" -> trim(label("Mail address attribute", optional(text()))), - "tls" -> trim(label("Enable TLS", optional(boolean()))), - "ssl" -> trim(label("Enable SSL", optional(boolean()))), - "keystore" -> trim(label("Keystore", optional(text()))) - )(Ldap.apply)) - )(SystemSettings.apply).verifying { settings => - if(settings.ssh && settings.baseUrl.isEmpty){ - Seq("baseUrl" -> "Base URL is required if SSH access is enabled.") - } else Nil - } - - private val pluginForm = mapping( - "pluginId" -> list(trim(label("", text()))) - )(PluginForm.apply) - - case class PluginForm(pluginIds: List[String]) - - get("/admin/system")(adminOnly { - admin.html.system(flash.get("info")) - }) - - post("/admin/system", form)(adminOnly { form => - saveSystemSettings(form) - - if(form.ssh && SshServer.isActive && context.settings.sshPort != form.sshPort){ - SshServer.stop() - } - - if(form.ssh && !SshServer.isActive && form.baseUrl.isDefined){ - SshServer.start( - form.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), - form.baseUrl.get) - } else if(!form.ssh && SshServer.isActive){ - SshServer.stop() - } - - flash += "info" -> "System settings has been updated." - redirect("/admin/system") - }) - -} diff --git a/src/main/scala/app/UserManagementController.scala b/src/main/scala/app/UserManagementController.scala deleted file mode 100644 index d98d703..0000000 --- a/src/main/scala/app/UserManagementController.scala +++ /dev/null @@ -1,203 +0,0 @@ -package app - -import service._ -import util.AdminAuthenticator -import util.StringUtil._ -import util.ControlUtil._ -import util.Directory._ -import util.Implicits._ -import jp.sf.amateras.scalatra.forms._ -import org.scalatra.i18n.Messages -import org.apache.commons.io.FileUtils - -class UserManagementController extends UserManagementControllerBase - with AccountService with RepositoryService with AdminAuthenticator - -trait UserManagementControllerBase extends AccountManagementControllerBase { - self: AccountService with RepositoryService with AdminAuthenticator => - - case class NewUserForm(userName: String, password: String, fullName: String, - mailAddress: String, isAdmin: Boolean, - url: Option[String], fileId: Option[String]) - - case class EditUserForm(userName: String, password: Option[String], fullName: String, - mailAddress: String, isAdmin: Boolean, url: Option[String], - fileId: Option[String], clearImage: Boolean, isRemoved: Boolean) - - case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], - members: String) - - case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], - members: String, clearImage: Boolean, isRemoved: Boolean) - - val newUserForm = mapping( - "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))), - "password" -> trim(label("Password" ,text(required, maxlength(20)))), - "fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))), - "mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))), - "isAdmin" -> trim(label("User Type" ,boolean())), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))) - )(NewUserForm.apply) - - val editUserForm = mapping( - "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))), - "password" -> trim(label("Password" ,optional(text(maxlength(20))))), - "fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))), - "mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))), - "isAdmin" -> trim(label("User Type" ,boolean())), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))), - "clearImage" -> trim(label("Clear image" ,boolean())), - "removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName")))) - )(EditUserForm.apply) - - val newGroupForm = mapping( - "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))), - "members" -> trim(label("Members" ,text(required, members))) - )(NewGroupForm.apply) - - val editGroupForm = mapping( - "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), - "url" -> trim(label("URL" ,optional(text(maxlength(200))))), - "fileId" -> trim(label("File ID" ,optional(text()))), - "members" -> trim(label("Members" ,text(required, members))), - "clearImage" -> trim(label("Clear image" ,boolean())), - "removed" -> trim(label("Disable" ,boolean())) - )(EditGroupForm.apply) - - get("/admin/users")(adminOnly { - val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false) - val users = getAllUsers(includeRemoved) - val members = users.collect { case account if(account.isGroupAccount) => - account.userName -> getGroupMembers(account.userName).map(_.userName) - }.toMap - - admin.users.html.list(users, members, includeRemoved) - }) - - get("/admin/users/_newuser")(adminOnly { - admin.users.html.user(None) - }) - - post("/admin/users/_newuser", newUserForm)(adminOnly { form => - createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url) - updateImage(form.userName, form.fileId, false) - redirect("/admin/users") - }) - - get("/admin/users/:userName/_edituser")(adminOnly { - val userName = params("userName") - admin.users.html.user(getAccountByUserName(userName, true)) - }) - - post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form => - val userName = params("userName") - getAccountByUserName(userName, true).map { account => - - if(form.isRemoved){ - // Remove repositories - getRepositoryNamesOfUser(userName).foreach { repositoryName => - deleteRepository(userName, repositoryName) - FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName)) - FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName)) - FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName)) - } - // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY - removeUserRelatedData(userName) - } - - updateAccount(account.copy( - password = form.password.map(sha1).getOrElse(account.password), - fullName = form.fullName, - mailAddress = form.mailAddress, - isAdmin = form.isAdmin, - url = form.url, - isRemoved = form.isRemoved)) - - updateImage(userName, form.fileId, form.clearImage) - redirect("/admin/users") - - } getOrElse NotFound - }) - - get("/admin/users/_newgroup")(adminOnly { - admin.users.html.group(None, Nil) - }) - - post("/admin/users/_newgroup", newGroupForm)(adminOnly { form => - createGroup(form.groupName, form.url) - updateGroupMembers(form.groupName, form.members.split(",").map { - _.split(":") match { - case Array(userName, isManager) => (userName, isManager.toBoolean) - } - }.toList) - updateImage(form.groupName, form.fileId, false) - redirect("/admin/users") - }) - - get("/admin/users/:groupName/_editgroup")(adminOnly { - defining(params("groupName")){ groupName => - admin.users.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName)) - } - }) - - post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form => - defining(params("groupName"), form.members.split(",").map { - _.split(":") match { - case Array(userName, isManager) => (userName, isManager.toBoolean) - } - }.toList){ case (groupName, members) => - getAccountByUserName(groupName, true).map { account => - updateGroup(groupName, form.url, form.isRemoved) - - if(form.isRemoved){ - // Remove from GROUP_MEMBER - updateGroupMembers(form.groupName, Nil) - // Remove repositories - getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => - deleteRepository(groupName, repositoryName) - FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName)) - FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName)) - FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName)) - } - } else { - // Update GROUP_MEMBER - updateGroupMembers(form.groupName, members) - // Update COLLABORATOR for group repositories - getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => - removeCollaborators(form.groupName, repositoryName) - members.foreach { case (userName, isManager) => - addCollaborator(form.groupName, repositoryName, userName) - } - } - } - - updateImage(form.groupName, form.fileId, form.clearImage) - redirect("/admin/users") - - } getOrElse NotFound - } - }) - - private def members: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = { - if(value.split(",").exists { - _.split(":") match { case Array(userName, isManager) => isManager.toBoolean } - }) None else Some("Must select one manager at least.") - } - } - - protected def disableByNotYourself(paramName: String): Constraint = new Constraint() { - override def validate(name: String, value: String, messages: Messages): Option[String] = { - params.get(paramName).flatMap { userName => - if(userName == context.loginAccount.get.userName) - Some("You can't disable your account yourself") - else - None - } - } - } -} diff --git a/src/main/scala/app/WikiController.scala b/src/main/scala/app/WikiController.scala deleted file mode 100644 index c273018..0000000 --- a/src/main/scala/app/WikiController.scala +++ /dev/null @@ -1,205 +0,0 @@ -package app - -import service._ -import util._ -import util.Directory._ -import util.ControlUtil._ -import util.Implicits._ -import jp.sf.amateras.scalatra.forms._ -import org.eclipse.jgit.api.Git -import org.scalatra.i18n.Messages -import java.util.ResourceBundle - -class WikiController extends WikiControllerBase - with WikiService with RepositoryService with AccountService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator - -trait WikiControllerBase extends ControllerBase { - self: WikiService with RepositoryService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator => - - case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String) - - val newForm = mapping( - "pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename, unique))), - "content" -> trim(label("Content" , text(required, conflictForNew))), - "message" -> trim(label("Message" , optional(text()))), - "currentPageName" -> trim(label("Current page name" , text())), - "id" -> trim(label("Latest commit id" , text())) - )(WikiPageEditForm.apply) - - val editForm = mapping( - "pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename))), - "content" -> trim(label("Content" , text(required, conflictForEdit))), - "message" -> trim(label("Message" , optional(text()))), - "currentPageName" -> trim(label("Current page name" , text(required))), - "id" -> trim(label("Latest commit id" , text(required))) - )(WikiPageEditForm.apply) - - get("/:owner/:repository/wiki")(referrersOnly { repository => - getWikiPage(repository.owner, repository.name, "Home").map { page => - wiki.html.page("Home", page, getWikiPageList(repository.owner, repository.name), - repository, hasWritePermission(repository.owner, repository.name, context.loginAccount)) - } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit") - }) - - get("/:owner/:repository/wiki/:page")(referrersOnly { repository => - val pageName = StringUtil.urlDecode(params("page")) - - getWikiPage(repository.owner, repository.name, pageName).map { page => - wiki.html.page(pageName, page, getWikiPageList(repository.owner, repository.name), - repository, hasWritePermission(repository.owner, repository.name, context.loginAccount)) - } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit") - }) - - get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository => - val pageName = StringUtil.urlDecode(params("page")) - - using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => - JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match { - case Right((logs, hasNext)) => wiki.html.history(Some(pageName), logs, repository) - case Left(_) => NotFound - } - } - }) - - get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository => - val pageName = StringUtil.urlDecode(params("page")) - 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).filter(_.newPath == pageName + ".md"), repository, - hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info")) - } - }) - - get("/:owner/:repository/wiki/_compare/:commitId")(referrersOnly { repository => - val Array(from, to) = params("commitId").split("\\.\\.\\.") - - using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => - wiki.html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true), repository, - hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info")) - } - }) - - get("/:owner/:repository/wiki/:page/_revert/:commitId")(collaboratorsOnly { repository => - val pageName = StringUtil.urlDecode(params("page")) - val Array(from, to) = params("commitId").split("\\.\\.\\.") - - if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, Some(pageName))){ - redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}") - } else { - flash += "info" -> "This patch was not able to be reversed." - redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_compare/${from}...${to}") - } - }) - - get("/:owner/:repository/wiki/_revert/:commitId")(collaboratorsOnly { repository => - 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/") - } else { - flash += "info" -> "This patch was not able to be reversed." - redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}") - } - }) - - get("/:owner/:repository/wiki/:page/_edit")(collaboratorsOnly { repository => - val pageName = StringUtil.urlDecode(params("page")) - wiki.html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository) - }) - - post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) => - defining(context.loginAccount.get){ loginAccount => - saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName, - form.content, loginAccount, form.message.getOrElse(""), Some(form.id)).map { commitId => - updateLastActivityDate(repository.owner, repository.name) - recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId) - } - redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}") - } - }) - - get("/:owner/:repository/wiki/_new")(collaboratorsOnly { - wiki.html.edit("", None, _) - }) - - post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) => - defining(context.loginAccount.get){ loginAccount => - saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName, - form.content, loginAccount, form.message.getOrElse(""), None) - - updateLastActivityDate(repository.owner, repository.name) - recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName) - - redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}") - } - }) - - get("/:owner/:repository/wiki/:page/_delete")(collaboratorsOnly { repository => - val pageName = StringUtil.urlDecode(params("page")) - - defining(context.loginAccount.get){ loginAccount => - deleteWikiPage(repository.owner, repository.name, pageName, loginAccount.fullName, loginAccount.mailAddress, s"Destroyed ${pageName}") - updateLastActivityDate(repository.owner, repository.name) - - redirect(s"/${repository.owner}/${repository.name}/wiki") - } - }) - - get("/:owner/:repository/wiki/_pages")(referrersOnly { repository => - wiki.html.pages(getWikiPageList(repository.owner, repository.name), repository, - hasWritePermission(repository.owner, repository.name, context.loginAccount)) - }) - - get("/:owner/:repository/wiki/_history")(referrersOnly { repository => - using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => - JGitUtil.getCommitLog(git, "master") match { - case Right((logs, hasNext)) => wiki.html.history(None, logs, repository) - case Left(_) => NotFound - } - } - }) - - get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository => - val path = multiParams("splat").head - - getFileContent(repository.owner, repository.name, path).map { bytes => - RawData(FileUtil.getContentType(path, bytes), bytes) - } getOrElse NotFound - }) - - private def unique: Constraint = new Constraint(){ - override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = - getWikiPageList(params("owner"), params("repository")).find(_ == value).map(_ => "Page already exists.") - } - - private def pagename: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = - if(value.exists("\\/:*?\"<>|".contains(_))){ - Some(s"${name} contains invalid character.") - } else if(value.startsWith("_") || value.startsWith("-")){ - Some(s"${name} starts with invalid character.") - } else { - None - } - } - - private def conflictForNew: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = { - targetWikiPage.map { _ => - "Someone has created the wiki since you started. Please reload this page and re-apply your changes." - } - } - } - - private def conflictForEdit: Constraint = new Constraint(){ - override def validate(name: String, value: String, messages: Messages): Option[String] = { - targetWikiPage.filter(_.id != params("id")).map{ _ => - "Someone has edited the wiki since you started. Please reload this page and re-apply your changes." - } - } - } - - private def targetWikiPage = getWikiPage(params("owner"), params("repository"), params("pageName")) - -} diff --git a/src/main/scala/gitbucket/core/controller/AccountController.scala b/src/main/scala/gitbucket/core/controller/AccountController.scala new file mode 100644 index 0000000..afbe12f --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/AccountController.scala @@ -0,0 +1,471 @@ +package gitbucket.core.controller + +import gitbucket.core.account.html +import gitbucket.core.helper +import gitbucket.core.model.GroupMember +import gitbucket.core.util._ +import gitbucket.core.util.Implicits._ +import gitbucket.core.util.Directory._ +import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.StringUtil._ +import gitbucket.core.ssh.SshUtil +import gitbucket.core.service._ +import jp.sf.amateras.scalatra.forms._ +import org.apache.commons.io.FileUtils +import org.scalatra.i18n.Messages +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.{FileMode, Constants} +import org.eclipse.jgit.dircache.DirCache + + +class AccountController extends AccountControllerBase + with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService + with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator + +trait AccountControllerBase extends AccountManagementControllerBase { + self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService + with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator => + + case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String, + url: Option[String], fileId: Option[String]) + + case class AccountEditForm(password: Option[String], fullName: String, mailAddress: String, + url: Option[String], fileId: Option[String], clearImage: Boolean) + + case class SshKeyForm(title: String, publicKey: String) + + val newForm = mapping( + "userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))), + "password" -> trim(label("Password" , text(required, maxlength(20)))), + "fullName" -> trim(label("Full Name" , text(required, maxlength(100)))), + "mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))), + "url" -> trim(label("URL" , optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" , optional(text()))) + )(AccountNewForm.apply) + + val editForm = mapping( + "password" -> trim(label("Password" , optional(text(maxlength(20))))), + "fullName" -> trim(label("Full Name" , text(required, maxlength(100)))), + "mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress("userName")))), + "url" -> trim(label("URL" , optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" , optional(text()))), + "clearImage" -> trim(label("Clear image" , boolean())) + )(AccountEditForm.apply) + + val sshKeyForm = mapping( + "title" -> trim(label("Title", text(required, maxlength(100)))), + "publicKey" -> trim(label("Key" , text(required, validPublicKey))) + )(SshKeyForm.apply) + + case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String) + case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String, clearImage: Boolean) + + val newGroupForm = mapping( + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))) + )(NewGroupForm.apply) + + val editGroupForm = mapping( + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))), + "clearImage" -> trim(label("Clear image" ,boolean())) + )(EditGroupForm.apply) + + case class RepositoryCreationForm(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) + case class ForkRepositoryForm(owner: String, name: String) + + val newRepositoryForm = mapping( + "owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))), + "name" -> trim(label("Repository name", text(required, maxlength(40), identifier, uniqueRepository))), + "description" -> trim(label("Description" , optional(text()))), + "isPrivate" -> trim(label("Repository Type", boolean())), + "createReadme" -> trim(label("Create README" , boolean())) + )(RepositoryCreationForm.apply) + + val forkRepositoryForm = mapping( + "owner" -> trim(label("Repository owner", text(required))), + "name" -> trim(label("Repository name", text(required))) + )(ForkRepositoryForm.apply) + + case class AccountForm(accountName: String) + + val accountForm = mapping( + "account" -> trim(label("Group/User name", text(required, validAccountName))) + )(AccountForm.apply) + + /** + * Displays user information. + */ + get("/:userName") { + val userName = params("userName") + getAccountByUserName(userName).map { account => + params.getOrElse("tab", "repositories") match { + // Public Activity + case "activity" => + gitbucket.core.account.html.activity(account, + if(account.isGroupAccount) Nil else getGroupsByUserName(userName), + getActivitiesByUser(userName, true)) + + // Members + case "members" if(account.isGroupAccount) => { + val members = getGroupMembers(account.userName) + gitbucket.core.account.html.members(account, members.map(_.userName), + context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })) + } + + // Repositories + case _ => { + val members = getGroupMembers(account.userName) + gitbucket.core.account.html.repositories(account, + if(account.isGroupAccount) Nil else getGroupsByUserName(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 => + RawData(FileUtil.getMimeType(image), new java.io.File(getUserUploadDir(userName), image)) + } getOrElse { + contentType = "image/png" + Thread.currentThread.getContextClassLoader.getResourceAsStream("noimage.png") + } + } + + get("/:userName/_edit")(oneselfOnly { + val userName = params("userName") + getAccountByUserName(userName).map { x => + html.edit(x, flash.get("info")) + } getOrElse NotFound + }) + + post("/:userName/_edit", editForm)(oneselfOnly { form => + val userName = params("userName") + getAccountByUserName(userName).map { account => + updateAccount(account.copy( + password = form.password.map(sha1).getOrElse(account.password), + fullName = form.fullName, + mailAddress = form.mailAddress, + url = form.url)) + + updateImage(userName, form.fileId, form.clearImage) + flash += "info" -> "Account information has been updated." + redirect(s"/${userName}/_edit") + + } getOrElse NotFound + }) + + get("/:userName/_delete")(oneselfOnly { + val userName = params("userName") + + getAccountByUserName(userName, true).foreach { account => + // Remove repositories + getRepositoryNamesOfUser(userName).foreach { repositoryName => + deleteRepository(userName, repositoryName) + FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName)) + FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName)) + FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName)) + } + // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY + removeUserRelatedData(userName) + + updateAccount(account.copy(isRemoved = true)) + } + + session.invalidate + redirect("/") + }) + + get("/:userName/_ssh")(oneselfOnly { + val userName = params("userName") + getAccountByUserName(userName).map { x => + html.ssh(x, getPublicKeys(x.userName)) + } getOrElse NotFound + }) + + post("/:userName/_ssh", sshKeyForm)(oneselfOnly { form => + val userName = params("userName") + addPublicKey(userName, form.title, form.publicKey) + redirect(s"/${userName}/_ssh") + }) + + get("/:userName/_ssh/delete/:id")(oneselfOnly { + val userName = params("userName") + val sshKeyId = params("id").toInt + deletePublicKey(userName, sshKeyId) + redirect(s"/${userName}/_ssh") + }) + + get("/register"){ + if(context.settings.allowAccountRegistration){ + if(context.loginAccount.isDefined){ + redirect("/") + } else { + html.register() + } + } else NotFound + } + + post("/register", newForm){ form => + if(context.settings.allowAccountRegistration){ + createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url) + updateImage(form.userName, form.fileId, false) + redirect("/signin") + } else NotFound + } + + get("/groups/new")(usersOnly { + html.group(None, List(GroupMember("", context.loginAccount.get.userName, true))) + }) + + post("/groups/new", newGroupForm)(usersOnly { form => + createGroup(form.groupName, form.url) + updateGroupMembers(form.groupName, form.members.split(",").map { + _.split(":") match { + case Array(userName, isManager) => (userName, isManager.toBoolean) + } + }.toList) + updateImage(form.groupName, form.fileId, false) + redirect(s"/${form.groupName}") + }) + + get("/:groupName/_editgroup")(managersOnly { + defining(params("groupName")){ groupName => + html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName)) + } + }) + + get("/:groupName/_deletegroup")(managersOnly { + defining(params("groupName")){ groupName => + // Remove from GROUP_MEMBER + updateGroupMembers(groupName, Nil) + // Remove repositories + getRepositoryNamesOfUser(groupName).foreach { repositoryName => + deleteRepository(groupName, repositoryName) + FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName)) + FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName)) + FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName)) + } + } + redirect("/") + }) + + post("/:groupName/_editgroup", editGroupForm)(managersOnly { form => + defining(params("groupName"), form.members.split(",").map { + _.split(":") match { + case Array(userName, isManager) => (userName, isManager.toBoolean) + } + }.toList){ case (groupName, members) => + getAccountByUserName(groupName, true).map { account => + updateGroup(groupName, form.url, false) + + // Update GROUP_MEMBER + updateGroupMembers(form.groupName, members) + // Update COLLABORATOR for group repositories + getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => + removeCollaborators(form.groupName, repositoryName) + members.foreach { case (userName, isManager) => + addCollaborator(form.groupName, repositoryName, userName) + } + } + + updateImage(form.groupName, form.fileId, form.clearImage) + redirect(s"/${form.groupName}") + + } getOrElse NotFound + } + }) + + /** + * Show the new repository form. + */ + get("/new")(usersOnly { + html.newrepo(getGroupsByUserName(context.loginAccount.get.userName), context.settings.isCreateRepoOptionPublic) + }) + + /** + * Create new repository. + */ + post("/new", newRepositoryForm)(usersOnly { form => + LockUtil.lock(s"${form.owner}/${form.name}"){ + if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){ + val ownerAccount = getAccountByUserName(form.owner).get + val loginAccount = context.loginAccount.get + val loginUserName = loginAccount.userName + + // Insert to the database at first + createRepository(form.name, form.owner, form.description, form.isPrivate) + + // Add collaborators for group repository + if(ownerAccount.isGroupAccount){ + getGroupMembers(form.owner).foreach { member => + addCollaborator(form.owner, form.name, member.userName) + } + } + + // Insert default labels + insertDefaultLabels(form.owner, form.name) + + // Create the actual repository + val gitdir = getRepositoryDir(form.owner, form.name) + JGitUtil.initRepository(gitdir) + + if(form.createReadme){ + 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" + } + + builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE, + inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")))) + builder.finish() + + JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), + Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit") + } + } + + // Create Wiki repository + createWikiRepository(loginAccount, form.owner, form.name) + + // Record activity + recordCreateRepositoryActivity(form.owner, form.name, loginUserName) + } + + // redirect to the repository + redirect(s"/${form.owner}/${form.name}") + } + }) + + get("/:owner/:repository/fork")(readableUsersOnly { repository => + val loginAccount = context.loginAccount.get + val loginUserName = loginAccount.userName + val groups = getGroupsByUserName(loginUserName) + groups match { + case _: List[String] => + val managerPermissions = groups.map { group => + val members = getGroupMembers(group) + context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }) + } + helper.html.forkrepository( + repository, + (groups zip managerPermissions).toMap + ) + case _ => redirect(s"/${loginUserName}") + } + }) + + post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) => + val loginAccount = context.loginAccount.get + val loginUserName = loginAccount.userName + val accountName = form.accountName + + LockUtil.lock(s"${accountName}/${repository.name}"){ + if(getRepository(accountName, repository.name, baseUrl).isDefined || + (accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){ + // redirect to the repository if repository already exists + redirect(s"/${accountName}/${repository.name}") + } else { + // Insert to the database at first + val originUserName = repository.repository.originUserName.getOrElse(repository.owner) + val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name) + + createRepository( + repositoryName = repository.name, + userName = accountName, + description = repository.repository.description, + isPrivate = repository.repository.isPrivate, + originRepositoryName = Some(originRepositoryName), + originUserName = Some(originUserName), + parentRepositoryName = Some(repository.name), + parentUserName = Some(repository.owner) + ) + + // Insert default labels + insertDefaultLabels(accountName, repository.name) + + // clone repository actually + JGitUtil.cloneRepository( + getRepositoryDir(repository.owner, repository.name), + getRepositoryDir(accountName, repository.name)) + + // Create Wiki repository + JGitUtil.cloneRepository( + getWikiRepositoryDir(repository.owner, repository.name), + getWikiRepositoryDir(accountName, repository.name)) + + // Record activity + recordForkActivity(repository.owner, repository.name, loginUserName, accountName) + // redirect to the repository + redirect(s"/${accountName}/${repository.name}") + } + } + }) + + private def insertDefaultLabels(userName: String, repositoryName: String): Unit = { + createLabel(userName, repositoryName, "bug", "fc2929") + createLabel(userName, repositoryName, "duplicate", "cccccc") + createLabel(userName, repositoryName, "enhancement", "84b6eb") + createLabel(userName, repositoryName, "invalid", "e6e6e6") + createLabel(userName, repositoryName, "question", "cc317c") + createLabel(userName, repositoryName, "wontfix", "ffffff") + } + + private def existsAccount: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = + if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None + } + + private def uniqueRepository: Constraint = new Constraint(){ + override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = + params.get("owner").flatMap { userName => + getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.") + } + } + + private def members: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = { + if(value.split(",").exists { + _.split(":") match { case Array(userName, isManager) => isManager.toBoolean } + }) None else Some("Must select one manager at least.") + } + } + + private def validPublicKey: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = SshUtil.str2PublicKey(value) match { + case Some(_) => None + case None => Some("Key is invalid.") + } + } + + private def validAccountName: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = { + getAccountByUserName(value) match { + case Some(_) => None + case None => Some("Invalid Group/User Account.") + } + } + } +} diff --git a/src/main/scala/gitbucket/core/controller/AnonymousAccessController.scala b/src/main/scala/gitbucket/core/controller/AnonymousAccessController.scala new file mode 100644 index 0000000..180b164 --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/AnonymousAccessController.scala @@ -0,0 +1,14 @@ +package gitbucket.core.controller + +class AnonymousAccessController extends AnonymousAccessControllerBase + +trait AnonymousAccessControllerBase extends ControllerBase { + get(!context.settings.allowAnonymousAccess, context.loginAccount.isEmpty) { + if(!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") && + !context.currentPath.startsWith("/register")) { + Unauthorized() + } else { + pass() + } + } +} diff --git a/src/main/scala/gitbucket/core/controller/ControllerBase.scala b/src/main/scala/gitbucket/core/controller/ControllerBase.scala new file mode 100644 index 0000000..1fa9710 --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/ControllerBase.scala @@ -0,0 +1,213 @@ +package gitbucket.core.controller + +import gitbucket.core.service.{AccountService, SystemSettingsService} +import gitbucket.core.util._ +import gitbucket.core.util.Implicits._ +import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.Directory._ +import gitbucket.core.model.Account +import org.scalatra._ +import org.scalatra.json._ +import org.json4s._ +import jp.sf.amateras.scalatra.forms._ +import org.apache.commons.io.FileUtils +import javax.servlet.http.{HttpServletResponse, HttpServletRequest} +import javax.servlet.{FilterChain, ServletResponse, ServletRequest} +import org.scalatra.i18n._ + +/** + * Provides generic features for controller implementations. + */ +abstract class ControllerBase extends ScalatraFilter + with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations + with SystemSettingsService { + + implicit val jsonFormats = DefaultFormats + +// TODO Scala 2.11 +// // Don't set content type via Accept header. +// override def format(implicit request: HttpServletRequest) = "" + + override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = try { + val httpRequest = request.asInstanceOf[HttpServletRequest] + val httpResponse = response.asInstanceOf[HttpServletResponse] + val context = request.getServletContext.getContextPath + val path = httpRequest.getRequestURI.substring(context.length) + + if(path.startsWith("/console/")){ + val account = httpRequest.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account] + val baseUrl = this.baseUrl(httpRequest) + if(account == null){ + // Redirect to login form + httpResponse.sendRedirect(baseUrl + "/signin?redirect=" + StringUtil.urlEncode(path)) + } else if(account.isAdmin){ + // H2 Console (administrators only) + chain.doFilter(request, response) + } else { + // Redirect to dashboard + httpResponse.sendRedirect(baseUrl + "/") + } + } else if(path.startsWith("/git/")){ + // Git repository + chain.doFilter(request, response) + } else { + // Scalatra actions + super.doFilter(request, response, chain) + } + } finally { + contextCache.remove(); + } + + private val contextCache = new java.lang.ThreadLocal[Context]() + + /** + * Returns the context object for the request. + */ + implicit def context: Context = { + contextCache.get match { + case null => { + val context = Context(loadSystemSettings(), LoginAccount, request) + contextCache.set(context) + context + } + case context => context + } + } + + private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount) + + def ajaxGet(path : String)(action : => Any) : Route = + super.get(path){ + request.setAttribute(Keys.Request.Ajax, "true") + action + } + + override def ajaxGet[T](path : String, form : ValueType[T])(action : T => Any) : Route = + super.ajaxGet(path, form){ form => + request.setAttribute(Keys.Request.Ajax, "true") + action(form) + } + + def ajaxPost(path : String)(action : => Any) : Route = + super.post(path){ + request.setAttribute(Keys.Request.Ajax, "true") + action + } + + override def ajaxPost[T](path : String, form : ValueType[T])(action : T => Any) : Route = + super.ajaxPost(path, form){ form => + request.setAttribute(Keys.Request.Ajax, "true") + action(form) + } + + protected def NotFound() = + if(request.hasAttribute(Keys.Request.Ajax)){ + org.scalatra.NotFound() + } else { + org.scalatra.NotFound(gitbucket.core.html.error("Not Found")) + } + + protected def Unauthorized()(implicit context: Context) = + if(request.hasAttribute(Keys.Request.Ajax)){ + org.scalatra.Unauthorized() + } else { + if(context.loginAccount.isDefined){ + org.scalatra.Unauthorized(redirect("/")) + } else { + if(request.getMethod.toUpperCase == "POST"){ + org.scalatra.Unauthorized(redirect("/signin")) + } else { + org.scalatra.Unauthorized(redirect("/signin?redirect=" + StringUtil.urlEncode( + defining(request.getQueryString){ queryString => + request.getRequestURI.substring(request.getContextPath.length) + (if(queryString != null) "?" + queryString else "") + } + ))) + } + } + } + + // TODO Scala 2.11 + override def url(path: String, params: Iterable[(String, Any)] = Iterable.empty, + includeContextPath: Boolean = true, includeServletPath: Boolean = true, + absolutize: Boolean = true, withSessionId: Boolean = true) + (implicit request: HttpServletRequest, response: HttpServletResponse): String = + if (path.startsWith("http")) path + else baseUrl + super.url(path, params, false, false, false) + + /** + * Use this method to response the raw data against XSS. + */ + protected def RawData[T](contentType: String, rawData: T): T = { + if(contentType.split(";").head.trim.toLowerCase.startsWith("text/html")){ + this.contentType = "text/plain" + } else { + this.contentType = contentType + } + response.addHeader("X-Content-Type-Options", "nosniff") + rawData + } +} + +/** + * Context object for the current request. + */ +case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){ + + 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. + * + * If object has not been cached with the specified key then retrieves by given action. + * Cached object are available during a request. + */ + def cache[A](key: String)(action: => A): A = + defining(Keys.Request.Cache(key)){ cacheKey => + Option(request.getAttribute(cacheKey).asInstanceOf[A]).getOrElse { + val newObject = action + request.setAttribute(cacheKey, newObject) + newObject + } + } + +} + +/** + * Base trait for controllers which manages account information. + */ +trait AccountManagementControllerBase extends ControllerBase { + self: AccountService => + + protected def updateImage(userName: String, fileId: Option[String], clearImage: Boolean): Unit = + if(clearImage){ + getAccountByUserName(userName).flatMap(_.image).map { image => + new java.io.File(getUserUploadDir(userName), image).delete() + updateAvatarImage(userName, None) + } + } else { + fileId.map { fileId => + val filename = "avatar." + FileUtil.getExtension(session.getAndRemove(Keys.Session.Upload(fileId)).get) + FileUtils.moveFile( + new java.io.File(getTemporaryDir(session.getId), fileId), + new java.io.File(getUserUploadDir(userName), filename) + ) + updateAvatarImage(userName, Some(filename)) + } + } + + protected def uniqueUserName: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = + getAccountByUserName(value, true).map { _ => "User already exists." } + } + + protected def uniqueMailAddress(paramName: String = ""): Constraint = new Constraint(){ + override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = + getAccountByMailAddress(value, true) + .filter { x => if(paramName.isEmpty) true else Some(x.userName) != params.get(paramName) } + .map { _ => "Mail address is already registered." } + } + +} diff --git a/src/main/scala/gitbucket/core/controller/DashboardController.scala b/src/main/scala/gitbucket/core/controller/DashboardController.scala new file mode 100644 index 0000000..713e9c0 --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/DashboardController.scala @@ -0,0 +1,138 @@ +package gitbucket.core.controller + +import gitbucket.core.dashboard.html +import gitbucket.core.service.{RepositoryService, PullRequestService, AccountService, IssuesService} +import gitbucket.core.util.{StringUtil, Keys, UsersAuthenticator} +import gitbucket.core.util.Implicits._ +import gitbucket.core.service.IssuesService._ + +class DashboardController extends DashboardControllerBase + with IssuesService with PullRequestService with RepositoryService with AccountService + with UsersAuthenticator + +trait DashboardControllerBase extends ControllerBase { + self: IssuesService with PullRequestService with RepositoryService with AccountService + with UsersAuthenticator => + + get("/dashboard/issues")(usersOnly { + val q = request.getParameter("q") + val account = context.loginAccount.get + Option(q).map { q => + val condition = IssueSearchCondition(q, Map[String, Int]()) + q match { + case q if(q.contains("is:pr")) => redirect(s"/dashboard/pulls?q=${StringUtil.urlEncode(q)}") + case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/issues/created_by${condition.toURL}") + case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/issues/assigned${condition.toURL}") + case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/issues/mentioned${condition.toURL}") + case _ => searchIssues("created_by") + } + } getOrElse { + searchIssues("created_by") + } + }) + + get("/dashboard/issues/assigned")(usersOnly { + searchIssues("assigned") + }) + + get("/dashboard/issues/created_by")(usersOnly { + searchIssues("created_by") + }) + + get("/dashboard/issues/mentioned")(usersOnly { + searchIssues("mentioned") + }) + + get("/dashboard/pulls")(usersOnly { + val q = request.getParameter("q") + val account = context.loginAccount.get + Option(q).map { q => + val condition = IssueSearchCondition(q, Map[String, Int]()) + q match { + case q if(q.contains("is:issue")) => redirect(s"/dashboard/issues?q=${StringUtil.urlEncode(q)}") + case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/pulls/created_by${condition.toURL}") + case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/pulls/assigned${condition.toURL}") + case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/pulls/mentioned${condition.toURL}") + case _ => searchPullRequests("created_by") + } + } getOrElse { + searchPullRequests("created_by") + } + }) + + get("/dashboard/pulls/created_by")(usersOnly { + searchPullRequests("created_by") + }) + + get("/dashboard/pulls/assigned")(usersOnly { + searchPullRequests("assigned") + }) + + get("/dashboard/pulls/mentioned")(usersOnly { + searchPullRequests("mentioned") + }) + + private def getOrCreateCondition(key: String, filter: String, userName: String) = { + val condition = session.putAndGet(key, if(request.hasQueryString){ + val q = request.getParameter("q") + if(q == null){ + IssueSearchCondition(request) + } else { + IssueSearchCondition(q, Map[String, Int]()) + } + } else session.getAs[IssueSearchCondition](key).getOrElse(IssueSearchCondition())) + + filter match { + case "assigned" => condition.copy(assigned = Some(userName), author = None , mentioned = None) + case "mentioned" => condition.copy(assigned = None , author = None , mentioned = Some(userName)) + case _ => condition.copy(assigned = None , author = Some(userName), mentioned = None) + } + } + + private def searchIssues(filter: String) = { + import IssuesService._ + + val userName = context.loginAccount.get.userName + val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName) + val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name) + val page = IssueSearchCondition.page(request) + + html.issues( + searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*), + page, + countIssue(condition.copy(state = "open" ), false, userRepos: _*), + countIssue(condition.copy(state = "closed"), false, userRepos: _*), + filter match { + case "assigned" => condition.copy(assigned = Some(userName)) + case "mentioned" => condition.copy(mentioned = Some(userName)) + case _ => condition.copy(author = Some(userName)) + }, + filter, + getGroupNames(userName)) + } + + private def searchPullRequests(filter: String) = { + import IssuesService._ + import PullRequestService._ + + val userName = context.loginAccount.get.userName + val condition = getOrCreateCondition(Keys.Session.DashboardPulls, filter, userName) + val allRepos = getAllRepositories(userName) + val page = IssueSearchCondition.page(request) + + html.pulls( + searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*), + page, + countIssue(condition.copy(state = "open" ), true, allRepos: _*), + countIssue(condition.copy(state = "closed"), true, allRepos: _*), + filter match { + case "assigned" => condition.copy(assigned = Some(userName)) + case "mentioned" => condition.copy(mentioned = Some(userName)) + case _ => condition.copy(author = Some(userName)) + }, + filter, + getGroupNames(userName)) + } + + +} diff --git a/src/main/scala/gitbucket/core/controller/FileUploadController.scala b/src/main/scala/gitbucket/core/controller/FileUploadController.scala new file mode 100644 index 0000000..c3503ac --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/FileUploadController.scala @@ -0,0 +1,44 @@ +package gitbucket.core.controller + +import gitbucket.core.util.{Keys, FileUtil} +import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.Directory._ +import org.scalatra._ +import org.scalatra.servlet.{MultipartConfig, FileUploadSupport, FileItem} +import org.apache.commons.io.FileUtils + +/** + * Provides Ajax based file upload functionality. + * + * This servlet saves uploaded file. + */ +class FileUploadController extends ScalatraServlet with FileUploadSupport { + + configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024))) + + post("/image"){ + 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 + "." + FileUtil.getExtension(file.getName)), 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/gitbucket/core/controller/IndexController.scala b/src/main/scala/gitbucket/core/controller/IndexController.scala new file mode 100644 index 0000000..242d67d --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/IndexController.scala @@ -0,0 +1,109 @@ +package gitbucket.core.controller + +import gitbucket.core.html +import gitbucket.core.helper.xml +import gitbucket.core.model.Account +import gitbucket.core.service.{RepositoryService, ActivityService, AccountService} +import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator} +import gitbucket.core.util.Implicits._ +import jp.sf.amateras.scalatra.forms._ + +class IndexController extends IndexControllerBase + with RepositoryService with ActivityService with AccountService with UsersAuthenticator + +trait IndexControllerBase extends ControllerBase { + self: RepositoryService with ActivityService with AccountService with UsersAuthenticator => + + case class SignInForm(userName: String, password: String) + + val form = mapping( + "userName" -> trim(label("Username", text(required))), + "password" -> trim(label("Password", text(required))) + )(SignInForm.apply) + + get("/"){ + val loginAccount = context.loginAccount + if(loginAccount.isEmpty) { + html.index(getRecentActivities(), + getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true), + loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil) + ) + } else { + val loginUserName = loginAccount.get.userName + val loginUserGroups = getGroupsByUserName(loginUserName) + var visibleOwnerSet : Set[String] = Set(loginUserName) + + visibleOwnerSet ++= loginUserGroups + + html.index(getRecentActivitiesByOwners(visibleOwnerSet), + getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true), + loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil) + ) + } + } + + get("/signin"){ + val redirect = params.get("redirect") + if(redirect.isDefined && redirect.get.startsWith("/")){ + flash += Keys.Flash.Redirect -> redirect.get + } + html.signin() + } + + post("/signin", form){ form => + authenticate(context.settings, form.userName, form.password) match { + case Some(account) => signin(account) + case None => redirect("/signin") + } + } + + get("/signout"){ + session.invalidate + redirect("/") + } + + get("/activities.atom"){ + contentType = "application/atom+xml; type=feed" + xml.feed(getRecentActivities()) + } + + /** + * Set account information into HttpSession and redirect. + */ + private def signin(account: Account) = { + session.setAttribute(Keys.Session.LoginAccount, account) + updateLastLoginDate(account.userName) + + if(LDAPUtil.isDummyMailAddress(account)) { + redirect("/" + account.userName + "/_edit") + } + + flash.get(Keys.Flash.Redirect).asInstanceOf[Option[String]].map { redirectUrl => + if(redirectUrl.stripSuffix("/") == request.getContextPath){ + redirect("/") + } else { + redirect(redirectUrl) + } + }.getOrElse { + redirect("/") + } + } + + /** + * JSON API for collaborator completion. + */ + get("/_user/proposals")(usersOnly { + contentType = formats("json") + org.json4s.jackson.Serialization.write( + Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray) + ) + }) + + /** + * JSON APU for checking user existence. + */ + post("/_user/existence")(usersOnly { + getAccountByUserName(params("userName")).isDefined + }) + +} diff --git a/src/main/scala/gitbucket/core/controller/IssuesController.scala b/src/main/scala/gitbucket/core/controller/IssuesController.scala new file mode 100644 index 0000000..da67d04 --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/IssuesController.scala @@ -0,0 +1,423 @@ +package gitbucket.core.controller + +import gitbucket.core.issues.html +import gitbucket.core.model.Issue +import gitbucket.core.service._ +import gitbucket.core.util._ +import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.Implicits._ +import gitbucket.core.view +import gitbucket.core.view.Markdown +import jp.sf.amateras.scalatra.forms._ + +import IssuesService._ +import org.scalatra.Ok + +class IssuesController extends IssuesControllerBase + with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService + with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator + +trait IssuesControllerBase extends ControllerBase { + self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService + with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator => + + case class IssueCreateForm(title: String, content: Option[String], + assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String]) + case class CommentForm(issueId: Int, content: String) + case class IssueStateForm(issueId: Int, content: Option[String]) + + val issueCreateForm = mapping( + "title" -> trim(label("Title", text(required))), + "content" -> trim(optional(text())), + "assignedUserName" -> trim(optional(text())), + "milestoneId" -> trim(optional(number())), + "labelNames" -> trim(optional(text())) + )(IssueCreateForm.apply) + + val issueTitleEditForm = mapping( + "title" -> trim(label("Title", text(required))) + )(x => x) + val issueEditForm = mapping( + "content" -> trim(optional(text())) + )(x => x) + + val commentForm = mapping( + "issueId" -> label("Issue Id", number()), + "content" -> trim(label("Comment", text(required))) + )(CommentForm.apply) + + val issueStateForm = mapping( + "issueId" -> label("Issue Id", number()), + "content" -> trim(optional(text())) + )(IssueStateForm.apply) + + get("/:owner/:repository/issues")(referrersOnly { repository => + val q = request.getParameter("q") + if(Option(q).exists(_.contains("is:pr"))){ + redirect(s"/${repository.owner}/${repository.name}/pulls?q=" + StringUtil.urlEncode(q)) + } else { + searchIssues(repository) + } + }) + + get("/:owner/:repository/issues/:id")(referrersOnly { repository => + defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) => + getIssue(owner, name, issueId) map { + html.issue( + _, + getComments(owner, name, issueId.toInt), + getIssueLabels(owner, name, issueId.toInt), + (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, + getMilestonesWithIssueCount(owner, name), + getLabels(owner, name), + hasWritePermission(owner, name, context.loginAccount), + repository) + } getOrElse NotFound + } + }) + + get("/:owner/:repository/issues/new")(readableUsersOnly { repository => + defining(repository.owner, repository.name){ case (owner, name) => + html.create( + (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, + getMilestones(owner, name), + getLabels(owner, name), + hasWritePermission(owner, name, context.loginAccount), + repository) + } + }) + + post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) => + defining(repository.owner, repository.name){ case (owner, name) => + val writable = hasWritePermission(owner, name, context.loginAccount) + val userName = context.loginAccount.get.userName + + // insert issue + val issueId = createIssue(owner, name, userName, form.title, form.content, + if(writable) form.assignedUserName else None, + if(writable) form.milestoneId else None) + + // insert labels + if(writable){ + form.labelNames.map { value => + val labels = getLabels(owner, name) + value.split(",").foreach { labelName => + labels.find(_.labelName == labelName).map { label => + registerIssueLabel(owner, name, issueId, label.labelId) + } + } + } + } + + // record activity + recordCreateIssueActivity(owner, name, userName, issueId, form.title) + + // extract references and create refer comment + getIssue(owner, name, issueId.toString).foreach { issue => + createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) + } + + // notifications + Notifier().toNotify(repository, issueId, form.content.getOrElse("")){ + Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") + } + + redirect(s"/${owner}/${name}/issues/${issueId}") + } + }) + + ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) => + defining(repository.owner, repository.name){ case (owner, name) => + getIssue(owner, name, params("id")).map { issue => + if(isEditable(owner, name, issue.openedUserName)){ + // update issue + updateIssue(owner, name, issue.issueId, title, issue.content) + // extract references and create refer comment + createReferComment(owner, name, issue.copy(title = title), title) + + redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") + } else Unauthorized + } getOrElse NotFound + } + }) + + ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) => + defining(repository.owner, repository.name){ case (owner, name) => + getIssue(owner, name, params("id")).map { issue => + if(isEditable(owner, name, issue.openedUserName)){ + // update issue + updateIssue(owner, name, issue.issueId, issue.title, content) + // extract references and create refer comment + createReferComment(owner, name, issue, content.getOrElse("")) + + redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") + } else Unauthorized + } getOrElse NotFound + } + }) + + post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => + handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) => + redirect(s"/${repository.owner}/${repository.name}/${ + if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") + } getOrElse NotFound + }) + + post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) => + handleComment(form.issueId, form.content, repository)() map { case (issue, id) => + redirect(s"/${repository.owner}/${repository.name}/${ + if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") + } getOrElse NotFound + }) + + ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => + defining(repository.owner, repository.name){ case (owner, name) => + getComment(owner, name, params("id")).map { comment => + if(isEditable(owner, name, comment.commentedUserName)){ + updateComment(comment.commentId, form.content) + redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}") + } else Unauthorized + } getOrElse NotFound + } + }) + + ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository => + defining(repository.owner, repository.name){ case (owner, name) => + getComment(owner, name, params("id")).map { comment => + if(isEditable(owner, name, comment.commentedUserName)){ + Ok(deleteComment(comment.commentId)) + } else Unauthorized + } getOrElse NotFound + } + }) + + ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository => + getIssue(repository.owner, repository.name, params("id")) map { x => + if(isEditable(x.userName, x.repositoryName, x.openedUserName)){ + params.get("dataType") collect { + case t if t == "html" => html.editissue( + x.content, x.issueId, x.userName, x.repositoryName) + } getOrElse { + contentType = formats("json") + org.json4s.jackson.Serialization.write( + Map("title" -> x.title, + "content" -> Markdown.toHtml(x.content getOrElse "No description given.", + repository, false, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName)) + )) + } + } else Unauthorized + } getOrElse NotFound + }) + + ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository => + getComment(repository.owner, repository.name, params("id")) map { x => + if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){ + params.get("dataType") collect { + case t if t == "html" => html.editcomment( + x.content, x.commentId, x.userName, x.repositoryName) + } getOrElse { + contentType = formats("json") + org.json4s.jackson.Serialization.write( + Map("content" -> view.Markdown.toHtml(x.content, + repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName)) + )) + } + } else Unauthorized + } getOrElse NotFound + }) + + ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository => + defining(params("id").toInt){ issueId => + registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) + html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) + } + }) + + ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository => + defining(params("id").toInt){ issueId => + deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) + html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) + } + }) + + ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository => + updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName")) + Ok("updated") + }) + + ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository => + updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId")) + milestoneId("milestoneId").map { milestoneId => + getMilestonesWithIssueCount(repository.owner, repository.name) + .find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) => + gitbucket.core.issues.milestones.html.progress(openCount + closeCount, closeCount) + } getOrElse NotFound + } getOrElse Ok() + }) + + post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => + defining(params.get("value")){ action => + action match { + case Some("open") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("reopen")) } + case Some("close") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("close")) } + case _ => // TODO BadRequest + } + } + }) + + post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository => + params("value").toIntOpt.map{ labelId => + executeBatch(repository) { issueId => + getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse { + registerIssueLabel(repository.owner, repository.name, issueId, labelId) + } + } + } getOrElse NotFound + }) + + post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository => + defining(assignedUserName("value")){ value => + executeBatch(repository) { + updateAssignedUserName(repository.owner, repository.name, _, value) + } + } + }) + + post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository => + defining(milestoneId("value")){ value => + executeBatch(repository) { + updateMilestoneId(repository.owner, repository.name, _, value) + } + } + }) + + get("/:owner/:repository/_attached/:file")(referrersOnly { repository => + (Directory.getAttachedDir(repository.owner, repository.name) match { + case dir if(dir.exists && dir.isDirectory) => + dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file => + RawData(FileUtil.getMimeType(file.getName), file) + } + case _ => None + }) getOrElse NotFound + }) + + val assignedUserName = (key: String) => params.get(key) filter (_.trim != "") + val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt) + + private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean = + hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName + + private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = { + params("checked").split(',') map(_.toInt) foreach execute + params("from") match { + case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues") + case "pulls" => redirect(s"/${repository.owner}/${repository.name}/pulls") + } + } + + private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = { + StringUtil.extractIssueId(message).foreach { issueId => + if(getIssue(owner, repository, issueId).isDefined){ + createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, + fromIssue.issueId + ":" + fromIssue.title, "refer") + } + } + } + + /** + * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]] + */ + private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo) + (getAction: Issue => Option[String] = + p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = { + + defining(repository.owner, repository.name){ case (owner, name) => + val userName = context.loginAccount.get.userName + + getIssue(owner, name, issueId.toString) flatMap { issue => + val (action, recordActivity) = + getAction(issue) + .collect { + case "close" if(!issue.closed) => true -> + (Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _)) + case "reopen" if(issue.closed) => false -> + (Some("reopen") -> Some(recordReopenIssueActivity _)) + } + .map { case (closed, t) => + updateClosed(owner, name, issueId, closed) + t + } + .getOrElse(None -> None) + + val commentId = (content, action) match { + case (None, None) => None + case (None, Some(action)) => Some(createComment(owner, name, userName, issueId, action.capitalize, action)) + case (Some(content), _) => Some(createComment(owner, name, userName, issueId, content, action.map(_+ "_comment").getOrElse("comment"))) + } + + // record comment activity if comment is entered + content foreach { + (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _) + (owner, name, userName, issueId, _) + } + recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) ) + + // extract references and create refer comment + content.map { content => + createReferComment(owner, name, issue, content) + } + + // notifications + Notifier() match { + case f => + content foreach { + f.toNotify(repository, issueId, _){ + Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${ + if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId.get}") + } + } + action foreach { + f.toNotify(repository, issueId, _){ + Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") + } + } + } + + commentId.map( issue -> _ ) + } + } + } + + private def searchIssues(repository: RepositoryService.RepositoryInfo) = { + defining(repository.owner, repository.name){ case (owner, repoName) => + val page = IssueSearchCondition.page(request) + val sessionKey = Keys.Session.Issues(owner, repoName) + + // retrieve search condition + val condition = session.putAndGet(sessionKey, + if(request.hasQueryString){ + val q = request.getParameter("q") + if(q == null || q.trim.isEmpty){ + IssueSearchCondition(request) + } else { + IssueSearchCondition(q, getMilestones(owner, repoName).map(x => (x.title, x.milestoneId)).toMap) + } + } else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) + ) + + html.list( + "issues", + searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), + page, + (getCollaborators(owner, repoName) :+ owner).sorted, + getMilestones(owner, repoName), + getLabels(owner, repoName), + countIssue(condition.copy(state = "open" ), false, owner -> repoName), + countIssue(condition.copy(state = "closed"), false, owner -> repoName), + condition, + repository, + hasWritePermission(owner, repoName, context.loginAccount)) + } + } + +} diff --git a/src/main/scala/gitbucket/core/controller/LabelsController.scala b/src/main/scala/gitbucket/core/controller/LabelsController.scala new file mode 100644 index 0000000..fa6ef7a --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/LabelsController.scala @@ -0,0 +1,83 @@ +package gitbucket.core.controller + +import gitbucket.core.issues.labels.html +import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService} +import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator} +import gitbucket.core.util.Implicits._ +import jp.sf.amateras.scalatra.forms._ +import org.scalatra.i18n.Messages +import org.scalatra.Ok + +class LabelsController extends LabelsControllerBase + with LabelsService with IssuesService with RepositoryService with AccountService +with ReferrerAuthenticator with CollaboratorsAuthenticator + +trait LabelsControllerBase extends ControllerBase { + self: LabelsService with IssuesService with RepositoryService + with ReferrerAuthenticator with CollaboratorsAuthenticator => + + case class LabelForm(labelName: String, color: String) + + val labelForm = mapping( + "labelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))), + "labelColor" -> trim(label("Color", text(required, color))) + )(LabelForm.apply) + + get("/:owner/:repository/issues/labels")(referrersOnly { repository => + html.list( + getLabels(repository.owner, repository.name), + countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), + repository, + hasWritePermission(repository.owner, repository.name, context.loginAccount)) + }) + + ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository => + html.edit(None, repository) + }) + + ajaxPost("/:owner/:repository/issues/labels/new", labelForm)(collaboratorsOnly { (form, repository) => + val labelId = createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1)) + html.label( + getLabel(repository.owner, repository.name, labelId).get, + // TODO futility + countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), + repository, + hasWritePermission(repository.owner, repository.name, context.loginAccount)) + }) + + ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository => + getLabel(repository.owner, repository.name, params("labelId").toInt).map { label => + html.edit(Some(label), repository) + } getOrElse NotFound() + }) + + ajaxPost("/:owner/:repository/issues/labels/:labelId/edit", labelForm)(collaboratorsOnly { (form, repository) => + updateLabel(repository.owner, repository.name, params("labelId").toInt, form.labelName, form.color.substring(1)) + html.label( + getLabel(repository.owner, repository.name, params("labelId").toInt).get, + // TODO futility + countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), + repository, + hasWritePermission(repository.owner, repository.name, context.loginAccount)) + }) + + ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository => + deleteLabel(repository.owner, repository.name, params("labelId").toInt) + Ok() + }) + + /** + * Constraint for the identifier such as user name, repository name or page name. + */ + private def labelName: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = + if(value.contains(',')){ + Some(s"${name} contains invalid character.") + } else if(value.startsWith("_") || value.startsWith("-")){ + Some(s"${name} starts with invalid character.") + } else { + None + } + } + +} diff --git a/src/main/scala/gitbucket/core/controller/MilestonesController.scala b/src/main/scala/gitbucket/core/controller/MilestonesController.scala new file mode 100644 index 0000000..eb4b714 --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/MilestonesController.scala @@ -0,0 +1,84 @@ +package gitbucket.core.controller + +import gitbucket.core.issues.milestones.html +import gitbucket.core.service.{RepositoryService, MilestonesService, AccountService} +import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator} +import gitbucket.core.util.Implicits._ +import jp.sf.amateras.scalatra.forms._ + +class MilestonesController extends MilestonesControllerBase + with MilestonesService with RepositoryService with AccountService + with ReferrerAuthenticator with CollaboratorsAuthenticator + +trait MilestonesControllerBase extends ControllerBase { + self: MilestonesService with RepositoryService + with ReferrerAuthenticator with CollaboratorsAuthenticator => + + case class MilestoneForm(title: String, description: Option[String], dueDate: Option[java.util.Date]) + + val milestoneForm = mapping( + "title" -> trim(label("Title", text(required, maxlength(100)))), + "description" -> trim(label("Description", optional(text()))), + "dueDate" -> trim(label("Due Date", optional(date()))) + )(MilestoneForm.apply) + + get("/:owner/:repository/issues/milestones")(referrersOnly { repository => + html.list( + params.getOrElse("state", "open"), + getMilestonesWithIssueCount(repository.owner, repository.name), + repository, + hasWritePermission(repository.owner, repository.name, context.loginAccount)) + }) + + get("/:owner/:repository/issues/milestones/new")(collaboratorsOnly { + html.edit(None, _) + }) + + post("/:owner/:repository/issues/milestones/new", milestoneForm)(collaboratorsOnly { (form, repository) => + createMilestone(repository.owner, repository.name, form.title, form.description, form.dueDate) + redirect(s"/${repository.owner}/${repository.name}/issues/milestones") + }) + + get("/:owner/:repository/issues/milestones/:milestoneId/edit")(collaboratorsOnly { repository => + params("milestoneId").toIntOpt.map{ milestoneId => + html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository) + } getOrElse NotFound + }) + + post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(collaboratorsOnly { (form, repository) => + params("milestoneId").toIntOpt.flatMap{ milestoneId => + getMilestone(repository.owner, repository.name, milestoneId).map { milestone => + updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate)) + redirect(s"/${repository.owner}/${repository.name}/issues/milestones") + } + } getOrElse NotFound + }) + + get("/:owner/:repository/issues/milestones/:milestoneId/close")(collaboratorsOnly { repository => + params("milestoneId").toIntOpt.flatMap{ milestoneId => + getMilestone(repository.owner, repository.name, milestoneId).map { milestone => + closeMilestone(milestone) + redirect(s"/${repository.owner}/${repository.name}/issues/milestones") + } + } getOrElse NotFound + }) + + get("/:owner/:repository/issues/milestones/:milestoneId/open")(collaboratorsOnly { repository => + params("milestoneId").toIntOpt.flatMap{ milestoneId => + getMilestone(repository.owner, repository.name, milestoneId).map { milestone => + openMilestone(milestone) + redirect(s"/${repository.owner}/${repository.name}/issues/milestones") + } + } getOrElse NotFound + }) + + get("/:owner/:repository/issues/milestones/:milestoneId/delete")(collaboratorsOnly { repository => + params("milestoneId").toIntOpt.flatMap{ milestoneId => + getMilestone(repository.owner, repository.name, milestoneId).map { milestone => + deleteMilestone(repository.owner, repository.name, milestone.milestoneId) + redirect(s"/${repository.owner}/${repository.name}/issues/milestones") + } + } getOrElse NotFound + }) + +} diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala new file mode 100644 index 0000000..a7200ca --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala @@ -0,0 +1,488 @@ +package gitbucket.core.controller + +import gitbucket.core.pulls.html +import gitbucket.core.util._ +import gitbucket.core.util.JGitUtil._ +import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.Implicits._ +import gitbucket.core.util.Directory._ +import gitbucket.core.view +import gitbucket.core.view.helpers +import org.eclipse.jgit.api.Git +import jp.sf.amateras.scalatra.forms._ +import org.eclipse.jgit.transport.RefSpec +import scala.collection.JavaConverters._ +import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent} +import gitbucket.core.service._ +import gitbucket.core.service.IssuesService._ +import gitbucket.core.service.PullRequestService._ +import gitbucket.core.service.WebHookService.WebHookPayload +import org.slf4j.LoggerFactory +import org.eclipse.jgit.merge.MergeStrategy +import org.eclipse.jgit.errors.NoMergeBaseException + + +class PullRequestsController extends PullRequestsControllerBase + with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService + with CommitsService with ActivityService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator + +trait PullRequestsControllerBase extends ControllerBase { + self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService + with CommitsService with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator => + + private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase]) + + val pullRequestForm = mapping( + "title" -> trim(label("Title" , text(required, maxlength(100)))), + "content" -> trim(label("Content", optional(text()))), + "targetUserName" -> trim(text(required, maxlength(100))), + "targetBranch" -> trim(text(required, maxlength(100))), + "requestUserName" -> trim(text(required, maxlength(100))), + "requestRepositoryName" -> trim(text(required, maxlength(100))), + "requestBranch" -> trim(text(required, maxlength(100))), + "commitIdFrom" -> trim(text(required, maxlength(40))), + "commitIdTo" -> trim(text(required, maxlength(40))) + )(PullRequestForm.apply) + + val mergeForm = mapping( + "message" -> trim(label("Message", text(required))) + )(MergeForm.apply) + + case class PullRequestForm( + title: String, + content: Option[String], + targetUserName: String, + targetBranch: String, + requestUserName: String, + requestRepositoryName: String, + requestBranch: String, + commitIdFrom: String, + commitIdTo: String) + + case class MergeForm(message: String) + + get("/:owner/:repository/pulls")(referrersOnly { repository => + val q = request.getParameter("q") + if(Option(q).exists(_.contains("is:issue"))){ + redirect(s"/${repository.owner}/${repository.name}/issues?q=" + StringUtil.urlEncode(q)) + } else { + searchPullRequests(None, repository) + } + }) + + get("/:owner/:repository/pull/:id")(referrersOnly { repository => + params("id").toIntOpt.flatMap{ issueId => + val owner = repository.owner + val name = repository.name + getPullRequest(owner, name, issueId) map { case(issue, pullreq) => + using(Git.open(getRepositoryDir(owner, name))){ git => + val (commits, diffs) = + getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo) + + html.pullreq( + issue, pullreq, + (commits.flatten.map(commit => getCommitComments(owner, name, commit.id, true)).flatten.toList ::: getComments(owner, name, issueId)) + .sortWith((a, b) => a.registeredDate before b.registeredDate), + getIssueLabels(owner, name, issueId), + (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, + getMilestonesWithIssueCount(owner, name), + getLabels(owner, name), + commits, + diffs, + hasWritePermission(owner, name, context.loginAccount), + repository) + } + } + } getOrElse NotFound + }) + + ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository => + params("id").toIntOpt.flatMap{ issueId => + val owner = repository.owner + val name = repository.name + getPullRequest(owner, name, issueId) map { case(issue, pullreq) => + html.mergeguide( + checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId), + pullreq, + s"${context.baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git") + } + } getOrElse NotFound + }) + + get("/:owner/:repository/pull/:id/delete/*")(collaboratorsOnly { repository => + params("id").toIntOpt.map { issueId => + val branchName = multiParams("splat").head + val userName = context.loginAccount.get.userName + if(repository.repository.defaultBranch != branchName){ + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + git.branchDelete().setForce(true).setBranchNames(branchName).call() + recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName) + } + } + createComment(repository.owner, repository.name, userName, issueId, branchName, "delete_branch") + redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") + } getOrElse NotFound + }) + + post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) => + params("id").toIntOpt.flatMap { issueId => + val owner = repository.owner + val name = repository.name + LockUtil.lock(s"${owner}/${name}"){ + getPullRequest(owner, name, issueId).map { case (issue, pullreq) => + using(Git.open(getRepositoryDir(owner, name))) { git => + // mark issue as merged and close. + val loginAccount = context.loginAccount.get + createComment(owner, name, loginAccount.userName, issueId, form.message, "merge") + createComment(owner, name, loginAccount.userName, issueId, "Close", "close") + updateClosed(owner, name, issueId, true) + + // record activity + recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message) + + // merge + val mergeBaseRefName = s"refs/heads/${pullreq.branch}" + val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true) + val mergeBaseTip = git.getRepository.resolve(mergeBaseRefName) + val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head") + val conflicted = try { + !merger.merge(mergeBaseTip, mergeTip) + } catch { + case e: NoMergeBaseException => true + } + if (conflicted) { + throw new RuntimeException("This pull request can't merge automatically.") + } + + // creates merge commit + val mergeCommit = new CommitBuilder() + mergeCommit.setTreeId(merger.getResultTreeId) + mergeCommit.setParentIds(Array[ObjectId](mergeBaseTip, mergeTip): _*) + val personIdent = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress) + mergeCommit.setAuthor(personIdent) + mergeCommit.setCommitter(personIdent) + mergeCommit.setMessage(s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" + + form.message) + + // insertObject and got mergeCommit Object Id + val inserter = git.getRepository.newObjectInserter + val mergeCommitId = inserter.insert(mergeCommit) + inserter.flush() + inserter.release() + + // update refs + val refUpdate = git.getRepository.updateRef(mergeBaseRefName) + refUpdate.setNewObjectId(mergeCommitId) + refUpdate.setForceUpdate(false) + refUpdate.setRefLogIdent(personIdent) + refUpdate.setRefLogMessage("merged", true) + refUpdate.update() + + val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom, + pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo) + + // close issue by content of pull request + 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) + } + issue.content match { + case Some(content) => closeIssuesFromMessage(content, loginAccount.userName, owner, name) + case _ => + } + closeIssuesFromMessage(form.message, loginAccount.userName, owner, name) + } + // call web hook + getWebHookURLs(owner, name) match { + case webHookURLs if(webHookURLs.nonEmpty) => + for(ownerAccount <- getAccountByUserName(owner)){ + callWebHook(owner, name, webHookURLs, + WebHookPayload(git, loginAccount, mergeBaseRefName, repository, commits.flatten.toList, ownerAccount)) + } + case _ => + } + + // notifications + Notifier().toNotify(repository, issueId, "merge"){ + Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}") + } + + redirect(s"/${owner}/${name}/pull/${issueId}") + } + } + } + } getOrElse NotFound + }) + + get("/:owner/:repository/compare")(referrersOnly { forkedRepository => + (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match { + case (Some(originUserName), Some(originRepositoryName)) => { + getRepository(originUserName, originRepositoryName, context.baseUrl).map { originRepository => + using( + Git.open(getRepositoryDir(originUserName, originRepositoryName)), + Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name)) + ){ (oldGit, newGit) => + val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2 + val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2 + + redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}") + } + } getOrElse NotFound + } + case _ => { + using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git => + JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) => + redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}") + } getOrElse { + redirect(s"/${forkedRepository.owner}/${forkedRepository.name}") + } + } + } + } + }) + + get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository => + val Seq(origin, forked) = multiParams("splat") + val (originOwner, originId) = parseCompareIdentifie(origin, forkedRepository.owner) + val (forkedOwner, forkedId) = parseCompareIdentifie(forked, forkedRepository.owner) + + (for( + originRepositoryName <- if(originOwner == forkedOwner){ + Some(forkedRepository.name) + } else { + forkedRepository.repository.originRepositoryName.orElse { + getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2) + } + }; + originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl) + ) yield { + using( + Git.open(getRepositoryDir(originRepository.owner, originRepository.name)), + Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name)) + ){ case (oldGit, newGit) => + val (oldId, newId) = + if(originRepository.branchList.contains(originId) && forkedRepository.branchList.contains(forkedId)){ + // Branch name + val rootId = JGitUtil.getForkedCommitId(oldGit, newGit, + originRepository.owner, originRepository.name, originId, + forkedRepository.owner, forkedRepository.name, forkedId) + + (oldGit.getRepository.resolve(rootId), newGit.getRepository.resolve(forkedId)) + } else { + // Commit id + (oldGit.getRepository.resolve(originId), newGit.getRepository.resolve(forkedId)) + } + + val (commits, diffs) = getRequestCompareInfo( + originRepository.owner, originRepository.name, oldId.getName, + forkedRepository.owner, forkedRepository.name, newId.getName) + + html.compare( + commits, + diffs, + (forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match { + case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName) + case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name) + }, + commits.flatten.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)).flatten.toList, + originId, + forkedId, + oldId.getName, + newId.getName, + forkedRepository, + originRepository, + forkedRepository, + hasWritePermission(forkedRepository.owner, forkedRepository.name, context.loginAccount)) + } + }) getOrElse NotFound + }) + + ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { forkedRepository => + val Seq(origin, forked) = multiParams("splat") + val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner) + val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner) + + (for( + originRepositoryName <- if(originOwner == forkedOwner){ + Some(forkedRepository.name) + } else { + forkedRepository.repository.originRepositoryName.orElse { + getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2) + } + }; + originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl) + ) yield { + using( + Git.open(getRepositoryDir(originRepository.owner, originRepository.name)), + Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name)) + ){ case (oldGit, newGit) => + val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2 + val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2 + + html.mergecheck( + checkConflict(originRepository.owner, originRepository.name, originBranch, + forkedRepository.owner, forkedRepository.name, forkedBranch)) + } + }) getOrElse NotFound + }) + + post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) => + val loginUserName = context.loginAccount.get.userName + + val issueId = createIssue( + owner = repository.owner, + repository = repository.name, + loginUser = loginUserName, + title = form.title, + content = form.content, + assignedUserName = None, + milestoneId = None, + isPullRequest = true) + + createPullRequest( + originUserName = repository.owner, + originRepositoryName = repository.name, + issueId = issueId, + originBranch = form.targetBranch, + requestUserName = form.requestUserName, + requestRepositoryName = form.requestRepositoryName, + requestBranch = form.requestBranch, + commitIdFrom = form.commitIdFrom, + commitIdTo = form.commitIdTo) + + // fetch requested branch + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + git.fetch + .setRemote(getRepositoryDir(form.requestUserName, form.requestRepositoryName).toURI.toString) + .setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head")) + .call + } + + // record activity + recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title) + + // notifications + Notifier().toNotify(repository, issueId, form.content.getOrElse("")){ + Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}") + } + + redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") + }) + + /** + * Checks whether conflict will be caused in merging. Returns true if conflict will be caused. + */ + private def checkConflict(userName: String, repositoryName: String, branch: String, + requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = { + LockUtil.lock(s"${userName}/${repositoryName}"){ + using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git => + val remoteRefName = s"refs/heads/${branch}" + val tmpRefName = s"refs/merge-check/${userName}/${branch}" + val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true) + try { + // fetch objects from origin repository branch + git.fetch + .setRemote(getRepositoryDir(userName, repositoryName).toURI.toString) + .setRefSpecs(refSpec) + .call + + // merge conflict check + val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true) + val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${requestBranch}") + val mergeTip = git.getRepository.resolve(tmpRefName) + try { + !merger.merge(mergeBaseTip, mergeTip) + } catch { + case e: NoMergeBaseException => true + } + } finally { + val refUpdate = git.getRepository.updateRef(refSpec.getDestination) + refUpdate.setForceUpdate(true) + refUpdate.delete() + } + } + } + } + + /** + * Checks whether conflict will be caused in merging within pull request. Returns true if conflict will be caused. + */ + private def checkConflictInPullRequest(userName: String, repositoryName: String, branch: String, + requestUserName: String, requestRepositoryName: String, requestBranch: String, + issueId: Int): Boolean = { + LockUtil.lock(s"${userName}/${repositoryName}") { + using(Git.open(getRepositoryDir(userName, repositoryName))) { git => + // merge + val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true) + val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${branch}") + val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head") + try { + !merger.merge(mergeBaseTip, mergeTip) + } catch { + case e: NoMergeBaseException => true + } + } + } + } + + /** + * Parses branch identifier and extracts owner and branch name as tuple. + * + * - "owner:branch" to ("owner", "branch") + * - "branch" to ("defaultOwner", "branch") + */ + private def parseCompareIdentifie(value: String, defaultOwner: String): (String, String) = + if(value.contains(':')){ + val array = value.split(":") + (array(0), array(1)) + } else { + (defaultOwner, value) + } + + private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String, + requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = + using( + Git.open(getRepositoryDir(userName, repositoryName)), + Git.open(getRepositoryDir(requestUserName, requestRepositoryName)) + ){ (oldGit, newGit) => + val oldId = oldGit.getRepository.resolve(branch) + val newId = newGit.getRepository.resolve(requestCommitId) + + val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit => + new CommitInfo(revCommit) + }.toList.splitWith { (commit1, commit2) => + helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime) + } + + val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true) + + (commits, diffs) + } + + private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) = + defining(repository.owner, repository.name){ case (owner, repoName) => + val page = IssueSearchCondition.page(request) + val sessionKey = Keys.Session.Pulls(owner, repoName) + + // retrieve search condition + val condition = session.putAndGet(sessionKey, + if(request.hasQueryString) IssueSearchCondition(request) + else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) + ) + + gitbucket.core.issues.html.list( + "pulls", + searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), + page, + (getCollaborators(owner, repoName) :+ owner).sorted, + getMilestones(owner, repoName), + getLabels(owner, repoName), + countIssue(condition.copy(state = "open" ), true, owner -> repoName), + countIssue(condition.copy(state = "closed"), true, owner -> repoName), + condition, + repository, + hasWritePermission(owner, repoName, context.loginAccount)) + } + +} diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala new file mode 100644 index 0000000..d6135df --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -0,0 +1,276 @@ +package gitbucket.core.controller + +import gitbucket.core.settings.html +import gitbucket.core.model.WebHook +import gitbucket.core.service.{RepositoryService, AccountService, WebHookService} +import gitbucket.core.service.WebHookService.WebHookPayload +import gitbucket.core.util._ +import gitbucket.core.util.JGitUtil._ +import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.Implicits._ +import gitbucket.core.util.Directory._ +import jp.sf.amateras.scalatra.forms._ +import org.apache.commons.io.FileUtils +import org.scalatra.i18n.Messages +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.Constants + +class RepositorySettingsController extends RepositorySettingsControllerBase + with RepositoryService with AccountService with WebHookService + with OwnerAuthenticator with UsersAuthenticator + +trait RepositorySettingsControllerBase extends ControllerBase { + self: RepositoryService with AccountService with WebHookService + with OwnerAuthenticator with UsersAuthenticator => + + // for repository options + case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean) + + val optionsForm = mapping( + "repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))), + "description" -> trim(label("Description" , optional(text()))), + "defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))), + "isPrivate" -> trim(label("Repository Type", boolean())) + )(OptionsForm.apply) + + // for collaborator addition + case class CollaboratorForm(userName: String) + + val collaboratorForm = mapping( + "userName" -> trim(label("Username", text(required, collaborator))) + )(CollaboratorForm.apply) + + // for web hook url addition + case class WebHookForm(url: String) + + val webHookForm = mapping( + "url" -> trim(label("url", text(required, webHook))) + )(WebHookForm.apply) + + // for transfer ownership + case class TransferOwnerShipForm(newOwner: String) + + val transferForm = mapping( + "newOwner" -> trim(label("New owner", text(required, transferUser))) + )(TransferOwnerShipForm.apply) + + /** + * Redirect to the Options page. + */ + get("/:owner/:repository/settings")(ownerOnly { repository => + redirect(s"/${repository.owner}/${repository.name}/settings/options") + }) + + /** + * Display the Options page. + */ + get("/:owner/:repository/settings/options")(ownerOnly { + html.options(_, flash.get("info")) + }) + + /** + * Save the repository options. + */ + post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) => + val defaultBranch = if(repository.branchList.isEmpty) "master" else form.defaultBranch + saveRepositoryOptions( + repository.owner, + repository.name, + form.description, + defaultBranch, + repository.repository.parentUserName.map { _ => + repository.repository.isPrivate + } getOrElse form.isPrivate + ) + // Change repository name + if(repository.name != form.repositoryName){ + // Update database + renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName) + // Move git repository + defining(getRepositoryDir(repository.owner, repository.name)){ dir => + FileUtils.moveDirectory(dir, getRepositoryDir(repository.owner, form.repositoryName)) + } + // Move wiki repository + defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir => + FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName)) + } + } + // Change repository HEAD + using(Git.open(getRepositoryDir(repository.owner, form.repositoryName))) { git => + git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + defaultBranch) + } + flash += "info" -> "Repository settings has been updated." + redirect(s"/${repository.owner}/${form.repositoryName}/settings/options") + }) + + /** + * Display the Collaborators page. + */ + get("/:owner/:repository/settings/collaborators")(ownerOnly { repository => + html.collaborators( + getCollaborators(repository.owner, repository.name), + getAccountByUserName(repository.owner).get.isGroupAccount, + repository) + }) + + /** + * Add the collaborator. + */ + post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) => + if(!getAccountByUserName(repository.owner).get.isGroupAccount){ + addCollaborator(repository.owner, repository.name, form.userName) + } + redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") + }) + + /** + * Add the collaborator. + */ + get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository => + if(!getAccountByUserName(repository.owner).get.isGroupAccount){ + removeCollaborator(repository.owner, repository.name, params("name")) + } + redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") + }) + + /** + * Display the web hook page. + */ + get("/:owner/:repository/settings/hooks")(ownerOnly { repository => + html.hooks(getWebHookURLs(repository.owner, repository.name), flash.get("url"), repository, flash.get("info")) + }) + + /** + * Add the web hook URL. + */ + post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) => + addWebHookURL(repository.owner, repository.name, form.url) + redirect(s"/${repository.owner}/${repository.name}/settings/hooks") + }) + + /** + * Delete the web hook URL. + */ + get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository => + deleteWebHookURL(repository.owner, repository.name, params("url")) + redirect(s"/${repository.owner}/${repository.name}/settings/hooks") + }) + + /** + * Send the test request to registered web hook URLs. + */ + post("/:owner/:repository/settings/hooks/test", webHookForm)(ownerOnly { (form, repository) => + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + import scala.collection.JavaConverters._ + val commits = git.log + .add(git.getRepository.resolve(repository.repository.defaultBranch)) + .setMaxCount(3) + .call.iterator.asScala.map(new CommitInfo(_)) + + getAccountByUserName(repository.owner).foreach { ownerAccount => + callWebHook(repository.owner, repository.name, + List(WebHook(repository.owner, repository.name, form.url)), + WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount) + ) + } + flash += "url" -> form.url + flash += "info" -> "Test payload deployed!" + } + redirect(s"/${repository.owner}/${repository.name}/settings/hooks") + }) + + /** + * Display the danger zone. + */ + get("/:owner/:repository/settings/danger")(ownerOnly { + html.danger(_) + }) + + /** + * Transfer repository ownership. + */ + post("/:owner/:repository/settings/transfer", transferForm)(ownerOnly { (form, repository) => + // Change repository owner + if(repository.owner != form.newOwner){ + LockUtil.lock(s"${repository.owner}/${repository.name}"){ + // Update database + renameRepository(repository.owner, repository.name, form.newOwner, repository.name) + // Move git repository + defining(getRepositoryDir(repository.owner, repository.name)){ dir => + FileUtils.moveDirectory(dir, getRepositoryDir(form.newOwner, repository.name)) + } + // Move wiki repository + defining(getWikiRepositoryDir(repository.owner, repository.name)){ dir => + FileUtils.moveDirectory(dir, getWikiRepositoryDir(form.newOwner, repository.name)) + } + } + } + redirect(s"/${form.newOwner}/${repository.name}") + }) + + /** + * Delete the repository. + */ + post("/:owner/:repository/settings/delete")(ownerOnly { repository => + LockUtil.lock(s"${repository.owner}/${repository.name}"){ + deleteRepository(repository.owner, repository.name) + + FileUtils.deleteDirectory(getRepositoryDir(repository.owner, repository.name)) + FileUtils.deleteDirectory(getWikiRepositoryDir(repository.owner, repository.name)) + FileUtils.deleteDirectory(getTemporaryDir(repository.owner, repository.name)) + } + redirect(s"/${repository.owner}") + }) + + /** + * Provides duplication check for web hook url. + */ + private def webHook: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = + getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.") + } + + /** + * Provides Constraint to validate the collaborator name. + */ + private def collaborator: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = + getAccountByUserName(value) match { + case None => Some("User does not exist.") + case Some(x) if(x.isGroupAccount) + => Some("User does not exist.") + case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName)) + => Some("User can access this repository already.") + case _ => None + } + } + + /** + * Duplicate check for the rename repository name. + */ + private def renameRepositoryName: Constraint = new Constraint(){ + override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = + params.get("repository").filter(_ != value).flatMap { _ => + params.get("owner").flatMap { userName => + getRepositoryNamesOfUser(userName).find(_ == value).map(_ => "Repository already exists.") + } + } + } + + /** + * Provides Constraint to validate the repository transfer user. + */ + private def transferUser: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = + getAccountByUserName(value) match { + case None => Some("User does not exist.") + case Some(x) => if(x.userName == params("owner")){ + Some("This is current repository owner.") + } else { + params.get("repository").flatMap { repositoryName => + getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map{ _ => "User already has same repository." } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala new file mode 100644 index 0000000..629410a --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala @@ -0,0 +1,568 @@ +package gitbucket.core.controller + +import gitbucket.core.repo.html +import gitbucket.core.helper +import gitbucket.core.service._ +import gitbucket.core.util._ +import gitbucket.core.util.JGitUtil._ +import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.Implicits._ +import gitbucket.core.util.Directory._ +import gitbucket.core.model.Account +import gitbucket.core.service.WebHookService.WebHookPayload +import gitbucket.core.view +import gitbucket.core.view.helpers +import org.scalatra._ + +import org.eclipse.jgit.api.{ArchiveCommand, Git} +import org.eclipse.jgit.archive.{TgzFormat, ZipFormat} +import org.eclipse.jgit.lib._ +import org.apache.commons.io.FileUtils +import org.eclipse.jgit.treewalk._ +import jp.sf.amateras.scalatra.forms._ +import org.eclipse.jgit.dircache.DirCache +import org.eclipse.jgit.revwalk.RevCommit + +class RepositoryViewerController extends RepositoryViewerControllerBase + with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService + with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService + + +/** + * The repository viewer. + */ +trait RepositoryViewerControllerBase extends ControllerBase { + self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService + with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService => + + ArchiveCommand.registerFormat("zip", new ZipFormat) + ArchiveCommand.registerFormat("tar.gz", new TgzFormat) + + case class EditorForm( + branch: String, + path: String, + content: String, + message: Option[String], + charset: String, + lineSeparator: String, + newFileName: String, + oldFileName: Option[String] + ) + + case class DeleteForm( + branch: String, + path: String, + message: Option[String], + fileName: String + ) + + case class CommentForm( + fileName: Option[String], + oldLineNumber: Option[Int], + newLineNumber: Option[Int], + content: String, + issueId: Option[Int] + ) + + 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))), + "lineSeparator" -> trim(label("Line Separator", 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) + + val commentForm = mapping( + "fileName" -> trim(label("Filename", optional(text()))), + "oldLineNumber" -> trim(label("Old line number", optional(number()))), + "newLineNumber" -> trim(label("New line number", optional(number()))), + "content" -> trim(label("Content", text(required))), + "issueId" -> trim(label("Issue Id", optional(number()))) + )(CommentForm.apply) + + /** + * Returns converted HTML from Markdown for preview. + */ + post("/:owner/:repository/_preview")(referrersOnly { repository => + contentType = "text/html" + helpers.markdown(params("content"), repository, + params("enableWikiLink").toBoolean, + params("enableRefsLink").toBoolean, + params("enableTaskList").toBoolean, + hasWritePermission(repository.owner, repository.name, context.loginAccount)) + }) + + /** + * Displays the file list of the repository root and the default branch. + */ + get("/:owner/:repository")(referrersOnly { + fileList(_) + }) + + /** + * Displays the file list of the specified path and branch. + */ + get("/:owner/:repository/tree/*")(referrersOnly { repository => + val (id, path) = splitPath(repository, multiParams("splat").head) + if(path.isEmpty){ + fileList(repository, id) + } else { + fileList(repository, id, path) + } + }) + + /** + * Displays the commit list of the specified resource. + */ + get("/:owner/:repository/commits/*")(referrersOnly { repository => + val (branchName, path) = splitPath(repository, multiParams("splat").head) + val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1) + + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + JGitUtil.getCommitLog(git, branchName, page, 30, path) match { + case Right((logs, hasNext)) => + html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository, + logs.splitWith{ (commit1, commit2) => + view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime) + }, page, hasNext, hasWritePermission(repository.owner, repository.name, context.loginAccount)) + case Left(_) => NotFound + } + } + }) + + get("/:owner/:repository/new/*")(collaboratorsOnly { repository => + val (branch, path) = splitPath(repository, multiParams("splat").head) + 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("/") + 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("/") + 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, + StringUtil.convertLineSeparator(form.content, form.lineSeparator), 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, + StringUtil.convertLineSeparator(form.content, form.lineSeparator), 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. + */ + get("/:owner/:repository/blob/*")(referrersOnly { repository => + val (id, path) = splitPath(repository, multiParams("splat").head) + val raw = params.get("raw").getOrElse("false").toBoolean + + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) + val lastModifiedCommit = JGitUtil.getLastModifiedCommit(git, revCommit, path) + getPathObjectId(git, path, revCommit).map { objectId => + if(raw){ + // Download + defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes => + RawData(FileUtil.getContentType(path, bytes), bytes) + } + } else { + html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId), + new JGitUtil.CommitInfo(lastModifiedCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount)) + } + } getOrElse NotFound + } + }) + + /** + * Displays details of the specified commit. + */ + get("/:owner/:repository/commit/:id")(referrersOnly { repository => + val id = params("id") + + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))){ revCommit => + JGitUtil.getDiffs(git, id) match { case (diffs, oldCommitId) => + html.commit(id, new JGitUtil.CommitInfo(revCommit), + JGitUtil.getBranchesOfCommit(git, revCommit.getName), + JGitUtil.getTagsOfCommit(git, revCommit.getName), + getCommitComments(repository.owner, repository.name, id, false), + repository, diffs, oldCommitId, hasWritePermission(repository.owner, repository.name, context.loginAccount)) + } + } + } + }) + + post("/:owner/:repository/commit/:id/comment/new", commentForm)(readableUsersOnly { (form, repository) => + val id = params("id") + createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName, form.content, + form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined) + form.issueId match { + case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content) + case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content) + } + redirect(s"/${repository.owner}/${repository.name}/commit/${id}") + }) + + ajaxGet("/:owner/:repository/commit/:id/comment/_form")(readableUsersOnly { repository => + val id = params("id") + val fileName = params.get("fileName") + val oldLineNumber = params.get("oldLineNumber") map (_.toInt) + val newLineNumber = params.get("newLineNumber") map (_.toInt) + val issueId = params.get("issueId") map (_.toInt) + html.commentform( + commitId = id, + fileName, oldLineNumber, newLineNumber, issueId, + hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount), + repository = repository + ) + }) + + ajaxPost("/:owner/:repository/commit/:id/comment/_data/new", commentForm)(readableUsersOnly { (form, repository) => + val id = params("id") + val commentId = createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName, + form.content, form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined) + form.issueId match { + case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content) + case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content) + } + helper.html.commitcomment(getCommitComment(repository.owner, repository.name, commentId.toString).get, + hasWritePermission(repository.owner, repository.name, context.loginAccount), repository) + }) + + ajaxGet("/:owner/:repository/commit_comments/_data/:id")(readableUsersOnly { repository => + getCommitComment(repository.owner, repository.name, params("id")) map { x => + if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){ + params.get("dataType") collect { + case t if t == "html" => html.editcomment( + x.content, x.commentId, x.userName, x.repositoryName) + } getOrElse { + contentType = formats("json") + org.json4s.jackson.Serialization.write( + Map("content" -> view.Markdown.toHtml(x.content, + repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName)) + )) + } + } else Unauthorized + } getOrElse NotFound + }) + + ajaxPost("/:owner/:repository/commit_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => + defining(repository.owner, repository.name){ case (owner, name) => + getCommitComment(owner, name, params("id")).map { comment => + if(isEditable(owner, name, comment.commentedUserName)){ + updateCommitComment(comment.commentId, form.content) + redirect(s"/${owner}/${name}/commit_comments/_data/${comment.commentId}") + } else Unauthorized + } getOrElse NotFound + } + }) + + ajaxPost("/:owner/:repository/commit_comments/delete/:id")(readableUsersOnly { repository => + defining(repository.owner, repository.name){ case (owner, name) => + getCommitComment(owner, name, params("id")).map { comment => + if(isEditable(owner, name, comment.commentedUserName)){ + Ok(deleteCommitComment(comment.commentId)) + } else Unauthorized + } getOrElse NotFound + } + }) + + /** + * Displays branches. + */ + get("/:owner/:repository/branches")(referrersOnly { repository => + val branches = JGitUtil.getBranches(repository.owner, repository.name, repository.repository.defaultBranch) + .sortBy(br => (br.mergeInfo.isEmpty, br.commitTime)) + .map(br => br -> getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId)) + .reverse + html.branches(branches, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository) + }) + + /** + * Creates a branch. + */ + post("/:owner/:repository/branches")(collaboratorsOnly { repository => + val newBranchName = params.getOrElse("new", halt(400)) + val fromBranchName = params.getOrElse("from", halt(400)) + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + JGitUtil.createBranch(git, fromBranchName, newBranchName) + } match { + case Right(message) => + flash += "info" -> message + redirect(s"/${repository.owner}/${repository.name}/tree/${StringUtil.urlEncode(newBranchName).replace("%2F", "/")}") + case Left(message) => + flash += "error" -> message + redirect(s"/${repository.owner}/${repository.name}/tree/${fromBranchName}") + } + }) + + /** + * Deletes branch. + */ + get("/:owner/:repository/delete/*")(collaboratorsOnly { repository => + val branchName = multiParams("splat").head + val userName = context.loginAccount.get.userName + if(repository.repository.defaultBranch != branchName){ + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + git.branchDelete().setForce(true).setBranchNames(branchName).call() + recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName) + } + } + redirect(s"/${repository.owner}/${repository.name}/branches") + }) + + /** + * Displays tags. + */ + get("/:owner/:repository/tags")(referrersOnly { + html.tags(_) + }) + + /** + * Download repository contents as an archive. + */ + get("/:owner/:repository/archive/*")(referrersOnly { repository => + multiParams("splat").head match { + case name if name.endsWith(".zip") => + archiveRepository(name, ".zip", repository) + case name if name.endsWith(".tar.gz") => + archiveRepository(name, ".tar.gz", repository) + case _ => BadRequest + } + }) + + get("/:owner/:repository/network/members")(referrersOnly { repository => + html.forked( + getRepository( + repository.repository.originUserName.getOrElse(repository.owner), + repository.repository.originRepositoryName.getOrElse(repository.name), + context.baseUrl), + getForkedRepositories( + repository.repository.originUserName.getOrElse(repository.owner), + repository.repository.originRepositoryName.getOrElse(repository.name)), + repository) + }) + + private def splitPath(repository: RepositoryService.RepositoryInfo, path: String): (String, String) = { + val id = repository.branchList.collectFirst { + case branch if(path == branch || path.startsWith(branch + "/")) => branch + } orElse repository.tags.collectFirst { + case tag if(path == tag.name || path.startsWith(tag.name + "/")) => tag.name + } getOrElse path.split("/")(0) + + (id, path.substring(id.length).stripPrefix("/")) + } + + + private val readmeFiles = view.helpers.renderableSuffixes.map(suffix => s"readme${suffix}") ++ Seq("readme.txt", "readme") + + /** + * 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) + * @return HTML of the file list + */ + private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = { + if(repository.commitCount == 0){ + html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount)) + } else { + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + // get specified commit + JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) => + defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit => + val lastModifiedCommit = if(path == ".") revCommit else JGitUtil.getLastModifiedCommit(git, revCommit, path) + // get files + val files = JGitUtil.getFileList(git, revision, path) + val parentPath = if (path == ".") Nil else path.split("/").toList + // process README.md or README.markdown + val readme = files.find { file => + readmeFiles.contains(file.name.toLowerCase) + }.map { file => + val path = (file.name :: parentPath.reverse).reverse + path -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId( + Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get) + } + + html.files(revision, repository, + if(path == ".") Nil else path.split("/").toList, // current path + context.loginAccount match { + case None => List() + case account: Option[Account] => getGroupsByUserName(account.get.userName) + }, // groups of current user + new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit + files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount), + flash.get("info"), flash.get("error")) + } + } getOrElse NotFound + } + } + } + + private def commitFile(repository: 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(headName) + + 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), + headName, 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() + + // update pull request + updatePullRequests(repository.owner, repository.name, branch) + + // record activity + recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch, + List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId)))) + + // close issue by commit message + closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name) + + // call web hook + val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId)) + getWebHookURLs(repository.owner, repository.name) match { + case webHookURLs if(webHookURLs.nonEmpty) => + for(ownerAccount <- getAccountByUserName(repository.owner)){ + callWebHook(repository.owner, repository.name, webHookURLs, + WebHookPayload(git, loginAccount, headName, repository, List(commit), ownerAccount)) + } + case _ => + } + } + } + } + + 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) + } + } + + private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): Unit = { + val revision = name.stripSuffix(suffix) + val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId) + if(workDir.exists) { + FileUtils.deleteDirectory(workDir) + } + workDir.mkdirs + + val filename = repository.name + "-" + + (if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + suffix + + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision)) + + contentType = "application/octet-stream" + response.setHeader("Content-Disposition", s"attachment; filename=${filename}") + response.setBufferSize(1024 * 1024); + + git.archive + .setFormat(suffix.tail) + .setTree(revCommit.getTree) + .setOutputStream(response.getOutputStream) + .call() + + Unit + } + } + + private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean = + hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName +} diff --git a/src/main/scala/gitbucket/core/controller/SearchController.scala b/src/main/scala/gitbucket/core/controller/SearchController.scala new file mode 100644 index 0000000..85f3907 --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/SearchController.scala @@ -0,0 +1,51 @@ +package gitbucket.core.controller + +import gitbucket.core.search.html +import gitbucket.core.service._ +import gitbucket.core.util.{StringUtil, ControlUtil, ReferrerAuthenticator, Implicits} +import ControlUtil._ +import Implicits._ +import jp.sf.amateras.scalatra.forms._ + +class SearchController extends SearchControllerBase + with RepositoryService with AccountService with ActivityService with RepositorySearchService with IssuesService with ReferrerAuthenticator + +trait SearchControllerBase extends ControllerBase { self: RepositoryService + with ActivityService with RepositorySearchService with ReferrerAuthenticator => + + val searchForm = mapping( + "query" -> trim(text(required)), + "owner" -> trim(text(required)), + "repository" -> trim(text(required)) + )(SearchForm.apply) + + case class SearchForm(query: String, owner: String, repository: String) + + post("/search", searchForm){ form => + redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}") + } + + get("/:owner/:repository/search")(referrersOnly { repository => + defining(params("q").trim, params.getOrElse("type", "code")){ case (query, target) => + val page = try { + val i = params.getOrElse("page", "1").toInt + if(i <= 0) 1 else i + } catch { + case e: NumberFormatException => 1 + } + + target.toLowerCase match { + case "issue" => html.issues( + searchIssues(repository.owner, repository.name, query), + countFiles(repository.owner, repository.name, query), + query, page, repository) + + case _ => html.code( + searchFiles(repository.owner, repository.name, query), + countIssues(repository.owner, repository.name, query), + query, page, repository) + } + } + }) + +} diff --git a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala new file mode 100644 index 0000000..6c558ba --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala @@ -0,0 +1,85 @@ +package gitbucket.core.controller + +import gitbucket.core.admin.html +import gitbucket.core.service.{AccountService, SystemSettingsService} +import gitbucket.core.util.AdminAuthenticator +import gitbucket.core.ssh.SshServer +import SystemSettingsService._ +import jp.sf.amateras.scalatra.forms._ + +class SystemSettingsController extends SystemSettingsControllerBase + with AccountService with AdminAuthenticator + +trait SystemSettingsControllerBase extends ControllerBase { + self: AccountService with AdminAuthenticator => + + private val form = mapping( + "baseUrl" -> trim(label("Base URL", optional(text()))), + "information" -> trim(label("Information", optional(text()))), + "allowAccountRegistration" -> trim(label("Account registration", boolean())), + "allowAnonymousAccess" -> trim(label("Anonymous access", boolean())), + "isCreateRepoOptionPublic" -> trim(label("Default option to create a new repository", boolean())), + "gravatar" -> trim(label("Gravatar", boolean())), + "notification" -> trim(label("Notification", boolean())), + "ssh" -> trim(label("SSH access", boolean())), + "sshPort" -> trim(label("SSH port", optional(number()))), + "smtp" -> optionalIfNotChecked("notification", mapping( + "host" -> trim(label("SMTP Host", text(required))), + "port" -> trim(label("SMTP Port", optional(number()))), + "user" -> trim(label("SMTP User", optional(text()))), + "password" -> trim(label("SMTP Password", optional(text()))), + "ssl" -> trim(label("Enable SSL", optional(boolean()))), + "fromAddress" -> trim(label("FROM Address", optional(text()))), + "fromName" -> trim(label("FROM Name", optional(text()))) + )(Smtp.apply)), + "ldapAuthentication" -> trim(label("LDAP", boolean())), + "ldap" -> optionalIfNotChecked("ldapAuthentication", mapping( + "host" -> trim(label("LDAP host", text(required))), + "port" -> trim(label("LDAP port", optional(number()))), + "bindDN" -> trim(label("Bind DN", optional(text()))), + "bindPassword" -> trim(label("Bind Password", optional(text()))), + "baseDN" -> trim(label("Base DN", text(required))), + "userNameAttribute" -> trim(label("User name attribute", text(required))), + "additionalFilterCondition"-> trim(label("Additional filter condition", optional(text()))), + "fullNameAttribute" -> trim(label("Full name attribute", optional(text()))), + "mailAttribute" -> trim(label("Mail address attribute", optional(text()))), + "tls" -> trim(label("Enable TLS", optional(boolean()))), + "ssl" -> trim(label("Enable SSL", optional(boolean()))), + "keystore" -> trim(label("Keystore", optional(text()))) + )(Ldap.apply)) + )(SystemSettings.apply).verifying { settings => + if(settings.ssh && settings.baseUrl.isEmpty){ + Seq("baseUrl" -> "Base URL is required if SSH access is enabled.") + } else Nil + } + + private val pluginForm = mapping( + "pluginId" -> list(trim(label("", text()))) + )(PluginForm.apply) + + case class PluginForm(pluginIds: List[String]) + + get("/admin/system")(adminOnly { + html.system(flash.get("info")) + }) + + post("/admin/system", form)(adminOnly { form => + saveSystemSettings(form) + + if(form.ssh && SshServer.isActive && context.settings.sshPort != form.sshPort){ + SshServer.stop() + } + + if(form.ssh && !SshServer.isActive && form.baseUrl.isDefined){ + SshServer.start( + form.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), + form.baseUrl.get) + } else if(!form.ssh && SshServer.isActive){ + SshServer.stop() + } + + flash += "info" -> "System settings has been updated." + redirect("/admin/system") + }) + +} diff --git a/src/main/scala/gitbucket/core/controller/UserManagementController.scala b/src/main/scala/gitbucket/core/controller/UserManagementController.scala new file mode 100644 index 0000000..48dcac0 --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/UserManagementController.scala @@ -0,0 +1,204 @@ +package gitbucket.core.controller + +import gitbucket.core.service.{RepositoryService, AccountService} +import gitbucket.core.admin.users.html +import gitbucket.core.util._ +import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.StringUtil._ +import gitbucket.core.util.Implicits._ +import gitbucket.core.util.Directory._ +import jp.sf.amateras.scalatra.forms._ +import org.scalatra.i18n.Messages +import org.apache.commons.io.FileUtils + +class UserManagementController extends UserManagementControllerBase + with AccountService with RepositoryService with AdminAuthenticator + +trait UserManagementControllerBase extends AccountManagementControllerBase { + self: AccountService with RepositoryService with AdminAuthenticator => + + case class NewUserForm(userName: String, password: String, fullName: String, + mailAddress: String, isAdmin: Boolean, + url: Option[String], fileId: Option[String]) + + case class EditUserForm(userName: String, password: Option[String], fullName: String, + mailAddress: String, isAdmin: Boolean, url: Option[String], + fileId: Option[String], clearImage: Boolean, isRemoved: Boolean) + + case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], + members: String) + + case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], + members: String, clearImage: Boolean, isRemoved: Boolean) + + val newUserForm = mapping( + "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))), + "password" -> trim(label("Password" ,text(required, maxlength(20)))), + "fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))), + "mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))), + "isAdmin" -> trim(label("User Type" ,boolean())), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))) + )(NewUserForm.apply) + + val editUserForm = mapping( + "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))), + "password" -> trim(label("Password" ,optional(text(maxlength(20))))), + "fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))), + "mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))), + "isAdmin" -> trim(label("User Type" ,boolean())), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "clearImage" -> trim(label("Clear image" ,boolean())), + "removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName")))) + )(EditUserForm.apply) + + val newGroupForm = mapping( + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))) + )(NewGroupForm.apply) + + val editGroupForm = mapping( + "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))), + "url" -> trim(label("URL" ,optional(text(maxlength(200))))), + "fileId" -> trim(label("File ID" ,optional(text()))), + "members" -> trim(label("Members" ,text(required, members))), + "clearImage" -> trim(label("Clear image" ,boolean())), + "removed" -> trim(label("Disable" ,boolean())) + )(EditGroupForm.apply) + + get("/admin/users")(adminOnly { + val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false) + val users = getAllUsers(includeRemoved) + val members = users.collect { case account if(account.isGroupAccount) => + account.userName -> getGroupMembers(account.userName).map(_.userName) + }.toMap + + html.list(users, members, includeRemoved) + }) + + get("/admin/users/_newuser")(adminOnly { + html.user(None) + }) + + post("/admin/users/_newuser", newUserForm)(adminOnly { form => + createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url) + updateImage(form.userName, form.fileId, false) + redirect("/admin/users") + }) + + get("/admin/users/:userName/_edituser")(adminOnly { + val userName = params("userName") + html.user(getAccountByUserName(userName, true)) + }) + + post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form => + val userName = params("userName") + getAccountByUserName(userName, true).map { account => + + if(form.isRemoved){ + // Remove repositories + getRepositoryNamesOfUser(userName).foreach { repositoryName => + deleteRepository(userName, repositoryName) + FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName)) + FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName)) + FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName)) + } + // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY + removeUserRelatedData(userName) + } + + updateAccount(account.copy( + password = form.password.map(sha1).getOrElse(account.password), + fullName = form.fullName, + mailAddress = form.mailAddress, + isAdmin = form.isAdmin, + url = form.url, + isRemoved = form.isRemoved)) + + updateImage(userName, form.fileId, form.clearImage) + redirect("/admin/users") + + } getOrElse NotFound + }) + + get("/admin/users/_newgroup")(adminOnly { + html.group(None, Nil) + }) + + post("/admin/users/_newgroup", newGroupForm)(adminOnly { form => + createGroup(form.groupName, form.url) + updateGroupMembers(form.groupName, form.members.split(",").map { + _.split(":") match { + case Array(userName, isManager) => (userName, isManager.toBoolean) + } + }.toList) + updateImage(form.groupName, form.fileId, false) + redirect("/admin/users") + }) + + get("/admin/users/:groupName/_editgroup")(adminOnly { + defining(params("groupName")){ groupName => + html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName)) + } + }) + + post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form => + defining(params("groupName"), form.members.split(",").map { + _.split(":") match { + case Array(userName, isManager) => (userName, isManager.toBoolean) + } + }.toList){ case (groupName, members) => + getAccountByUserName(groupName, true).map { account => + updateGroup(groupName, form.url, form.isRemoved) + + if(form.isRemoved){ + // Remove from GROUP_MEMBER + updateGroupMembers(form.groupName, Nil) + // Remove repositories + getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => + deleteRepository(groupName, repositoryName) + FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName)) + FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName)) + FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName)) + } + } else { + // Update GROUP_MEMBER + updateGroupMembers(form.groupName, members) + // Update COLLABORATOR for group repositories + getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => + removeCollaborators(form.groupName, repositoryName) + members.foreach { case (userName, isManager) => + addCollaborator(form.groupName, repositoryName, userName) + } + } + } + + updateImage(form.groupName, form.fileId, form.clearImage) + redirect("/admin/users") + + } getOrElse NotFound + } + }) + + private def members: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = { + if(value.split(",").exists { + _.split(":") match { case Array(userName, isManager) => isManager.toBoolean } + }) None else Some("Must select one manager at least.") + } + } + + protected def disableByNotYourself(paramName: String): Constraint = new Constraint() { + override def validate(name: String, value: String, messages: Messages): Option[String] = { + params.get(paramName).flatMap { userName => + if(userName == context.loginAccount.get.userName) + Some("You can't disable your account yourself") + else + None + } + } + } +} diff --git a/src/main/scala/gitbucket/core/controller/WikiController.scala b/src/main/scala/gitbucket/core/controller/WikiController.scala new file mode 100644 index 0000000..2bdc7a0 --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/WikiController.scala @@ -0,0 +1,205 @@ +package gitbucket.core.controller + +import gitbucket.core.wiki.html +import gitbucket.core.service.{RepositoryService, WikiService, ActivityService, AccountService} +import gitbucket.core.util._ +import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.Implicits._ +import gitbucket.core.util.Directory._ +import jp.sf.amateras.scalatra.forms._ +import org.eclipse.jgit.api.Git +import org.scalatra.i18n.Messages + +class WikiController extends WikiControllerBase + with WikiService with RepositoryService with AccountService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator + +trait WikiControllerBase extends ControllerBase { + self: WikiService with RepositoryService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator => + + case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String) + + val newForm = mapping( + "pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename, unique))), + "content" -> trim(label("Content" , text(required, conflictForNew))), + "message" -> trim(label("Message" , optional(text()))), + "currentPageName" -> trim(label("Current page name" , text())), + "id" -> trim(label("Latest commit id" , text())) + )(WikiPageEditForm.apply) + + val editForm = mapping( + "pageName" -> trim(label("Page name" , text(required, maxlength(40), pagename))), + "content" -> trim(label("Content" , text(required, conflictForEdit))), + "message" -> trim(label("Message" , optional(text()))), + "currentPageName" -> trim(label("Current page name" , text(required))), + "id" -> trim(label("Latest commit id" , text(required))) + )(WikiPageEditForm.apply) + + get("/:owner/:repository/wiki")(referrersOnly { repository => + getWikiPage(repository.owner, repository.name, "Home").map { page => + html.page("Home", page, getWikiPageList(repository.owner, repository.name), + repository, hasWritePermission(repository.owner, repository.name, context.loginAccount)) + } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit") + }) + + get("/:owner/:repository/wiki/:page")(referrersOnly { repository => + val pageName = StringUtil.urlDecode(params("page")) + + getWikiPage(repository.owner, repository.name, pageName).map { page => + html.page(pageName, page, getWikiPageList(repository.owner, repository.name), + repository, hasWritePermission(repository.owner, repository.name, context.loginAccount)) + } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit") + }) + + get("/:owner/:repository/wiki/:page/_history")(referrersOnly { repository => + val pageName = StringUtil.urlDecode(params("page")) + + using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => + JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match { + case Right((logs, hasNext)) => html.history(Some(pageName), logs, repository) + case Left(_) => NotFound + } + } + }) + + get("/:owner/:repository/wiki/:page/_compare/:commitId")(referrersOnly { repository => + val pageName = StringUtil.urlDecode(params("page")) + val Array(from, to) = params("commitId").split("\\.\\.\\.") + + using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => + 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")) + } + }) + + get("/:owner/:repository/wiki/_compare/:commitId")(referrersOnly { repository => + val Array(from, to) = params("commitId").split("\\.\\.\\.") + + using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => + html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true), repository, + hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info")) + } + }) + + get("/:owner/:repository/wiki/:page/_revert/:commitId")(collaboratorsOnly { repository => + val pageName = StringUtil.urlDecode(params("page")) + val Array(from, to) = params("commitId").split("\\.\\.\\.") + + if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, Some(pageName))){ + redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}") + } else { + flash += "info" -> "This patch was not able to be reversed." + redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_compare/${from}...${to}") + } + }) + + get("/:owner/:repository/wiki/_revert/:commitId")(collaboratorsOnly { repository => + 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/") + } else { + flash += "info" -> "This patch was not able to be reversed." + redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}") + } + }) + + get("/:owner/:repository/wiki/:page/_edit")(collaboratorsOnly { repository => + val pageName = StringUtil.urlDecode(params("page")) + html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository) + }) + + post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) => + defining(context.loginAccount.get){ loginAccount => + saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName, + form.content, loginAccount, form.message.getOrElse(""), Some(form.id)).map { commitId => + updateLastActivityDate(repository.owner, repository.name) + recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId) + } + redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}") + } + }) + + get("/:owner/:repository/wiki/_new")(collaboratorsOnly { + html.edit("", None, _) + }) + + post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) => + defining(context.loginAccount.get){ loginAccount => + saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName, + form.content, loginAccount, form.message.getOrElse(""), None) + + updateLastActivityDate(repository.owner, repository.name) + recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName) + + redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}") + } + }) + + get("/:owner/:repository/wiki/:page/_delete")(collaboratorsOnly { repository => + val pageName = StringUtil.urlDecode(params("page")) + + defining(context.loginAccount.get){ loginAccount => + deleteWikiPage(repository.owner, repository.name, pageName, loginAccount.fullName, loginAccount.mailAddress, s"Destroyed ${pageName}") + updateLastActivityDate(repository.owner, repository.name) + + redirect(s"/${repository.owner}/${repository.name}/wiki") + } + }) + + get("/:owner/:repository/wiki/_pages")(referrersOnly { repository => + html.pages(getWikiPageList(repository.owner, repository.name), repository, + hasWritePermission(repository.owner, repository.name, context.loginAccount)) + }) + + get("/:owner/:repository/wiki/_history")(referrersOnly { repository => + using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => + JGitUtil.getCommitLog(git, "master") match { + case Right((logs, hasNext)) => html.history(None, logs, repository) + case Left(_) => NotFound + } + } + }) + + get("/:owner/:repository/wiki/_blob/*")(referrersOnly { repository => + val path = multiParams("splat").head + + getFileContent(repository.owner, repository.name, path).map { bytes => + RawData(FileUtil.getContentType(path, bytes), bytes) + } getOrElse NotFound + }) + + private def unique: Constraint = new Constraint(){ + override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = + getWikiPageList(params("owner"), params("repository")).find(_ == value).map(_ => "Page already exists.") + } + + private def pagename: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = + if(value.exists("\\/:*?\"<>|".contains(_))){ + Some(s"${name} contains invalid character.") + } else if(value.startsWith("_") || value.startsWith("-")){ + Some(s"${name} starts with invalid character.") + } else { + None + } + } + + private def conflictForNew: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = { + targetWikiPage.map { _ => + "Someone has created the wiki since you started. Please reload this page and re-apply your changes." + } + } + } + + private def conflictForEdit: Constraint = new Constraint(){ + override def validate(name: String, value: String, messages: Messages): Option[String] = { + targetWikiPage.filter(_.id != params("id")).map{ _ => + "Someone has edited the wiki since you started. Please reload this page and re-apply your changes." + } + } + } + + private def targetWikiPage = getWikiPage(params("owner"), params("repository"), params("pageName")) + +} diff --git a/src/main/scala/gitbucket/core/model/Account.scala b/src/main/scala/gitbucket/core/model/Account.scala new file mode 100644 index 0000000..cd2190a --- /dev/null +++ b/src/main/scala/gitbucket/core/model/Account.scala @@ -0,0 +1,39 @@ +package gitbucket.core.model + +trait AccountComponent { self: Profile => + import profile.simple._ + import self._ + + lazy val Accounts = TableQuery[Accounts] + + class Accounts(tag: Tag) extends Table[Account](tag, "ACCOUNT") { + val userName = column[String]("USER_NAME", O PrimaryKey) + val fullName = column[String]("FULL_NAME") + val mailAddress = column[String]("MAIL_ADDRESS") + val password = column[String]("PASSWORD") + val isAdmin = column[Boolean]("ADMINISTRATOR") + val url = column[String]("URL") + val registeredDate = column[java.util.Date]("REGISTERED_DATE") + val updatedDate = column[java.util.Date]("UPDATED_DATE") + val lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE") + val image = column[String]("IMAGE") + val groupAccount = column[Boolean]("GROUP_ACCOUNT") + val removed = column[Boolean]("REMOVED") + def * = (userName, fullName, mailAddress, password, isAdmin, url.?, registeredDate, updatedDate, lastLoginDate.?, image.?, groupAccount, removed) <> (Account.tupled, Account.unapply) + } +} + +case class Account( + userName: String, + fullName: String, + mailAddress: String, + password: String, + isAdmin: Boolean, + url: Option[String], + registeredDate: java.util.Date, + updatedDate: java.util.Date, + lastLoginDate: Option[java.util.Date], + image: Option[String], + isGroupAccount: Boolean, + isRemoved: Boolean +) diff --git a/src/main/scala/gitbucket/core/model/Activity.scala b/src/main/scala/gitbucket/core/model/Activity.scala new file mode 100644 index 0000000..0f49ee3 --- /dev/null +++ b/src/main/scala/gitbucket/core/model/Activity.scala @@ -0,0 +1,29 @@ +package gitbucket.core.model + +trait ActivityComponent extends TemplateComponent { self: Profile => + import profile.simple._ + import self._ + + lazy val Activities = TableQuery[Activities] + + class Activities(tag: Tag) extends Table[Activity](tag, "ACTIVITY") with BasicTemplate { + val activityId = column[Int]("ACTIVITY_ID", O AutoInc) + val activityUserName = column[String]("ACTIVITY_USER_NAME") + val activityType = column[String]("ACTIVITY_TYPE") + val message = column[String]("MESSAGE") + val additionalInfo = column[String]("ADDITIONAL_INFO") + val activityDate = column[java.util.Date]("ACTIVITY_DATE") + def * = (userName, repositoryName, activityUserName, activityType, message, additionalInfo.?, activityDate, activityId) <> (Activity.tupled, Activity.unapply) + } +} + +case class Activity( + userName: String, + repositoryName: String, + activityUserName: String, + activityType: String, + message: String, + additionalInfo: Option[String], + activityDate: java.util.Date, + activityId: Int = 0 +) diff --git a/src/main/scala/gitbucket/core/model/BasicTemplate.scala b/src/main/scala/gitbucket/core/model/BasicTemplate.scala new file mode 100644 index 0000000..687d7a4 --- /dev/null +++ b/src/main/scala/gitbucket/core/model/BasicTemplate.scala @@ -0,0 +1,54 @@ +package gitbucket.core.model + +protected[model] trait TemplateComponent { self: Profile => + import profile.simple._ + + trait BasicTemplate { self: Table[_] => + val userName = column[String]("USER_NAME") + val repositoryName = column[String]("REPOSITORY_NAME") + + def byRepository(owner: String, repository: String) = + (userName === owner.bind) && (repositoryName === repository.bind) + + def byRepository(userName: Column[String], repositoryName: Column[String]) = + (this.userName === userName) && (this.repositoryName === repositoryName) + } + + trait IssueTemplate extends BasicTemplate { self: Table[_] => + val issueId = column[Int]("ISSUE_ID") + + def byIssue(owner: String, repository: String, issueId: Int) = + byRepository(owner, repository) && (this.issueId === issueId.bind) + + def byIssue(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = + byRepository(userName, repositoryName) && (this.issueId === issueId) + } + + trait LabelTemplate extends BasicTemplate { self: Table[_] => + val labelId = column[Int]("LABEL_ID") + + def byLabel(owner: String, repository: String, labelId: Int) = + byRepository(owner, repository) && (this.labelId === labelId.bind) + + def byLabel(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = + byRepository(userName, repositoryName) && (this.labelId === labelId) + } + + trait MilestoneTemplate extends BasicTemplate { self: Table[_] => + val milestoneId = column[Int]("MILESTONE_ID") + + def byMilestone(owner: String, repository: String, milestoneId: Int) = + byRepository(owner, repository) && (this.milestoneId === milestoneId.bind) + + def byMilestone(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) = + byRepository(userName, repositoryName) && (this.milestoneId === milestoneId) + } + + trait CommitTemplate extends BasicTemplate { self: Table[_] => + val commitId = column[String]("COMMIT_ID") + + def byCommit(owner: String, repository: String, commitId: String) = + byRepository(owner, repository) && (this.commitId === commitId) + } + +} diff --git a/src/main/scala/gitbucket/core/model/Collaborator.scala b/src/main/scala/gitbucket/core/model/Collaborator.scala new file mode 100644 index 0000000..55ae80f --- /dev/null +++ b/src/main/scala/gitbucket/core/model/Collaborator.scala @@ -0,0 +1,21 @@ +package gitbucket.core.model + +trait CollaboratorComponent extends TemplateComponent { self: Profile => + import profile.simple._ + + lazy val Collaborators = TableQuery[Collaborators] + + class Collaborators(tag: Tag) extends Table[Collaborator](tag, "COLLABORATOR") with BasicTemplate { + val collaboratorName = column[String]("COLLABORATOR_NAME") + def * = (userName, repositoryName, collaboratorName) <> (Collaborator.tupled, Collaborator.unapply) + + def byPrimaryKey(owner: String, repository: String, collaborator: String) = + byRepository(owner, repository) && (collaboratorName === collaborator.bind) + } +} + +case class Collaborator( + userName: String, + repositoryName: String, + collaboratorName: String +) diff --git a/src/main/scala/gitbucket/core/model/Comment.scala b/src/main/scala/gitbucket/core/model/Comment.scala new file mode 100644 index 0000000..5a11440 --- /dev/null +++ b/src/main/scala/gitbucket/core/model/Comment.scala @@ -0,0 +1,78 @@ +package gitbucket.core.model + +trait Comment { + val commentedUserName: String + val registeredDate: java.util.Date +} + +trait IssueCommentComponent extends TemplateComponent { self: Profile => + import profile.simple._ + import self._ + + lazy val IssueComments = new TableQuery(tag => new IssueComments(tag)){ + def autoInc = this returning this.map(_.commentId) + } + + class IssueComments(tag: Tag) extends Table[IssueComment](tag, "ISSUE_COMMENT") with IssueTemplate { + val commentId = column[Int]("COMMENT_ID", O AutoInc) + val action = column[String]("ACTION") + val commentedUserName = column[String]("COMMENTED_USER_NAME") + val content = column[String]("CONTENT") + val registeredDate = column[java.util.Date]("REGISTERED_DATE") + val updatedDate = column[java.util.Date]("UPDATED_DATE") + def * = (userName, repositoryName, issueId, commentId, action, commentedUserName, content, registeredDate, updatedDate) <> (IssueComment.tupled, IssueComment.unapply) + + def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind + } +} + +case class IssueComment ( + userName: String, + repositoryName: String, + issueId: Int, + commentId: Int = 0, + action: String, + commentedUserName: String, + content: String, + registeredDate: java.util.Date, + updatedDate: java.util.Date +) extends Comment + +trait CommitCommentComponent extends TemplateComponent { self: Profile => + import profile.simple._ + import self._ + + lazy val CommitComments = new TableQuery(tag => new CommitComments(tag)){ + def autoInc = this returning this.map(_.commentId) + } + + class CommitComments(tag: Tag) extends Table[CommitComment](tag, "COMMIT_COMMENT") with CommitTemplate { + val commentId = column[Int]("COMMENT_ID", O AutoInc) + val commentedUserName = column[String]("COMMENTED_USER_NAME") + val content = column[String]("CONTENT") + val fileName = column[Option[String]]("FILE_NAME") + val oldLine = column[Option[Int]]("OLD_LINE_NUMBER") + val newLine = column[Option[Int]]("NEW_LINE_NUMBER") + val registeredDate = column[java.util.Date]("REGISTERED_DATE") + val updatedDate = column[java.util.Date]("UPDATED_DATE") + val pullRequest = column[Boolean]("PULL_REQUEST") + def * = (userName, repositoryName, commitId, commentId, commentedUserName, content, fileName, oldLine, newLine, registeredDate, updatedDate, pullRequest) <> (CommitComment.tupled, CommitComment.unapply) + + def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind + } +} + +case class CommitComment( + userName: String, + repositoryName: String, + commitId: String, + commentId: Int = 0, + commentedUserName: String, + content: String, + fileName: Option[String], + oldLine: Option[Int], + newLine: Option[Int], + registeredDate: java.util.Date, + updatedDate: java.util.Date, + pullRequest: Boolean + ) extends Comment diff --git a/src/main/scala/gitbucket/core/model/GroupMembers.scala b/src/main/scala/gitbucket/core/model/GroupMembers.scala new file mode 100644 index 0000000..d8cdc9b --- /dev/null +++ b/src/main/scala/gitbucket/core/model/GroupMembers.scala @@ -0,0 +1,20 @@ +package gitbucket.core.model + +trait GroupMemberComponent { self: Profile => + import profile.simple._ + + lazy val GroupMembers = TableQuery[GroupMembers] + + class GroupMembers(tag: Tag) extends Table[GroupMember](tag, "GROUP_MEMBER") { + val groupName = column[String]("GROUP_NAME", O PrimaryKey) + val userName = column[String]("USER_NAME", O PrimaryKey) + val isManager = column[Boolean]("MANAGER") + def * = (groupName, userName, isManager) <> (GroupMember.tupled, GroupMember.unapply) + } +} + +case class GroupMember( + groupName: String, + userName: String, + isManager: Boolean +) diff --git a/src/main/scala/gitbucket/core/model/Issue.scala b/src/main/scala/gitbucket/core/model/Issue.scala new file mode 100644 index 0000000..24568d3 --- /dev/null +++ b/src/main/scala/gitbucket/core/model/Issue.scala @@ -0,0 +1,49 @@ +package gitbucket.core.model + +trait IssueComponent extends TemplateComponent { self: Profile => + import profile.simple._ + import self._ + + lazy val IssueId = TableQuery[IssueId] + lazy val IssueOutline = TableQuery[IssueOutline] + lazy val Issues = TableQuery[Issues] + + class IssueId(tag: Tag) extends Table[(String, String, Int)](tag, "ISSUE_ID") with IssueTemplate { + def * = (userName, repositoryName, issueId) + def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) + } + + class IssueOutline(tag: Tag) extends Table[(String, String, Int, Int)](tag, "ISSUE_OUTLINE_VIEW") with IssueTemplate { + val commentCount = column[Int]("COMMENT_COUNT") + def * = (userName, repositoryName, issueId, commentCount) + } + + class Issues(tag: Tag) extends Table[Issue](tag, "ISSUE") with IssueTemplate with MilestoneTemplate { + val openedUserName = column[String]("OPENED_USER_NAME") + val assignedUserName = column[String]("ASSIGNED_USER_NAME") + val title = column[String]("TITLE") + val content = column[String]("CONTENT") + val closed = column[Boolean]("CLOSED") + val registeredDate = column[java.util.Date]("REGISTERED_DATE") + val updatedDate = column[java.util.Date]("UPDATED_DATE") + val pullRequest = column[Boolean]("PULL_REQUEST") + def * = (userName, repositoryName, issueId, openedUserName, milestoneId.?, assignedUserName.?, title, content.?, closed, registeredDate, updatedDate, pullRequest) <> (Issue.tupled, Issue.unapply) + + def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId) + } +} + +case class Issue( + userName: String, + repositoryName: String, + issueId: Int, + openedUserName: String, + milestoneId: Option[Int], + assignedUserName: Option[String], + title: String, + content: Option[String], + closed: Boolean, + registeredDate: java.util.Date, + updatedDate: java.util.Date, + isPullRequest: Boolean +) diff --git a/src/main/scala/gitbucket/core/model/IssueLabels.scala b/src/main/scala/gitbucket/core/model/IssueLabels.scala new file mode 100644 index 0000000..c56cec7 --- /dev/null +++ b/src/main/scala/gitbucket/core/model/IssueLabels.scala @@ -0,0 +1,20 @@ +package gitbucket.core.model + +trait IssueLabelComponent extends TemplateComponent { self: Profile => + import profile.simple._ + + lazy val IssueLabels = TableQuery[IssueLabels] + + class IssueLabels(tag: Tag) extends Table[IssueLabel](tag, "ISSUE_LABEL") with IssueTemplate with LabelTemplate { + def * = (userName, repositoryName, issueId, labelId) <> (IssueLabel.tupled, IssueLabel.unapply) + def byPrimaryKey(owner: String, repository: String, issueId: Int, labelId: Int) = + byIssue(owner, repository, issueId) && (this.labelId === labelId.bind) + } +} + +case class IssueLabel( + userName: String, + repositoryName: String, + issueId: Int, + labelId: Int +) diff --git a/src/main/scala/gitbucket/core/model/Labels.scala b/src/main/scala/gitbucket/core/model/Labels.scala new file mode 100644 index 0000000..0143c9e --- /dev/null +++ b/src/main/scala/gitbucket/core/model/Labels.scala @@ -0,0 +1,37 @@ +package gitbucket.core.model + +trait LabelComponent extends TemplateComponent { self: Profile => + import profile.simple._ + + lazy val Labels = TableQuery[Labels] + + class Labels(tag: Tag) extends Table[Label](tag, "LABEL") with LabelTemplate { + override val labelId = column[Int]("LABEL_ID", O AutoInc) + val labelName = column[String]("LABEL_NAME") + val color = column[String]("COLOR") + def * = (userName, repositoryName, labelId, labelName, color) <> (Label.tupled, Label.unapply) + + def byPrimaryKey(owner: String, repository: String, labelId: Int) = byLabel(owner, repository, labelId) + def byPrimaryKey(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = byLabel(userName, repositoryName, labelId) + } +} + +case class Label( + userName: String, + repositoryName: String, + labelId: Int = 0, + labelName: String, + color: String){ + + val fontColor = { + val r = color.substring(0, 2) + val g = color.substring(2, 4) + val b = color.substring(4, 6) + + if(Integer.parseInt(r, 16) + Integer.parseInt(g, 16) + Integer.parseInt(b, 16) > 408){ + "000000" + } else { + "ffffff" + } + } +} diff --git a/src/main/scala/gitbucket/core/model/Milestone.scala b/src/main/scala/gitbucket/core/model/Milestone.scala new file mode 100644 index 0000000..5c09b5d --- /dev/null +++ b/src/main/scala/gitbucket/core/model/Milestone.scala @@ -0,0 +1,30 @@ +package gitbucket.core.model + +trait MilestoneComponent extends TemplateComponent { self: Profile => + import profile.simple._ + import self._ + + lazy val Milestones = TableQuery[Milestones] + + class Milestones(tag: Tag) extends Table[Milestone](tag, "MILESTONE") with MilestoneTemplate { + override val milestoneId = column[Int]("MILESTONE_ID", O AutoInc) + val title = column[String]("TITLE") + val description = column[String]("DESCRIPTION") + val dueDate = column[java.util.Date]("DUE_DATE") + val closedDate = column[java.util.Date]("CLOSED_DATE") + def * = (userName, repositoryName, milestoneId, title, description.?, dueDate.?, closedDate.?) <> (Milestone.tupled, Milestone.unapply) + + def byPrimaryKey(owner: String, repository: String, milestoneId: Int) = byMilestone(owner, repository, milestoneId) + def byPrimaryKey(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) = byMilestone(userName, repositoryName, milestoneId) + } +} + +case class Milestone( + userName: String, + repositoryName: String, + milestoneId: Int = 0, + title: String, + description: Option[String], + dueDate: Option[java.util.Date], + closedDate: Option[java.util.Date] +) diff --git a/src/main/scala/gitbucket/core/model/Plugin.scala b/src/main/scala/gitbucket/core/model/Plugin.scala new file mode 100644 index 0000000..1e8aac5 --- /dev/null +++ b/src/main/scala/gitbucket/core/model/Plugin.scala @@ -0,0 +1,19 @@ +package gitbucket.core.model + +trait PluginComponent extends TemplateComponent { self: Profile => + import profile.simple._ + import self._ + + lazy val Plugins = TableQuery[Plugins] + + class Plugins(tag: Tag) extends Table[Plugin](tag, "PLUGIN"){ + val pluginId = column[String]("PLUGIN_ID", O PrimaryKey) + val version = column[String]("VERSION") + def * = (pluginId, version) <> (Plugin.tupled, Plugin.unapply) + } +} + +case class Plugin( + pluginId: String, + version: String +) diff --git a/src/main/scala/gitbucket/core/model/Profile.scala b/src/main/scala/gitbucket/core/model/Profile.scala new file mode 100644 index 0000000..d09595d --- /dev/null +++ b/src/main/scala/gitbucket/core/model/Profile.scala @@ -0,0 +1,44 @@ +package gitbucket.core.model + +trait Profile { + val profile: slick.driver.JdbcProfile + import profile.simple._ + + // java.util.Date Mapped Column Types + implicit val dateColumnType = MappedColumnType.base[java.util.Date, java.sql.Timestamp]( + d => new java.sql.Timestamp(d.getTime), + t => new java.util.Date(t.getTime) + ) + + implicit class RichColumn(c1: Column[Boolean]){ + def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1 + } + + /** + * Returns system date. + */ + def currentDate = new java.util.Date() + +} + +trait CoreProfile extends Profile + with AccountComponent + with ActivityComponent + with CollaboratorComponent + with CommitCommentComponent + with GroupMemberComponent + with IssueComponent + with IssueCommentComponent + with IssueLabelComponent + with LabelComponent + with MilestoneComponent + with PullRequestComponent + with RepositoryComponent + with SshKeyComponent + with WebHookComponent + with PluginComponent { + + val profile = slick.driver.H2Driver +} + +object Profile extends CoreProfile diff --git a/src/main/scala/gitbucket/core/model/PullRequest.scala b/src/main/scala/gitbucket/core/model/PullRequest.scala new file mode 100644 index 0000000..ed5acac --- /dev/null +++ b/src/main/scala/gitbucket/core/model/PullRequest.scala @@ -0,0 +1,32 @@ +package gitbucket.core.model + +trait PullRequestComponent extends TemplateComponent { self: Profile => + import profile.simple._ + + lazy val PullRequests = TableQuery[PullRequests] + + class PullRequests(tag: Tag) extends Table[PullRequest](tag, "PULL_REQUEST") with IssueTemplate { + val branch = column[String]("BRANCH") + val requestUserName = column[String]("REQUEST_USER_NAME") + val requestRepositoryName = column[String]("REQUEST_REPOSITORY_NAME") + val requestBranch = column[String]("REQUEST_BRANCH") + val commitIdFrom = column[String]("COMMIT_ID_FROM") + val commitIdTo = column[String]("COMMIT_ID_TO") + def * = (userName, repositoryName, issueId, branch, requestUserName, requestRepositoryName, requestBranch, commitIdFrom, commitIdTo) <> (PullRequest.tupled, PullRequest.unapply) + + def byPrimaryKey(userName: String, repositoryName: String, issueId: Int) = byIssue(userName, repositoryName, issueId) + def byPrimaryKey(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = byIssue(userName, repositoryName, issueId) + } +} + +case class PullRequest( + userName: String, + repositoryName: String, + issueId: Int, + branch: String, + requestUserName: String, + requestRepositoryName: String, + requestBranch: String, + commitIdFrom: String, + commitIdTo: String +) diff --git a/src/main/scala/gitbucket/core/model/Repository.scala b/src/main/scala/gitbucket/core/model/Repository.scala new file mode 100644 index 0000000..789f957 --- /dev/null +++ b/src/main/scala/gitbucket/core/model/Repository.scala @@ -0,0 +1,39 @@ +package gitbucket.core.model + +trait RepositoryComponent extends TemplateComponent { self: Profile => + import profile.simple._ + import self._ + + lazy val Repositories = TableQuery[Repositories] + + class Repositories(tag: Tag) extends Table[Repository](tag, "REPOSITORY") with BasicTemplate { + val isPrivate = column[Boolean]("PRIVATE") + val description = column[String]("DESCRIPTION") + val defaultBranch = column[String]("DEFAULT_BRANCH") + val registeredDate = column[java.util.Date]("REGISTERED_DATE") + val updatedDate = column[java.util.Date]("UPDATED_DATE") + val lastActivityDate = column[java.util.Date]("LAST_ACTIVITY_DATE") + val originUserName = column[String]("ORIGIN_USER_NAME") + val originRepositoryName = column[String]("ORIGIN_REPOSITORY_NAME") + val parentUserName = column[String]("PARENT_USER_NAME") + val parentRepositoryName = column[String]("PARENT_REPOSITORY_NAME") + def * = (userName, repositoryName, isPrivate, description.?, defaultBranch, registeredDate, updatedDate, lastActivityDate, originUserName.?, originRepositoryName.?, parentUserName.?, parentRepositoryName.?) <> (Repository.tupled, Repository.unapply) + + def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) + } +} + +case class Repository( + userName: String, + repositoryName: String, + isPrivate: Boolean, + description: Option[String], + defaultBranch: String, + registeredDate: java.util.Date, + updatedDate: java.util.Date, + lastActivityDate: java.util.Date, + originUserName: Option[String], + originRepositoryName: Option[String], + parentUserName: Option[String], + parentRepositoryName: Option[String] +) diff --git a/src/main/scala/gitbucket/core/model/SshKey.scala b/src/main/scala/gitbucket/core/model/SshKey.scala new file mode 100644 index 0000000..fa7909e --- /dev/null +++ b/src/main/scala/gitbucket/core/model/SshKey.scala @@ -0,0 +1,24 @@ +package gitbucket.core.model + +trait SshKeyComponent { self: Profile => + import profile.simple._ + + lazy val SshKeys = TableQuery[SshKeys] + + class SshKeys(tag: Tag) extends Table[SshKey](tag, "SSH_KEY") { + val userName = column[String]("USER_NAME") + val sshKeyId = column[Int]("SSH_KEY_ID", O AutoInc) + val title = column[String]("TITLE") + val publicKey = column[String]("PUBLIC_KEY") + def * = (userName, sshKeyId, title, publicKey) <> (SshKey.tupled, SshKey.unapply) + + def byPrimaryKey(userName: String, sshKeyId: Int) = (this.userName === userName.bind) && (this.sshKeyId === sshKeyId.bind) + } +} + +case class SshKey( + userName: String, + sshKeyId: Int = 0, + title: String, + publicKey: String +) diff --git a/src/main/scala/gitbucket/core/model/WebHook.scala b/src/main/scala/gitbucket/core/model/WebHook.scala new file mode 100644 index 0000000..b6897da --- /dev/null +++ b/src/main/scala/gitbucket/core/model/WebHook.scala @@ -0,0 +1,20 @@ +package gitbucket.core.model + +trait WebHookComponent extends TemplateComponent { self: Profile => + import profile.simple._ + + lazy val WebHooks = TableQuery[WebHooks] + + class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate { + val url = column[String]("URL") + def * = (userName, repositoryName, url) <> (WebHook.tupled, WebHook.unapply) + + def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind) + } +} + +case class WebHook( + userName: String, + repositoryName: String, + url: String +) diff --git a/src/main/scala/gitbucket/core/model/package.scala b/src/main/scala/gitbucket/core/model/package.scala new file mode 100644 index 0000000..80e19cc --- /dev/null +++ b/src/main/scala/gitbucket/core/model/package.scala @@ -0,0 +1,5 @@ +package gitbucket.core + +package object model { + type Session = slick.jdbc.JdbcBackend#Session +} diff --git a/src/main/scala/gitbucket/core/plugin/Images.scala b/src/main/scala/gitbucket/core/plugin/Images.scala new file mode 100644 index 0000000..3c1f553 --- /dev/null +++ b/src/main/scala/gitbucket/core/plugin/Images.scala @@ -0,0 +1,10 @@ +package gitbucket.core.plugin + +/** + * Provides a helper method to generate data URI of images registered by plug-in. + */ +object Images { + + def dataURI(id: String) = s"data:image/png;base64,${PluginRegistry().getImage(id)}" + +} diff --git a/src/main/scala/gitbucket/core/plugin/Plugin.scala b/src/main/scala/gitbucket/core/plugin/Plugin.scala new file mode 100644 index 0000000..74c91b2 --- /dev/null +++ b/src/main/scala/gitbucket/core/plugin/Plugin.scala @@ -0,0 +1,28 @@ +package gitbucket.core.plugin + +import gitbucket.core.util.Version + +/** + * Trait for define plugin interface. + * To provide plugin, put Plugin class which mixed in this trait into the package root. + */ +trait Plugin { + + val pluginId: String + val pluginName: String + val description: String + val versions: Seq[Version] + + /** + * This method is invoked in initialization of plugin system. + * Register plugin functionality to PluginRegistry. + */ + def initialize(registry: PluginRegistry): Unit + + /** + * This method is invoked in shutdown of plugin system. + * If the plugin has any resources, release them in this method. + */ + def shutdown(registry: PluginRegistry): Unit + +} diff --git a/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala b/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala new file mode 100644 index 0000000..f1f1a17 --- /dev/null +++ b/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala @@ -0,0 +1,161 @@ +package gitbucket.core.plugin + +import java.io.{File, FilenameFilter, InputStream} +import java.net.URLClassLoader +import javax.servlet.ServletContext +import javax.servlet.http.{HttpServletRequest, HttpServletResponse} + +import gitbucket.core.controller.{Context, ControllerBase} +import gitbucket.core.service.RepositoryService.RepositoryInfo +import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.Directory._ +import gitbucket.core.util.JDBCUtil._ +import gitbucket.core.util.{Version, Versions} +import org.apache.commons.codec.binary.{Base64, StringUtils} +import org.slf4j.LoggerFactory + +import scala.collection.mutable +import scala.collection.mutable.ListBuffer + +class PluginRegistry { + + private val plugins = new ListBuffer[PluginInfo] + private val javaScripts = new ListBuffer[(String, String)] + private val controllers = new ListBuffer[(ControllerBase, String)] + private val images = mutable.Map[String, String]() + + def addPlugin(pluginInfo: PluginInfo): Unit = { + plugins += pluginInfo + } + + def getPlugins(): List[PluginInfo] = plugins.toList + + def addImage(id: String, in: InputStream): Unit = { + val bytes = using(in){ in => + val bytes = new Array[Byte](in.available) + in.read(bytes) + bytes + } + val encoded = StringUtils.newStringUtf8(Base64.encodeBase64(bytes, false)) + images += ((id, encoded)) + } + + def getImage(id: String): String = images(id) + + def addController(controller: ControllerBase, path: String): Unit = { + controllers += ((controller, path)) + } + + def getControllers(): List[(ControllerBase, String)] = controllers.toList + + def addJavaScript(path: String, script: String): Unit = { + javaScripts += Tuple2(path, script) + } + + //def getJavaScripts(): List[(String, String)] = javaScripts.toList + + def getJavaScript(currentPath: String): Option[String] = { + javaScripts.find(x => currentPath.matches(x._1)).map(_._2) + } + + private case class GlobalAction( + method: String, + path: String, + function: (HttpServletRequest, HttpServletResponse, Context) => Any + ) + + private case class RepositoryAction( + method: String, + path: String, + function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any + ) + +} + +/** + * Provides entry point to PluginRegistry. + */ +object PluginRegistry { + + private val logger = LoggerFactory.getLogger(classOf[PluginRegistry]) + + private val instance = new PluginRegistry() + + /** + * Returns the PluginRegistry singleton instance. + */ + def apply(): PluginRegistry = instance + + /** + * Initializes all installed plugins. + */ + def initialize(context: ServletContext, conn: java.sql.Connection): Unit = { + val pluginDir = new File(PluginHome) + if(pluginDir.exists && pluginDir.isDirectory){ + pluginDir.listFiles(new FilenameFilter { + override def accept(dir: File, name: String): Boolean = name.endsWith(".jar") + }).foreach { pluginJar => + val classLoader = new URLClassLoader(Array(pluginJar.toURI.toURL), Thread.currentThread.getContextClassLoader) + try { + val plugin = classLoader.loadClass("Plugin").newInstance().asInstanceOf[Plugin] + + // Migration + val headVersion = plugin.versions.head + val currentVersion = conn.find("SELECT * FROM PLUGIN WHERE PLUGIN_ID = ?", plugin.pluginId)(_.getString("VERSION")) match { + case Some(x) => { + val dim = x.split("\\.") + Version(dim(0).toInt, dim(1).toInt) + } + case None => Version(0, 0) + } + + Versions.update(conn, headVersion, currentVersion, plugin.versions, new URLClassLoader(Array(pluginJar.toURI.toURL))){ conn => + currentVersion.versionString match { + case "0.0" => + conn.update("INSERT INTO PLUGIN (PLUGIN_ID, VERSION) VALUES (?, ?)", plugin.pluginId, headVersion.versionString) + case _ => + conn.update("UPDATE PLUGIN SET VERSION = ? WHERE PLUGIN_ID = ?", headVersion.versionString, plugin.pluginId) + } + } + + // Initialize + plugin.initialize(instance) + instance.addPlugin(PluginInfo( + pluginId = plugin.pluginId, + pluginName = plugin.pluginName, + version = plugin.versions.head.versionString, + description = plugin.description, + pluginClass = plugin + )) + + } catch { + case e: Exception => { + logger.error(s"Error during plugin initialization", e) + } + } + } + } + } + + def shutdown(context: ServletContext): Unit = { + instance.getPlugins().foreach { pluginInfo => + try { + pluginInfo.pluginClass.shutdown(instance) + } catch { + case e: Exception => { + logger.error(s"Error during plugin shutdown", e) + } + } + } + } + + +} + +case class PluginInfo( + pluginId: String, + pluginName: String, + version: String, + description: String, + pluginClass: Plugin +) \ No newline at end of file diff --git a/src/main/scala/gitbucket/core/plugin/Results.scala b/src/main/scala/gitbucket/core/plugin/Results.scala new file mode 100644 index 0000000..8029e0d --- /dev/null +++ b/src/main/scala/gitbucket/core/plugin/Results.scala @@ -0,0 +1,11 @@ +package gitbucket.core.plugin + +import play.twirl.api.Html + +/** + * Defines result case classes returned by plugin controller. + */ +object Results { + case class Redirect(path: String) + case class Fragment(html: Html) +} diff --git a/src/main/scala/gitbucket/core/plugin/Sessions.scala b/src/main/scala/gitbucket/core/plugin/Sessions.scala new file mode 100644 index 0000000..dc38702 --- /dev/null +++ b/src/main/scala/gitbucket/core/plugin/Sessions.scala @@ -0,0 +1,11 @@ +package gitbucket.core.plugin + +import scala.slick.jdbc.JdbcBackend.Session + +/** + * Provides Slick Session to Plug-ins. + */ +object Sessions { + val sessions = new ThreadLocal[Session] + implicit def session: Session = sessions.get() +} diff --git a/src/main/scala/gitbucket/core/service/AccountService.scala b/src/main/scala/gitbucket/core/service/AccountService.scala new file mode 100644 index 0000000..917ef96 --- /dev/null +++ b/src/main/scala/gitbucket/core/service/AccountService.scala @@ -0,0 +1,178 @@ +package gitbucket.core.service + +import gitbucket.core.model.{GroupMember, Account} +import gitbucket.core.model.Profile._ +import gitbucket.core.util.{StringUtil, LDAPUtil} +import gitbucket.core.service.SystemSettingsService.SystemSettings +import profile.simple._ +import StringUtil._ +import org.slf4j.LoggerFactory +// TODO Why is direct import required? +import gitbucket.core.model.Profile.dateColumnType + +trait AccountService { + + private val logger = LoggerFactory.getLogger(classOf[AccountService]) + + def authenticate(settings: SystemSettings, userName: String, password: String)(implicit s: Session): Option[Account] = + if(settings.ldapAuthentication){ + ldapAuthentication(settings, userName, password) + } else { + defaultAuthentication(userName, password) + } + + /** + * Authenticate by internal database. + */ + private def defaultAuthentication(userName: String, password: String)(implicit s: Session) = { + getAccountByUserName(userName).collect { + case account if(!account.isGroupAccount && account.password == sha1(password)) => Some(account) + } getOrElse None + } + + /** + * Authenticate by LDAP. + */ + private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) + (implicit s: Session): Option[Account] = { + LDAPUtil.authenticate(settings.ldap.get, userName, password) match { + case Right(ldapUserInfo) => { + // Create or update account by LDAP information + getAccountByUserName(ldapUserInfo.userName, true) match { + case Some(x) if(!x.isRemoved) => { + if(settings.ldap.get.mailAttribute.getOrElse("").isEmpty) { + updateAccount(x.copy(fullName = ldapUserInfo.fullName)) + } else { + updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName)) + } + getAccountByUserName(ldapUserInfo.userName) + } + case Some(x) if(x.isRemoved) => { + logger.info("LDAP Authentication Failed: Account is already registered but disabled.") + defaultAuthentication(userName, password) + } + case None => getAccountByMailAddress(ldapUserInfo.mailAddress, true) match { + case Some(x) if(!x.isRemoved) => { + updateAccount(x.copy(fullName = ldapUserInfo.fullName)) + getAccountByUserName(ldapUserInfo.userName) + } + case Some(x) if(x.isRemoved) => { + logger.info("LDAP Authentication Failed: Account is already registered but disabled.") + defaultAuthentication(userName, password) + } + case None => { + createAccount(ldapUserInfo.userName, "", ldapUserInfo.fullName, ldapUserInfo.mailAddress, false, None) + getAccountByUserName(ldapUserInfo.userName) + } + } + } + } + case Left(errorMessage) => { + logger.info(s"LDAP Authentication Failed: ${errorMessage}") + defaultAuthentication(userName, password) + } + } + } + + def getAccountByUserName(userName: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] = + Accounts filter(t => (t.userName === userName.bind) && (t.removed === false.bind, !includeRemoved)) firstOption + + def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] = + Accounts filter(t => (t.mailAddress.toLowerCase === mailAddress.toLowerCase.bind) && (t.removed === false.bind, !includeRemoved)) firstOption + + def getAllUsers(includeRemoved: Boolean = true)(implicit s: Session): List[Account] = + if(includeRemoved){ + Accounts sortBy(_.userName) list + } else { + Accounts filter (_.removed === false.bind) sortBy(_.userName) list + } + + def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String]) + (implicit s: Session): Unit = + Accounts insert Account( + userName = userName, + password = password, + fullName = fullName, + mailAddress = mailAddress, + isAdmin = isAdmin, + url = url, + registeredDate = currentDate, + updatedDate = currentDate, + lastLoginDate = None, + image = None, + isGroupAccount = false, + isRemoved = false) + + def updateAccount(account: Account)(implicit s: Session): Unit = + Accounts + .filter { a => a.userName === account.userName.bind } + .map { a => (a.password, a.fullName, a.mailAddress, a.isAdmin, a.url.?, a.registeredDate, a.updatedDate, a.lastLoginDate.?, a.removed) } + .update ( + account.password, + account.fullName, + account.mailAddress, + account.isAdmin, + account.url, + account.registeredDate, + currentDate, + account.lastLoginDate, + account.isRemoved) + + def updateAvatarImage(userName: String, image: Option[String])(implicit s: Session): Unit = + Accounts.filter(_.userName === userName.bind).map(_.image.?).update(image) + + def updateLastLoginDate(userName: String)(implicit s: Session): Unit = + Accounts.filter(_.userName === userName.bind).map(_.lastLoginDate).update(currentDate) + + def createGroup(groupName: String, url: Option[String])(implicit s: Session): Unit = + Accounts insert Account( + userName = groupName, + password = "", + fullName = groupName, + mailAddress = groupName + "@devnull", + isAdmin = false, + url = url, + registeredDate = currentDate, + updatedDate = currentDate, + lastLoginDate = None, + image = None, + isGroupAccount = true, + isRemoved = false) + + def updateGroup(groupName: String, url: Option[String], removed: Boolean)(implicit s: Session): Unit = + Accounts.filter(_.userName === groupName.bind).map(t => t.url.? -> t.removed).update(url, removed) + + def updateGroupMembers(groupName: String, members: List[(String, Boolean)])(implicit s: Session): Unit = { + GroupMembers.filter(_.groupName === groupName.bind).delete + members.foreach { case (userName, isManager) => + GroupMembers insert GroupMember (groupName, userName, isManager) + } + } + + def getGroupMembers(groupName: String)(implicit s: Session): List[GroupMember] = + GroupMembers + .filter(_.groupName === groupName.bind) + .sortBy(_.userName) + .list + + def getGroupsByUserName(userName: String)(implicit s: Session): List[String] = + GroupMembers + .filter(_.userName === userName.bind) + .sortBy(_.groupName) + .map(_.groupName) + .list + + def removeUserRelatedData(userName: String)(implicit s: Session): Unit = { + GroupMembers.filter(_.userName === userName.bind).delete + Collaborators.filter(_.collaboratorName === userName.bind).delete + Repositories.filter(_.userName === userName.bind).delete + } + + def getGroupNames(userName: String)(implicit s: Session): List[String] = { + List(userName) ++ + Collaborators.filter(_.collaboratorName === userName.bind).sortBy(_.userName).map(_.userName).list + } + +} + +object AccountService extends AccountService diff --git a/src/main/scala/gitbucket/core/service/ActivityService.scala b/src/main/scala/gitbucket/core/service/ActivityService.scala new file mode 100644 index 0000000..be9205f --- /dev/null +++ b/src/main/scala/gitbucket/core/service/ActivityService.scala @@ -0,0 +1,189 @@ +package gitbucket.core.service + +import gitbucket.core.model.Activity +import gitbucket.core.model.Profile._ +import gitbucket.core.util.JGitUtil +import profile.simple._ + +trait ActivityService { + + def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit s: Session): List[Activity] = + Activities + .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) + .filter { case (t1, t2) => + if(isPublic){ + (t1.activityUserName === activityUserName.bind) && (t2.isPrivate === false.bind) + } else { + (t1.activityUserName === activityUserName.bind) + } + } + .sortBy { case (t1, t2) => t1.activityId desc } + .map { case (t1, t2) => t1 } + .take(30) + .list + + def getRecentActivities()(implicit s: Session): List[Activity] = + Activities + .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) + .filter { case (t1, t2) => t2.isPrivate === false.bind } + .sortBy { case (t1, t2) => t1.activityId desc } + .map { case (t1, t2) => t1 } + .take(30) + .list + + def getRecentActivitiesByOwners(owners : Set[String])(implicit s: Session): List[Activity] = + Activities + .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) + .filter { case (t1, t2) => (t2.isPrivate === false.bind) || (t2.userName inSetBind owners) } + .sortBy { case (t1, t2) => t1.activityId desc } + .map { case (t1, t2) => t1 } + .take(30) + .list + + def recordCreateRepositoryActivity(userName: String, repositoryName: String, activityUserName: String) + (implicit s: Session): Unit = + Activities insert Activity(userName, repositoryName, activityUserName, + "create_repository", + s"[user:${activityUserName}] created [repo:${userName}/${repositoryName}]", + None, + currentDate) + + def recordCreateIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String) + (implicit s: Session): Unit = + Activities insert Activity(userName, repositoryName, activityUserName, + "open_issue", + s"[user:${activityUserName}] opened issue [issue:${userName}/${repositoryName}#${issueId}]", + Some(title), + currentDate) + + def recordCloseIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String) + (implicit s: Session): Unit = + Activities insert Activity(userName, repositoryName, activityUserName, + "close_issue", + s"[user:${activityUserName}] closed issue [issue:${userName}/${repositoryName}#${issueId}]", + Some(title), + currentDate) + + def recordClosePullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String) + (implicit s: Session): Unit = + Activities insert Activity(userName, repositoryName, activityUserName, + "close_issue", + s"[user:${activityUserName}] closed pull request [pullreq:${userName}/${repositoryName}#${issueId}]", + Some(title), + currentDate) + + def recordReopenIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String) + (implicit s: Session): Unit = + Activities insert Activity(userName, repositoryName, activityUserName, + "reopen_issue", + s"[user:${activityUserName}] reopened issue [issue:${userName}/${repositoryName}#${issueId}]", + Some(title), + currentDate) + + def recordCommentIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String) + (implicit s: Session): Unit = + Activities insert Activity(userName, repositoryName, activityUserName, + "comment_issue", + s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]", + Some(cut(comment, 200)), + currentDate) + + def recordCommentPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String) + (implicit s: Session): Unit = + Activities insert Activity(userName, repositoryName, activityUserName, + "comment_issue", + s"[user:${activityUserName}] commented on pull request [pullreq:${userName}/${repositoryName}#${issueId}]", + Some(cut(comment, 200)), + currentDate) + + def recordCommentCommitActivity(userName: String, repositoryName: String, activityUserName: String, commitId: String, comment: String) + (implicit s: Session): Unit = + Activities insert Activity(userName, repositoryName, activityUserName, + "comment_commit", + s"[user:${activityUserName}] commented on commit [commit:${userName}/${repositoryName}@${commitId}]", + Some(cut(comment, 200)), + currentDate + ) + + def recordCreateWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String) + (implicit s: Session): Unit = + Activities insert Activity(userName, repositoryName, activityUserName, + "create_wiki", + s"[user:${activityUserName}] created the [repo:${userName}/${repositoryName}] wiki", + Some(pageName), + currentDate) + + def recordEditWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String, commitId: String) + (implicit s: Session): Unit = + Activities insert Activity(userName, repositoryName, activityUserName, + "edit_wiki", + s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki", + Some(pageName + ":" + commitId), + currentDate) + + def recordPushActivity(userName: String, repositoryName: String, activityUserName: String, + branchName: String, commits: List[JGitUtil.CommitInfo])(implicit s: Session): Unit = + Activities insert Activity(userName, repositoryName, activityUserName, + "push", + s"[user:${activityUserName}] pushed to [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]", + Some(commits.map { commit => commit.id + ":" + commit.shortMessage }.mkString("\n")), + currentDate) + + def recordCreateTagActivity(userName: String, repositoryName: String, activityUserName: String, + tagName: String, commits: List[JGitUtil.CommitInfo])(implicit s: Session): Unit = + Activities insert Activity(userName, repositoryName, activityUserName, + "create_tag", + s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]", + None, + currentDate) + + def recordDeleteTagActivity(userName: String, repositoryName: String, activityUserName: String, + tagName: String, commits: List[JGitUtil.CommitInfo])(implicit s: Session): Unit = + Activities insert Activity(userName, repositoryName, activityUserName, + "delete_tag", + s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]", + None, + currentDate) + + def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) + (implicit s: Session): Unit = + Activities insert Activity(userName, repositoryName, activityUserName, + "create_branch", + s"[user:${activityUserName}] created branch [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]", + None, + currentDate) + + def recordDeleteBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) + (implicit s: Session): Unit = + Activities insert Activity(userName, repositoryName, activityUserName, + "delete_branch", + s"[user:${activityUserName}] deleted branch ${branchName} at [repo:${userName}/${repositoryName}]", + None, + currentDate) + + def recordForkActivity(userName: String, repositoryName: String, activityUserName: String, forkedUserName: String)(implicit s: Session): Unit = + Activities insert Activity(userName, repositoryName, activityUserName, + "fork", + s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${forkedUserName}/${repositoryName}]", + None, + currentDate) + + def recordPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String) + (implicit s: Session): Unit = + Activities insert Activity(userName, repositoryName, activityUserName, + "open_pullreq", + s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]", + Some(title), + currentDate) + + def recordMergeActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, message: String) + (implicit s: Session): Unit = + Activities insert Activity(userName, repositoryName, activityUserName, + "merge_pullreq", + s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]", + Some(message), + currentDate) + + private def cut(value: String, length: Int): String = + if(value.length > length) value.substring(0, length) + "..." else value +} diff --git a/src/main/scala/gitbucket/core/service/CommitsService.scala b/src/main/scala/gitbucket/core/service/CommitsService.scala new file mode 100644 index 0000000..fbef7cd --- /dev/null +++ b/src/main/scala/gitbucket/core/service/CommitsService.scala @@ -0,0 +1,53 @@ +package gitbucket.core.service + +import gitbucket.core.model.CommitComment +import gitbucket.core.util.{StringUtil, Implicits} + +import scala.slick.jdbc.{StaticQuery => Q} +import Q.interpolation +import gitbucket.core.model.Profile._ +import profile.simple._ +import Implicits._ +import StringUtil._ + + +trait CommitsService { + + def getCommitComments(owner: String, repository: String, commitId: String, pullRequest: Boolean)(implicit s: Session) = + CommitComments filter { + t => t.byCommit(owner, repository, commitId) && (t.pullRequest === pullRequest || pullRequest) + } list + + def getCommitComment(owner: String, repository: String, commentId: String)(implicit s: Session) = + if (commentId forall (_.isDigit)) + CommitComments filter { t => + t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository) + } firstOption + else + None + + def createCommitComment(owner: String, repository: String, commitId: String, loginUser: String, + content: String, fileName: Option[String], oldLine: Option[Int], newLine: Option[Int], pullRequest: Boolean)(implicit s: Session): Int = + CommitComments.autoInc insert CommitComment( + userName = owner, + repositoryName = repository, + commitId = commitId, + commentedUserName = loginUser, + content = content, + fileName = fileName, + oldLine = oldLine, + newLine = newLine, + registeredDate = currentDate, + updatedDate = currentDate, + pullRequest = pullRequest) + + def updateCommitComment(commentId: Int, content: String)(implicit s: Session) = + CommitComments + .filter (_.byPrimaryKey(commentId)) + .map { t => + t.content -> t.updatedDate + }.update (content, currentDate) + + def deleteCommitComment(commentId: Int)(implicit s: Session) = + CommitComments filter (_.byPrimaryKey(commentId)) delete +} diff --git a/src/main/scala/gitbucket/core/service/IssuesService.scala b/src/main/scala/gitbucket/core/service/IssuesService.scala new file mode 100644 index 0000000..0d41287 --- /dev/null +++ b/src/main/scala/gitbucket/core/service/IssuesService.scala @@ -0,0 +1,467 @@ +package gitbucket.core.service + +import gitbucket.core.model._ +import gitbucket.core.util.StringUtil._ +import gitbucket.core.util.Implicits._ +import scala.slick.jdbc.{StaticQuery => Q} +import Q.interpolation + +import gitbucket.core.model.Profile._ +import profile.simple._ + +trait IssuesService { + import IssuesService._ + + def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) = + if (issueId forall (_.isDigit)) + Issues filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption + else None + + def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) = + IssueComments filter (_.byIssue(owner, repository, issueId)) list + + def getComment(owner: String, repository: String, commentId: String)(implicit s: Session) = + if (commentId forall (_.isDigit)) + IssueComments filter { t => + t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository) + } firstOption + else None + + def getIssueLabels(owner: String, repository: String, issueId: Int)(implicit s: Session) = + IssueLabels + .innerJoin(Labels).on { (t1, t2) => + t1.byLabel(t2.userName, t2.repositoryName, t2.labelId) + } + .filter ( _._1.byIssue(owner, repository, issueId) ) + .map ( _._2 ) + .list + + def getIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) = + IssueLabels filter (_.byPrimaryKey(owner, repository, issueId, labelId)) firstOption + + /** + * Returns the count of the search result against issues. + * + * @param condition the search condition + * @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request. + * @param repos Tuple of the repository owner and the repository name + * @return the count of the search result + */ + def countIssue(condition: IssueSearchCondition, onlyPullRequest: Boolean, + repos: (String, String)*)(implicit s: Session): Int = + Query(searchIssueQuery(repos, condition, onlyPullRequest).length).first + + /** + * Returns the Map which contains issue count for each labels. + * + * @param owner the repository owner + * @param repository the repository name + * @param condition the search condition + * @return the Map which contains issue count for each labels (key is label name, value is issue count) + */ + def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition, + filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = { + + searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), false) + .innerJoin(IssueLabels).on { (t1, t2) => + t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) + } + .innerJoin(Labels).on { case ((t1, t2), t3) => + t2.byLabel(t3.userName, t3.repositoryName, t3.labelId) + } + .groupBy { case ((t1, t2), t3) => + t3.labelName + } + .map { case (labelName, t) => + labelName -> t.length + } + .toMap + } + + /** + * Returns the search result against issues. + * + * @param condition the search condition + * @param pullRequest if true then returns only pull requests, false then returns only issues. + * @param offset the offset for pagination + * @param limit the limit for pagination + * @param repos Tuple of the repository owner and the repository name + * @return the search result (list of tuples which contain issue, labels and comment count) + */ + def searchIssue(condition: IssueSearchCondition, pullRequest: Boolean, offset: Int, limit: Int, repos: (String, String)*) + (implicit s: Session): List[IssueInfo] = { + + // get issues and comment count and labels + searchIssueQuery(repos, condition, pullRequest) + .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } + .sortBy { case (t1, t2) => + (condition.sort match { + case "created" => t1.registeredDate + case "comments" => t2.commentCount + case "updated" => t1.updatedDate + }) match { + case sort => condition.direction match { + case "asc" => sort asc + case "desc" => sort desc + } + } + } + .drop(offset).take(limit) + .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } + .leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) } + .leftJoin (Milestones) .on { case ((((t1, t2), t3), t4), t5) => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) } + .map { case ((((t1, t2), t3), t4), t5) => + (t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?, t5.title.?) + } + .list + .splitWith { (c1, c2) => + c1._1.userName == c2._1.userName && + c1._1.repositoryName == c2._1.repositoryName && + c1._1.issueId == c2._1.issueId + } + .map { issues => issues.head match { + case (issue, commentCount, _, _, _, milestone) => + IssueInfo(issue, + issues.flatMap { t => t._3.map ( + Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get) + )} toList, + milestone, + commentCount) + }} toList + } + + /** + * Assembles query for conditional issue searching. + */ + private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, pullRequest: Boolean)(implicit s: Session) = + Issues filter { t1 => + repos + .map { case (owner, repository) => t1.byRepository(owner, repository) } + .foldLeft[Column[Boolean]](false) ( _ || _ ) && + (t1.closed === (condition.state == "closed").bind) && + //(t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) && + (t1.milestoneId.? isEmpty, condition.milestone == Some(None)) && + (t1.assignedUserName === condition.assigned.get.bind, condition.assigned.isDefined) && + (t1.openedUserName === condition.author.get.bind, condition.author.isDefined) && + (t1.pullRequest === pullRequest.bind) && + // Milestone filter + (Milestones filter { t2 => + (t2.byPrimaryKey(t1.userName, t1.repositoryName, t1.milestoneId)) && + (t2.title === condition.milestone.get.get.bind) + } exists, condition.milestone.flatten.isDefined) && + // Label filter + (IssueLabels filter { t2 => + (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && + (t2.labelId in + (Labels filter { t3 => + (t3.byRepository(t1.userName, t1.repositoryName)) && + (t3.labelName inSetBind condition.labels) + } map(_.labelId))) + } exists, condition.labels.nonEmpty) && + // Visibility filter + (Repositories filter { t2 => + (t2.byRepository(t1.userName, t1.repositoryName)) && + (t2.isPrivate === (condition.visibility == Some("private")).bind) + } exists, condition.visibility.nonEmpty) && + // Organization (group) filter + (t1.userName inSetBind condition.groups, condition.groups.nonEmpty) && + // Mentioned filter + ((t1.openedUserName === condition.mentioned.get.bind) || t1.assignedUserName === condition.mentioned.get.bind || + (IssueComments filter { t2 => + (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === condition.mentioned.get.bind) + } exists), condition.mentioned.isDefined) + } + + def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String], + assignedUserName: Option[String], milestoneId: Option[Int], + isPullRequest: Boolean = false)(implicit s: Session) = + // next id number + sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int] + .firstOption.filter { id => + Issues insert Issue( + owner, + repository, + id, + loginUser, + milestoneId, + assignedUserName, + title, + content, + false, + currentDate, + currentDate, + isPullRequest) + + // increment issue id + IssueId + .filter (_.byPrimaryKey(owner, repository)) + .map (_.issueId) + .update (id) > 0 + } get + + def registerIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) = + IssueLabels insert IssueLabel(owner, repository, issueId, labelId) + + def deleteIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) = + IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete + + def createComment(owner: String, repository: String, loginUser: String, + issueId: Int, content: String, action: String)(implicit s: Session): Int = + IssueComments.autoInc insert IssueComment( + userName = owner, + repositoryName = repository, + issueId = issueId, + action = action, + commentedUserName = loginUser, + content = content, + registeredDate = currentDate, + updatedDate = currentDate) + + def updateIssue(owner: String, repository: String, issueId: Int, + title: String, content: Option[String])(implicit s: Session) = + Issues + .filter (_.byPrimaryKey(owner, repository, issueId)) + .map { t => + (t.title, t.content.?, t.updatedDate) + } + .update (title, content, currentDate) + + def updateAssignedUserName(owner: String, repository: String, issueId: Int, + assignedUserName: Option[String])(implicit s: Session) = + Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName) + + def updateMilestoneId(owner: String, repository: String, issueId: Int, + milestoneId: Option[Int])(implicit s: Session) = + Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId) + + def updateComment(commentId: Int, content: String)(implicit s: Session) = + IssueComments + .filter (_.byPrimaryKey(commentId)) + .map { t => + t.content -> t.updatedDate + } + .update (content, currentDate) + + def deleteComment(commentId: Int)(implicit s: Session) = + IssueComments filter (_.byPrimaryKey(commentId)) delete + + def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean)(implicit s: Session) = + Issues + .filter (_.byPrimaryKey(owner, repository, issueId)) + .map { t => + t.closed -> t.updatedDate + } + .update (closed, currentDate) + + /** + * Search issues by keyword. + * + * @param owner the repository owner + * @param repository the repository name + * @param query the keywords separated by whitespace. + * @return issues with comment count and matched content of issue or comment + */ + def searchIssuesByKeyword(owner: String, repository: String, query: String) + (implicit s: Session): List[(Issue, Int, String)] = { + import slick.driver.JdbcDriver.likeEncode + val keywords = splitWords(query.toLowerCase) + + // Search Issue + val issues = Issues + .filter(_.byRepository(owner, repository)) + .innerJoin(IssueOutline).on { case (t1, t2) => + t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) + } + .filter { case (t1, t2) => + keywords.map { keyword => + (t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) || + (t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) + } .reduceLeft(_ && _) + } + .map { case (t1, t2) => + (t1, 0, t1.content.?, t2.commentCount) + } + + // Search IssueComment + val comments = IssueComments + .filter(_.byRepository(owner, repository)) + .innerJoin(Issues).on { case (t1, t2) => + t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) + } + .innerJoin(IssueOutline).on { case ((t1, t2), t3) => + t2.byIssue(t3.userName, t3.repositoryName, t3.issueId) + } + .filter { case ((t1, t2), t3) => + keywords.map { query => + t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^') + }.reduceLeft(_ && _) + } + .map { case ((t1, t2), t3) => + (t2, t1.commentId, t1.content.?, t3.commentCount) + } + + issues.union(comments).sortBy { case (issue, commentId, _, _) => + issue.issueId -> commentId + }.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) => + issue1.issueId == issue2.issueId + }.map { _.head match { + case (issue, _, content, commentCount) => (issue, commentCount, content.getOrElse("")) + } + }.toList + } + + def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String)(implicit s: Session) = { + extractCloseId(message).foreach { issueId => + for(issue <- getIssue(owner, repository, issueId) if !issue.closed){ + createComment(owner, repository, userName, issue.issueId, "Close", "close") + updateClosed(owner, repository, issue.issueId, true) + } + } + } +} + +object IssuesService { + import javax.servlet.http.HttpServletRequest + + val IssueLimit = 30 + + case class IssueSearchCondition( + labels: Set[String] = Set.empty, + milestone: Option[Option[String]] = None, + author: Option[String] = None, + assigned: Option[String] = None, + mentioned: Option[String] = None, + state: String = "open", + sort: String = "created", + direction: String = "desc", + visibility: Option[String] = None, + groups: Set[String] = Set.empty){ + + def isEmpty: Boolean = { + labels.isEmpty && milestone.isEmpty && author.isEmpty && assigned.isEmpty && + state == "open" && sort == "created" && direction == "desc" && visibility.isEmpty + } + + def nonEmpty: Boolean = !isEmpty + + def toFilterString: String = ( + List( + Some(s"is:${state}"), + author.map(author => s"author:${author}"), + assigned.map(assignee => s"assignee:${assignee}"), + mentioned.map(mentioned => s"mentions:${mentioned}") + ).flatten ++ + labels.map(label => s"label:${label}") ++ + List( + milestone.map { _ match { + case Some(x) => s"milestone:${x}" + case None => "no:milestone" + }}, + (sort, direction) match { + case ("created" , "desc") => None + case ("created" , "asc" ) => Some("sort:created-asc") + case ("comments", "desc") => Some("sort:comments-desc") + case ("comments", "asc" ) => Some("sort:comments-asc") + case ("updated" , "desc") => Some("sort:updated-desc") + case ("updated" , "asc" ) => Some("sort:updated-asc") + }, + visibility.map(visibility => s"visibility:${visibility}") + ).flatten ++ + groups.map(group => s"group:${group}") + ).mkString(" ") + + def toURL: String = + "?" + List( + if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))), + milestone.map { _ match { + case Some(x) => "milestone=" + urlEncode(x) + case None => "milestone=none" + }}, + author .map(x => "author=" + urlEncode(x)), + assigned .map(x => "assigned=" + urlEncode(x)), + mentioned.map(x => "mentioned=" + urlEncode(x)), + Some("state=" + urlEncode(state)), + Some("sort=" + urlEncode(sort)), + Some("direction=" + urlEncode(direction)), + visibility.map(x => "visibility=" + urlEncode(x)), + if(groups.isEmpty) None else Some("groups=" + urlEncode(groups.mkString(","))) + ).flatten.mkString("&") + + } + + object IssueSearchCondition { + + private def param(request: HttpServletRequest, name: String, allow: Seq[String] = Nil): Option[String] = { + val value = request.getParameter(name) + if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value) + } + + /** + * Restores IssueSearchCondition instance from filter query. + */ + def apply(filter: String, milestones: Map[String, Int]): IssueSearchCondition = { + val conditions = filter.split("[  \t]+").map { x => + val dim = x.split(":") + dim(0) -> dim(1) + }.groupBy(_._1).map { case (key, values) => + key -> values.map(_._2).toSeq + } + + val (sort, direction) = conditions.get("sort").flatMap(_.headOption).getOrElse("created-desc") match { + case "created-asc" => ("created" , "asc" ) + case "comments-desc" => ("comments", "desc") + case "comments-asc" => ("comments", "asc" ) + case "updated-desc" => ("comments", "desc") + case "updated-asc" => ("comments", "asc" ) + case _ => ("created" , "desc") + } + + IssueSearchCondition( + conditions.get("label").map(_.toSet).getOrElse(Set.empty), + conditions.get("milestone").flatMap(_.headOption) match { + case None => None + case Some("none") => Some(None) + case Some(x) => Some(Some(x)) //milestones.get(x).map(x => Some(x)) + }, + conditions.get("author").flatMap(_.headOption), + conditions.get("assignee").flatMap(_.headOption), + conditions.get("mentions").flatMap(_.headOption), + conditions.get("is").getOrElse(Seq.empty).find(x => x == "open" || x == "closed").getOrElse("open"), + sort, + direction, + conditions.get("visibility").flatMap(_.headOption), + conditions.get("group").map(_.toSet).getOrElse(Set.empty) + ) + } + + /** + * Restores IssueSearchCondition instance from request parameters. + */ + def apply(request: HttpServletRequest): IssueSearchCondition = + IssueSearchCondition( + param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty), + param(request, "milestone").map { + case "none" => None + case x => Some(x) + }, + param(request, "author"), + param(request, "assigned"), + param(request, "mentioned"), + param(request, "state", Seq("open", "closed")).getOrElse("open"), + param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"), + param(request, "direction", Seq("asc", "desc")).getOrElse("desc"), + param(request, "visibility"), + param(request, "groups").map(_.split(",").toSet).getOrElse(Set.empty) + ) + + def page(request: HttpServletRequest) = try { + val i = param(request, "page").getOrElse("1").toInt + if(i <= 0) 1 else i + } catch { + case e: NumberFormatException => 1 + } + } + + case class IssueInfo(issue: Issue, labels: List[Label], milestone: Option[String], commentCount: Int) + +} diff --git a/src/main/scala/gitbucket/core/service/LabelsService.scala b/src/main/scala/gitbucket/core/service/LabelsService.scala new file mode 100644 index 0000000..35b5d2d --- /dev/null +++ b/src/main/scala/gitbucket/core/service/LabelsService.scala @@ -0,0 +1,34 @@ +package gitbucket.core.service + +import gitbucket.core.model.Label +import gitbucket.core.model.Profile._ +import profile.simple._ + +trait LabelsService { + + def getLabels(owner: String, repository: String)(implicit s: Session): List[Label] = + Labels.filter(_.byRepository(owner, repository)).sortBy(_.labelName asc).list + + def getLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Option[Label] = + Labels.filter(_.byPrimaryKey(owner, repository, labelId)).firstOption + + def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Int = + Labels returning Labels.map(_.labelId) += Label( + userName = owner, + repositoryName = repository, + labelName = labelName, + color = color + ) + + def updateLabel(owner: String, repository: String, labelId: Int, labelName: String, color: String) + (implicit s: Session): Unit = + Labels.filter(_.byPrimaryKey(owner, repository, labelId)) + .map(t => t.labelName -> t.color) + .update(labelName, color) + + def deleteLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Unit = { + IssueLabels.filter(_.byLabel(owner, repository, labelId)).delete + Labels.filter(_.byPrimaryKey(owner, repository, labelId)).delete + } + +} diff --git a/src/main/scala/gitbucket/core/service/MilestonesService.scala b/src/main/scala/gitbucket/core/service/MilestonesService.scala new file mode 100644 index 0000000..691ca2e --- /dev/null +++ b/src/main/scala/gitbucket/core/service/MilestonesService.scala @@ -0,0 +1,57 @@ +package gitbucket.core.service + +import gitbucket.core.model.Milestone +import gitbucket.core.model.Profile._ +import profile.simple._ +// TODO Why is direct import required? +import gitbucket.core.model.Profile.dateColumnType + +trait MilestonesService { + + def createMilestone(owner: String, repository: String, title: String, description: Option[String], + dueDate: Option[java.util.Date])(implicit s: Session): Unit = + Milestones insert Milestone( + userName = owner, + repositoryName = repository, + title = title, + description = description, + dueDate = dueDate, + closedDate = None + ) + + def updateMilestone(milestone: Milestone)(implicit s: Session): Unit = + Milestones + .filter (t => t.byPrimaryKey(milestone.userName, milestone.repositoryName, milestone.milestoneId)) + .map (t => (t.title, t.description.?, t.dueDate.?, t.closedDate.?)) + .update (milestone.title, milestone.description, milestone.dueDate, milestone.closedDate) + + def openMilestone(milestone: Milestone)(implicit s: Session): Unit = + updateMilestone(milestone.copy(closedDate = None)) + + def closeMilestone(milestone: Milestone)(implicit s: Session): Unit = + updateMilestone(milestone.copy(closedDate = Some(currentDate))) + + def deleteMilestone(owner: String, repository: String, milestoneId: Int)(implicit s: Session): Unit = { + Issues.filter(_.byMilestone(owner, repository, milestoneId)).map(_.milestoneId.?).update(None) + Milestones.filter(_.byPrimaryKey(owner, repository, milestoneId)).delete + } + + def getMilestone(owner: String, repository: String, milestoneId: Int)(implicit s: Session): Option[Milestone] = + Milestones.filter(_.byPrimaryKey(owner, repository, milestoneId)).firstOption + + def getMilestonesWithIssueCount(owner: String, repository: String)(implicit s: Session): List[(Milestone, Int, Int)] = { + val counts = Issues + .filter { t => (t.byRepository(owner, repository)) && (t.milestoneId.? isDefined) } + .groupBy { t => t.milestoneId -> t.closed } + .map { case (t1, t2) => t1._1 -> t1._2 -> t2.length } + .toMap + + getMilestones(owner, repository).map { milestone => + (milestone, counts.getOrElse((milestone.milestoneId, false), 0), counts.getOrElse((milestone.milestoneId, true), 0)) + } + } + + def getMilestones(owner: String, repository: String)(implicit s: Session): List[Milestone] = + Milestones.filter(_.byRepository(owner, repository)).sortBy(_.milestoneId asc).list + +} diff --git a/src/main/scala/gitbucket/core/service/PluginService.scala b/src/main/scala/gitbucket/core/service/PluginService.scala new file mode 100644 index 0000000..99a20d8 --- /dev/null +++ b/src/main/scala/gitbucket/core/service/PluginService.scala @@ -0,0 +1,24 @@ +package gitbucket.core.service + +import gitbucket.core.model.Plugin +import gitbucket.core.model.Profile._ +import profile.simple._ + +trait PluginService { + + def getPlugins()(implicit s: Session): List[Plugin] = + Plugins.sortBy(_.pluginId).list + + def registerPlugin(plugin: Plugin)(implicit s: Session): Unit = + Plugins.insert(plugin) + + def updatePlugin(plugin: Plugin)(implicit s: Session): Unit = + Plugins.filter(_.pluginId === plugin.pluginId.bind).map(_.version).update(plugin.version) + + def deletePlugin(pluginId: String)(implicit s: Session): Unit = + Plugins.filter(_.pluginId === pluginId.bind).delete + + def getPlugin(pluginId: String)(implicit s: Session): Option[Plugin] = + Plugins.filter(_.pluginId === pluginId.bind).firstOption + +} diff --git a/src/main/scala/gitbucket/core/service/PullRequestService.scala b/src/main/scala/gitbucket/core/service/PullRequestService.scala new file mode 100644 index 0000000..456980a --- /dev/null +++ b/src/main/scala/gitbucket/core/service/PullRequestService.scala @@ -0,0 +1,125 @@ +package gitbucket.core.service + +import gitbucket.core.model.{Issue, PullRequest} +import gitbucket.core.model.Profile._ +import gitbucket.core.util.JGitUtil +import profile.simple._ + +trait PullRequestService { self: IssuesService => + import PullRequestService._ + + def getPullRequest(owner: String, repository: String, issueId: Int) + (implicit s: Session): Option[(Issue, PullRequest)] = + getIssue(owner, repository, issueId.toString).flatMap{ issue => + PullRequests.filter(_.byPrimaryKey(owner, repository, issueId)).firstOption.map{ + pullreq => (issue, pullreq) + } + } + + def updateCommitId(owner: String, repository: String, issueId: Int, commitIdTo: String, commitIdFrom: String) + (implicit s: Session): Unit = + PullRequests.filter(_.byPrimaryKey(owner, repository, issueId)) + .map(pr => pr.commitIdTo -> pr.commitIdFrom) + .update((commitIdTo, commitIdFrom)) + + def getPullRequestCountGroupByUser(closed: Boolean, owner: Option[String], repository: Option[String]) + (implicit s: Session): List[PullRequestCount] = + PullRequests + .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) } + .filter { case (t1, t2) => + (t2.closed === closed.bind) && + (t1.userName === owner.get.bind, owner.isDefined) && + (t1.repositoryName === repository.get.bind, repository.isDefined) + } + .groupBy { case (t1, t2) => t2.openedUserName } + .map { case (userName, t) => userName -> t.length } + .sortBy(_._2 desc) + .list + .map { x => PullRequestCount(x._1, x._2) } + +// def getAllPullRequestCountGroupByUser(closed: Boolean, userName: String)(implicit s: Session): List[PullRequestCount] = +// PullRequests +// .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) } +// .innerJoin(Repositories).on { case ((t1, t2), t3) => t2.byRepository(t3.userName, t3.repositoryName) } +// .filter { case ((t1, t2), t3) => +// (t2.closed === closed.bind) && +// ( +// (t3.isPrivate === false.bind) || +// (t3.userName === userName.bind) || +// (Collaborators.filter { t4 => t4.byRepository(t3.userName, t3.repositoryName) && (t4.collaboratorName === userName.bind)} exists) +// ) +// } +// .groupBy { case ((t1, t2), t3) => t2.openedUserName } +// .map { case (userName, t) => userName -> t.length } +// .sortBy(_._2 desc) +// .list +// .map { x => PullRequestCount(x._1, x._2) } + + def createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int, + originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String, + commitIdFrom: String, commitIdTo: String)(implicit s: Session): Unit = + PullRequests insert PullRequest( + originUserName, + originRepositoryName, + issueId, + originBranch, + requestUserName, + requestRepositoryName, + requestBranch, + commitIdFrom, + commitIdTo) + + def getPullRequestsByRequest(userName: String, repositoryName: String, branch: String, closed: Boolean) + (implicit s: Session): List[PullRequest] = + PullRequests + .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) } + .filter { case (t1, t2) => + (t1.requestUserName === userName.bind) && + (t1.requestRepositoryName === repositoryName.bind) && + (t1.requestBranch === branch.bind) && + (t2.closed === closed.bind) + } + .map { case (t1, t2) => t1 } + .list + + /** + * Fetch pull request contents into refs/pull/${issueId}/head and update pull request table. + */ + def updatePullRequests(owner: String, repository: String, branch: String)(implicit s: Session): Unit = + getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq => + if(Repositories.filter(_.byRepository(pullreq.userName, pullreq.repositoryName)).exists.run){ + val (commitIdTo, commitIdFrom) = JGitUtil.updatePullRequest( + pullreq.userName, pullreq.repositoryName, pullreq.branch, pullreq.issueId, + pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch) + updateCommitId(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo, commitIdFrom) + } + } + + def getPullRequestByRequestCommit(userName: String, repositoryName: String, toBranch:String, fromBranch: String, commitId: String) + (implicit s: Session): Option[(PullRequest, Issue)] = { + if(toBranch == fromBranch){ + None + } else { + PullRequests + .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) } + .filter { case (t1, t2) => + (t1.userName === userName.bind) && + (t1.repositoryName === repositoryName.bind) && + (t1.branch === toBranch.bind) && + (t1.requestUserName === userName.bind) && + (t1.requestRepositoryName === repositoryName.bind) && + (t1.requestBranch === fromBranch.bind) && + (t1.commitIdTo === commitId.bind) + } + .firstOption + } + } +} + +object PullRequestService { + + val PullRequestLimit = 25 + + case class PullRequestCount(userName: String, count: Int) + +} diff --git a/src/main/scala/gitbucket/core/service/RepositorySearchService.scala b/src/main/scala/gitbucket/core/service/RepositorySearchService.scala new file mode 100644 index 0000000..84e94e4 --- /dev/null +++ b/src/main/scala/gitbucket/core/service/RepositorySearchService.scala @@ -0,0 +1,130 @@ +package gitbucket.core.service + +import gitbucket.core.model.Issue +import gitbucket.core.util._ +import gitbucket.core.util.StringUtil +import Directory._ +import ControlUtil._ +import org.eclipse.jgit.revwalk.RevWalk +import org.eclipse.jgit.treewalk.TreeWalk +import org.eclipse.jgit.lib.FileMode +import org.eclipse.jgit.api.Git +import gitbucket.core.model.Profile._ +import profile.simple._ + +trait RepositorySearchService { self: IssuesService => + import RepositorySearchService._ + + def countIssues(owner: String, repository: String, query: String)(implicit session: Session): Int = + searchIssuesByKeyword(owner, repository, query).length + + def searchIssues(owner: String, repository: String, query: String)(implicit session: Session): List[IssueSearchResult] = + searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) => + IssueSearchResult( + issue.issueId, + issue.isPullRequest, + issue.title, + issue.openedUserName, + issue.registeredDate, + commentCount, + getHighlightText(content, query)._1) + } + + def countFiles(owner: String, repository: String, query: String): Int = + using(Git.open(getRepositoryDir(owner, repository))){ git => + if(JGitUtil.isEmpty(git)) 0 else searchRepositoryFiles(git, query).length + } + + def searchFiles(owner: String, repository: String, query: String): List[FileSearchResult] = + using(Git.open(getRepositoryDir(owner, repository))){ git => + if(JGitUtil.isEmpty(git)){ + Nil + } else { + val files = searchRepositoryFiles(git, query) + val commits = JGitUtil.getLatestCommitFromPaths(git, files.map(_._1), "HEAD") + files.map { case (path, text) => + val (highlightText, lineNumber) = getHighlightText(text, query) + FileSearchResult( + path, + commits(path).getCommitterIdent.getWhen, + highlightText, + lineNumber) + } + } + } + + private def searchRepositoryFiles(git: Git, query: String): List[(String, String)] = { + val revWalk = new RevWalk(git.getRepository) + val objectId = git.getRepository.resolve("HEAD") + val revCommit = revWalk.parseCommit(objectId) + val treeWalk = new TreeWalk(git.getRepository) + treeWalk.setRecursive(true) + treeWalk.addTree(revCommit.getTree) + + val keywords = StringUtil.splitWords(query.toLowerCase) + val list = new scala.collection.mutable.ListBuffer[(String, String)] + + while (treeWalk.next()) { + val mode = treeWalk.getFileMode(0) + if(mode == FileMode.REGULAR_FILE || mode == FileMode.EXECUTABLE_FILE){ + JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).foreach { bytes => + if(FileUtil.isText(bytes)){ + val text = StringUtil.convertFromByteArray(bytes) + val lowerText = text.toLowerCase + val indices = keywords.map(lowerText.indexOf _) + if(!indices.exists(_ < 0)){ + list.append((treeWalk.getPathString, text)) + } + } + } + } + } + treeWalk.release + revWalk.release + + list.toList + } + +} + +object RepositorySearchService { + + val CodeLimit = 10 + val IssueLimit = 10 + + def getHighlightText(content: String, query: String): (String, Int) = { + val keywords = StringUtil.splitWords(query.toLowerCase) + val lowerText = content.toLowerCase + val indices = keywords.map(lowerText.indexOf _) + + if(!indices.exists(_ < 0)){ + val lineNumber = content.substring(0, indices.min).split("\n").size - 1 + val highlightText = StringUtil.escapeHtml(content.split("\n").drop(lineNumber).take(5).mkString("\n")) + .replaceAll("(?i)(" + keywords.map("\\Q" + _ + "\\E").mkString("|") + ")", + "$1") + (highlightText, lineNumber + 1) + } else { + (content.split("\n").take(5).mkString("\n"), 1) + } + } + + case class SearchResult( + files : List[(String, String)], + issues: List[(Issue, Int, String)]) + + case class IssueSearchResult( + issueId: Int, + isPullRequest: Boolean, + title: String, + openedUserName: String, + registeredDate: java.util.Date, + commentCount: Int, + highlightText: String) + + case class FileSearchResult( + path: String, + lastModified: java.util.Date, + highlightText: String, + highlightLineNumber: Int) + +} diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala new file mode 100644 index 0000000..ef99da1 --- /dev/null +++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala @@ -0,0 +1,399 @@ +package gitbucket.core.service + +import gitbucket.core.model.{Collaborator, Repository, Account} +import gitbucket.core.model.Profile._ +import gitbucket.core.util.JGitUtil +import profile.simple._ + +trait RepositoryService { self: AccountService => + import RepositoryService._ + + /** + * Creates a new repository. + * + * @param repositoryName the repository name + * @param userName the user name of the repository owner + * @param description the repository description + * @param isPrivate the repository type (private is true, otherwise false) + * @param originRepositoryName specify for the forked repository. (default is None) + * @param originUserName specify for the forked repository. (default is None) + */ + def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean, + originRepositoryName: Option[String] = None, originUserName: Option[String] = None, + parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None) + (implicit s: Session): Unit = { + Repositories insert + Repository( + userName = userName, + repositoryName = repositoryName, + isPrivate = isPrivate, + description = description, + defaultBranch = "master", + registeredDate = currentDate, + updatedDate = currentDate, + lastActivityDate = currentDate, + originUserName = originUserName, + originRepositoryName = originRepositoryName, + parentUserName = parentUserName, + parentRepositoryName = parentRepositoryName) + + IssueId insert (userName, repositoryName, 0) + } + + def renameRepository(oldUserName: String, oldRepositoryName: String, newUserName: String, newRepositoryName: String) + (implicit s: Session): Unit = { + getAccountByUserName(newUserName).foreach { account => + (Repositories filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository => + Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName) + + val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val issueComments = IssueComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val commitComments = CommitComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list + val collaborators = Collaborators .filter(_.byRepository(oldUserName, oldRepositoryName)).list + + Repositories.filter { t => + (t.originUserName === oldUserName.bind) && (t.originRepositoryName === oldRepositoryName.bind) + }.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName) + + Repositories.filter { t => + (t.parentUserName === oldUserName.bind) && (t.parentRepositoryName === oldRepositoryName.bind) + }.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName) + + PullRequests.filter { t => + t.requestRepositoryName === oldRepositoryName.bind + }.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName) + + // Updates activity fk before deleting repository because activity is sorted by activityId + // and it can't be changed by deleting-and-inserting record. + Activities.filter(_.byRepository(oldUserName, oldRepositoryName)).list.foreach { activity => + Activities.filter(_.activityId === activity.activityId.bind) + .map(x => (x.userName, x.repositoryName)).update(newUserName, newRepositoryName) + } + + deleteRepository(oldUserName, oldRepositoryName) + + WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + Milestones.insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*) + + val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list + Issues.insertAll(issues.map { x => x.copy( + userName = newUserName, + repositoryName = newRepositoryName, + milestoneId = x.milestoneId.map { id => + newMilestones.find(_.title == milestones.find(_.milestoneId == id).get.title).get.milestoneId + } + )} :_*) + + PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + + // Convert labelId + val oldLabelMap = labels.map(x => (x.labelId, x.labelName)).toMap + val newLabelMap = Labels.filter(_.byRepository(newUserName, newRepositoryName)).map(x => (x.labelName, x.labelId)).list.toMap + IssueLabels.insertAll(issueLabels.map(x => x.copy( + labelId = newLabelMap(oldLabelMap(x.labelId)), + userName = newUserName, + repositoryName = newRepositoryName + )) :_*) + + if(account.isGroupAccount){ + Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*) + } else { + Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + } + + // Update activity messages + Activities.filter { t => + (t.message like s"%:${oldUserName}/${oldRepositoryName}]%") || + (t.message like s"%:${oldUserName}/${oldRepositoryName}#%") || + (t.message like s"%:${oldUserName}/${oldRepositoryName}@%") + }.map { t => t.activityId -> t.message }.list.foreach { case (activityId, message) => + Activities.filter(_.activityId === activityId.bind).map(_.message).update( + message + .replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]") + .replace(s"[branch:${oldUserName}/${oldRepositoryName}#" ,s"[branch:${newUserName}/${newRepositoryName}#") + .replace(s"[tag:${oldUserName}/${oldRepositoryName}#" ,s"[tag:${newUserName}/${newRepositoryName}#") + .replace(s"[pullreq:${oldUserName}/${oldRepositoryName}#",s"[pullreq:${newUserName}/${newRepositoryName}#") + .replace(s"[issue:${oldUserName}/${oldRepositoryName}#" ,s"[issue:${newUserName}/${newRepositoryName}#") + .replace(s"[commit:${oldUserName}/${oldRepositoryName}@" ,s"[commit:${newUserName}/${newRepositoryName}@") + ) + } + } + } + } + + def deleteRepository(userName: String, repositoryName: String)(implicit s: Session): Unit = { + Activities .filter(_.byRepository(userName, repositoryName)).delete + Collaborators .filter(_.byRepository(userName, repositoryName)).delete + CommitComments.filter(_.byRepository(userName, repositoryName)).delete + IssueLabels .filter(_.byRepository(userName, repositoryName)).delete + Labels .filter(_.byRepository(userName, repositoryName)).delete + IssueComments .filter(_.byRepository(userName, repositoryName)).delete + PullRequests .filter(_.byRepository(userName, repositoryName)).delete + Issues .filter(_.byRepository(userName, repositoryName)).delete + IssueId .filter(_.byRepository(userName, repositoryName)).delete + Milestones .filter(_.byRepository(userName, repositoryName)).delete + WebHooks .filter(_.byRepository(userName, repositoryName)).delete + Repositories .filter(_.byRepository(userName, repositoryName)).delete + + // Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME + Repositories + .filter { x => (x.originUserName === userName.bind) && (x.originRepositoryName === repositoryName.bind) } + .map { x => (x.userName, x.repositoryName) } + .list + .foreach { case (userName, repositoryName) => + Repositories + .filter(_.byRepository(userName, repositoryName)) + .map(x => (x.originUserName?, x.originRepositoryName?)) + .update(None, None) + } + + // Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME + Repositories + .filter { x => (x.parentUserName === userName.bind) && (x.parentRepositoryName === repositoryName.bind) } + .map { x => (x.userName, x.repositoryName) } + .list + .foreach { case (userName, repositoryName) => + Repositories + .filter(_.byRepository(userName, repositoryName)) + .map(x => (x.parentUserName?, x.parentRepositoryName?)) + .update(None, None) + } + } + + /** + * Returns the repository names of the specified user. + * + * @param userName the user name of repository owner + * @return the list of repository names + */ + def getRepositoryNamesOfUser(userName: String)(implicit s: Session): List[String] = + Repositories filter(_.userName === userName.bind) map (_.repositoryName) list + + /** + * Returns the specified repository information. + * + * @param userName the user name of the repository owner + * @param repositoryName the repository name + * @param baseUrl the base url of this application + * @return the repository information + */ + def getRepository(userName: String, repositoryName: String, baseUrl: String)(implicit s: Session): Option[RepositoryInfo] = { + (Repositories filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository => + // for getting issue count and pull request count + val issues = Issues.filter { t => + t.byRepository(repository.userName, repository.repositoryName) && (t.closed === false.bind) + }.map(_.pullRequest).list + + new RepositoryInfo( + JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), + repository, + issues.count(_ == false), + issues.count(_ == true), + getForkedCount( + repository.originUserName.getOrElse(repository.userName), + repository.originRepositoryName.getOrElse(repository.repositoryName) + ), + getRepositoryManagers(repository.userName)) + } + } + + /** + * Returns the repositories without private repository that user does not have access right. + * Include public repository, private own repository and private but collaborator repository. + * + * @param userName the user name of collaborator + * @return the repository infomation list + */ + def getAllRepositories(userName: String)(implicit s: Session): List[(String, String)] = { + Repositories.filter { t1 => + (t1.isPrivate === false.bind) || + (t1.userName === userName.bind) || + (Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName === userName.bind)} exists) + }.sortBy(_.lastActivityDate desc).map{ t => + (t.userName, t.repositoryName) + }.list + } + + def getUserRepositories(userName: String, baseUrl: String, withoutPhysicalInfo: Boolean = false) + (implicit s: Session): List[RepositoryInfo] = { + Repositories.filter { t1 => + (t1.userName === userName.bind) || + (Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName === userName.bind)} exists) + }.sortBy(_.lastActivityDate desc).list.map{ repository => + new RepositoryInfo( + if(withoutPhysicalInfo){ + new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl) + } else { + JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl) + }, + repository, + getForkedCount( + repository.originUserName.getOrElse(repository.userName), + repository.originRepositoryName.getOrElse(repository.repositoryName) + ), + getRepositoryManagers(repository.userName)) + } + } + + /** + * Returns the list of visible repositories for the specified user. + * If repositoryUserName is given then filters results by repository owner. + * + * @param loginAccount the logged in account + * @param baseUrl the base url of this application + * @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user) + * @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count, + * branches and tags + * @return the repository information which is sorted in descending order of lastActivityDate. + */ + def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None, + withoutPhysicalInfo: Boolean = false) + (implicit s: Session): List[RepositoryInfo] = { + (loginAccount match { + // for Administrators + case Some(x) if(x.isAdmin) => Repositories + // for Normal Users + case Some(x) if(!x.isAdmin) => + Repositories filter { t => (t.isPrivate === false.bind) || (t.userName === x.userName) || + (Collaborators.filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName === x.userName.bind)} exists) + } + // for Guests + case None => Repositories filter(_.isPrivate === false.bind) + }).filter { t => + repositoryUserName.map { userName => t.userName === userName.bind } getOrElse LiteralColumn(true) + }.sortBy(_.lastActivityDate desc).list.map{ repository => + new RepositoryInfo( + if(withoutPhysicalInfo){ + new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl) + } else { + JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl) + }, + repository, + getForkedCount( + repository.originUserName.getOrElse(repository.userName), + repository.originRepositoryName.getOrElse(repository.repositoryName) + ), + getRepositoryManagers(repository.userName)) + } + } + + private def getRepositoryManagers(userName: String)(implicit s: Session): Seq[String] = + if(getAccountByUserName(userName).exists(_.isGroupAccount)){ + getGroupMembers(userName).collect { case x if(x.isManager) => x.userName } + } else { + Seq(userName) + } + + /** + * Updates the last activity date of the repository. + */ + def updateLastActivityDate(userName: String, repositoryName: String)(implicit s: Session): Unit = + Repositories.filter(_.byRepository(userName, repositoryName)).map(_.lastActivityDate).update(currentDate) + + /** + * Save repository options. + */ + def saveRepositoryOptions(userName: String, repositoryName: String, + description: Option[String], defaultBranch: String, isPrivate: Boolean)(implicit s: Session): Unit = + Repositories.filter(_.byRepository(userName, repositoryName)) + .map { r => (r.description.?, r.defaultBranch, r.isPrivate, r.updatedDate) } + .update (description, defaultBranch, isPrivate, currentDate) + + /** + * Add collaborator to the repository. + * + * @param userName the user name of the repository owner + * @param repositoryName the repository name + * @param collaboratorName the collaborator name + */ + def addCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit = + Collaborators insert Collaborator(userName, repositoryName, collaboratorName) + + /** + * Remove collaborator from the repository. + * + * @param userName the user name of the repository owner + * @param repositoryName the repository name + * @param collaboratorName the collaborator name + */ + def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit = + Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete + + /** + * Remove all collaborators from the repository. + * + * @param userName the user name of the repository owner + * @param repositoryName the repository name + */ + def removeCollaborators(userName: String, repositoryName: String)(implicit s: Session): Unit = + Collaborators.filter(_.byRepository(userName, repositoryName)).delete + + /** + * Returns the list of collaborators name which is sorted with ascending order. + * + * @param userName the user name of the repository owner + * @param repositoryName the repository name + * @return the list of collaborators name + */ + def getCollaborators(userName: String, repositoryName: String)(implicit s: Session): List[String] = + Collaborators.filter(_.byRepository(userName, repositoryName)).sortBy(_.collaboratorName).map(_.collaboratorName).list + + def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = { + loginAccount match { + case Some(a) if(a.isAdmin) => true + case Some(a) if(a.userName == owner) => true + case Some(a) if(getCollaborators(owner, repository).contains(a.userName)) => true + case _ => false + } + } + + private def getForkedCount(userName: String, repositoryName: String)(implicit s: Session): Int = + Query(Repositories.filter { t => + (t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind) + }.length).first + + + def getForkedRepositories(userName: String, repositoryName: String)(implicit s: Session): List[(String, String)] = + Repositories.filter { t => + (t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind) + } + .sortBy(_.userName asc).map(t => t.userName -> t.repositoryName).list + +} + +object RepositoryService { + + case class RepositoryInfo(owner: String, name: String, httpUrl: String, repository: Repository, + issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int, + branchList: Seq[String], tags: Seq[JGitUtil.TagInfo], managers: Seq[String]){ + + lazy val host = """^https?://(.+?)(:\d+)?/""".r.findFirstMatchIn(httpUrl).get.group(1) + + def sshUrl(port: Int, userName: String) = s"ssh://${userName}@${host}:${port}/${owner}/${name}.git" + + /** + * Creates instance with issue count and pull request count. + */ + def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) = + this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers) + + /** + * Creates instance without issue count and pull request count. + */ + def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) = + this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers) + } + + case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode]) + +} diff --git a/src/main/scala/gitbucket/core/service/RequestCache.scala b/src/main/scala/gitbucket/core/service/RequestCache.scala new file mode 100644 index 0000000..768a3b4 --- /dev/null +++ b/src/main/scala/gitbucket/core/service/RequestCache.scala @@ -0,0 +1,36 @@ +package gitbucket.core.service + +import gitbucket.core.model.{Session, Issue, Account} +import gitbucket.core.util.Implicits +import gitbucket.core.controller.Context +import Implicits.request2Session + +/** + * This service is used for a view helper mainly. + * + * It may be called many times in one request, so each method stores + * its result into the cache which available during a request. + */ +trait RequestCache extends SystemSettingsService with AccountService with IssuesService { + + private implicit def context2Session(implicit context: Context): Session = + request2Session(context.request) + + def getIssue(userName: String, repositoryName: String, issueId: String)(implicit context: Context): Option[Issue] = { + context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){ + super.getIssue(userName, repositoryName, issueId) + } + } + + def getAccountByUserName(userName: String)(implicit context: Context): Option[Account] = { + context.cache(s"account.${userName}"){ + super.getAccountByUserName(userName) + } + } + + def getAccountByMailAddress(mailAddress: String)(implicit context: Context): Option[Account] = { + context.cache(s"account.${mailAddress}"){ + super.getAccountByMailAddress(mailAddress) + } + } +} diff --git a/src/main/scala/gitbucket/core/service/SshKeyService.scala b/src/main/scala/gitbucket/core/service/SshKeyService.scala new file mode 100644 index 0000000..4113d2c --- /dev/null +++ b/src/main/scala/gitbucket/core/service/SshKeyService.scala @@ -0,0 +1,18 @@ +package gitbucket.core.service + +import gitbucket.core.model.SshKey +import gitbucket.core.model.Profile._ +import profile.simple._ + +trait SshKeyService { + + def addPublicKey(userName: String, title: String, publicKey: String)(implicit s: Session): Unit = + SshKeys insert SshKey(userName = userName, title = title, publicKey = publicKey) + + def getPublicKeys(userName: String)(implicit s: Session): List[SshKey] = + SshKeys.filter(_.userName === userName.bind).sortBy(_.sshKeyId).list + + def deletePublicKey(userName: String, sshKeyId: Int)(implicit s: Session): Unit = + SshKeys filter (_.byPrimaryKey(userName, sshKeyId)) delete + +} diff --git a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala new file mode 100644 index 0000000..6a3b8d3 --- /dev/null +++ b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala @@ -0,0 +1,214 @@ +package gitbucket.core.service + +import gitbucket.core.util.{Directory, ControlUtil} +import Directory._ +import ControlUtil._ +import SystemSettingsService._ +import javax.servlet.http.HttpServletRequest + +trait SystemSettingsService { + + def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl(request) + + def saveSystemSettings(settings: SystemSettings): Unit = { + defining(new java.util.Properties()){ props => + settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", ""))) + settings.information.foreach(x => props.setProperty(Information, x)) + props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString) + props.setProperty(AllowAnonymousAccess, settings.allowAnonymousAccess.toString) + props.setProperty(IsCreateRepoOptionPublic, settings.isCreateRepoOptionPublic.toString) + props.setProperty(Gravatar, settings.gravatar.toString) + props.setProperty(Notification, settings.notification.toString) + props.setProperty(Ssh, settings.ssh.toString) + settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString)) + if(settings.notification) { + settings.smtp.foreach { smtp => + props.setProperty(SmtpHost, smtp.host) + smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString)) + smtp.user.foreach(props.setProperty(SmtpUser, _)) + smtp.password.foreach(props.setProperty(SmtpPassword, _)) + smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString)) + smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _)) + smtp.fromName.foreach(props.setProperty(SmtpFromName, _)) + } + } + props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString) + if(settings.ldapAuthentication){ + settings.ldap.map { ldap => + props.setProperty(LdapHost, ldap.host) + ldap.port.foreach(x => props.setProperty(LdapPort, x.toString)) + ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x)) + ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x)) + props.setProperty(LdapBaseDN, ldap.baseDN) + props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute) + ldap.additionalFilterCondition.foreach(x => props.setProperty(LdapAdditionalFilterCondition, x)) + ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x)) + ldap.mailAttribute.foreach(x => props.setProperty(LdapMailAddressAttribute, x)) + ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString)) + ldap.ssl.foreach(x => props.setProperty(LdapSsl, x.toString)) + ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x)) + } + } + using(new java.io.FileOutputStream(GitBucketConf)){ out => + props.store(out, null) + } + } + } + + + def loadSystemSettings(): SystemSettings = { + defining(new java.util.Properties()){ props => + if(GitBucketConf.exists){ + using(new java.io.FileInputStream(GitBucketConf)){ in => + props.load(in) + } + } + SystemSettings( + getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")), + getOptionValue[String](props, Information, None), + getValue(props, AllowAccountRegistration, false), + getValue(props, AllowAnonymousAccess, true), + getValue(props, IsCreateRepoOptionPublic, true), + getValue(props, Gravatar, true), + getValue(props, Notification, false), + getValue(props, Ssh, false), + getOptionValue(props, SshPort, Some(DefaultSshPort)), + if(getValue(props, Notification, false)){ + Some(Smtp( + getValue(props, SmtpHost, ""), + getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)), + getOptionValue(props, SmtpUser, None), + getOptionValue(props, SmtpPassword, None), + getOptionValue[Boolean](props, SmtpSsl, None), + getOptionValue(props, SmtpFromAddress, None), + getOptionValue(props, SmtpFromName, None))) + } else { + None + }, + getValue(props, LdapAuthentication, false), + if(getValue(props, LdapAuthentication, false)){ + Some(Ldap( + getValue(props, LdapHost, ""), + getOptionValue(props, LdapPort, Some(DefaultLdapPort)), + getOptionValue(props, LdapBindDN, None), + getOptionValue(props, LdapBindPassword, None), + getValue(props, LdapBaseDN, ""), + getValue(props, LdapUserNameAttribute, ""), + getOptionValue(props, LdapAdditionalFilterCondition, None), + getOptionValue(props, LdapFullNameAttribute, None), + getOptionValue(props, LdapMailAddressAttribute, None), + getOptionValue[Boolean](props, LdapTls, None), + getOptionValue[Boolean](props, LdapSsl, None), + getOptionValue(props, LdapKeystore, None))) + } else { + None + } + ) + } + } + +} + +object SystemSettingsService { + import scala.reflect.ClassTag + + case class SystemSettings( + baseUrl: Option[String], + information: Option[String], + allowAccountRegistration: Boolean, + allowAnonymousAccess: Boolean, + isCreateRepoOptionPublic: Boolean, + gravatar: Boolean, + notification: Boolean, + ssh: Boolean, + sshPort: Option[Int], + smtp: Option[Smtp], + ldapAuthentication: Boolean, + 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)) + } + }.stripSuffix("/") + } + + case class Ldap( + host: String, + port: Option[Int], + bindDN: Option[String], + bindPassword: Option[String], + baseDN: String, + userNameAttribute: String, + additionalFilterCondition: Option[String], + fullNameAttribute: Option[String], + mailAttribute: Option[String], + tls: Option[Boolean], + ssl: Option[Boolean], + keystore: Option[String]) + + case class Smtp( + host: String, + port: Option[Int], + user: Option[String], + password: Option[String], + ssl: Option[Boolean], + fromAddress: Option[String], + fromName: Option[String]) + + val DefaultSshPort = 29418 + val DefaultSmtpPort = 25 + val DefaultLdapPort = 389 + + private val BaseURL = "base_url" + private val Information = "information" + private val AllowAccountRegistration = "allow_account_registration" + private val AllowAnonymousAccess = "allow_anonymous_access" + private val IsCreateRepoOptionPublic = "is_create_repository_option_public" + private val Gravatar = "gravatar" + private val Notification = "notification" + private val Ssh = "ssh" + private val SshPort = "ssh.port" + private val SmtpHost = "smtp.host" + private val SmtpPort = "smtp.port" + private val SmtpUser = "smtp.user" + private val SmtpPassword = "smtp.password" + private val SmtpSsl = "smtp.ssl" + private val SmtpFromAddress = "smtp.from_address" + private val SmtpFromName = "smtp.from_name" + private val LdapAuthentication = "ldap_authentication" + private val LdapHost = "ldap.host" + private val LdapPort = "ldap.port" + private val LdapBindDN = "ldap.bindDN" + private val LdapBindPassword = "ldap.bind_password" + private val LdapBaseDN = "ldap.baseDN" + private val LdapUserNameAttribute = "ldap.username_attribute" + private val LdapAdditionalFilterCondition = "ldap.additional_filter_condition" + private val LdapFullNameAttribute = "ldap.fullname_attribute" + private val LdapMailAddressAttribute = "ldap.mail_attribute" + private val LdapTls = "ldap.tls" + private val LdapSsl = "ldap.ssl" + private val LdapKeystore = "ldap.keystore" + + private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = + defining(props.getProperty(key)){ value => + if(value == null || value.isEmpty) default + else convertType(value).asInstanceOf[A] + } + + private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] = + defining(props.getProperty(key)){ value => + if(value == null || value.isEmpty) default + else Some(convertType(value)).asInstanceOf[Option[A]] + } + + private def convertType[A: ClassTag](value: String) = + defining(implicitly[ClassTag[A]].runtimeClass){ c => + if(c == classOf[Boolean]) value.toBoolean + else if(c == classOf[Int]) value.toInt + else value + } + +// // TODO temporary flag +// val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean + +} diff --git a/src/main/scala/gitbucket/core/service/WebHookService.scala b/src/main/scala/gitbucket/core/service/WebHookService.scala new file mode 100644 index 0000000..f4d8d4c --- /dev/null +++ b/src/main/scala/gitbucket/core/service/WebHookService.scala @@ -0,0 +1,144 @@ +package gitbucket.core.service + +import gitbucket.core.model.{WebHook, Account} +import gitbucket.core.model.Profile._ +import gitbucket.core.service.RepositoryService.RepositoryInfo +import gitbucket.core.util.JGitUtil +import profile.simple._ +import org.slf4j.LoggerFactory +import RepositoryService.RepositoryInfo +import org.eclipse.jgit.diff.DiffEntry +import JGitUtil.CommitInfo +import org.eclipse.jgit.api.Git +import org.apache.http.message.BasicNameValuePair +import org.apache.http.client.entity.UrlEncodedFormEntity +import org.apache.http.NameValuePair + +trait WebHookService { + import WebHookService._ + + private val logger = LoggerFactory.getLogger(classOf[WebHookService]) + + def getWebHookURLs(owner: String, repository: String)(implicit s: Session): List[WebHook] = + WebHooks.filter(_.byRepository(owner, repository)).sortBy(_.url).list + + def addWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit = + WebHooks insert WebHook(owner, repository, url) + + def deleteWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit = + WebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete + + def callWebHook(owner: String, repository: String, webHookURLs: List[WebHook], payload: WebHookPayload): Unit = { + import org.json4s._ + import org.json4s.jackson.Serialization + import org.json4s.jackson.Serialization.{read, write} + import org.apache.http.client.methods.HttpPost + import org.apache.http.impl.client.HttpClientBuilder + import scala.concurrent._ + import ExecutionContext.Implicits.global + + logger.debug("start callWebHook") + implicit val formats = Serialization.formats(NoTypeHints) + + if(webHookURLs.nonEmpty){ + val json = write(payload) + val httpClient = HttpClientBuilder.create.build + + webHookURLs.foreach { webHookUrl => + val f = Future { + logger.debug(s"start web hook invocation for ${webHookUrl}") + val httpPost = new HttpPost(webHookUrl.url) + + val params: java.util.List[NameValuePair] = new java.util.ArrayList() + params.add(new BasicNameValuePair("payload", json)) + httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8")) + + httpClient.execute(httpPost) + httpPost.releaseConnection() + logger.debug(s"end web hook invocation for ${webHookUrl}") + } + f.onSuccess { + case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}") + } + f.onFailure { + case t => logger.error(s"Failed: web hook request to ${webHookUrl.url}", t) + } + } + } + logger.debug("end callWebHook") + } + +} + +object WebHookService { + + case class WebHookPayload( + pusher: WebHookUser, + ref: String, + commits: List[WebHookCommit], + repository: WebHookRepository) + + object WebHookPayload { + def apply(git: Git, pusher: Account, refName: String, repositoryInfo: RepositoryInfo, + commits: List[CommitInfo], repositoryOwner: Account): WebHookPayload = + WebHookPayload( + WebHookUser(pusher.fullName, pusher.mailAddress), + refName, + commits.map { commit => + val diffs = JGitUtil.getDiffs(git, commit.id, false) + val commitUrl = repositoryInfo.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/commit/" + commit.id + + WebHookCommit( + id = commit.id, + message = commit.fullMessage, + timestamp = commit.commitTime.toString, + url = commitUrl, + added = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.ADD) => x.newPath }, + removed = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.DELETE) => x.oldPath }, + modified = diffs._1.collect { case x if(x.changeType != DiffEntry.ChangeType.ADD && + x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath }, + author = WebHookUser( + name = commit.committerName, + email = commit.committerEmailAddress + ) + ) + }, + WebHookRepository( + name = repositoryInfo.name, + url = repositoryInfo.httpUrl, + description = repositoryInfo.repository.description.getOrElse(""), + watchers = 0, + forks = repositoryInfo.forkedCount, + `private` = repositoryInfo.repository.isPrivate, + owner = WebHookUser( + name = repositoryOwner.userName, + email = repositoryOwner.mailAddress + ) + ) + ) + } + + case class WebHookCommit( + id: String, + message: String, + timestamp: String, + url: String, + added: List[String], + removed: List[String], + modified: List[String], + author: WebHookUser) + + case class WebHookRepository( + name: String, + url: String, + description: String, + watchers: Int, + forks: Int, + `private`: Boolean, + owner: WebHookUser) + + case class WebHookUser( + name: String, + email: String) + +} diff --git a/src/main/scala/gitbucket/core/service/WikiService.scala b/src/main/scala/gitbucket/core/service/WikiService.scala new file mode 100644 index 0000000..f317c51 --- /dev/null +++ b/src/main/scala/gitbucket/core/service/WikiService.scala @@ -0,0 +1,282 @@ +package gitbucket.core.service + +import java.util.Date +import gitbucket.core.model.Account +import gitbucket.core.util._ +import gitbucket.core.util.ControlUtil._ +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.treewalk.CanonicalTreeParser +import org.eclipse.jgit.lib._ +import org.eclipse.jgit.dircache.DirCache +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._ +import RepositoryService.RepositoryInfo + +object WikiService { + + /** + * The model for wiki page. + * + * @param name the page name + * @param content the page content + * @param committer the last committer + * @param time the last modified time + * @param id the latest commit id + */ + case class WikiPageInfo(name: String, content: String, committer: String, time: Date, id: String) + + /** + * The model for wiki page history. + * + * @param name the page name + * @param committer the committer the committer + * @param message the commit message + * @param date the commit date + */ + case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date) + + def httpUrl(repository: RepositoryInfo) = repository.httpUrl.replaceFirst("\\.git\\Z", ".wiki.git") + + def sshUrl(repository: RepositoryInfo, settings: SystemSettingsService.SystemSettings, userName: String) = + repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), userName).replaceFirst("\\.git\\Z", ".wiki.git") +} + +trait WikiService { + import WikiService._ + + def createWikiRepository(loginAccount: Account, owner: String, repository: String): Unit = + LockUtil.lock(s"${owner}/${repository}/wiki"){ + defining(Directory.getWikiRepositoryDir(owner, repository)){ dir => + if(!dir.exists){ + JGitUtil.initRepository(dir) + saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None) + } + } + } + + /** + * Returns the wiki page. + */ + def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = { + using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => + if(!JGitUtil.isEmpty(git)){ + JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file => + WikiPageInfo(file.name, StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes), + file.author, file.time, file.commitId) + } + } else None + } + } + + /** + * Returns the content of the specified file. + */ + def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] = + using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => + if(!JGitUtil.isEmpty(git)){ + val index = path.lastIndexOf('/') + val parentPath = if(index < 0) "." else path.substring(0, index) + val fileName = if(index < 0) path else path.substring(index + 1) + + JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file => + git.getRepository.open(file.id).getBytes + } + } else None + } + + /** + * Returns the list of wiki page names. + */ + def getWikiPageList(owner: String, repository: String): List[String] = { + using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => + JGitUtil.getFileList(git, "master", ".") + .filter(_.name.endsWith(".md")) + .map(_.name.stripSuffix(".md")) + .sortBy(x => x) + } + } + + /** + * Reverts specified changes. + */ + def revertWikiPage(owner: String, repository: String, from: String, to: String, + committer: Account, pageName: Option[String]): Boolean = { + + 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}")) + + val newTreeIter = new CanonicalTreeParser + newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}")) + + val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff => + pageName match { + case Some(x) => diff.getNewPath == x + ".md" + case None => true + } + } + + val patch = using(new java.io.ByteArrayOutputStream()){ out => + val formatter = new DiffFormatter(out) + formatter.setRepository(git.getRepository) + formatter.format(diffs.asJava) + new String(out.toByteArray, "UTF-8") + } + + 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.stripSuffix(".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}") + + JGitUtil.processTree(git, headId){ (path, tree) => + 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), + Constants.HEAD, 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: Account, message: String, currentId: Option[String]): Option[String] = { + 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 created = true + var updated = false + var removed = false + + if(headId != null){ + 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) + } + } + } + + if(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), + Constants.HEAD, 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.getName) + } else None + } + } + } + + /** + * Delete the wiki page. + */ + def deleteWikiPage(owner: String, repository: String, pageName: String, + committer: String, mailAddress: String, message: String): Unit = { + 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 + + 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), + Constants.HEAD, committer, mailAddress, message) + } + } + } + } + +} diff --git a/src/main/scala/gitbucket/core/servlet/BasicAuthenticationFilter.scala b/src/main/scala/gitbucket/core/servlet/BasicAuthenticationFilter.scala new file mode 100644 index 0000000..ebd7bc8 --- /dev/null +++ b/src/main/scala/gitbucket/core/servlet/BasicAuthenticationFilter.scala @@ -0,0 +1,91 @@ +package gitbucket.core.servlet + +import javax.servlet._ +import javax.servlet.http._ +import gitbucket.core.service.{RepositoryService, AccountService, SystemSettingsService} +import gitbucket.core.util.{ControlUtil, Keys, Implicits} +import org.slf4j.LoggerFactory +import Implicits._ +import ControlUtil._ + +/** + * Provides BASIC Authentication for [[GitRepositoryServlet]]. + */ +class BasicAuthenticationFilter extends Filter with RepositoryService with AccountService with SystemSettingsService { + + private val logger = LoggerFactory.getLogger(classOf[BasicAuthenticationFilter]) + + def init(config: FilterConfig) = {} + + def destroy(): Unit = {} + + def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = { + implicit val request = req.asInstanceOf[HttpServletRequest] + val response = res.asInstanceOf[HttpServletResponse] + + val wrappedResponse = new HttpServletResponseWrapper(response){ + override def setCharacterEncoding(encoding: String) = {} + } + + val isUpdating = request.getRequestURI.endsWith("/git-receive-pack") || "service=git-receive-pack".equals(request.getQueryString) + + val settings = loadSystemSettings() + + try { + defining(request.paths){ + case Array(_, repositoryOwner, repositoryName, _*) => + getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match { + case Some(repository) => { + if(!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess){ + chain.doFilter(req, wrappedResponse) + } else { + request.getHeader("Authorization") match { + case null => requireAuth(response) + case auth => decodeAuthHeader(auth).split(":") match { + case Array(username, password) => { + authenticate(settings, username, password) match { + case Some(account) => { + if(isUpdating && hasWritePermission(repository.owner, repository.name, Some(account))){ + request.setAttribute(Keys.Request.UserName, account.userName) + } + chain.doFilter(req, wrappedResponse) + } + case None => requireAuth(response) + } + } + case _ => requireAuth(response) + } + } + } + } + case None => { + logger.debug(s"Repository ${repositoryOwner}/${repositoryName} is not found.") + response.sendError(HttpServletResponse.SC_NOT_FOUND) + } + } + case _ => { + logger.debug(s"Not enough path arguments: ${request.paths}") + response.sendError(HttpServletResponse.SC_NOT_FOUND) + } + } + } catch { + case ex: Exception => { + logger.error("error", ex) + requireAuth(response) + } + } + } + + private def requireAuth(response: HttpServletResponse): Unit = { + response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"") + response.sendError(HttpServletResponse.SC_UNAUTHORIZED) + } + + private def decodeAuthHeader(header: String): String = { + try { + new String(new sun.misc.BASE64Decoder().decodeBuffer(header.substring(6))) + } catch { + case _: Throwable => "" + } + } +} \ No newline at end of file diff --git a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala new file mode 100644 index 0000000..cea10d2 --- /dev/null +++ b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala @@ -0,0 +1,215 @@ +package gitbucket.core.servlet + +import gitbucket.core.model.Session +import gitbucket.core.service._ +import gitbucket.core.util._ +import org.eclipse.jgit.http.server.GitServlet +import org.eclipse.jgit.lib._ +import org.eclipse.jgit.transport._ +import org.eclipse.jgit.transport.resolver._ +import org.slf4j.LoggerFactory + +import javax.servlet.ServletConfig +import javax.servlet.ServletContext +import javax.servlet.http.{HttpServletResponse, HttpServletRequest} +import gitbucket.core.util.StringUtil +import gitbucket.core.util.ControlUtil._ +import gitbucket.core.util.Implicits._ +import WebHookService._ +import org.eclipse.jgit.api.Git +import JGitUtil.CommitInfo +import IssuesService.IssueSearchCondition + +/** + * Provides Git repository via HTTP. + * + * This servlet provides only Git repository functionality. + * Authentication is provided by [[BasicAuthenticationFilter]]. + */ +class GitRepositoryServlet extends GitServlet with SystemSettingsService { + + private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet]) + + override def init(config: ServletConfig): Unit = { + setReceivePackFactory(new GitBucketReceivePackFactory()) + + // TODO are there any other ways...? + super.init(new ServletConfig(){ + def getInitParameter(name: String): String = name match { + case "base-path" => Directory.RepositoryHome + case "export-all" => "true" + case name => config.getInitParameter(name) + } + def getInitParameterNames(): java.util.Enumeration[String] = { + config.getInitParameterNames + } + + def getServletContext(): ServletContext = config.getServletContext + def getServletName(): String = config.getServletName + }) + + super.init(config) + } + + override def service(req: HttpServletRequest, res: HttpServletResponse): Unit = { + val agent = req.getHeader("USER-AGENT") + val index = req.getRequestURI.indexOf(".git") + if(index >= 0 && (agent == null || agent.toLowerCase.indexOf("git/") < 0)){ + // redirect for browsers + val paths = req.getRequestURI.substring(0, index).split("/") + res.sendRedirect(baseUrl(req) + "/" + paths.dropRight(1).last + "/" + paths.last) + } else { + // response for git client + super.service(req, res) + } + } +} + +class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService { + + private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory]) + + override def create(request: HttpServletRequest, db: Repository): ReceivePack = { + val receivePack = new ReceivePack(db) + val pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String] + + logger.debug("requestURI: " + request.getRequestURI) + logger.debug("pusher:" + pusher) + + defining(request.paths){ paths => + val owner = paths(1) + val repository = paths(2).stripSuffix(".git") + + logger.debug("repository:" + owner + "/" + repository) + + if(!repository.endsWith(".wiki")){ + defining(request) { implicit r => + val hook = new CommitLogHook(owner, repository, pusher, baseUrl) + receivePack.setPreReceiveHook(hook) + receivePack.setPostReceiveHook(hook) + } + } + receivePack + } + } +} + +import scala.collection.JavaConverters._ + +class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: Session) + 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 => + val pushedIds = scala.collection.mutable.Set[String]() + commands.asScala.foreach { command => + logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}") + val refName = command.getRefName.split("/") + val branchName = refName.drop(2).mkString("/") + val commits = if (refName(1) == "tags") { + Nil + } else { + command.getType match { + case ReceiveCommand.Type.DELETE => Nil + case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name) + } + } + + // Retrieve all issue count in the repository + val issueCount = + countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) + + countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository) + + // Extract new commit and apply issue comment + val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch + val newCommits = commits.flatMap { commit => + if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) { + if (issueCount > 0) { + pushedIds.add(commit.id) + createIssueComment(commit) + // close issues + if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){ + closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository) + } + } + Some(commit) + } else None + } + + // record activity + if(refName(1) == "heads"){ + command.getType match { + case ReceiveCommand.Type.CREATE => recordCreateBranchActivity(owner, repository, pusher, branchName) + case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, pusher, branchName, newCommits) + case ReceiveCommand.Type.DELETE => recordDeleteBranchActivity(owner, repository, pusher, branchName) + case _ => + } + } else if(refName(1) == "tags"){ + command.getType match { + case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, pusher, branchName, newCommits) + case ReceiveCommand.Type.DELETE => recordDeleteTagActivity(owner, repository, pusher, branchName, newCommits) + case _ => + } + } + + if(refName(1) == "heads"){ + command.getType match { + case ReceiveCommand.Type.CREATE | + ReceiveCommand.Type.UPDATE | + ReceiveCommand.Type.UPDATE_NONFASTFORWARD => + updatePullRequests(owner, repository, branchName) + case _ => + } + } + + // call web hook + getWebHookURLs(owner, repository) match { + case webHookURLs if(webHookURLs.nonEmpty) => + for(pusherAccount <- getAccountByUserName(pusher); + ownerAccount <- getAccountByUserName(owner); + repositoryInfo <- getRepository(owner, repository, baseUrl)){ + callWebHook(owner, repository, webHookURLs, + WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount)) + } + case _ => + } + } + } + // update repository last modified time. + updateLastActivityDate(owner, repository) + } catch { + case ex: Exception => { + logger.error(ex.toString, ex) + throw ex + } + } + } + + private def createIssueComment(commit: CommitInfo) = { + StringUtil.extractIssueId(commit.fullMessage).foreach { issueId => + if(getIssue(owner, repository, issueId).isDefined){ + getAccountByMailAddress(commit.committerEmailAddress).foreach { account => + createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit") + } + } + } + } +} diff --git a/src/main/scala/gitbucket/core/servlet/InitializeListener.scala b/src/main/scala/gitbucket/core/servlet/InitializeListener.scala new file mode 100644 index 0000000..84d7de4 --- /dev/null +++ b/src/main/scala/gitbucket/core/servlet/InitializeListener.scala @@ -0,0 +1,206 @@ +package gitbucket.core.servlet + +import java.io.File +import java.sql.{DriverManager, Connection} +import gitbucket.core.plugin.PluginRegistry +import gitbucket.core.util._ +import org.apache.commons.io.FileUtils +import javax.servlet.{ServletContextListener, ServletContextEvent} +import org.slf4j.LoggerFactory +import Directory._ +import ControlUtil._ +import JDBCUtil._ +import org.eclipse.jgit.api.Git +import gitbucket.core.util.Versions +import gitbucket.core.util.Directory +import gitbucket.core.plugin._ + +object AutoUpdate { + + /** + * The history of versions. A head of this sequence is the current BitBucket version. + */ + val versions = Seq( + new Version(2, 8), + new Version(2, 7) { + override def update(conn: Connection, cl: ClassLoader): Unit = { + super.update(conn, cl) + conn.select("SELECT * FROM REPOSITORY"){ rs => + // Rename attached files directory from /issues to /comments + val userName = rs.getString("USER_NAME") + val repoName = rs.getString("REPOSITORY_NAME") + defining(Directory.getAttachedDir(userName, repoName)){ newDir => + val oldDir = new File(newDir.getParentFile, "issues") + if(oldDir.exists && oldDir.isDirectory){ + oldDir.renameTo(newDir) + } + } + // Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME if it does not exist + val originalUserName = rs.getString("ORIGIN_USER_NAME") + val originalRepoName = rs.getString("ORIGIN_REPOSITORY_NAME") + if(originalUserName != null && originalRepoName != null){ + if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", + originalUserName, originalRepoName) == 0){ + conn.update("UPDATE REPOSITORY SET ORIGIN_USER_NAME = NULL, ORIGIN_REPOSITORY_NAME = NULL " + + "WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName) + } + } + // Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME if it does not exist + val parentUserName = rs.getString("PARENT_USER_NAME") + val parentRepoName = rs.getString("PARENT_REPOSITORY_NAME") + if(parentUserName != null && parentRepoName != null){ + if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", + parentUserName, parentRepoName) == 0){ + conn.update("UPDATE REPOSITORY SET PARENT_USER_NAME = NULL, PARENT_REPOSITORY_NAME = NULL " + + "WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName) + } + } + } + } + }, + new Version(2, 6), + new Version(2, 5), + new Version(2, 4), + new Version(2, 3) { + override def update(conn: Connection, cl: ClassLoader): Unit = { + super.update(conn, cl) + conn.select("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'"){ rs => + val curInfo = rs.getString("ADDITIONAL_INFO") + val newInfo = curInfo.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n") + if (curInfo != newInfo) { + conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID")) + } + } + ignore { + FileUtils.deleteDirectory(Directory.getPluginCacheDir()) + //FileUtils.deleteDirectory(new File(Directory.PluginHome)) + } + } + }, + new Version(2, 2), + new Version(2, 1), + new Version(2, 0){ + override def update(conn: Connection, cl: ClassLoader): Unit = { + import eu.medsea.mimeutil.{MimeUtil2, MimeType} + + val mimeUtil = new MimeUtil2() + mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector") + + super.update(conn, cl) + conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs => + defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir => + if(dir.exists && dir.isDirectory){ + dir.listFiles.foreach { file => + if(file.getName.indexOf('.') < 0){ + val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString + if(mimeType.startsWith("image/")){ + file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1))) + } + } + } + } + } + } + } + }, + Version(1, 13), + Version(1, 12), + Version(1, 11), + Version(1, 10), + Version(1, 9), + Version(1, 8), + Version(1, 7), + Version(1, 6), + Version(1, 5), + Version(1, 4), + new Version(1, 3){ + override def update(conn: Connection, cl: ClassLoader): Unit = { + super.update(conn, cl) + // Fix wiki repository configuration + conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs => + using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git => + defining(git.getRepository.getConfig){ config => + if(!config.getBoolean("http", "receivepack", false)){ + config.setBoolean("http", null, "receivepack", true) + config.save + } + } + } + } + } + }, + Version(1, 2), + Version(1, 1), + Version(1, 0), + Version(0, 0) + ) + + /** + * The head version of BitBucket. + */ + val headVersion = versions.head + + /** + * The version file (GITBUCKET_HOME/version). + */ + lazy val versionFile = new File(GitBucketHome, "version") + + /** + * Returns the current version from the version file. + */ + def getCurrentVersion(): Version = { + if(versionFile.exists){ + FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match { + case Array(majorVersion, minorVersion) => { + versions.find { v => + v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt + }.getOrElse(Version(0, 0)) + } + case _ => Version(0, 0) + } + } else Version(0, 0) + } + +} + +/** + * Initialize GitBucket system. + * Update database schema and load plug-ins automatically in the context initializing. + */ +class InitializeListener extends ServletContextListener { + import AutoUpdate._ + + private val logger = LoggerFactory.getLogger(classOf[InitializeListener]) + + override def contextInitialized(event: ServletContextEvent): Unit = { + val dataDir = event.getServletContext.getInitParameter("gitbucket.home") + if(dataDir != null){ + System.setProperty("gitbucket.home", dataDir) + } + org.h2.Driver.load() + + defining(getConnection()){ conn => + // Migration + logger.debug("Start schema update") + Versions.update(conn, headVersion, getCurrentVersion(), versions, Thread.currentThread.getContextClassLoader){ conn => + FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8") + } + // Load plugins + logger.debug("Initialize plugins") + PluginRegistry.initialize(event.getServletContext, conn) + } + + } + + def contextDestroyed(event: ServletContextEvent): Unit = { + // Shutdown plugins + PluginRegistry.shutdown(event.getServletContext) + } + + private def getConnection(): Connection = + DriverManager.getConnection( + DatabaseConfig.url, + DatabaseConfig.user, + DatabaseConfig.password) + +} diff --git a/src/main/scala/gitbucket/core/servlet/SessionCleanupListener.scala b/src/main/scala/gitbucket/core/servlet/SessionCleanupListener.scala new file mode 100644 index 0000000..97ab13b --- /dev/null +++ b/src/main/scala/gitbucket/core/servlet/SessionCleanupListener.scala @@ -0,0 +1,17 @@ +package gitbucket.core.servlet + +import javax.servlet.http.{HttpSessionEvent, HttpSessionListener} +import gitbucket.core.util.Directory +import org.apache.commons.io.FileUtils +import Directory._ + +/** + * Removes session associated temporary files when session is destroyed. + */ +class SessionCleanupListener extends HttpSessionListener { + + def sessionCreated(se: HttpSessionEvent): Unit = {} + + def sessionDestroyed(se: HttpSessionEvent): Unit = FileUtils.deleteDirectory(getTemporaryDir(se.getSession.getId)) + +} diff --git a/src/main/scala/gitbucket/core/servlet/TransactionFilter.scala b/src/main/scala/gitbucket/core/servlet/TransactionFilter.scala new file mode 100644 index 0000000..2fdd17a --- /dev/null +++ b/src/main/scala/gitbucket/core/servlet/TransactionFilter.scala @@ -0,0 +1,60 @@ +package gitbucket.core.servlet + +import javax.servlet._ +import javax.servlet.http.HttpServletRequest +import com.mchange.v2.c3p0.ComboPooledDataSource +import gitbucket.core.util.DatabaseConfig +import org.slf4j.LoggerFactory +import slick.jdbc.JdbcBackend.{Database => SlickDatabase, Session} +import gitbucket.core.util.Keys + +/** + * Controls the transaction with the open session in view pattern. + */ +class TransactionFilter extends Filter { + + private val logger = LoggerFactory.getLogger(classOf[TransactionFilter]) + + def init(config: FilterConfig) = {} + + def destroy(): Unit = {} + + def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = { + if(req.asInstanceOf[HttpServletRequest].getRequestURI().startsWith("/assets/")){ + // assets don't need transaction + chain.doFilter(req, res) + } else { + Database() withTransaction { session => + logger.debug("begin transaction") + req.setAttribute(Keys.Request.DBSession, session) + chain.doFilter(req, res) + logger.debug("end transaction") + } + } + } + +} + +object Database { + + private val logger = LoggerFactory.getLogger(Database.getClass) + + private val db: SlickDatabase = { + val datasource = new ComboPooledDataSource + + datasource.setDriverClass(DatabaseConfig.driver) + datasource.setJdbcUrl(DatabaseConfig.url) + datasource.setUser(DatabaseConfig.user) + datasource.setPassword(DatabaseConfig.password) + + logger.debug("load database connection pool") + + SlickDatabase.forDataSource(datasource) + } + + def apply(): SlickDatabase = db + + def getSession(req: ServletRequest): Session = + req.getAttribute(Keys.Request.DBSession).asInstanceOf[Session] + +} diff --git a/src/main/scala/gitbucket/core/ssh/GitCommand.scala b/src/main/scala/gitbucket/core/ssh/GitCommand.scala new file mode 100644 index 0000000..c78ff82 --- /dev/null +++ b/src/main/scala/gitbucket/core/ssh/GitCommand.scala @@ -0,0 +1,133 @@ +package gitbucket.core.ssh + +import gitbucket.core.model.Session +import gitbucket.core.service.{RepositoryService, AccountService, SystemSettingsService} +import gitbucket.core.servlet.{Database, CommitLogHook} +import gitbucket.core.util.{Directory, ControlUtil} +import org.apache.sshd.server.{CommandFactory, Environment, ExitCallback, Command} +import org.slf4j.LoggerFactory +import java.io.{InputStream, OutputStream} +import ControlUtil._ +import org.eclipse.jgit.api.Git +import Directory._ +import org.eclipse.jgit.transport.{ReceivePack, UploadPack} +import org.apache.sshd.server.command.UnknownCommand +import org.eclipse.jgit.errors.RepositoryNotFoundException + +object GitCommand { + val CommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+).git'\Z""".r +} + +abstract class GitCommand(val owner: String, val repoName: String) extends Command { + self: RepositoryService with AccountService => + + private val logger = LoggerFactory.getLogger(classOf[GitCommand]) + protected var err: OutputStream = null + protected var in: InputStream = null + protected var out: OutputStream = null + protected var callback: ExitCallback = null + + protected def runTask(user: String)(implicit session: Session): Unit + + private def newTask(user: String): Runnable = new Runnable { + override def run(): Unit = { + Database() withSession { implicit session => + try { + runTask(user) + callback.onExit(0) + } catch { + case e: RepositoryNotFoundException => + logger.info(e.getMessage) + callback.onExit(1, "Repository Not Found") + case e: Throwable => + logger.error(e.getMessage, e) + callback.onExit(1) + } + } + } + } + + override def start(env: Environment): Unit = { + val user = env.getEnv.get("USER") + val thread = new Thread(newTask(user)) + thread.start() + } + + override def destroy(): Unit = {} + + override def setExitCallback(callback: ExitCallback): Unit = { + this.callback = callback + } + + override def setErrorStream(err: OutputStream): Unit = { + this.err = err + } + + override def setOutputStream(out: OutputStream): Unit = { + this.out = out + } + + override def setInputStream(in: InputStream): Unit = { + this.in = in + } + + protected def isWritableUser(username: String, repositoryInfo: RepositoryService.RepositoryInfo) + (implicit session: Session): Boolean = + getAccountByUserName(username) match { + case Some(account) => hasWritePermission(repositoryInfo.owner, repositoryInfo.name, Some(account)) + case None => false + } + +} + +class GitUploadPack(owner: String, repoName: String, baseUrl: String) extends GitCommand(owner, repoName) + with RepositoryService with AccountService { + + override protected def runTask(user: String)(implicit session: Session): Unit = { + getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo => + if(!repositoryInfo.repository.isPrivate || isWritableUser(user, repositoryInfo)){ + using(Git.open(getRepositoryDir(owner, repoName))) { git => + val repository = git.getRepository + val upload = new UploadPack(repository) + upload.upload(in, out, err) + } + } + } + } + +} + +class GitReceivePack(owner: String, repoName: String, baseUrl: String) extends GitCommand(owner, repoName) + with SystemSettingsService with RepositoryService with AccountService { + + override protected def runTask(user: String)(implicit session: Session): Unit = { + getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo => + if(isWritableUser(user, repositoryInfo)){ + using(Git.open(getRepositoryDir(owner, repoName))) { git => + val repository = git.getRepository + val receive = new ReceivePack(repository) + if(!repoName.endsWith(".wiki")){ + val hook = new CommitLogHook(owner, repoName, user, baseUrl) + receive.setPreReceiveHook(hook) + receive.setPostReceiveHook(hook) + } + receive.receive(in, out, err) + } + } + } + } + +} + +class GitCommandFactory(baseUrl: String) extends CommandFactory { + private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory]) + + override def createCommand(command: String): Command = { + logger.debug(s"command: $command") + command match { + case GitCommand.CommandRegex("upload", owner, repoName) => new GitUploadPack(owner, repoName, baseUrl) + case GitCommand.CommandRegex("receive", owner, repoName) => new GitReceivePack(owner, repoName, baseUrl) + case _ => new UnknownCommand(command) + } + } +} diff --git a/src/main/scala/gitbucket/core/ssh/NoShell.scala b/src/main/scala/gitbucket/core/ssh/NoShell.scala new file mode 100644 index 0000000..bd30ccf --- /dev/null +++ b/src/main/scala/gitbucket/core/ssh/NoShell.scala @@ -0,0 +1,62 @@ +package gitbucket.core.ssh + +import gitbucket.core.service.SystemSettingsService +import org.apache.sshd.common.Factory +import org.apache.sshd.server.{Environment, ExitCallback, Command} +import java.io.{OutputStream, InputStream} +import org.eclipse.jgit.lib.Constants + +class NoShell extends Factory[Command] with SystemSettingsService { + override def create(): Command = new Command() { + private var in: InputStream = null + private var out: OutputStream = null + private var err: OutputStream = null + private var callback: ExitCallback = null + + override def start(env: Environment): Unit = { + val user = env.getEnv.get("USER") + val port = loadSystemSettings().sshPort.getOrElse(SystemSettingsService.DefaultSshPort) + val message = + """ + | Welcome to + | _____ _ _ ____ _ _ + | / ____| (_) | | | _ \ | | | | + | | | __ _ | |_ | |_) | _ _ ___ | | __ ___ | |_ + | | | |_ | | | | __| | _ < | | | | / __| | |/ / / _ \ | __| + | | |__| | | | | |_ | |_) | | |_| | | (__ | < | __/ | |_ + | \_____| |_| \__| |____/ \__,_| \___| |_|\_\ \___| \__| + | + | Successfully SSH Access. + | But interactive shell is disabled. + | + | Please use: + | + | git clone ssh://%s@GITBUCKET_HOST:%d/OWNER/REPOSITORY_NAME.git + """.stripMargin.format(user, port).replace("\n", "\r\n") + "\r\n" + err.write(Constants.encode(message)) + err.flush() + in.close() + out.close() + err.close() + callback.onExit(127) + } + + override def destroy(): Unit = {} + + override def setInputStream(in: InputStream): Unit = { + this.in = in + } + + override def setOutputStream(out: OutputStream): Unit = { + this.out = out + } + + override def setErrorStream(err: OutputStream): Unit = { + this.err = err + } + + override def setExitCallback(callback: ExitCallback): Unit = { + this.callback = callback + } + } +} diff --git a/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala b/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala new file mode 100644 index 0000000..daf5c30 --- /dev/null +++ b/src/main/scala/gitbucket/core/ssh/PublicKeyAuthenticator.scala @@ -0,0 +1,22 @@ +package gitbucket.core.ssh + +import gitbucket.core.service.SshKeyService +import gitbucket.core.servlet.Database +import org.apache.sshd.server.PublickeyAuthenticator +import org.apache.sshd.server.session.ServerSession +import java.security.PublicKey + +class PublicKeyAuthenticator extends PublickeyAuthenticator with SshKeyService { + + override def authenticate(username: String, key: PublicKey, session: ServerSession): Boolean = { + Database() withSession { implicit session => + getPublicKeys(username).exists { sshKey => + SshUtil.str2PublicKey(sshKey.publicKey) match { + case Some(publicKey) => key.equals(publicKey) + case _ => false + } + } + } + } + +} diff --git a/src/main/scala/gitbucket/core/ssh/SshServerListener.scala b/src/main/scala/gitbucket/core/ssh/SshServerListener.scala new file mode 100644 index 0000000..1288eb1 --- /dev/null +++ b/src/main/scala/gitbucket/core/ssh/SshServerListener.scala @@ -0,0 +1,69 @@ +package gitbucket.core.ssh + +import javax.servlet.{ServletContextEvent, ServletContextListener} +import gitbucket.core.service.SystemSettingsService +import gitbucket.core.util.Directory +import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider +import org.slf4j.LoggerFactory +import java.util.concurrent.atomic.AtomicBoolean + +object SshServer { + private val logger = LoggerFactory.getLogger(SshServer.getClass) + private val server = org.apache.sshd.SshServer.setUpDefaultServer() + private val active = new AtomicBoolean(false) + + private def configure(port: Int, baseUrl: String) = { + server.setPort(port) + server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(s"${Directory.GitBucketHome}/gitbucket.ser")) + server.setPublickeyAuthenticator(new PublicKeyAuthenticator) + server.setCommandFactory(new GitCommandFactory(baseUrl)) + server.setShellFactory(new NoShell) + } + + def start(port: Int, baseUrl: String) = { + if(active.compareAndSet(false, true)){ + configure(port, baseUrl) + server.start() + logger.info(s"Start SSH Server Listen on ${server.getPort}") + } + } + + def stop() = { + if(active.compareAndSet(true, false)){ + server.stop(true) + logger.info("SSH Server is stopped.") + } + } + + def isActive = active.get +} + +/* + * Start a SSH Server Daemon + * + * How to use: + * git clone ssh://username@host_or_ip:29418/owner/repository_name.git + */ +class SshServerListener extends ServletContextListener with SystemSettingsService { + + private val logger = LoggerFactory.getLogger(classOf[SshServerListener]) + + override def contextInitialized(sce: ServletContextEvent): Unit = { + val settings = loadSystemSettings() + if(settings.ssh){ + settings.baseUrl match { + case None => + logger.error("Could not start SshServer because the baseUrl is not configured.") + case Some(baseUrl) => + SshServer.start(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), baseUrl) + } + } + } + + override def contextDestroyed(sce: ServletContextEvent): Unit = { + if(loadSystemSettings().ssh){ + SshServer.stop() + } + } + +} diff --git a/src/main/scala/gitbucket/core/ssh/SshUtil.scala b/src/main/scala/gitbucket/core/ssh/SshUtil.scala new file mode 100644 index 0000000..512332c --- /dev/null +++ b/src/main/scala/gitbucket/core/ssh/SshUtil.scala @@ -0,0 +1,36 @@ +package gitbucket.core.ssh + +import java.security.PublicKey +import org.slf4j.LoggerFactory +import org.apache.commons.codec.binary.Base64 +import org.eclipse.jgit.lib.Constants +import org.apache.sshd.common.util.{KeyUtils, Buffer} + +object SshUtil { + + private val logger = LoggerFactory.getLogger(SshUtil.getClass) + + def str2PublicKey(key: String): Option[PublicKey] = { + // TODO RFC 4716 Public Key is not supported... + val parts = key.split(" ") + if (parts.size < 2) { + logger.debug(s"Invalid PublicKey Format: ${key}") + return None + } + try { + val encodedKey = parts(1) + val decode = Base64.decodeBase64(Constants.encodeASCII(encodedKey)) + Some(new Buffer(decode).getRawPublicKey) + } catch { + case e: Throwable => + logger.debug(e.getMessage, e) + None + } + } + + def fingerPrint(key: String): Option[String] = str2PublicKey(key) match { + case Some(publicKey) => Some(KeyUtils.getFingerPrint(publicKey)) + case None => None + } + +} diff --git a/src/main/scala/gitbucket/core/util/Authenticator.scala b/src/main/scala/gitbucket/core/util/Authenticator.scala new file mode 100644 index 0000000..49de665 --- /dev/null +++ b/src/main/scala/gitbucket/core/util/Authenticator.scala @@ -0,0 +1,181 @@ +package gitbucket.core.util + +import gitbucket.core.controller.ControllerBase +import gitbucket.core.service.{RepositoryService, AccountService} +import RepositoryService.RepositoryInfo +import Implicits._ +import ControlUtil._ + +/** + * Allows only oneself and administrators. + */ +trait OneselfAuthenticator { self: ControllerBase => + protected def oneselfOnly(action: => Any) = { authenticate(action) } + protected def oneselfOnly[T](action: T => Any) = (form: T) => { authenticate(action(form)) } + + private def authenticate(action: => Any) = { + { + defining(request.paths){ paths => + context.loginAccount match { + case Some(x) if(x.isAdmin) => action + case Some(x) if(paths(0) == x.userName) => action + case _ => Unauthorized() + } + } + } + } +} + +/** + * Allows only the repository owner and administrators. + */ +trait OwnerAuthenticator { self: ControllerBase with RepositoryService with AccountService => + protected def ownerOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } + protected def ownerOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } + + private def authenticate(action: (RepositoryInfo) => Any) = { + { + defining(request.paths){ paths => + getRepository(paths(0), paths(1), baseUrl).map { repository => + context.loginAccount match { + case Some(x) if(x.isAdmin) => action(repository) + case Some(x) if(repository.owner == x.userName) => action(repository) + case Some(x) if(getGroupMembers(repository.owner).exists { member => + member.userName == x.userName && member.isManager == true + }) => action(repository) + case _ => Unauthorized() + } + } getOrElse NotFound() + } + } + } +} + +/** + * Allows only signed in users. + */ +trait UsersAuthenticator { self: ControllerBase => + protected def usersOnly(action: => Any) = { authenticate(action) } + protected def usersOnly[T](action: T => Any) = (form: T) => { authenticate(action(form)) } + + private def authenticate(action: => Any) = { + { + context.loginAccount match { + case Some(x) => action + case None => Unauthorized() + } + } + } +} + +/** + * Allows only administrators. + */ +trait AdminAuthenticator { self: ControllerBase => + protected def adminOnly(action: => Any) = { authenticate(action) } + protected def adminOnly[T](action: T => Any) = (form: T) => { authenticate(action(form)) } + + private def authenticate(action: => Any) = { + { + context.loginAccount match { + case Some(x) if(x.isAdmin) => action + case _ => Unauthorized() + } + } + } +} + +/** + * Allows only collaborators and administrators. + */ +trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService => + protected def collaboratorsOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } + protected def collaboratorsOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } + + private def authenticate(action: (RepositoryInfo) => Any) = { + { + defining(request.paths){ paths => + getRepository(paths(0), paths(1), baseUrl).map { repository => + context.loginAccount match { + case Some(x) if(x.isAdmin) => action(repository) + case Some(x) if(paths(0) == x.userName) => action(repository) + case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository) + case _ => Unauthorized() + } + } getOrElse NotFound() + } + } + } +} + +/** + * Allows only the repository owner (or manager for group repository) and administrators. + */ +trait ReferrerAuthenticator { self: ControllerBase with RepositoryService => + protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } + protected def referrersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } + + private def authenticate(action: (RepositoryInfo) => Any) = { + { + defining(request.paths){ paths => + getRepository(paths(0), paths(1), baseUrl).map { repository => + if(!repository.repository.isPrivate){ + action(repository) + } else { + context.loginAccount match { + case Some(x) if(x.isAdmin) => action(repository) + case Some(x) if(paths(0) == x.userName) => action(repository) + case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository) + case _ => Unauthorized() + } + } + } getOrElse NotFound() + } + } + } +} + +/** + * Allows only signed in users which can access the repository. + */ +trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService => + protected def readableUsersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } + protected def readableUsersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } + + private def authenticate(action: (RepositoryInfo) => Any) = { + { + defining(request.paths){ paths => + getRepository(paths(0), paths(1), baseUrl).map { repository => + context.loginAccount match { + case Some(x) if(x.isAdmin) => action(repository) + case Some(x) if(!repository.repository.isPrivate) => action(repository) + case Some(x) if(paths(0) == x.userName) => action(repository) + case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository) + case _ => Unauthorized() + } + } getOrElse NotFound() + } + } + } +} + +/** + * Allows only the group managers. + */ +trait GroupManagerAuthenticator { self: ControllerBase with AccountService => + protected def managersOnly(action: => Any) = { authenticate(action) } + protected def managersOnly[T](action: T => Any) = (form: T) => { authenticate(action(form)) } + + private def authenticate(action: => Any) = { + { + defining(request.paths){ paths => + context.loginAccount match { + case Some(x) if(getGroupMembers(paths(0)).exists { member => + member.userName == x.userName && member.isManager + }) => action + case _ => Unauthorized() + } + } + } + } +} diff --git a/src/main/scala/gitbucket/core/util/ControlUtil.scala b/src/main/scala/gitbucket/core/util/ControlUtil.scala new file mode 100644 index 0000000..268e692 --- /dev/null +++ b/src/main/scala/gitbucket/core/util/ControlUtil.scala @@ -0,0 +1,46 @@ +package gitbucket.core.util + +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.revwalk.RevWalk +import org.eclipse.jgit.treewalk.TreeWalk +import scala.util.control.Exception._ +import scala.language.reflectiveCalls + +/** + * Provides control facilities. + */ +object ControlUtil { + + def defining[A, B](value: A)(f: A => B): B = f(value) + + def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B = + try f(resource) finally { + if(resource != null){ + ignoring(classOf[Throwable]) { + resource.close() + } + } + } + + def using[T](git: Git)(f: Git => T): T = + try f(git) finally git.getRepository.close() + + def using[T](git1: Git, git2: Git)(f: (Git, Git) => T): T = + try f(git1, git2) finally { + git1.getRepository.close() + git2.getRepository.close() + } + + def using[T](revWalk: RevWalk)(f: RevWalk => T): T = + try f(revWalk) finally revWalk.release() + + def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T = + try f(treeWalk) finally treeWalk.release() + + def ignore[T](f: => Unit): Unit = try { + f + } catch { + case e: Exception => () + } + +} diff --git a/src/main/scala/gitbucket/core/util/DatabaseConfig.scala b/src/main/scala/gitbucket/core/util/DatabaseConfig.scala new file mode 100644 index 0000000..ffe7cee --- /dev/null +++ b/src/main/scala/gitbucket/core/util/DatabaseConfig.scala @@ -0,0 +1,19 @@ +package gitbucket.core.util + +import com.typesafe.config.ConfigFactory +import Directory.DatabaseHome + +object DatabaseConfig { + + private val config = ConfigFactory.load("database") + private val dbUrl = config.getString("db.url") + + def url(directory: Option[String]): String = + dbUrl.replace("${DatabaseHome}", directory.getOrElse(DatabaseHome)) + + val url: String = url(None) + val user: String = config.getString("db.user") + val password: String = config.getString("db.password") + val driver: String = config.getString("db.driver") + +} diff --git a/src/main/scala/gitbucket/core/util/Directory.scala b/src/main/scala/gitbucket/core/util/Directory.scala new file mode 100644 index 0000000..de97bbb --- /dev/null +++ b/src/main/scala/gitbucket/core/util/Directory.scala @@ -0,0 +1,87 @@ +package gitbucket.core.util + +import java.io.File +import ControlUtil._ +import org.apache.commons.io.FileUtils + +/** + * Provides directories used by GitBucket. + */ +object Directory { + + val GitBucketHome = (System.getProperty("gitbucket.home") match { + // -Dgitbucket.home= + case path if(path != null) => new File(path) + case _ => scala.util.Properties.envOrNone("GITBUCKET_HOME") match { + // environment variable GITBUCKET_HOME + case Some(env) => new File(env) + // default is HOME/.gitbucket + case None => { + val oldHome = new File(System.getProperty("user.home"), "gitbucket") + if(oldHome.exists && oldHome.isDirectory && new File(oldHome, "version").exists){ + //FileUtils.moveDirectory(oldHome, newHome) + oldHome + } else { + new File(System.getProperty("user.home"), ".gitbucket") + } + } + } + }).getAbsolutePath + + val GitBucketConf = new File(GitBucketHome, "gitbucket.conf") + + val RepositoryHome = s"${GitBucketHome}/repositories" + + val DatabaseHome = s"${GitBucketHome}/data" + + val PluginHome = s"${GitBucketHome}/plugins" + + val TemporaryHome = s"${GitBucketHome}/tmp" + + /** + * Substance directory of the repository. + */ + def getRepositoryDir(owner: String, repository: String): File = + 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}/comments") + + /** + * 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"${TemporaryHome}/_upload/${sessionId}") + + /** + * Root of temporary directories for the specified repository. + */ + def getTemporaryDir(owner: String, repository: String): File = + new File(s"${TemporaryHome}/${owner}/${repository}") + + /** + * Root of plugin cache directory. Plugin repositories are cloned into this directory. + */ + def getPluginCacheDir(): File = new File(s"${TemporaryHome}/_plugins") + + /** + * Temporary directory which is used to create an archive to download repository contents. + */ + def getDownloadWorkDir(owner: String, repository: String, sessionId: String): File = + new File(getTemporaryDir(owner, repository), s"download/${sessionId}") + + /** + * Substance directory of the wiki repository. + */ + def getWikiRepositoryDir(owner: String, repository: String): File = + new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git") + +} \ No newline at end of file diff --git a/src/main/scala/gitbucket/core/util/FileUtil.scala b/src/main/scala/gitbucket/core/util/FileUtil.scala new file mode 100644 index 0000000..d3428fb --- /dev/null +++ b/src/main/scala/gitbucket/core/util/FileUtil.scala @@ -0,0 +1,53 @@ +package gitbucket.core.util + +import org.apache.commons.io.FileUtils +import java.net.URLConnection +import java.io.File +import ControlUtil._ +import scala.util.Random + +object FileUtil { + + def getMimeType(name: String): String = + defining(URLConnection.getFileNameMap()){ fileNameMap => + fileNameMap.getContentTypeFor(name) match { + case null => "application/octet-stream" + case mimeType => mimeType + } + } + + def getContentType(name: String, bytes: Array[Byte]): String = { + defining(getMimeType(name)){ mimeType => + if(mimeType == "application/octet-stream" && isText(bytes)){ + "text/plain" + } else { + mimeType + } + } + } + + 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 generateFileId: String = System.currentTimeMillis + Random.alphanumeric.take(10).mkString + + def getExtension(name: String): String = + name.lastIndexOf('.') match { + case i if(i >= 0) => name.substring(i + 1) + case _ => "" + } + + def withTmpDir[A](dir: File)(action: File => A): A = { + if(dir.exists()){ + FileUtils.deleteDirectory(dir) + } + try { + action(dir) + } finally { + FileUtils.deleteDirectory(dir) + } + } +} diff --git a/src/main/scala/gitbucket/core/util/Implicits.scala b/src/main/scala/gitbucket/core/util/Implicits.scala new file mode 100644 index 0000000..abe71ea --- /dev/null +++ b/src/main/scala/gitbucket/core/util/Implicits.scala @@ -0,0 +1,83 @@ +package gitbucket.core.util + +import gitbucket.core.servlet.Database + +import scala.util.matching.Regex +import scala.util.control.Exception._ +import slick.jdbc.JdbcBackend +import javax.servlet.http.{HttpSession, HttpServletRequest} + +/** + * Provides some usable implicit conversions. + */ +object Implicits { + + // Convert to slick session. + implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request) + + implicit class RichSeq[A](seq: Seq[A]) { + + def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition) + + @scala.annotation.tailrec + private def split[A](list: Seq[A], result: Seq[Seq[A]] = Nil)(condition: (A, A) => Boolean): Seq[Seq[A]] = + list match { + case x :: xs => { + xs.span(condition(x, _)) match { + case (matched, remained) => split(remained, result :+ (x :: matched))(condition) + } + } + case Nil => result + } + } + + implicit class RichString(value: String){ + def replaceBy(regex: Regex)(replace: Regex.MatchData => Option[String]): String = { + val sb = new StringBuilder() + var i = 0 + regex.findAllIn(value).matchData.foreach { m => + sb.append(value.substring(i, m.start)) + i = m.end + replace(m) match { + case Some(s) => sb.append(s) + case None => sb.append(m.matched) + } + } + if(i < value.length){ + sb.append(value.substring(i)) + } + sb.toString + } + + def toIntOpt: Option[Int] = catching(classOf[NumberFormatException]) opt { + Integer.parseInt(value) + } + } + + implicit class RichRequest(request: HttpServletRequest){ + + def paths: Array[String] = request.getRequestURI.substring(request.getContextPath.length + 1).split("/") + + def hasQueryString: Boolean = request.getQueryString != null + + def hasAttribute(name: String): Boolean = request.getAttribute(name) != null + + } + + implicit class RichSession(session: HttpSession){ + + def putAndGet[T](key: String, value: T): T = { + session.setAttribute(key, value) + value + } + + def getAndRemove[T](key: String): Option[T] = { + val value = session.getAttribute(key).asInstanceOf[T] + if(value == null){ + session.removeAttribute(key) + } + Option(value) + } + } + +} diff --git a/src/main/scala/gitbucket/core/util/JDBCUtil.scala b/src/main/scala/gitbucket/core/util/JDBCUtil.scala new file mode 100644 index 0000000..41a2b1a --- /dev/null +++ b/src/main/scala/gitbucket/core/util/JDBCUtil.scala @@ -0,0 +1,63 @@ +package gitbucket.core.util + +import java.sql._ +import ControlUtil._ +import scala.collection.mutable.ListBuffer + +/** + * Provides implicit class which extends java.sql.Connection. + * This is used in automatic migration in [[servlet.AutoUpdateListener]]. + */ +object JDBCUtil { + + implicit class RichConnection(conn: Connection){ + + def update(sql: String, params: Any*): Int = { + execute(sql, params: _*){ stmt => + stmt.executeUpdate() + } + } + + def find[T](sql: String, params: Any*)(f: ResultSet => T): Option[T] = { + execute(sql, params: _*){ stmt => + using(stmt.executeQuery()){ rs => + if(rs.next) Some(f(rs)) else None + } + } + } + + def select[T](sql: String, params: Any*)(f: ResultSet => T): Seq[T] = { + execute(sql, params: _*){ stmt => + using(stmt.executeQuery()){ rs => + val list = new ListBuffer[T] + while(rs.next){ + list += f(rs) + } + list.toSeq + } + } + } + + def selectInt(sql: String, params: Any*): Int = { + execute(sql, params: _*){ stmt => + using(stmt.executeQuery()){ rs => + if(rs.next) rs.getInt(1) else 0 + } + } + } + + private def execute[T](sql: String, params: Any*)(f: (PreparedStatement) => T): T = { + using(conn.prepareStatement(sql)){ stmt => + params.zipWithIndex.foreach { case (p, i) => + p match { + case x: Int => stmt.setInt(i + 1, x) + case x: String => stmt.setString(i + 1, x) + } + } + f(stmt) + } + } + + } + +} diff --git a/src/main/scala/gitbucket/core/util/JGitUtil.scala b/src/main/scala/gitbucket/core/util/JGitUtil.scala new file mode 100644 index 0000000..d2d531b --- /dev/null +++ b/src/main/scala/gitbucket/core/util/JGitUtil.scala @@ -0,0 +1,751 @@ +package gitbucket.core.util + +import gitbucket.core.service.RepositoryService +import org.eclipse.jgit.api.Git +import Directory._ +import StringUtil._ +import ControlUtil._ +import scala.annotation.tailrec +import scala.collection.JavaConverters._ +import org.eclipse.jgit.lib._ +import org.eclipse.jgit.revwalk._ +import org.eclipse.jgit.revwalk.filter._ +import org.eclipse.jgit.treewalk._ +import org.eclipse.jgit.treewalk.filter._ +import org.eclipse.jgit.diff.DiffEntry.ChangeType +import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException} +import org.eclipse.jgit.transport.RefSpec +import java.util.Date +import org.eclipse.jgit.api.errors.{JGitInternalException, InvalidRefNameException, RefAlreadyExistsException, NoHeadException} +import org.eclipse.jgit.dircache.DirCacheEntry +import org.slf4j.LoggerFactory + +/** + * Provides complex JGit operations. + */ +object JGitUtil { + + private val logger = LoggerFactory.getLogger(JGitUtil.getClass) + + /** + * The repository data. + * + * @param owner the user name of the repository owner + * @param name the repository name + * @param url the repository URL + * @param commitCount the commit count. If the repository has over 1000 commits then this property is 1001. + * @param branchList the list of branch names + * @param tags the list of tags + */ + case class RepositoryInfo(owner: String, name: String, url: String, commitCount: Int, branchList: List[String], tags: List[TagInfo]){ + def this(owner: String, name: String, baseUrl: String) = { + this(owner, name, s"${baseUrl}/git/${owner}/${name}.git", 0, Nil, Nil) + } + } + + /** + * The file data for the file list of the repository viewer. + * + * @param id the object id + * @param isDirectory whether is it directory + * @param name the file (or directory) name + * @param message the last commit message + * @param commitId the last commit id + * @param time the last modified time + * @param author the last committer name + * @param mailAddress the committer's mail address + * @param linkUrl the url of submodule + */ + case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, message: String, commitId: String, + time: Date, author: String, mailAddress: String, linkUrl: Option[String]) + + /** + * The commit data. + * + * @param id the commit id + * @param shortMessage the short message + * @param fullMessage the full message + * @param parents the list of parent commit id + * @param authorTime the author time + * @param authorName the author name + * @param authorEmailAddress the mail address of the author + * @param commitTime the commit time + * @param committerName the committer name + * @param committerEmailAddress the mail address of the committer + */ + case class CommitInfo(id: String, shortMessage: String, fullMessage: String, parents: List[String], + authorTime: Date, authorName: String, authorEmailAddress: String, + commitTime: Date, committerName: String, committerEmailAddress: String){ + + def this(rev: org.eclipse.jgit.revwalk.RevCommit) = this( + rev.getName, + rev.getShortMessage, + rev.getFullMessage, + rev.getParents().map(_.name).toList, + rev.getAuthorIdent.getWhen, + rev.getAuthorIdent.getName, + rev.getAuthorIdent.getEmailAddress, + rev.getCommitterIdent.getWhen, + rev.getCommitterIdent.getName, + rev.getCommitterIdent.getEmailAddress) + + val summary = getSummaryMessage(fullMessage, shortMessage) + + val description = defining(fullMessage.trim.indexOf("\n")){ i => + if(i >= 0){ + Some(fullMessage.trim.substring(i).trim) + } else None + } + + def isDifferentFromAuthor: Boolean = authorName != committerName || authorEmailAddress != committerEmailAddress + } + + case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String]) + + /** + * The file content data for the file content view of the repository viewer. + * + * @param viewType "image", "large" or "other" + * @param content the string content + * @param charset the character encoding + */ + case class ContentInfo(viewType: String, content: Option[String], charset: Option[String]){ + /** + * the line separator of this content ("LF" or "CRLF") + */ + val lineSeparator: String = if(content.exists(_.indexOf("\r\n") >= 0)) "CRLF" else "LF" + } + + /** + * The tag data. + * + * @param name the tag name + * @param time the tagged date + * @param id the commit id + */ + case class TagInfo(name: String, time: Date, id: String) + + /** + * The submodule data + * + * @param name the module name + * @param path the path in the repository + * @param url the repository url of this module + */ + case class SubmoduleInfo(name: String, path: String, url: String) + + case class BranchMergeInfo(ahead: Int, behind: Int, isMerged: Boolean) + + case class BranchInfo(name: String, committerName: String, commitTime: Date, committerEmailAddress:String, mergeInfo: Option[BranchMergeInfo], commitId: String) + + /** + * Returns RevCommit from the commit or tag id. + * + * @param git the Git object + * @param objectId the ObjectId of the commit or tag + * @return the RevCommit for the specified commit or tag + */ + def getRevCommitFromId(git: Git, objectId: ObjectId): RevCommit = { + val revWalk = new RevWalk(git.getRepository) + val revCommit = revWalk.parseAny(objectId) match { + case r: RevTag => revWalk.parseCommit(r.getObject) + case _ => revWalk.parseCommit(objectId) + } + revWalk.dispose + revCommit + } + + /** + * Returns the repository information. It contains branch names and tag names. + */ + def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = { + using(Git.open(getRepositoryDir(owner, repository))){ git => + try { + // get commit count + val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10001).sum + + RepositoryInfo( + owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", + // commit count + commitCount, + // branches + git.branchList.call.asScala.map { ref => + ref.getName.stripPrefix("refs/heads/") + }.toList, + // tags + git.tagList.call.asScala.map { ref => + val revCommit = getRevCommitFromId(git, ref.getObjectId) + TagInfo(ref.getName.stripPrefix("refs/tags/"), revCommit.getCommitterIdent.getWhen, revCommit.getName) + }.toList + ) + } catch { + // not initialized + case e: NoHeadException => RepositoryInfo( + owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", 0, Nil, Nil) + + } + } + } + + /** + * Returns the file list of the specified path. + * + * @param git the Git object + * @param revision the branch name or commit id + * @param path the directory path (optional) + * @return HTML of the file list + */ + def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = { + var list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])] + + using(new RevWalk(git.getRepository)){ revWalk => + val objectId = git.getRepository.resolve(revision) + val revCommit = revWalk.parseCommit(objectId) + + val treeWalk = if (path == ".") { + val treeWalk = new TreeWalk(git.getRepository) + treeWalk.addTree(revCommit.getTree) + treeWalk + } else { + val treeWalk = TreeWalk.forPath(git.getRepository, path, revCommit.getTree) + treeWalk.enterSubtree() + treeWalk + } + + using(treeWalk) { treeWalk => + while (treeWalk.next()) { + // submodule + val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){ + getSubmodules(git, revCommit.getTree).find(_.path == treeWalk.getPathString).map(_.url) + } else None + + list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl)) + } + + list.transform(tuple => + if (tuple._2 != FileMode.TREE) + tuple + else + simplifyPath(tuple) + ) + + @tailrec + def simplifyPath(tuple: (ObjectId, FileMode, String, String, Option[String])): (ObjectId, FileMode, String, String, Option[String]) = { + val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])] + using(new TreeWalk(git.getRepository)) { walk => + walk.addTree(tuple._1) + while (walk.next() && list.size < 2) { + val linkUrl = if (walk.getFileMode(0) == FileMode.GITLINK) { + getSubmodules(git, revCommit.getTree).find(_.path == walk.getPathString).map(_.url) + } else None + list.append((walk.getObjectId(0), walk.getFileMode(0), tuple._3 + "/" + walk.getPathString, tuple._4 + "/" + walk.getNameString, linkUrl)) + } + } + if (list.size != 1 || list.exists(_._2 != FileMode.TREE)) + tuple + else + simplifyPath(list(0)) + } + } + } + + val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision) + list.map { case (objectId, fileMode, path, name, linkUrl) => + defining(commits(path)){ commit => + FileInfo( + objectId, + fileMode == FileMode.TREE || fileMode == FileMode.GITLINK, + name, + getSummaryMessage(commit.getFullMessage, commit.getShortMessage), + commit.getName, + commit.getAuthorIdent.getWhen, + commit.getAuthorIdent.getName, + commit.getAuthorIdent.getEmailAddress, + linkUrl) + } + }.sortWith { (file1, file2) => + (file1.isDirectory, file2.isDirectory) match { + case (true , false) => true + case (false, true ) => false + case _ => file1.name.compareTo(file2.name) < 0 + } + }.toList + } + + /** + * Returns the first line of the commit message. + */ + private def getSummaryMessage(fullMessage: String, shortMessage: String): String = { + defining(fullMessage.trim.indexOf("\n")){ i => + defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine => + if(firstLine.length > shortMessage.length) shortMessage else firstLine + } + } + } + + /** + * Returns the commit list of the specified branch. + * + * @param git the Git object + * @param revision the branch name or commit id + * @param page the page number (1-) + * @param limit the number of commit info per page. 0 (default) means unlimited. + * @param path filters by this path. default is no filter. + * @return a tuple of the commit list and whether has next, or the error message + */ + def getCommitLog(git: Git, revision: String, page: Int = 1, limit: Int = 0, path: String = ""): Either[String, (List[CommitInfo], Boolean)] = { + val fixedPage = if(page <= 0) 1 else page + + @scala.annotation.tailrec + def getCommitLog(i: java.util.Iterator[RevCommit], count: Int, logs: List[CommitInfo]): (List[CommitInfo], Boolean) = + i.hasNext match { + case true if(limit <= 0 || logs.size < limit) => { + val commit = i.next + getCommitLog(i, count + 1, if(limit <= 0 || (fixedPage - 1) * limit <= count) logs :+ new CommitInfo(commit) else logs) + } + case _ => (logs, i.hasNext) + } + + using(new RevWalk(git.getRepository)){ revWalk => + defining(git.getRepository.resolve(revision)){ objectId => + if(objectId == null){ + Left(s"${revision} can't be resolved.") + } else { + revWalk.markStart(revWalk.parseCommit(objectId)) + if(path.nonEmpty){ + revWalk.setRevFilter(new RevFilter(){ + def include(walk: RevWalk, commit: RevCommit): Boolean = { + getDiffs(git, commit.getName, false)._1.find(_.newPath == path).nonEmpty + } + override def clone(): RevFilter = this + }) + } + Right(getCommitLog(revWalk.iterator, 0, Nil)) + } + } + } + } + + def getCommitLogs(git: Git, begin: String, includesLastCommit: Boolean = false) + (endCondition: RevCommit => Boolean): List[CommitInfo] = { + @scala.annotation.tailrec + def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[CommitInfo]): List[CommitInfo] = + i.hasNext match { + case true => { + val revCommit = i.next + if(endCondition(revCommit)){ + if(includesLastCommit) logs :+ new CommitInfo(revCommit) else logs + } else { + getCommitLog(i, logs :+ new CommitInfo(revCommit)) + } + } + case false => logs + } + + using(new RevWalk(git.getRepository)){ revWalk => + revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(begin))) + getCommitLog(revWalk.iterator, Nil).reverse + } + } + + + /** + * Returns the commit list between two revisions. + * + * @param git the Git object + * @param from the from revision + * @param to the to revision + * @return the commit list + */ + // TODO swap parameters 'from' and 'to'!? + def getCommitLog(git: Git, from: String, to: String): List[CommitInfo] = + getCommitLogs(git, to)(_.getName == from) + + /** + * Returns the latest RevCommit of the specified path. + * + * @param git the Git object + * @param path the path + * @param revision the branch name or commit id + * @return the latest commit + */ + def getLatestCommitFromPath(git: Git, path: String, revision: String): Option[RevCommit] = + getLatestCommitFromPaths(git, List(path), revision).get(path) + + /** + * Returns the list of latest RevCommit of the specified paths. + * + * @param git the Git object + * @param paths the list of paths + * @param revision the branch name or commit id + * @return the list of latest commit + */ + def getLatestCommitFromPaths(git: Git, paths: List[String], revision: String): Map[String, RevCommit] = { + val start = getRevCommitFromId(git, git.getRepository.resolve(revision)) + paths.map { path => + val commit = git.log.add(start).addPath(path).setMaxCount(1).call.iterator.next + (path, commit) + }.toMap + } + + /** + * Returns the tuple of diff of the given commit and the previous commit id. + */ + def getDiffs(git: Git, id: String, fetchContent: Boolean = true): (List[DiffInfo], Option[String]) = { + @scala.annotation.tailrec + def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[RevCommit]): List[RevCommit] = + i.hasNext match { + case true if(logs.size < 2) => getCommitLog(i, logs :+ i.next) + case _ => logs + } + + using(new RevWalk(git.getRepository)){ revWalk => + revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(id))) + val commits = getCommitLog(revWalk.iterator, Nil) + val revCommit = commits(0) + + if(commits.length >= 2){ + // not initial commit + val oldCommit = if(revCommit.getParentCount >= 2) { + // merge commit + revCommit.getParents.head + } else { + commits(1) + } + (getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName)) + + } else { + // initial commit + using(new TreeWalk(git.getRepository)){ treeWalk => + treeWalk.addTree(revCommit.getTree) + val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]() + while(treeWalk.next){ + buffer.append((if(!fetchContent){ + DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None) + } else { + DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, + JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray)) + })) + } + (buffer.toList, None) + } + } + } + } + + def getDiffs(git: Git, from: String, to: String, fetchContent: Boolean): List[DiffInfo] = { + val reader = git.getRepository.newObjectReader + val oldTreeIter = new CanonicalTreeParser + oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}")) + + val newTreeIter = new CanonicalTreeParser + newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}")) + + import scala.collection.JavaConverters._ + git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff => + if(!fetchContent || FileUtil.isImage(diff.getOldPath) || FileUtil.isImage(diff.getNewPath)){ + DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None) + } else { + DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, + JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray), + JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray)) + } + }.toList + } + + + /** + * Returns the list of branch names of the specified commit. + */ + def getBranchesOfCommit(git: Git, commitId: String): List[String] = + using(new RevWalk(git.getRepository)){ revWalk => + defining(revWalk.parseCommit(git.getRepository.resolve(commitId + "^0"))){ commit => + git.getRepository.getAllRefs.entrySet.asScala.filter { e => + (e.getKey.startsWith(Constants.R_HEADS) && revWalk.isMergedInto(commit, revWalk.parseCommit(e.getValue.getObjectId))) + }.map { e => + e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_HEADS.length) + }.toList.sorted + } + } + + /** + * Returns the list of tags of the specified commit. + */ + def getTagsOfCommit(git: Git, commitId: String): List[String] = + using(new RevWalk(git.getRepository)){ revWalk => + defining(revWalk.parseCommit(git.getRepository.resolve(commitId + "^0"))){ commit => + git.getRepository.getAllRefs.entrySet.asScala.filter { e => + (e.getKey.startsWith(Constants.R_TAGS) && revWalk.isMergedInto(commit, revWalk.parseCommit(e.getValue.getObjectId))) + }.map { e => + e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_TAGS.length) + }.toList.sorted.reverse + } + } + + def initRepository(dir: java.io.File): Unit = + using(new RepositoryBuilder().setGitDir(dir).setBare.build){ repository => + repository.create + setReceivePack(repository) + } + + def cloneRepository(from: java.io.File, to: java.io.File): Unit = + using(Git.cloneRepository.setURI(from.toURI.toString).setDirectory(to).setBare(true).call){ git => + setReceivePack(git.getRepository) + } + + def isEmpty(git: Git): Boolean = git.getRepository.resolve(Constants.HEAD) == null + + private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit = + defining(repository.getConfig){ config => + config.setBoolean("http", null, "receivepack", true) + config.save + } + + def getDefaultBranch(git: Git, repository: RepositoryService.RepositoryInfo, + revstr: String = ""): Option[(ObjectId, String)] = { + Seq( + Some(if(revstr.isEmpty) repository.repository.defaultBranch else revstr), + repository.branchList.headOption + ).flatMap { + case Some(rev) => Some((git.getRepository.resolve(rev), rev)) + case None => None + }.find(_._1 != null) + } + + def createBranch(git: Git, fromBranch: String, newBranch: String) = { + try { + git.branchCreate().setStartPoint(fromBranch).setName(newBranch).call() + Right("Branch created.") + } catch { + case e: RefAlreadyExistsException => Left("Sorry, that branch already exists.") + // JGitInternalException occurs when new branch name is 'a' and the branch whose name is 'a/*' exists. + case _: InvalidRefNameException | _: JGitInternalException => Left("Sorry, that name is invalid.") + } + } + + 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, + ref: String, fullName: String, mailAddress: String, message: String): ObjectId = { + 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() + inserter.release() + + val refUpdate = git.getRepository.updateRef(ref) + refUpdate.setNewObjectId(newHeadId) + refUpdate.update() + + newHeadId + } + + /** + * Read submodule information from .gitmodules + */ + def getSubmodules(git: Git, tree: RevTree): List[SubmoduleInfo] = { + val repository = git.getRepository + getContentFromPath(git, tree, ".gitmodules", true).map { bytes => + (try { + val config = new BlobBasedConfig(repository.getConfig(), bytes) + config.getSubsections("submodule").asScala.map { module => + val path = config.getString("submodule", module, "path") + val url = config.getString("submodule", module, "url") + SubmoduleInfo(module, path, url) + } + } catch { + case e: ConfigInvalidException => { + logger.error("Failed to load .gitmodules file for " + repository.getDirectory(), e) + Nil + } + }).toList + } getOrElse Nil + } + + /** + * Get object content of the given path as byte array from the Git repository. + * + * @param git the Git object + * @param revTree the rev tree + * @param path the path + * @param fetchLargeFile if false then returns None for the large file + * @return the byte array of content or None if object does not exist + */ + def getContentFromPath(git: Git, revTree: RevTree, path: String, fetchLargeFile: Boolean): Option[Array[Byte]] = { + @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(revTree) + treeWalk.setRecursive(true) + getPathObjectId(path, treeWalk) + } flatMap { objectId => + getContentFromId(git, objectId, fetchLargeFile) + } + } + + 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. + * + * @param git the Git object + * @param id the object id + * @param fetchLargeFile if false then returns None for the large file + * @return the byte array of content or None if object does not exist + */ + def getContentFromId(git: Git, id: ObjectId, fetchLargeFile: Boolean): Option[Array[Byte]] = try { + val loader = git.getRepository.getObjectDatabase.open(id) + if(fetchLargeFile == false && FileUtil.isLarge(loader.getSize)){ + None + } else { + using(git.getRepository.getObjectDatabase){ db => + Some(db.open(id).getBytes) + } + } + } catch { + 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 + } + + /** + * Fetch pull request contents into refs/pull/${issueId}/head and return (commitIdTo, commitIdFrom) + */ + def updatePullRequest(userName: String, repositoryName:String, branch: String, issueId: Int, + requestUserName: String, requestRepositoryName: String, requestBranch: String):(String, String) = + using(Git.open(Directory.getRepositoryDir(userName, repositoryName)), + Git.open(Directory.getRepositoryDir(requestUserName, requestRepositoryName))){ (oldGit, newGit) => + oldGit.fetch + .setRemote(Directory.getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString) + .setRefSpecs(new RefSpec(s"refs/heads/${requestBranch}:refs/pull/${issueId}/head").setForceUpdate(true)) + .call + + val commitIdTo = oldGit.getRepository.resolve(s"refs/pull/${issueId}/head").getName + val commitIdFrom = getForkedCommitId(oldGit, newGit, + userName, repositoryName, branch, + requestUserName, requestRepositoryName, requestBranch) + (commitIdTo, commitIdFrom) + } + + /** + * Returns the last modified commit of specified path + * @param git the Git object + * @param startCommit the search base commit id + * @param path the path of target file or directory + * @return the last modified commit of specified path + */ + def getLastModifiedCommit(git: Git, startCommit: RevCommit, path: String): RevCommit = { + return git.log.add(startCommit).addPath(path).setMaxCount(1).call.iterator.next + } + + def getBranches(owner: String, name: String, defaultBranch: String): Seq[BranchInfo] = { + using(Git.open(getRepositoryDir(owner, name))){ git => + val repo = git.getRepository + val defaultObject = repo.resolve(defaultBranch) + git.branchList.call.asScala.map { ref => + val walk = new RevWalk(repo) + try{ + val defaultCommit = walk.parseCommit(defaultObject) + val branchName = ref.getName.stripPrefix("refs/heads/") + val branchCommit = if(branchName == defaultBranch){ + defaultCommit + }else{ + walk.parseCommit(ref.getObjectId) + } + val when = branchCommit.getCommitterIdent.getWhen + val committer = branchCommit.getCommitterIdent.getName + val committerEmail = branchCommit.getCommitterIdent.getEmailAddress + val mergeInfo = if(branchName==defaultBranch){ + None + }else{ + walk.reset() + walk.setRevFilter( RevFilter.MERGE_BASE ) + walk.markStart(branchCommit) + walk.markStart(defaultCommit) + val mergeBase = walk.next() + walk.reset() + walk.setRevFilter(RevFilter.ALL) + Some(BranchMergeInfo( + ahead = RevWalkUtils.count(walk, branchCommit, mergeBase), + behind = RevWalkUtils.count(walk, defaultCommit, mergeBase), + isMerged = walk.isMergedInto(branchCommit, defaultCommit))) + } + BranchInfo(branchName, committer, when, committerEmail, mergeInfo, ref.getObjectId.name) + } finally { + walk.dispose(); + } + } + } + } +} diff --git a/src/main/scala/gitbucket/core/util/Keys.scala b/src/main/scala/gitbucket/core/util/Keys.scala new file mode 100644 index 0000000..a830344 --- /dev/null +++ b/src/main/scala/gitbucket/core/util/Keys.scala @@ -0,0 +1,86 @@ +package gitbucket.core.util + +/** + * Define key strings for request attributes, session attributes or flash attributes. + */ +object Keys { + + /** + * Define session keys. + */ + object Session { + + /** + * Session key for the logged in account information. + */ + val LoginAccount = "loginAccount" + + /** + * Session key for the issue search condition in dashboard. + */ + val DashboardIssues = "dashboard/issues" + + /** + * Session key for the pull request search condition in dashboard. + */ + val DashboardPulls = "dashboard/pulls" + + /** + * Generate session key for the issue search condition. + */ + def Issues(owner: String, name: String) = s"${owner}/${name}/issues" + + /** + * Generate session key for the pull request search condition. + */ + def Pulls(owner: String, name: String) = s"${owner}/${name}/pulls" + + /** + * Generate session key for the upload filename. + */ + def Upload(fileId: String) = s"upload_${fileId}" + + } + + object Flash { + + /** + * Flash key for the redirect URL. + */ + val Redirect = "redirect" + + /** + * Flash key for the information message. + */ + val Info = "info" + + } + + /** + * Define request keys. + */ + object Request { + + /** + * Request key for the Slick Session. + */ + val DBSession = "DB_SESSION" + + /** + * Request key for the Ajax request flag. + */ + val Ajax = "AJAX" + + /** + * Request key for the username which is used during Git repository access. + */ + val UserName = "USER_NAME" + + /** + * Generate request key for the request cache. + */ + def Cache(key: String) = s"cache.${key}" + + } + +} diff --git a/src/main/scala/gitbucket/core/util/LDAPUtil.scala b/src/main/scala/gitbucket/core/util/LDAPUtil.scala new file mode 100644 index 0000000..98daddc --- /dev/null +++ b/src/main/scala/gitbucket/core/util/LDAPUtil.scala @@ -0,0 +1,193 @@ +package gitbucket.core.util + +import gitbucket.core.model.Account +import ControlUtil._ +import gitbucket.core.service.SystemSettingsService +import gitbucket.core.service.SystemSettingsService.Ldap +import com.novell.ldap._ +import java.security.Security +import org.slf4j.LoggerFactory +import scala.annotation.tailrec + +/** + * Utility for LDAP authentication. + */ +object LDAPUtil { + + private val LDAP_VERSION: Int = LDAPConnection.LDAP_V3 + private val logger = LoggerFactory.getLogger(getClass().getName()) + + private val LDAP_DUMMY_MAL = "@ldap-devnull" + + /** + * Returns true if mail address ends with "@ldap-devnull" + */ + def isDummyMailAddress(account: Account): Boolean = { + account.mailAddress.endsWith(LDAP_DUMMY_MAL) + } + + /** + * Creates dummy address (userName@ldap-devnull) for LDAP login. + * + * If mail address is not managed in LDAP server, GitBucket stores this dummy address in first LDAP login. + * GitBucket does not send any mails to this dummy address. And these users must input their mail address + * at the first step after LDAP authentication. + */ + def createDummyMailAddress(userName: String): String = { + userName + LDAP_DUMMY_MAL + } + + /** + * Try authentication by LDAP using given configuration. + * Returns Right(LDAPUserInfo) if authentication is successful, otherwise Left(errorMessage). + */ + def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, LDAPUserInfo] = { + bind( + host = ldapSettings.host, + port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort), + dn = ldapSettings.bindDN.getOrElse(""), + password = ldapSettings.bindPassword.getOrElse(""), + tls = ldapSettings.tls.getOrElse(false), + ssl = ldapSettings.ssl.getOrElse(false), + keystore = ldapSettings.keystore.getOrElse(""), + error = "System LDAP authentication failed." + ){ conn => + findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute, ldapSettings.additionalFilterCondition) match { + case Some(userDN) => userAuthentication(ldapSettings, userDN, userName, password) + case None => Left("User does not exist.") + } + } + } + + private def userAuthentication(ldapSettings: Ldap, userDN: String, userName: String, password: String): Either[String, LDAPUserInfo] = { + bind( + host = ldapSettings.host, + port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort), + dn = userDN, + password = password, + tls = ldapSettings.tls.getOrElse(false), + ssl = ldapSettings.ssl.getOrElse(false), + keystore = ldapSettings.keystore.getOrElse(""), + error = "User LDAP Authentication Failed." + ){ conn => + if(ldapSettings.mailAttribute.getOrElse("").isEmpty) { + Right(LDAPUserInfo( + userName = userName, + fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute => + findFullName(conn, userDN, ldapSettings.userNameAttribute, userName, fullNameAttribute) + }.getOrElse(userName), + mailAddress = createDummyMailAddress(userName))) + } else { + findMailAddress(conn, userDN, ldapSettings.userNameAttribute, userName, ldapSettings.mailAttribute.get) match { + case Some(mailAddress) => Right(LDAPUserInfo( + userName = getUserNameFromMailAddress(userName), + fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute => + findFullName(conn, userDN, ldapSettings.userNameAttribute, userName, fullNameAttribute) + }.getOrElse(userName), + mailAddress = mailAddress)) + case None => Left("Can't find mail address.") + } + } + } + } + + private def getUserNameFromMailAddress(userName: String): String = { + (userName.indexOf('@') match { + case i if i >= 0 => userName.substring(0, i) + case i => userName + }).replaceAll("[^a-zA-Z0-9\\-_.]", "").replaceAll("^[_\\-]", "") + } + + private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, ssl: Boolean, keystore: String, error: String) + (f: LDAPConnection => Either[String, A]): Either[String, A] = { + if (tls) { + // Dynamically set Sun as the security provider + Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider()) + + if (keystore.compareTo("") != 0) { + // Dynamically set the property that JSSE uses to identify + // the keystore that holds trusted root certificates + System.setProperty("javax.net.ssl.trustStore", keystore) + } + } + + val conn: LDAPConnection = + if(ssl) { + new LDAPConnection(new LDAPJSSESecureSocketFactory()) + }else { + new LDAPConnection(new LDAPJSSEStartTLSFactory()) + } + + try { + // Connect to the server + conn.connect(host, port) + + if (tls) { + // Secure the connection + conn.startTLS() + } + + // Bind to the server + conn.bind(LDAP_VERSION, dn, password.getBytes) + + // Execute a given function and returns a its result + f(conn) + + } catch { + case e: Exception => { + // Provide more information if something goes wrong + logger.info("" + e) + + if (conn.isConnected) { + conn.disconnect() + } + // Returns an error message + Left(error) + } + } + } + + /** + * Search a specified user and returns userDN if exists. + */ + private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String, additionalFilterCondition: Option[String]): Option[String] = { + @tailrec + def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = { + if(results.hasMore){ + getEntries(results, entries :+ (try { + Option(results.next) + } catch { + case ex: LDAPReferralException => None // NOTE(tanacasino): Referral follow is off. so ignores it.(for AD) + })) + } else { + entries.flatten + } + } + + val filterCond = additionalFilterCondition.getOrElse("") match { + case "" => userNameAttribute + "=" + userName + case x => "(&(" + x + ")(" + userNameAttribute + "=" + userName + "))" + } + + getEntries(conn.search(baseDN, LDAPConnection.SCOPE_SUB, filterCond, null, false)).collectFirst { + case x => x.getDN + } + } + + private def findMailAddress(conn: LDAPConnection, userDN: String, userNameAttribute: String, userName: String, mailAttribute: String): Option[String] = + defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, userNameAttribute + "=" + userName, Array[String](mailAttribute), false)){ results => + if(results.hasMore) { + Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue) + } else None + } + + private def findFullName(conn: LDAPConnection, userDN: String, userNameAttribute: String, userName: String, nameAttribute: String): Option[String] = + defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, userNameAttribute + "=" + userName, Array[String](nameAttribute), false)){ results => + if(results.hasMore) { + Option(results.next.getAttribute(nameAttribute)).map(_.getStringValue) + } else None + } + + case class LDAPUserInfo(userName: String, fullName: String, mailAddress: String) + +} diff --git a/src/main/scala/gitbucket/core/util/LockUtil.scala b/src/main/scala/gitbucket/core/util/LockUtil.scala new file mode 100644 index 0000000..4b13d17 --- /dev/null +++ b/src/main/scala/gitbucket/core/util/LockUtil.scala @@ -0,0 +1,36 @@ +package gitbucket.core.util + +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.locks.{ReentrantLock, Lock} +import ControlUtil._ + +object LockUtil { + + /** + * lock objects + */ + private val locks = new ConcurrentHashMap[String, Lock]() + + /** + * Returns the lock object for the specified repository. + */ + private def getLockObject(key: String): Lock = synchronized { + if(!locks.containsKey(key)){ + locks.put(key, new ReentrantLock()) + } + locks.get(key) + } + + /** + * Synchronizes a given function which modifies the working copy of the wiki repository. + */ + def lock[T](key: String)(f: => T): T = defining(getLockObject(key)){ lock => + try { + lock.lock() + f + } finally { + lock.unlock() + } + } + +} diff --git a/src/main/scala/gitbucket/core/util/Notifier.scala b/src/main/scala/gitbucket/core/util/Notifier.scala new file mode 100644 index 0000000..9e80917 --- /dev/null +++ b/src/main/scala/gitbucket/core/util/Notifier.scala @@ -0,0 +1,118 @@ +package gitbucket.core.util + +import gitbucket.core.model.{Session, Issue} +import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, SystemSettingsService} +import gitbucket.core.servlet.Database +import gitbucket.core.view.Markdown + +import scala.concurrent._ +import ExecutionContext.Implicits.global +import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail} +import org.slf4j.LoggerFactory + +import gitbucket.core.controller.Context +import SystemSettingsService.Smtp +import ControlUtil.defining + +trait Notifier extends RepositoryService with AccountService with IssuesService { + def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) + (msg: String => String)(implicit context: Context): Unit + + protected def recipients(issue: Issue)(notify: String => Unit)(implicit session: Session, context: Context) = + ( + // individual repository's owner + issue.userName :: + // collaborators + getCollaborators(issue.userName, issue.repositoryName) ::: + // participants + issue.openedUserName :: + getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName) + ) + .distinct + .withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded + .foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) filterNot (LDAPUtil.isDummyMailAddress(_)) foreach (x => notify(x.mailAddress)) ) + +} + +object Notifier { + // TODO We want to be able to switch to mock. + def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match { + case settings if settings.notification => new Mailer(settings.smtp.get) + case _ => new MockMailer + } + + def msgIssue(url: String) = (content: String) => s""" + |${content}
+ |--
+ |View it on GitBucket + """.stripMargin + + def msgPullRequest(url: String) = (content: String) => s""" + |${content}
+ |View, comment on, or merge it at:
+ |${url} + """.stripMargin + + def msgComment(url: String) = (content: String) => s""" + |${content}
+ |--
+ |View it on GitBucket + """.stripMargin + + def msgStatus(url: String) = (content: String) => s""" + |${content} #${url split('/') last} + """.stripMargin +} + +class Mailer(private val smtp: Smtp) extends Notifier { + private val logger = LoggerFactory.getLogger(classOf[Mailer]) + + def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) + (msg: String => String)(implicit context: Context) = { + val database = Database() + + val f = Future { + database withSession { implicit session => + getIssue(r.owner, r.name, issueId.toString) foreach { issue => + defining( + s"[${r.name}] ${issue.title} (#${issueId})" -> + msg(Markdown.toHtml(content, r, false, true))) { case (subject, msg) => + recipients(issue) { to => + val email = new HtmlEmail + email.setHostName(smtp.host) + email.setSmtpPort(smtp.port.get) + smtp.user.foreach { user => + email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse(""))) + } + smtp.ssl.foreach { ssl => + email.setSSLOnConnect(ssl) + } + smtp.fromAddress + .map (_ -> smtp.fromName.orNull) + .orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName)) + .foreach { case (address, name) => + email.setFrom(address, name) + } + email.setCharset("UTF-8") + email.setSubject(subject) + email.setHtmlMsg(msg) + + email.addTo(to).send + } + } + } + } + "Notifications Successful." + } + f onSuccess { + case s => logger.debug(s) + } + f onFailure { + case t => logger.error("Notifications Failed.", t) + } + } +} +class MockMailer extends Notifier { + def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) + (msg: String => String)(implicit context: Context): Unit = {} +} diff --git a/src/main/scala/gitbucket/core/util/StringUtil.scala b/src/main/scala/gitbucket/core/util/StringUtil.scala new file mode 100644 index 0000000..4bb1cb0 --- /dev/null +++ b/src/main/scala/gitbucket/core/util/StringUtil.scala @@ -0,0 +1,83 @@ +package gitbucket.core.util + +import java.net.{URLDecoder, URLEncoder} +import org.mozilla.universalchardet.UniversalDetector +import ControlUtil._ +import org.apache.commons.io.input.BOMInputStream +import org.apache.commons.io.IOUtils + +object StringUtil { + + def sha1(value: String): String = + defining(java.security.MessageDigest.getInstance("SHA-1")){ md => + md.update(value.getBytes) + md.digest.map(b => "%02x".format(b)).mkString + } + + def md5(value: String): String = { + val md = java.security.MessageDigest.getInstance("MD5") + md.update(value.getBytes) + md.digest.map(b => "%02x".format(b)).mkString + } + + def urlEncode(value: String): String = URLEncoder.encode(value, "UTF-8") + + def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8") + + def splitWords(value: String): Array[String] = value.split("[ \\t ]+") + + def escapeHtml(value: String): String = + value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """) + + /** + * Make string from byte array. Character encoding is detected automatically by [[StringUtil.detectEncoding]]. + * And if given bytes contains UTF-8 BOM, it's removed from returned string. + */ + def convertFromByteArray(content: Array[Byte]): String = + IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(content)), detectEncoding(content)) + + def detectEncoding(content: Array[Byte]): String = + defining(new UniversalDetector(null)){ detector => + detector.handleData(content, 0, content.length) + detector.dataEnd() + detector.getDetectedCharset match { + case null => "UTF-8" + case e => e + } + } + + /** + * Converts line separator in the given content. + * + * @param content the content + * @param lineSeparator "LF" or "CRLF" + * @return the converted content + */ + def convertLineSeparator(content: String, lineSeparator: String): String = { + val lf = content.replace("\r\n", "\n").replace("\r", "\n") + if(lineSeparator == "CRLF"){ + lf.replace("\n", "\r\n") + } else { + lf + } + } + + /** + * Extract issue id like ```#issueId``` from the given message. + * + *@param message the message which may contains issue id + * @return the iterator of issue id + */ + def extractIssueId(message: String): Iterator[String] = + "(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(message).matchData.map(_.group(2)) + + /** + * Extract close issue id like ```close #issueId ``` from the given message. + * + * @param message the message which may contains close command + * @return the iterator of issue id + */ + def extractCloseId(message: String): Iterator[String] = + "(?i)(? + if(in != null){ + val sql = IOUtils.toString(in, "UTF-8") + using(conn.createStatement()){ stmt => + logger.debug(sqlPath + "=" + sql) + stmt.executeUpdate(sql) + } + } + } + } + + + /** + * MAJOR.MINOR + */ + val versionString = s"${majorVersion}.${minorVersion}" + +} + +object Versions { + + private val logger = LoggerFactory.getLogger(Versions.getClass) + + def update(conn: Connection, headVersion: Version, currentVersion: Version, versions: Seq[Version], cl: ClassLoader) + (save: Connection => Unit): Unit = { + logger.debug("Start schema update") + try { + if(currentVersion == headVersion){ + logger.debug("No update") + } else if(currentVersion.versionString != "0.0" && !versions.contains(currentVersion)){ + logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.") + } else { + versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn, cl)) + save(conn) + logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}") + } + } catch { + case ex: Throwable => { + logger.error("Failed to schema update", ex) + ex.printStackTrace() + conn.rollback() + } + } + logger.debug("End schema update") + } + +} + diff --git a/src/main/scala/gitbucket/core/view/AvatarImageProvider.scala b/src/main/scala/gitbucket/core/view/AvatarImageProvider.scala new file mode 100644 index 0000000..8d316ab --- /dev/null +++ b/src/main/scala/gitbucket/core/view/AvatarImageProvider.scala @@ -0,0 +1,52 @@ +package gitbucket.core.view + +import gitbucket.core.controller.Context +import gitbucket.core.service.RequestCache +import gitbucket.core.util.StringUtil +import play.twirl.api.Html + +trait AvatarImageProvider { self: RequestCache => + + /** + * Returns <img> which displays the avatar icon. + * Looks up Gravatar if avatar icon has not been configured in user settings. + */ + protected def getAvatarImageHtml(userName: String, size: Int, + mailAddress: String = "", tooltip: Boolean = false)(implicit context: Context): Html = { + + val src = if(mailAddress.isEmpty){ + // 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}&d=retro&r=g""" + } else { + s"""${context.path}/${account.userName}/_avatar""" + } + } getOrElse { + s"""${context.path}/_unknown/_avatar""" + } + } else { + // 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}&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}&d=retro&r=g""" + } else { + s"""${context.path}/_unknown/_avatar""" + } + } + } + + if(tooltip){ + Html(s"""""") + } else { + Html(s"""""") + } + } + +} \ No newline at end of file diff --git a/src/main/scala/gitbucket/core/view/LinkConverter.scala b/src/main/scala/gitbucket/core/view/LinkConverter.scala new file mode 100644 index 0000000..951a45d --- /dev/null +++ b/src/main/scala/gitbucket/core/view/LinkConverter.scala @@ -0,0 +1,36 @@ +package gitbucket.core.view + +import gitbucket.core.controller.Context +import gitbucket.core.service.{RepositoryService, RequestCache} +import gitbucket.core.util.Implicits +import gitbucket.core.util.Implicits.RichString + +trait LinkConverter { self: RequestCache => + + /** + * Converts issue id, username and commit id to link. + */ + protected def convertRefsLinks(value: String, repository: RepositoryService.RepositoryInfo, + issueIdPrefix: String = "#")(implicit context: Context): String = { + value + // escape HTML tags + .replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """) + // convert issue id to link + .replaceBy(("(?<=(^|\\W))" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m => + getIssue(repository.owner, repository.name, m.group(2)) match { + case Some(issue) if(issue.isPullRequest) + => Some(s"""#${m.group(2)}""") + case Some(_) => Some(s"""#${m.group(2)}""") + case None => Some(s"""#${m.group(2)}""") + } + } + // convert @username to link + .replaceBy("(?<=(^|\\W))@([a-zA-Z0-9\\-_]+)(?=(\\W|$))".r){ m => + getAccountByUserName(m.group(2)).map { _ => + s"""@${m.group(2)}""" + } + } + // convert commit id to link + .replaceAll("(?<=(^|\\W))([a-f0-9]{40})(?=(\\W|$))", s"""$$2""") + } +} diff --git a/src/main/scala/gitbucket/core/view/Markdown.scala b/src/main/scala/gitbucket/core/view/Markdown.scala new file mode 100644 index 0000000..fd0b19c --- /dev/null +++ b/src/main/scala/gitbucket/core/view/Markdown.scala @@ -0,0 +1,247 @@ +package gitbucket.core.view + +import java.text.Normalizer +import java.util.Locale +import java.util.regex.Pattern + +import gitbucket.core.controller.Context +import gitbucket.core.service.{RepositoryService, RequestCache, WikiService} +import gitbucket.core.util.StringUtil +import org.parboiled.common.StringUtils +import org.pegdown.LinkRenderer.Rendering +import org.pegdown._ +import org.pegdown.ast._ + +import scala.collection.JavaConverters._ + +object Markdown { + + /** + * Converts Markdown of Wiki pages to HTML. + */ + def toHtml(markdown: String, + repository: RepositoryService.RepositoryInfo, + enableWikiLink: Boolean, + enableRefsLink: Boolean, + enableTaskList: Boolean = false, + hasWritePermission: Boolean = false, + pages: List[String] = Nil)(implicit context: Context): String = { + + // escape issue id + val s = if(enableRefsLink){ + markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2") + } else markdown + + // escape task list + val source = if(enableTaskList){ + GitBucketHtmlSerializer.escapeTaskList(s) + } else s + + val rootNode = new PegDownProcessor( + Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS | Extensions.SUPPRESS_ALL_HTML + ).parseMarkdown(source.toCharArray) + + new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission, pages).toHtml(rootNode) + } +} + +class GitBucketLinkRender( + context: Context, + repository: RepositoryService.RepositoryInfo, + enableWikiLink: Boolean, + pages: List[String]) extends LinkRenderer with WikiService { + + override def render(node: WikiLinkNode): Rendering = { + if(enableWikiLink){ + try { + val text = node.getText + val (label, page) = if(text.contains('|')){ + val i = text.indexOf('|') + (text.substring(0, i), text.substring(i + 1)) + } else { + (text, text) + } + + val url = repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/" + StringUtil.urlEncode(page) + + if(pages.contains(page)){ + new Rendering(url, label) + } else { + new Rendering(url, label).withAttribute("class", "absent") + } + } catch { + case e: java.io.UnsupportedEncodingException => throw new IllegalStateException + } + } else { + super.render(node) + } + } +} + +class GitBucketVerbatimSerializer extends VerbatimSerializer { + def serialize(node: VerbatimNode, printer: Printer): Unit = { + printer.println.print("") + var text: String = node.getText + while (text.charAt(0) == '\n') { + printer.print("
") + text = text.substring(1) + } + printer.printEncoded(text) + printer.print("") + } +} + +class GitBucketHtmlSerializer( + markdown: String, + repository: RepositoryService.RepositoryInfo, + enableWikiLink: Boolean, + enableRefsLink: Boolean, + enableTaskList: Boolean, + hasWritePermission: Boolean, + pages: List[String] + )(implicit val context: Context) extends ToHtmlSerializer( + new GitBucketLinkRender(context, repository, enableWikiLink, pages), + Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava + ) with LinkConverter with RequestCache { + + override protected def printImageTag(imageNode: SuperNode, url: String): Unit = { + printer.print("") + .print("\"").printEncoded(printChildrenToString(imageNode)).print("\"/") + } + + override protected def printLink(rendering: LinkRenderer.Rendering): Unit = { + printer.print('<').print('a') + printAttribute("href", fixUrl(rendering.href)) + for (attr <- rendering.attributes.asScala) { + printAttribute(attr.name, attr.value) + } + printer.print('>').print(rendering.text).print("") + } + + private def fixUrl(url: String, isImage: Boolean = false): String = { + if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#") || url.startsWith("/")){ + url + } else if(!enableWikiLink){ + if(context.currentPath.contains("/blob/")){ + url + (if(isImage) "?raw=true" else "") + } else if(context.currentPath.contains("/tree/")){ + val paths = context.currentPath.split("/") + val branch = if(paths.length > 3) paths.drop(4).mkString("/") else repository.repository.defaultBranch + repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "") + } else { + val paths = context.currentPath.split("/") + val branch = if(paths.length > 3) paths.last else repository.repository.defaultBranch + repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "") + } + } else { + repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url + } + } + + private def printAttribute(name: String, value: String): Unit = { + printer.print(' ').print(name).print('=').print('"').print(value).print('"') + } + + private def printHeaderTag(node: HeaderNode): Unit = { + val tag = s"h${node.getLevel}" + val headerTextString = printChildrenToString(node) + val anchorName = GitBucketHtmlSerializer.generateAnchorName(headerTextString) + printer.print(s"""<$tag class="markdown-head">""") + printer.print(s"""""") + printer.print(s"""""") + visitChildren(node) + printer.print(s"") + } + + override def visit(node: HeaderNode): Unit = { + printHeaderTag(node) + } + + override def visit(node: TextNode): Unit = { + // convert commit id and username to link. + val t = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText + + // convert task list to checkbox. + val text = if(enableTaskList) GitBucketHtmlSerializer.convertCheckBox(t, hasWritePermission) else t + + if (abbreviations.isEmpty) { + printer.print(text) + } else { + printWithAbbreviations(text) + } + } + + override def visit(node: BulletListNode): Unit = { + if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) { + printer.println().print("""
    """).indent(+2) + visitChildren(node) + printer.indent(-2).println().print("
") + } else { + printIndentedTag(node, "ul") + } + } + + override def visit(node: ListItemNode): Unit = { + if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) { + printer.println() + printer.print("""
  • """) + visitChildren(node) + printer.print("
  • ") + } else { + printer.println() + printTag(node, "li") + } + } + + override def visit(node: ExpLinkNode) { + printLink(linkRenderer.render(node, printLinkChildrenToString(node))) + } + + def printLinkChildrenToString(node: SuperNode) = { + val priorPrinter = printer + printer = new Printer() + visitLinkChildren(node) + val result = printer.getString() + printer = priorPrinter + result + } + + def visitLinkChildren(node: SuperNode) { + import scala.collection.JavaConversions._ + node.getChildren.foreach(child => child match { + case node: ExpImageNode => visitLinkChild(node) + case node: SuperNode => visitLinkChildren(node) + case _ => child.accept(this) + }) + } + + def visitLinkChild(node: ExpImageNode) { + printer.print("\"").printEncoded(printChildrenToString(node)).print("\"/") + } +} + +object GitBucketHtmlSerializer { + + private val Whitespace = "[\\s]".r + + def generateAnchorName(text: String): String = { + val noWhitespace = Whitespace.replaceAllIn(text, "-") + val normalized = Normalizer.normalize(noWhitespace, Normalizer.Form.NFD) + val noSpecialChars = StringUtil.urlEncode(normalized) + noSpecialChars.toLowerCase(Locale.ENGLISH) + } + + def escapeTaskList(text: String): String = { + Pattern.compile("""^( *)- \[([x| ])\] """, Pattern.MULTILINE).matcher(text).replaceAll("$1* task:$2: ") + } + + def convertCheckBox(text: String, hasWritePermission: Boolean): String = { + val disabled = if (hasWritePermission) "" else "disabled" + text.replaceAll("task:x:", """") + .replaceAll("task: :", """") + } +} diff --git a/src/main/scala/gitbucket/core/view/Pagination.scala b/src/main/scala/gitbucket/core/view/Pagination.scala new file mode 100644 index 0000000..ad91368 --- /dev/null +++ b/src/main/scala/gitbucket/core/view/Pagination.scala @@ -0,0 +1,50 @@ +package gitbucket.core.view + +/** + * Provides control information for pagination. + * This class is used by paginator.scala.html. + * + * @param page the current page number + * @param count the total record count + * @param limit the limit record count per one page + * @param width the width (number of cells) of the paginator + */ +case class Pagination(page: Int, count: Int, limit: Int, width: Int){ + + /** + * max page number + */ + val max = (count - 1) / limit + 1 + + /** + * whether to omit the left side + */ + val omitLeft = width / 2 < page + + /** + * whether to omit the right side + */ + val omitRight = max - width / 2 > page + + /** + * Returns true if given page number is visible. + */ + def visibleFor(i: Int): Boolean = { + if(i == 1 || i == max){ + true + } else { + val leftRange = page - width / 2 + (if(omitLeft) 2 else 0) + val rightRange = page + width / 2 - (if(omitRight) 2 else 0) + + val fixedRange = if(leftRange < 1){ + (1, rightRange + (leftRange * -1) + 1) + } else if(rightRange > max){ + (leftRange - (rightRange - max), max) + } else { + (leftRange, rightRange) + } + + (i >= fixedRange._1 && i <= fixedRange._2) + } + } +} diff --git a/src/main/scala/gitbucket/core/view/helpers.scala b/src/main/scala/gitbucket/core/view/helpers.scala new file mode 100644 index 0000000..7d7b2b2 --- /dev/null +++ b/src/main/scala/gitbucket/core/view/helpers.scala @@ -0,0 +1,266 @@ +package gitbucket.core.view + +import java.text.SimpleDateFormat +import java.util.{Date, Locale, TimeZone} + +import gitbucket.core.controller.Context +import gitbucket.core.service.{RepositoryService, RequestCache} +import gitbucket.core.util.{JGitUtil, StringUtil} +import play.twirl.api.Html + +/** + * Provides helper methods for Twirl templates. + */ +object helpers extends AvatarImageProvider with LinkConverter with RequestCache { + + /** + * Format java.util.Date to "yyyy-MM-dd HH:mm:ss". + */ + def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date) + + val timeUnits = List( + (1000L, "second"), + (1000L * 60, "minute"), + (1000L * 60 * 60, "hour"), + (1000L * 60 * 60 * 24, "day"), + (1000L * 60 * 60 * 24 * 30, "month"), + (1000L * 60 * 60 * 24 * 365, "year") + ).reverse + + /** + * Format java.util.Date to "x {seconds/minutes/hours/days/months/years} ago" + */ + def datetimeAgo(date: Date): String = { + val duration = new Date().getTime - date.getTime + timeUnits.find(tuple => duration / tuple._1 > 0) match { + case Some((unitValue, unitString)) => + val value = duration / unitValue + s"${value} ${unitString}${if (value > 1) "s" else ""} ago" + case None => "just now" + } + } + + /** + * Format java.util.Date to "x {seconds/minutes/hours/days} ago" + * If duration over 1 month, format to "d MMM (yyyy)" + */ + def datetimeAgoRecentOnly(date: Date): String = { + val duration = new Date().getTime - date.getTime + timeUnits.find(tuple => duration / tuple._1 > 0) match { + case Some((_, "month")) => s"on ${new SimpleDateFormat("d MMM", Locale.ENGLISH).format(date)}" + case Some((_, "year")) => s"on ${new SimpleDateFormat("d MMM yyyy", Locale.ENGLISH).format(date)}" + case Some((unitValue, unitString)) => + val value = duration / unitValue + s"${value} ${unitString}${if (value > 1) "s" else ""} ago" + case None => "just now" + } + } + + + /** + * Format java.util.Date to "yyyy-MM-dd'T'hh:mm:ss'Z'". + */ + def datetimeRFC3339(date: Date): String = { + val sf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") + sf.setTimeZone(TimeZone.getTimeZone("UTC")) + sf.format(date) + } + + /** + * Format java.util.Date to "yyyy-MM-dd". + */ + def date(date: Date): String = new SimpleDateFormat("yyyy-MM-dd").format(date) + + /** + * Returns singular if count is 1, otherwise plural. + * If plural is not specified, returns singular + "s" as plural. + */ + def plural(count: Int, singular: String, plural: String = ""): String = + if(count == 1) singular else if(plural.isEmpty) singular + "s" else plural + + private[this] val renderersBySuffix: Seq[(String, (List[String], String, String, RepositoryService.RepositoryInfo, Boolean, Boolean, 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)) + ) + + def renderableSuffixes: Seq[String] = renderersBySuffix.map(_._1) + + /** + * Converts Markdown of Wiki pages to HTML. + */ + def markdown(value: String, + repository: RepositoryService.RepositoryInfo, + enableWikiLink: Boolean, + enableRefsLink: Boolean, + enableTaskList: Boolean = false, + hasWritePermission: Boolean = false, + pages: List[String] = Nil)(implicit context: Context): Html = + Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission, pages)) + + def renderMarkup(filePath: List[String], fileContent: String, branch: String, + repository: RepositoryService.RepositoryInfo, + enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: Context): Html = { + + val fileNameLower = filePath.reverse.head.toLowerCase + renderersBySuffix.find { case (suffix, _) => fileNameLower.endsWith(suffix) } match { + case Some((_, handler)) => handler(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) + case None => Html( + s"${ + fileContent.split("(\\r\\n)|\\n").map(xml.Utility.escape(_)).mkString("
    ") + }
    " + ) + } + } + + /** + * 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. + */ + def avatar(userName: String, size: Int, tooltip: Boolean = false)(implicit context: Context): Html = + getAvatarImageHtml(userName, size, "", tooltip) + + /** + * Returns <img> which displays the avatar icon for the given mail address. + * This method looks up Gravatar if avatar icon has not been configured in user settings. + */ + def avatar(commit: JGitUtil.CommitInfo, size: Int)(implicit context: Context): Html = + getAvatarImageHtml(commit.authorName, size, commit.authorEmailAddress) + + /** + * Converts commit id, issue id and username to the link. + */ + def link(value: String, repository: RepositoryService.RepositoryInfo)(implicit context: Context): Html = + Html(convertRefsLinks(value, repository)) + + def cut(value: String, length: Int): String = + if(value.length > length){ + value.substring(0, length) + "..." + } else { + value + } + + import scala.util.matching.Regex._ + implicit class RegexReplaceString(s: String) { + def replaceAll(pattern: String, replacer: (Match) => String): String = { + pattern.r.replaceAllIn(s, replacer) + } + } + + /** + * Convert link notations in the activity message. + */ + def activityMessage(message: String)(implicit context: Context): Html = + Html(message + .replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""$$1/$$2#$$3""") + .replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""$$1/$$2#$$3""") + .replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]" , s"""$$1/$$2""") + .replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""${m.group(3)}""") + .replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""${m.group(3)}""") + .replaceAll("\\[user:([^\\s]+?)\\]" , (m: Match) => user(m.group(1)).body) + .replaceAll("\\[commit:([^\\s]+?)/([^\\s]+?)\\@([^\\s]+?)\\]", (m: Match) => s"""${m.group(1)}/${m.group(2)}@${m.group(3).substring(0, 7)}""") + ) + + /** + * URL encode except '/'. + */ + def encodeRefName(value: String): String = StringUtil.urlEncode(value).replace("%2F", "/") + + def urlEncode(value: String): String = StringUtil.urlEncode(value) + + def urlEncode(value: Option[String]): String = value.map(urlEncode).getOrElse("") + + /** + * Generates the url to the repository. + */ + def url(repository: RepositoryService.RepositoryInfo)(implicit context: Context): String = + s"${context.path}/${repository.owner}/${repository.name}" + + /** + * Generates the url to the account page. + */ + def url(userName: String)(implicit context: Context): String = s"${context.path}/${userName}" + + /** + * Returns the url to the root of assets. + */ + def assets(implicit context: Context): String = s"${context.path}/assets" + + /** + * Generates the text link to the account page. + * If user does not exist or disabled, this method returns user name as text without link. + */ + def user(userName: String, mailAddress: String = "", styleClass: String = "")(implicit context: Context): Html = + userWithContent(userName, mailAddress, styleClass)(Html(userName)) + + /** + * Generates the avatar link to the account page. + * If user does not exist or disabled, this method returns avatar image without link. + */ + def avatarLink(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false)(implicit context: Context): Html = + userWithContent(userName, mailAddress)(avatar(userName, size, tooltip)) + + private def userWithContent(userName: String, mailAddress: String = "", styleClass: String = "")(content: Html)(implicit context: Context): Html = + (if(mailAddress.isEmpty){ + getAccountByUserName(userName) + } else { + getAccountByMailAddress(mailAddress) + }).map { account => + Html(s"""${content}""") + } getOrElse content + + + /** + * Test whether the given Date is past date. + */ + 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(".yml")) => "yaml" + case _ => "plain_text" + } + } + + def pre(value: Html): Html = Html(s"
    ${value.body.trim.split("\n").map(_.trim).mkString("\n")}
    ") + + /** + * Implicit conversion to add mkHtml() to Seq[Html]. + */ + implicit class RichHtmlSeq(seq: Seq[Html]) { + def mkHtml(separator: String) = Html(seq.mkString(separator)) + def mkHtml(separator: scala.xml.Elem) = Html(seq.mkString(separator.toString)) + } + +} diff --git a/src/main/scala/model/Account.scala b/src/main/scala/model/Account.scala deleted file mode 100644 index 012c559..0000000 --- a/src/main/scala/model/Account.scala +++ /dev/null @@ -1,39 +0,0 @@ -package model - -trait AccountComponent { self: Profile => - import profile.simple._ - import self._ - - lazy val Accounts = TableQuery[Accounts] - - class Accounts(tag: Tag) extends Table[Account](tag, "ACCOUNT") { - val userName = column[String]("USER_NAME", O PrimaryKey) - val fullName = column[String]("FULL_NAME") - val mailAddress = column[String]("MAIL_ADDRESS") - val password = column[String]("PASSWORD") - val isAdmin = column[Boolean]("ADMINISTRATOR") - val url = column[String]("URL") - val registeredDate = column[java.util.Date]("REGISTERED_DATE") - val updatedDate = column[java.util.Date]("UPDATED_DATE") - val lastLoginDate = column[java.util.Date]("LAST_LOGIN_DATE") - val image = column[String]("IMAGE") - val groupAccount = column[Boolean]("GROUP_ACCOUNT") - val removed = column[Boolean]("REMOVED") - def * = (userName, fullName, mailAddress, password, isAdmin, url.?, registeredDate, updatedDate, lastLoginDate.?, image.?, groupAccount, removed) <> (Account.tupled, Account.unapply) - } -} - -case class Account( - userName: String, - fullName: String, - mailAddress: String, - password: String, - isAdmin: Boolean, - url: Option[String], - registeredDate: java.util.Date, - updatedDate: java.util.Date, - lastLoginDate: Option[java.util.Date], - image: Option[String], - isGroupAccount: Boolean, - isRemoved: Boolean -) diff --git a/src/main/scala/model/Activity.scala b/src/main/scala/model/Activity.scala deleted file mode 100644 index 8e3960e..0000000 --- a/src/main/scala/model/Activity.scala +++ /dev/null @@ -1,29 +0,0 @@ -package model - -trait ActivityComponent extends TemplateComponent { self: Profile => - import profile.simple._ - import self._ - - lazy val Activities = TableQuery[Activities] - - class Activities(tag: Tag) extends Table[Activity](tag, "ACTIVITY") with BasicTemplate { - val activityId = column[Int]("ACTIVITY_ID", O AutoInc) - val activityUserName = column[String]("ACTIVITY_USER_NAME") - val activityType = column[String]("ACTIVITY_TYPE") - val message = column[String]("MESSAGE") - val additionalInfo = column[String]("ADDITIONAL_INFO") - val activityDate = column[java.util.Date]("ACTIVITY_DATE") - def * = (userName, repositoryName, activityUserName, activityType, message, additionalInfo.?, activityDate, activityId) <> (Activity.tupled, Activity.unapply) - } -} - -case class Activity( - userName: String, - repositoryName: String, - activityUserName: String, - activityType: String, - message: String, - additionalInfo: Option[String], - activityDate: java.util.Date, - activityId: Int = 0 -) diff --git a/src/main/scala/model/BasicTemplate.scala b/src/main/scala/model/BasicTemplate.scala deleted file mode 100644 index 52e0a11..0000000 --- a/src/main/scala/model/BasicTemplate.scala +++ /dev/null @@ -1,54 +0,0 @@ -package model - -protected[model] trait TemplateComponent { self: Profile => - import profile.simple._ - - trait BasicTemplate { self: Table[_] => - val userName = column[String]("USER_NAME") - val repositoryName = column[String]("REPOSITORY_NAME") - - def byRepository(owner: String, repository: String) = - (userName === owner.bind) && (repositoryName === repository.bind) - - def byRepository(userName: Column[String], repositoryName: Column[String]) = - (this.userName === userName) && (this.repositoryName === repositoryName) - } - - trait IssueTemplate extends BasicTemplate { self: Table[_] => - val issueId = column[Int]("ISSUE_ID") - - def byIssue(owner: String, repository: String, issueId: Int) = - byRepository(owner, repository) && (this.issueId === issueId.bind) - - def byIssue(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = - byRepository(userName, repositoryName) && (this.issueId === issueId) - } - - trait LabelTemplate extends BasicTemplate { self: Table[_] => - val labelId = column[Int]("LABEL_ID") - - def byLabel(owner: String, repository: String, labelId: Int) = - byRepository(owner, repository) && (this.labelId === labelId.bind) - - def byLabel(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = - byRepository(userName, repositoryName) && (this.labelId === labelId) - } - - trait MilestoneTemplate extends BasicTemplate { self: Table[_] => - val milestoneId = column[Int]("MILESTONE_ID") - - def byMilestone(owner: String, repository: String, milestoneId: Int) = - byRepository(owner, repository) && (this.milestoneId === milestoneId.bind) - - def byMilestone(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) = - byRepository(userName, repositoryName) && (this.milestoneId === milestoneId) - } - - trait CommitTemplate extends BasicTemplate { self: Table[_] => - val commitId = column[String]("COMMIT_ID") - - def byCommit(owner: String, repository: String, commitId: String) = - byRepository(owner, repository) && (this.commitId === commitId) - } - -} diff --git a/src/main/scala/model/Collaborator.scala b/src/main/scala/model/Collaborator.scala deleted file mode 100644 index 88311e1..0000000 --- a/src/main/scala/model/Collaborator.scala +++ /dev/null @@ -1,21 +0,0 @@ -package model - -trait CollaboratorComponent extends TemplateComponent { self: Profile => - import profile.simple._ - - lazy val Collaborators = TableQuery[Collaborators] - - class Collaborators(tag: Tag) extends Table[Collaborator](tag, "COLLABORATOR") with BasicTemplate { - val collaboratorName = column[String]("COLLABORATOR_NAME") - def * = (userName, repositoryName, collaboratorName) <> (Collaborator.tupled, Collaborator.unapply) - - def byPrimaryKey(owner: String, repository: String, collaborator: String) = - byRepository(owner, repository) && (collaboratorName === collaborator.bind) - } -} - -case class Collaborator( - userName: String, - repositoryName: String, - collaboratorName: String -) diff --git a/src/main/scala/model/Comment.scala b/src/main/scala/model/Comment.scala deleted file mode 100644 index 9569871..0000000 --- a/src/main/scala/model/Comment.scala +++ /dev/null @@ -1,78 +0,0 @@ -package model - -trait Comment { - val commentedUserName: String - val registeredDate: java.util.Date -} - -trait IssueCommentComponent extends TemplateComponent { self: Profile => - import profile.simple._ - import self._ - - lazy val IssueComments = new TableQuery(tag => new IssueComments(tag)){ - def autoInc = this returning this.map(_.commentId) - } - - class IssueComments(tag: Tag) extends Table[IssueComment](tag, "ISSUE_COMMENT") with IssueTemplate { - val commentId = column[Int]("COMMENT_ID", O AutoInc) - val action = column[String]("ACTION") - val commentedUserName = column[String]("COMMENTED_USER_NAME") - val content = column[String]("CONTENT") - val registeredDate = column[java.util.Date]("REGISTERED_DATE") - val updatedDate = column[java.util.Date]("UPDATED_DATE") - def * = (userName, repositoryName, issueId, commentId, action, commentedUserName, content, registeredDate, updatedDate) <> (IssueComment.tupled, IssueComment.unapply) - - def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind - } -} - -case class IssueComment ( - userName: String, - repositoryName: String, - issueId: Int, - commentId: Int = 0, - action: String, - commentedUserName: String, - content: String, - registeredDate: java.util.Date, - updatedDate: java.util.Date -) extends Comment - -trait CommitCommentComponent extends TemplateComponent { self: Profile => - import profile.simple._ - import self._ - - lazy val CommitComments = new TableQuery(tag => new CommitComments(tag)){ - def autoInc = this returning this.map(_.commentId) - } - - class CommitComments(tag: Tag) extends Table[CommitComment](tag, "COMMIT_COMMENT") with CommitTemplate { - val commentId = column[Int]("COMMENT_ID", O AutoInc) - val commentedUserName = column[String]("COMMENTED_USER_NAME") - val content = column[String]("CONTENT") - val fileName = column[Option[String]]("FILE_NAME") - val oldLine = column[Option[Int]]("OLD_LINE_NUMBER") - val newLine = column[Option[Int]]("NEW_LINE_NUMBER") - val registeredDate = column[java.util.Date]("REGISTERED_DATE") - val updatedDate = column[java.util.Date]("UPDATED_DATE") - val pullRequest = column[Boolean]("PULL_REQUEST") - def * = (userName, repositoryName, commitId, commentId, commentedUserName, content, fileName, oldLine, newLine, registeredDate, updatedDate, pullRequest) <> (CommitComment.tupled, CommitComment.unapply) - - def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind - } -} - -case class CommitComment( - userName: String, - repositoryName: String, - commitId: String, - commentId: Int = 0, - commentedUserName: String, - content: String, - fileName: Option[String], - oldLine: Option[Int], - newLine: Option[Int], - registeredDate: java.util.Date, - updatedDate: java.util.Date, - pullRequest: Boolean - ) extends Comment diff --git a/src/main/scala/model/GroupMembers.scala b/src/main/scala/model/GroupMembers.scala deleted file mode 100644 index f0161d3..0000000 --- a/src/main/scala/model/GroupMembers.scala +++ /dev/null @@ -1,20 +0,0 @@ -package model - -trait GroupMemberComponent { self: Profile => - import profile.simple._ - - lazy val GroupMembers = TableQuery[GroupMembers] - - class GroupMembers(tag: Tag) extends Table[GroupMember](tag, "GROUP_MEMBER") { - val groupName = column[String]("GROUP_NAME", O PrimaryKey) - val userName = column[String]("USER_NAME", O PrimaryKey) - val isManager = column[Boolean]("MANAGER") - def * = (groupName, userName, isManager) <> (GroupMember.tupled, GroupMember.unapply) - } -} - -case class GroupMember( - groupName: String, - userName: String, - isManager: Boolean -) diff --git a/src/main/scala/model/Issue.scala b/src/main/scala/model/Issue.scala deleted file mode 100644 index 85c6014..0000000 --- a/src/main/scala/model/Issue.scala +++ /dev/null @@ -1,49 +0,0 @@ -package model - -trait IssueComponent extends TemplateComponent { self: Profile => - import profile.simple._ - import self._ - - lazy val IssueId = TableQuery[IssueId] - lazy val IssueOutline = TableQuery[IssueOutline] - lazy val Issues = TableQuery[Issues] - - class IssueId(tag: Tag) extends Table[(String, String, Int)](tag, "ISSUE_ID") with IssueTemplate { - def * = (userName, repositoryName, issueId) - def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) - } - - class IssueOutline(tag: Tag) extends Table[(String, String, Int, Int)](tag, "ISSUE_OUTLINE_VIEW") with IssueTemplate { - val commentCount = column[Int]("COMMENT_COUNT") - def * = (userName, repositoryName, issueId, commentCount) - } - - class Issues(tag: Tag) extends Table[Issue](tag, "ISSUE") with IssueTemplate with MilestoneTemplate { - val openedUserName = column[String]("OPENED_USER_NAME") - val assignedUserName = column[String]("ASSIGNED_USER_NAME") - val title = column[String]("TITLE") - val content = column[String]("CONTENT") - val closed = column[Boolean]("CLOSED") - val registeredDate = column[java.util.Date]("REGISTERED_DATE") - val updatedDate = column[java.util.Date]("UPDATED_DATE") - val pullRequest = column[Boolean]("PULL_REQUEST") - def * = (userName, repositoryName, issueId, openedUserName, milestoneId.?, assignedUserName.?, title, content.?, closed, registeredDate, updatedDate, pullRequest) <> (Issue.tupled, Issue.unapply) - - def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId) - } -} - -case class Issue( - userName: String, - repositoryName: String, - issueId: Int, - openedUserName: String, - milestoneId: Option[Int], - assignedUserName: Option[String], - title: String, - content: Option[String], - closed: Boolean, - registeredDate: java.util.Date, - updatedDate: java.util.Date, - isPullRequest: Boolean -) diff --git a/src/main/scala/model/IssueLabels.scala b/src/main/scala/model/IssueLabels.scala deleted file mode 100644 index 5d42272..0000000 --- a/src/main/scala/model/IssueLabels.scala +++ /dev/null @@ -1,20 +0,0 @@ -package model - -trait IssueLabelComponent extends TemplateComponent { self: Profile => - import profile.simple._ - - lazy val IssueLabels = TableQuery[IssueLabels] - - class IssueLabels(tag: Tag) extends Table[IssueLabel](tag, "ISSUE_LABEL") with IssueTemplate with LabelTemplate { - def * = (userName, repositoryName, issueId, labelId) <> (IssueLabel.tupled, IssueLabel.unapply) - def byPrimaryKey(owner: String, repository: String, issueId: Int, labelId: Int) = - byIssue(owner, repository, issueId) && (this.labelId === labelId.bind) - } -} - -case class IssueLabel( - userName: String, - repositoryName: String, - issueId: Int, - labelId: Int -) diff --git a/src/main/scala/model/Labels.scala b/src/main/scala/model/Labels.scala deleted file mode 100644 index 47c6a2b..0000000 --- a/src/main/scala/model/Labels.scala +++ /dev/null @@ -1,37 +0,0 @@ -package model - -trait LabelComponent extends TemplateComponent { self: Profile => - import profile.simple._ - - lazy val Labels = TableQuery[Labels] - - class Labels(tag: Tag) extends Table[Label](tag, "LABEL") with LabelTemplate { - override val labelId = column[Int]("LABEL_ID", O AutoInc) - val labelName = column[String]("LABEL_NAME") - val color = column[String]("COLOR") - def * = (userName, repositoryName, labelId, labelName, color) <> (Label.tupled, Label.unapply) - - def byPrimaryKey(owner: String, repository: String, labelId: Int) = byLabel(owner, repository, labelId) - def byPrimaryKey(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = byLabel(userName, repositoryName, labelId) - } -} - -case class Label( - userName: String, - repositoryName: String, - labelId: Int = 0, - labelName: String, - color: String){ - - val fontColor = { - val r = color.substring(0, 2) - val g = color.substring(2, 4) - val b = color.substring(4, 6) - - if(Integer.parseInt(r, 16) + Integer.parseInt(g, 16) + Integer.parseInt(b, 16) > 408){ - "000000" - } else { - "ffffff" - } - } -} diff --git a/src/main/scala/model/Milestone.scala b/src/main/scala/model/Milestone.scala deleted file mode 100644 index c392219..0000000 --- a/src/main/scala/model/Milestone.scala +++ /dev/null @@ -1,30 +0,0 @@ -package model - -trait MilestoneComponent extends TemplateComponent { self: Profile => - import profile.simple._ - import self._ - - lazy val Milestones = TableQuery[Milestones] - - class Milestones(tag: Tag) extends Table[Milestone](tag, "MILESTONE") with MilestoneTemplate { - override val milestoneId = column[Int]("MILESTONE_ID", O AutoInc) - val title = column[String]("TITLE") - val description = column[String]("DESCRIPTION") - val dueDate = column[java.util.Date]("DUE_DATE") - val closedDate = column[java.util.Date]("CLOSED_DATE") - def * = (userName, repositoryName, milestoneId, title, description.?, dueDate.?, closedDate.?) <> (Milestone.tupled, Milestone.unapply) - - def byPrimaryKey(owner: String, repository: String, milestoneId: Int) = byMilestone(owner, repository, milestoneId) - def byPrimaryKey(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) = byMilestone(userName, repositoryName, milestoneId) - } -} - -case class Milestone( - userName: String, - repositoryName: String, - milestoneId: Int = 0, - title: String, - description: Option[String], - dueDate: Option[java.util.Date], - closedDate: Option[java.util.Date] -) diff --git a/src/main/scala/model/Plugin.scala b/src/main/scala/model/Plugin.scala deleted file mode 100644 index bc85ca0..0000000 --- a/src/main/scala/model/Plugin.scala +++ /dev/null @@ -1,19 +0,0 @@ -package model - -trait PluginComponent extends TemplateComponent { self: Profile => - import profile.simple._ - import self._ - - lazy val Plugins = TableQuery[Plugins] - - class Plugins(tag: Tag) extends Table[Plugin](tag, "PLUGIN"){ - val pluginId = column[String]("PLUGIN_ID", O PrimaryKey) - val version = column[String]("VERSION") - def * = (pluginId, version) <> (Plugin.tupled, Plugin.unapply) - } -} - -case class Plugin( - pluginId: String, - version: String -) diff --git a/src/main/scala/model/Profile.scala b/src/main/scala/model/Profile.scala deleted file mode 100644 index aae4a46..0000000 --- a/src/main/scala/model/Profile.scala +++ /dev/null @@ -1,45 +0,0 @@ -package model - -trait Profile { - val profile: slick.driver.JdbcProfile - import profile.simple._ - - // java.util.Date Mapped Column Types - implicit val dateColumnType = MappedColumnType.base[java.util.Date, java.sql.Timestamp]( - d => new java.sql.Timestamp(d.getTime), - t => new java.util.Date(t.getTime) - ) - - implicit class RichColumn(c1: Column[Boolean]){ - def &&(c2: => Column[Boolean], guard: => Boolean): Column[Boolean] = if(guard) c1 && c2 else c1 - } - -} - -trait ProfileBase extends Profile - with AccountComponent - with ActivityComponent - with CollaboratorComponent - with CommitCommentComponent - with GroupMemberComponent - with IssueComponent - with IssueCommentComponent - with IssueLabelComponent - with LabelComponent - with MilestoneComponent - with PullRequestComponent - with RepositoryComponent - with SshKeyComponent - with WebHookComponent - with PluginComponent { - - val profile = slick.driver.H2Driver - - /** - * Returns system date. - */ - def currentDate = new java.util.Date() - -} - -object Profile extends ProfileBase \ No newline at end of file diff --git a/src/main/scala/model/PullRequest.scala b/src/main/scala/model/PullRequest.scala deleted file mode 100644 index 3ba87ea..0000000 --- a/src/main/scala/model/PullRequest.scala +++ /dev/null @@ -1,32 +0,0 @@ -package model - -trait PullRequestComponent extends TemplateComponent { self: Profile => - import profile.simple._ - - lazy val PullRequests = TableQuery[PullRequests] - - class PullRequests(tag: Tag) extends Table[PullRequest](tag, "PULL_REQUEST") with IssueTemplate { - val branch = column[String]("BRANCH") - val requestUserName = column[String]("REQUEST_USER_NAME") - val requestRepositoryName = column[String]("REQUEST_REPOSITORY_NAME") - val requestBranch = column[String]("REQUEST_BRANCH") - val commitIdFrom = column[String]("COMMIT_ID_FROM") - val commitIdTo = column[String]("COMMIT_ID_TO") - def * = (userName, repositoryName, issueId, branch, requestUserName, requestRepositoryName, requestBranch, commitIdFrom, commitIdTo) <> (PullRequest.tupled, PullRequest.unapply) - - def byPrimaryKey(userName: String, repositoryName: String, issueId: Int) = byIssue(userName, repositoryName, issueId) - def byPrimaryKey(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = byIssue(userName, repositoryName, issueId) - } -} - -case class PullRequest( - userName: String, - repositoryName: String, - issueId: Int, - branch: String, - requestUserName: String, - requestRepositoryName: String, - requestBranch: String, - commitIdFrom: String, - commitIdTo: String -) diff --git a/src/main/scala/model/Repository.scala b/src/main/scala/model/Repository.scala deleted file mode 100644 index 5a888fc..0000000 --- a/src/main/scala/model/Repository.scala +++ /dev/null @@ -1,39 +0,0 @@ -package model - -trait RepositoryComponent extends TemplateComponent { self: Profile => - import profile.simple._ - import self._ - - lazy val Repositories = TableQuery[Repositories] - - class Repositories(tag: Tag) extends Table[Repository](tag, "REPOSITORY") with BasicTemplate { - val isPrivate = column[Boolean]("PRIVATE") - val description = column[String]("DESCRIPTION") - val defaultBranch = column[String]("DEFAULT_BRANCH") - val registeredDate = column[java.util.Date]("REGISTERED_DATE") - val updatedDate = column[java.util.Date]("UPDATED_DATE") - val lastActivityDate = column[java.util.Date]("LAST_ACTIVITY_DATE") - val originUserName = column[String]("ORIGIN_USER_NAME") - val originRepositoryName = column[String]("ORIGIN_REPOSITORY_NAME") - val parentUserName = column[String]("PARENT_USER_NAME") - val parentRepositoryName = column[String]("PARENT_REPOSITORY_NAME") - def * = (userName, repositoryName, isPrivate, description.?, defaultBranch, registeredDate, updatedDate, lastActivityDate, originUserName.?, originRepositoryName.?, parentUserName.?, parentRepositoryName.?) <> (Repository.tupled, Repository.unapply) - - def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) - } -} - -case class Repository( - userName: String, - repositoryName: String, - isPrivate: Boolean, - description: Option[String], - defaultBranch: String, - registeredDate: java.util.Date, - updatedDate: java.util.Date, - lastActivityDate: java.util.Date, - originUserName: Option[String], - originRepositoryName: Option[String], - parentUserName: Option[String], - parentRepositoryName: Option[String] -) diff --git a/src/main/scala/model/SshKey.scala b/src/main/scala/model/SshKey.scala deleted file mode 100644 index dcf3463..0000000 --- a/src/main/scala/model/SshKey.scala +++ /dev/null @@ -1,24 +0,0 @@ -package model - -trait SshKeyComponent { self: Profile => - import profile.simple._ - - lazy val SshKeys = TableQuery[SshKeys] - - class SshKeys(tag: Tag) extends Table[SshKey](tag, "SSH_KEY") { - val userName = column[String]("USER_NAME") - val sshKeyId = column[Int]("SSH_KEY_ID", O AutoInc) - val title = column[String]("TITLE") - val publicKey = column[String]("PUBLIC_KEY") - def * = (userName, sshKeyId, title, publicKey) <> (SshKey.tupled, SshKey.unapply) - - def byPrimaryKey(userName: String, sshKeyId: Int) = (this.userName === userName.bind) && (this.sshKeyId === sshKeyId.bind) - } -} - -case class SshKey( - userName: String, - sshKeyId: Int = 0, - title: String, - publicKey: String -) diff --git a/src/main/scala/model/WebHook.scala b/src/main/scala/model/WebHook.scala deleted file mode 100644 index 4c13c87..0000000 --- a/src/main/scala/model/WebHook.scala +++ /dev/null @@ -1,20 +0,0 @@ -package model - -trait WebHookComponent extends TemplateComponent { self: Profile => - import profile.simple._ - - lazy val WebHooks = TableQuery[WebHooks] - - class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate { - val url = column[String]("URL") - def * = (userName, repositoryName, url) <> (WebHook.tupled, WebHook.unapply) - - def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind) - } -} - -case class WebHook( - userName: String, - repositoryName: String, - url: String -) diff --git a/src/main/scala/model/package.scala b/src/main/scala/model/package.scala deleted file mode 100644 index c65e72e..0000000 --- a/src/main/scala/model/package.scala +++ /dev/null @@ -1,3 +0,0 @@ -package object model { - type Session = slick.jdbc.JdbcBackend#Session -} diff --git a/src/main/scala/plugin/Images.scala b/src/main/scala/plugin/Images.scala deleted file mode 100644 index b569f07..0000000 --- a/src/main/scala/plugin/Images.scala +++ /dev/null @@ -1,10 +0,0 @@ -package plugin - -/** - * Provides a helper method to generate data URI of images registered by plug-in. - */ -object Images { - - def dataURI(id: String) = s"data:image/png;base64,${PluginRegistry().getImage(id)}" - -} diff --git a/src/main/scala/plugin/Plugin.scala b/src/main/scala/plugin/Plugin.scala deleted file mode 100644 index c176874..0000000 --- a/src/main/scala/plugin/Plugin.scala +++ /dev/null @@ -1,30 +0,0 @@ -package plugin - -import javax.servlet.ServletContext - -import util.Version - -/** - * Trait for define plugin interface. - * To provide plugin, put Plugin class which mixed in this trait into the package root. - */ -trait Plugin { - - val pluginId: String - val pluginName: String - val description: String - val versions: Seq[Version] - - /** - * This method is invoked in initialization of plugin system. - * Register plugin functionality to PluginRegistry. - */ - def initialize(registry: PluginRegistry): Unit - - /** - * This method is invoked in shutdown of plugin system. - * If the plugin has any resources, release them in this method. - */ - def shutdown(registry: PluginRegistry): Unit - -} diff --git a/src/main/scala/plugin/PluginRegistory.scala b/src/main/scala/plugin/PluginRegistory.scala deleted file mode 100644 index ebdf7db..0000000 --- a/src/main/scala/plugin/PluginRegistory.scala +++ /dev/null @@ -1,161 +0,0 @@ -package plugin - -import java.io.{InputStream, FilenameFilter, File} -import java.net.URLClassLoader -import javax.servlet.ServletContext -import javax.servlet.http.{HttpServletRequest, HttpServletResponse} - -import org.slf4j.LoggerFactory -import org.apache.commons.codec.binary.{StringUtils, Base64} -import service.RepositoryService.RepositoryInfo -import util.Directory._ -import util.JDBCUtil._ -import util.ControlUtil._ -import util.{Version, Versions} - -import scala.collection.mutable -import scala.collection.mutable.ListBuffer -import app.{ControllerBase, Context} - -class PluginRegistry { - - private val plugins = new ListBuffer[PluginInfo] - private val javaScripts = new ListBuffer[(String, String)] - private val controllers = new ListBuffer[(ControllerBase, String)] - private val images = mutable.Map[String, String]() - - def addPlugin(pluginInfo: PluginInfo): Unit = { - plugins += pluginInfo - } - - def getPlugins(): List[PluginInfo] = plugins.toList - - def addImage(id: String, in: InputStream): Unit = { - val bytes = using(in){ in => - val bytes = new Array[Byte](in.available) - in.read(bytes) - bytes - } - val encoded = StringUtils.newStringUtf8(Base64.encodeBase64(bytes, false)) - images += ((id, encoded)) - } - - def getImage(id: String): String = images(id) - - def addController(controller: ControllerBase, path: String): Unit = { - controllers += ((controller, path)) - } - - def getControllers(): List[(ControllerBase, String)] = controllers.toList - - def addJavaScript(path: String, script: String): Unit = { - javaScripts += Tuple2(path, script) - } - - //def getJavaScripts(): List[(String, String)] = javaScripts.toList - - def getJavaScript(currentPath: String): Option[String] = { - javaScripts.find(x => currentPath.matches(x._1)).map(_._2) - } - - private case class GlobalAction( - method: String, - path: String, - function: (HttpServletRequest, HttpServletResponse, Context) => Any - ) - - private case class RepositoryAction( - method: String, - path: String, - function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any - ) - -} - -/** - * Provides entry point to PluginRegistry. - */ -object PluginRegistry { - - private val logger = LoggerFactory.getLogger(classOf[PluginRegistry]) - - private val instance = new PluginRegistry() - - /** - * Returns the PluginRegistry singleton instance. - */ - def apply(): PluginRegistry = instance - - /** - * Initializes all installed plugins. - */ - def initialize(context: ServletContext, conn: java.sql.Connection): Unit = { - val pluginDir = new File(PluginHome) - if(pluginDir.exists && pluginDir.isDirectory){ - pluginDir.listFiles(new FilenameFilter { - override def accept(dir: File, name: String): Boolean = name.endsWith(".jar") - }).foreach { pluginJar => - val classLoader = new URLClassLoader(Array(pluginJar.toURI.toURL), Thread.currentThread.getContextClassLoader) - try { - val plugin = classLoader.loadClass("Plugin").newInstance().asInstanceOf[Plugin] - - // Migration - val headVersion = plugin.versions.head - val currentVersion = conn.find("SELECT * FROM PLUGIN WHERE PLUGIN_ID = ?", plugin.pluginId)(_.getString("VERSION")) match { - case Some(x) => { - val dim = x.split("\\.") - Version(dim(0).toInt, dim(1).toInt) - } - case None => Version(0, 0) - } - - Versions.update(conn, headVersion, currentVersion, plugin.versions, new URLClassLoader(Array(pluginJar.toURI.toURL))){ conn => - currentVersion.versionString match { - case "0.0" => - conn.update("INSERT INTO PLUGIN (PLUGIN_ID, VERSION) VALUES (?, ?)", plugin.pluginId, headVersion.versionString) - case _ => - conn.update("UPDATE PLUGIN SET VERSION = ? WHERE PLUGIN_ID = ?", headVersion.versionString, plugin.pluginId) - } - } - - // Initialize - plugin.initialize(instance) - instance.addPlugin(PluginInfo( - pluginId = plugin.pluginId, - pluginName = plugin.pluginName, - version = plugin.versions.head.versionString, - description = plugin.description, - pluginClass = plugin - )) - - } catch { - case e: Exception => { - logger.error(s"Error during plugin initialization", e) - } - } - } - } - } - - def shutdown(context: ServletContext): Unit = { - instance.getPlugins().foreach { pluginInfo => - try { - pluginInfo.pluginClass.shutdown(instance) - } catch { - case e: Exception => { - logger.error(s"Error during plugin shutdown", e) - } - } - } - } - - -} - -case class PluginInfo( - pluginId: String, - pluginName: String, - version: String, - description: String, - pluginClass: Plugin -) \ No newline at end of file diff --git a/src/main/scala/plugin/Results.scala b/src/main/scala/plugin/Results.scala deleted file mode 100644 index 18fdb7f..0000000 --- a/src/main/scala/plugin/Results.scala +++ /dev/null @@ -1,11 +0,0 @@ -package plugin - -import play.twirl.api.Html - -/** - * Defines result case classes returned by plugin controller. - */ -object Results { - case class Redirect(path: String) - case class Fragment(html: Html) -} diff --git a/src/main/scala/plugin/Sessions.scala b/src/main/scala/plugin/Sessions.scala deleted file mode 100644 index 7398c9a..0000000 --- a/src/main/scala/plugin/Sessions.scala +++ /dev/null @@ -1,11 +0,0 @@ -package plugin - -import slick.jdbc.JdbcBackend.Session - -/** - * Provides Slick Session to Plug-ins. - */ -object Sessions { - val sessions = new ThreadLocal[Session] - implicit def session: Session = sessions.get() -} diff --git a/src/main/scala/service/AccountService.scala b/src/main/scala/service/AccountService.scala deleted file mode 100644 index c502eb7..0000000 --- a/src/main/scala/service/AccountService.scala +++ /dev/null @@ -1,178 +0,0 @@ -package service - -import model.Profile._ -import profile.simple._ -import model.{Account, GroupMember} -// TODO [Slick 2.0]NOT import directly? -import model.Profile.dateColumnType -import service.SystemSettingsService.SystemSettings -import util.StringUtil._ -import util.LDAPUtil -import org.slf4j.LoggerFactory - -trait AccountService { - - private val logger = LoggerFactory.getLogger(classOf[AccountService]) - - def authenticate(settings: SystemSettings, userName: String, password: String)(implicit s: Session): Option[Account] = - if(settings.ldapAuthentication){ - ldapAuthentication(settings, userName, password) - } else { - defaultAuthentication(userName, password) - } - - /** - * Authenticate by internal database. - */ - private def defaultAuthentication(userName: String, password: String)(implicit s: Session) = { - getAccountByUserName(userName).collect { - case account if(!account.isGroupAccount && account.password == sha1(password)) => Some(account) - } getOrElse None - } - - /** - * Authenticate by LDAP. - */ - private def ldapAuthentication(settings: SystemSettings, userName: String, password: String) - (implicit s: Session): Option[Account] = { - LDAPUtil.authenticate(settings.ldap.get, userName, password) match { - case Right(ldapUserInfo) => { - // Create or update account by LDAP information - getAccountByUserName(ldapUserInfo.userName, true) match { - case Some(x) if(!x.isRemoved) => { - if(settings.ldap.get.mailAttribute.getOrElse("").isEmpty) { - updateAccount(x.copy(fullName = ldapUserInfo.fullName)) - } else { - updateAccount(x.copy(mailAddress = ldapUserInfo.mailAddress, fullName = ldapUserInfo.fullName)) - } - getAccountByUserName(ldapUserInfo.userName) - } - case Some(x) if(x.isRemoved) => { - logger.info("LDAP Authentication Failed: Account is already registered but disabled.") - defaultAuthentication(userName, password) - } - case None => getAccountByMailAddress(ldapUserInfo.mailAddress, true) match { - case Some(x) if(!x.isRemoved) => { - updateAccount(x.copy(fullName = ldapUserInfo.fullName)) - getAccountByUserName(ldapUserInfo.userName) - } - case Some(x) if(x.isRemoved) => { - logger.info("LDAP Authentication Failed: Account is already registered but disabled.") - defaultAuthentication(userName, password) - } - case None => { - createAccount(ldapUserInfo.userName, "", ldapUserInfo.fullName, ldapUserInfo.mailAddress, false, None) - getAccountByUserName(ldapUserInfo.userName) - } - } - } - } - case Left(errorMessage) => { - logger.info(s"LDAP Authentication Failed: ${errorMessage}") - defaultAuthentication(userName, password) - } - } - } - - def getAccountByUserName(userName: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] = - Accounts filter(t => (t.userName === userName.bind) && (t.removed === false.bind, !includeRemoved)) firstOption - - def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] = - Accounts filter(t => (t.mailAddress.toLowerCase === mailAddress.toLowerCase.bind) && (t.removed === false.bind, !includeRemoved)) firstOption - - def getAllUsers(includeRemoved: Boolean = true)(implicit s: Session): List[Account] = - if(includeRemoved){ - Accounts sortBy(_.userName) list - } else { - Accounts filter (_.removed === false.bind) sortBy(_.userName) list - } - - def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String]) - (implicit s: Session): Unit = - Accounts insert Account( - userName = userName, - password = password, - fullName = fullName, - mailAddress = mailAddress, - isAdmin = isAdmin, - url = url, - registeredDate = currentDate, - updatedDate = currentDate, - lastLoginDate = None, - image = None, - isGroupAccount = false, - isRemoved = false) - - def updateAccount(account: Account)(implicit s: Session): Unit = - Accounts - .filter { a => a.userName === account.userName.bind } - .map { a => (a.password, a.fullName, a.mailAddress, a.isAdmin, a.url.?, a.registeredDate, a.updatedDate, a.lastLoginDate.?, a.removed) } - .update ( - account.password, - account.fullName, - account.mailAddress, - account.isAdmin, - account.url, - account.registeredDate, - currentDate, - account.lastLoginDate, - account.isRemoved) - - def updateAvatarImage(userName: String, image: Option[String])(implicit s: Session): Unit = - Accounts.filter(_.userName === userName.bind).map(_.image.?).update(image) - - def updateLastLoginDate(userName: String)(implicit s: Session): Unit = - Accounts.filter(_.userName === userName.bind).map(_.lastLoginDate).update(currentDate) - - def createGroup(groupName: String, url: Option[String])(implicit s: Session): Unit = - Accounts insert Account( - userName = groupName, - password = "", - fullName = groupName, - mailAddress = groupName + "@devnull", - isAdmin = false, - url = url, - registeredDate = currentDate, - updatedDate = currentDate, - lastLoginDate = None, - image = None, - isGroupAccount = true, - isRemoved = false) - - def updateGroup(groupName: String, url: Option[String], removed: Boolean)(implicit s: Session): Unit = - Accounts.filter(_.userName === groupName.bind).map(t => t.url.? -> t.removed).update(url, removed) - - def updateGroupMembers(groupName: String, members: List[(String, Boolean)])(implicit s: Session): Unit = { - GroupMembers.filter(_.groupName === groupName.bind).delete - members.foreach { case (userName, isManager) => - GroupMembers insert GroupMember (groupName, userName, isManager) - } - } - - def getGroupMembers(groupName: String)(implicit s: Session): List[GroupMember] = - GroupMembers - .filter(_.groupName === groupName.bind) - .sortBy(_.userName) - .list - - def getGroupsByUserName(userName: String)(implicit s: Session): List[String] = - GroupMembers - .filter(_.userName === userName.bind) - .sortBy(_.groupName) - .map(_.groupName) - .list - - def removeUserRelatedData(userName: String)(implicit s: Session): Unit = { - GroupMembers.filter(_.userName === userName.bind).delete - Collaborators.filter(_.collaboratorName === userName.bind).delete - Repositories.filter(_.userName === userName.bind).delete - } - - def getGroupNames(userName: String)(implicit s: Session): List[String] = { - List(userName) ++ - Collaborators.filter(_.collaboratorName === userName.bind).sortBy(_.userName).map(_.userName).list - } - -} - -object AccountService extends AccountService diff --git a/src/main/scala/service/ActivityService.scala b/src/main/scala/service/ActivityService.scala deleted file mode 100644 index b1e8202..0000000 --- a/src/main/scala/service/ActivityService.scala +++ /dev/null @@ -1,188 +0,0 @@ -package service - -import model.Profile._ -import profile.simple._ -import model.Activity - -trait ActivityService { - - def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit s: Session): List[Activity] = - Activities - .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) - .filter { case (t1, t2) => - if(isPublic){ - (t1.activityUserName === activityUserName.bind) && (t2.isPrivate === false.bind) - } else { - (t1.activityUserName === activityUserName.bind) - } - } - .sortBy { case (t1, t2) => t1.activityId desc } - .map { case (t1, t2) => t1 } - .take(30) - .list - - def getRecentActivities()(implicit s: Session): List[Activity] = - Activities - .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) - .filter { case (t1, t2) => t2.isPrivate === false.bind } - .sortBy { case (t1, t2) => t1.activityId desc } - .map { case (t1, t2) => t1 } - .take(30) - .list - - def getRecentActivitiesByOwners(owners : Set[String])(implicit s: Session): List[Activity] = - Activities - .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) - .filter { case (t1, t2) => (t2.isPrivate === false.bind) || (t2.userName inSetBind owners) } - .sortBy { case (t1, t2) => t1.activityId desc } - .map { case (t1, t2) => t1 } - .take(30) - .list - - def recordCreateRepositoryActivity(userName: String, repositoryName: String, activityUserName: String) - (implicit s: Session): Unit = - Activities insert Activity(userName, repositoryName, activityUserName, - "create_repository", - s"[user:${activityUserName}] created [repo:${userName}/${repositoryName}]", - None, - currentDate) - - def recordCreateIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String) - (implicit s: Session): Unit = - Activities insert Activity(userName, repositoryName, activityUserName, - "open_issue", - s"[user:${activityUserName}] opened issue [issue:${userName}/${repositoryName}#${issueId}]", - Some(title), - currentDate) - - def recordCloseIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String) - (implicit s: Session): Unit = - Activities insert Activity(userName, repositoryName, activityUserName, - "close_issue", - s"[user:${activityUserName}] closed issue [issue:${userName}/${repositoryName}#${issueId}]", - Some(title), - currentDate) - - def recordClosePullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String) - (implicit s: Session): Unit = - Activities insert Activity(userName, repositoryName, activityUserName, - "close_issue", - s"[user:${activityUserName}] closed pull request [pullreq:${userName}/${repositoryName}#${issueId}]", - Some(title), - currentDate) - - def recordReopenIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String) - (implicit s: Session): Unit = - Activities insert Activity(userName, repositoryName, activityUserName, - "reopen_issue", - s"[user:${activityUserName}] reopened issue [issue:${userName}/${repositoryName}#${issueId}]", - Some(title), - currentDate) - - def recordCommentIssueActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String) - (implicit s: Session): Unit = - Activities insert Activity(userName, repositoryName, activityUserName, - "comment_issue", - s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]", - Some(cut(comment, 200)), - currentDate) - - def recordCommentPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, comment: String) - (implicit s: Session): Unit = - Activities insert Activity(userName, repositoryName, activityUserName, - "comment_issue", - s"[user:${activityUserName}] commented on pull request [pullreq:${userName}/${repositoryName}#${issueId}]", - Some(cut(comment, 200)), - currentDate) - - def recordCommentCommitActivity(userName: String, repositoryName: String, activityUserName: String, commitId: String, comment: String) - (implicit s: Session): Unit = - Activities insert Activity(userName, repositoryName, activityUserName, - "comment_commit", - s"[user:${activityUserName}] commented on commit [commit:${userName}/${repositoryName}@${commitId}]", - Some(cut(comment, 200)), - currentDate - ) - - def recordCreateWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String) - (implicit s: Session): Unit = - Activities insert Activity(userName, repositoryName, activityUserName, - "create_wiki", - s"[user:${activityUserName}] created the [repo:${userName}/${repositoryName}] wiki", - Some(pageName), - currentDate) - - def recordEditWikiPageActivity(userName: String, repositoryName: String, activityUserName: String, pageName: String, commitId: String) - (implicit s: Session): Unit = - Activities insert Activity(userName, repositoryName, activityUserName, - "edit_wiki", - s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki", - Some(pageName + ":" + commitId), - currentDate) - - def recordPushActivity(userName: String, repositoryName: String, activityUserName: String, - branchName: String, commits: List[util.JGitUtil.CommitInfo])(implicit s: Session): Unit = - Activities insert Activity(userName, repositoryName, activityUserName, - "push", - s"[user:${activityUserName}] pushed to [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]", - Some(commits.map { commit => commit.id + ":" + commit.shortMessage }.mkString("\n")), - currentDate) - - def recordCreateTagActivity(userName: String, repositoryName: String, activityUserName: String, - tagName: String, commits: List[util.JGitUtil.CommitInfo])(implicit s: Session): Unit = - Activities insert Activity(userName, repositoryName, activityUserName, - "create_tag", - s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]", - None, - currentDate) - - def recordDeleteTagActivity(userName: String, repositoryName: String, activityUserName: String, - tagName: String, commits: List[util.JGitUtil.CommitInfo])(implicit s: Session): Unit = - Activities insert Activity(userName, repositoryName, activityUserName, - "delete_tag", - s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]", - None, - currentDate) - - def recordCreateBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) - (implicit s: Session): Unit = - Activities insert Activity(userName, repositoryName, activityUserName, - "create_branch", - s"[user:${activityUserName}] created branch [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]", - None, - currentDate) - - def recordDeleteBranchActivity(userName: String, repositoryName: String, activityUserName: String, branchName: String) - (implicit s: Session): Unit = - Activities insert Activity(userName, repositoryName, activityUserName, - "delete_branch", - s"[user:${activityUserName}] deleted branch ${branchName} at [repo:${userName}/${repositoryName}]", - None, - currentDate) - - def recordForkActivity(userName: String, repositoryName: String, activityUserName: String, forkedUserName: String)(implicit s: Session): Unit = - Activities insert Activity(userName, repositoryName, activityUserName, - "fork", - s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${forkedUserName}/${repositoryName}]", - None, - currentDate) - - def recordPullRequestActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, title: String) - (implicit s: Session): Unit = - Activities insert Activity(userName, repositoryName, activityUserName, - "open_pullreq", - s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]", - Some(title), - currentDate) - - def recordMergeActivity(userName: String, repositoryName: String, activityUserName: String, issueId: Int, message: String) - (implicit s: Session): Unit = - Activities insert Activity(userName, repositoryName, activityUserName, - "merge_pullreq", - s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]", - Some(message), - currentDate) - - 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/CommitsService.scala b/src/main/scala/service/CommitsService.scala deleted file mode 100644 index 6f70e3c..0000000 --- a/src/main/scala/service/CommitsService.scala +++ /dev/null @@ -1,52 +0,0 @@ -package service - -import scala.slick.jdbc.{StaticQuery => Q} -import Q.interpolation - -import model.Profile._ -import profile.simple._ -import model.CommitComment -import util.Implicits._ -import util.StringUtil._ - - -trait CommitsService { - - def getCommitComments(owner: String, repository: String, commitId: String, pullRequest: Boolean)(implicit s: Session) = - CommitComments filter { - t => t.byCommit(owner, repository, commitId) && (t.pullRequest === pullRequest || pullRequest) - } list - - def getCommitComment(owner: String, repository: String, commentId: String)(implicit s: Session) = - if (commentId forall (_.isDigit)) - CommitComments filter { t => - t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository) - } firstOption - else - None - - def createCommitComment(owner: String, repository: String, commitId: String, loginUser: String, - content: String, fileName: Option[String], oldLine: Option[Int], newLine: Option[Int], pullRequest: Boolean)(implicit s: Session): Int = - CommitComments.autoInc insert CommitComment( - userName = owner, - repositoryName = repository, - commitId = commitId, - commentedUserName = loginUser, - content = content, - fileName = fileName, - oldLine = oldLine, - newLine = newLine, - registeredDate = currentDate, - updatedDate = currentDate, - pullRequest = pullRequest) - - def updateCommitComment(commentId: Int, content: String)(implicit s: Session) = - CommitComments - .filter (_.byPrimaryKey(commentId)) - .map { t => - t.content -> t.updatedDate - }.update (content, currentDate) - - def deleteCommitComment(commentId: Int)(implicit s: Session) = - CommitComments filter (_.byPrimaryKey(commentId)) delete -} diff --git a/src/main/scala/service/IssuesService.scala b/src/main/scala/service/IssuesService.scala deleted file mode 100644 index cf7ac00..0000000 --- a/src/main/scala/service/IssuesService.scala +++ /dev/null @@ -1,467 +0,0 @@ -package service - -import scala.slick.jdbc.{StaticQuery => Q} -import Q.interpolation - -import model.Profile._ -import profile.simple._ -import model.{Issue, IssueComment, IssueLabel, Label} -import util.Implicits._ -import util.StringUtil._ - -trait IssuesService { - import IssuesService._ - - def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) = - if (issueId forall (_.isDigit)) - Issues filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption - else None - - def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) = - IssueComments filter (_.byIssue(owner, repository, issueId)) list - - def getComment(owner: String, repository: String, commentId: String)(implicit s: Session) = - if (commentId forall (_.isDigit)) - IssueComments filter { t => - t.byPrimaryKey(commentId.toInt) && t.byRepository(owner, repository) - } firstOption - else None - - def getIssueLabels(owner: String, repository: String, issueId: Int)(implicit s: Session) = - IssueLabels - .innerJoin(Labels).on { (t1, t2) => - t1.byLabel(t2.userName, t2.repositoryName, t2.labelId) - } - .filter ( _._1.byIssue(owner, repository, issueId) ) - .map ( _._2 ) - .list - - def getIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) = - IssueLabels filter (_.byPrimaryKey(owner, repository, issueId, labelId)) firstOption - - /** - * Returns the count of the search result against issues. - * - * @param condition the search condition - * @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request. - * @param repos Tuple of the repository owner and the repository name - * @return the count of the search result - */ - def countIssue(condition: IssueSearchCondition, onlyPullRequest: Boolean, - repos: (String, String)*)(implicit s: Session): Int = - Query(searchIssueQuery(repos, condition, onlyPullRequest).length).first - - /** - * Returns the Map which contains issue count for each labels. - * - * @param owner the repository owner - * @param repository the repository name - * @param condition the search condition - * @return the Map which contains issue count for each labels (key is label name, value is issue count) - */ - def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition, - filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = { - - searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), false) - .innerJoin(IssueLabels).on { (t1, t2) => - t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) - } - .innerJoin(Labels).on { case ((t1, t2), t3) => - t2.byLabel(t3.userName, t3.repositoryName, t3.labelId) - } - .groupBy { case ((t1, t2), t3) => - t3.labelName - } - .map { case (labelName, t) => - labelName -> t.length - } - .toMap - } - - /** - * Returns the search result against issues. - * - * @param condition the search condition - * @param pullRequest if true then returns only pull requests, false then returns only issues. - * @param offset the offset for pagination - * @param limit the limit for pagination - * @param repos Tuple of the repository owner and the repository name - * @return the search result (list of tuples which contain issue, labels and comment count) - */ - def searchIssue(condition: IssueSearchCondition, pullRequest: Boolean, offset: Int, limit: Int, repos: (String, String)*) - (implicit s: Session): List[IssueInfo] = { - - // get issues and comment count and labels - searchIssueQuery(repos, condition, pullRequest) - .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } - .sortBy { case (t1, t2) => - (condition.sort match { - case "created" => t1.registeredDate - case "comments" => t2.commentCount - case "updated" => t1.updatedDate - }) match { - case sort => condition.direction match { - case "asc" => sort asc - case "desc" => sort desc - } - } - } - .drop(offset).take(limit) - .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } - .leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) } - .leftJoin (Milestones) .on { case ((((t1, t2), t3), t4), t5) => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) } - .map { case ((((t1, t2), t3), t4), t5) => - (t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?, t5.title.?) - } - .list - .splitWith { (c1, c2) => - c1._1.userName == c2._1.userName && - c1._1.repositoryName == c2._1.repositoryName && - c1._1.issueId == c2._1.issueId - } - .map { issues => issues.head match { - case (issue, commentCount, _, _, _, milestone) => - IssueInfo(issue, - issues.flatMap { t => t._3.map ( - Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get) - )} toList, - milestone, - commentCount) - }} toList - } - - /** - * Assembles query for conditional issue searching. - */ - private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, pullRequest: Boolean)(implicit s: Session) = - Issues filter { t1 => - repos - .map { case (owner, repository) => t1.byRepository(owner, repository) } - .foldLeft[Column[Boolean]](false) ( _ || _ ) && - (t1.closed === (condition.state == "closed").bind) && - //(t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) && - (t1.milestoneId.? isEmpty, condition.milestone == Some(None)) && - (t1.assignedUserName === condition.assigned.get.bind, condition.assigned.isDefined) && - (t1.openedUserName === condition.author.get.bind, condition.author.isDefined) && - (t1.pullRequest === pullRequest.bind) && - // Milestone filter - (Milestones filter { t2 => - (t2.byPrimaryKey(t1.userName, t1.repositoryName, t1.milestoneId)) && - (t2.title === condition.milestone.get.get.bind) - } exists, condition.milestone.flatten.isDefined) && - // Label filter - (IssueLabels filter { t2 => - (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && - (t2.labelId in - (Labels filter { t3 => - (t3.byRepository(t1.userName, t1.repositoryName)) && - (t3.labelName inSetBind condition.labels) - } map(_.labelId))) - } exists, condition.labels.nonEmpty) && - // Visibility filter - (Repositories filter { t2 => - (t2.byRepository(t1.userName, t1.repositoryName)) && - (t2.isPrivate === (condition.visibility == Some("private")).bind) - } exists, condition.visibility.nonEmpty) && - // Organization (group) filter - (t1.userName inSetBind condition.groups, condition.groups.nonEmpty) && - // Mentioned filter - ((t1.openedUserName === condition.mentioned.get.bind) || t1.assignedUserName === condition.mentioned.get.bind || - (IssueComments filter { t2 => - (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === condition.mentioned.get.bind) - } exists), condition.mentioned.isDefined) - } - - def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String], - assignedUserName: Option[String], milestoneId: Option[Int], - isPullRequest: Boolean = false)(implicit s: Session) = - // next id number - sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int] - .firstOption.filter { id => - Issues insert Issue( - owner, - repository, - id, - loginUser, - milestoneId, - assignedUserName, - title, - content, - false, - currentDate, - currentDate, - isPullRequest) - - // increment issue id - IssueId - .filter (_.byPrimaryKey(owner, repository)) - .map (_.issueId) - .update (id) > 0 - } get - - def registerIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) = - IssueLabels insert IssueLabel(owner, repository, issueId, labelId) - - def deleteIssueLabel(owner: String, repository: String, issueId: Int, labelId: Int)(implicit s: Session) = - IssueLabels filter(_.byPrimaryKey(owner, repository, issueId, labelId)) delete - - def createComment(owner: String, repository: String, loginUser: String, - issueId: Int, content: String, action: String)(implicit s: Session): Int = - IssueComments.autoInc insert IssueComment( - userName = owner, - repositoryName = repository, - issueId = issueId, - action = action, - commentedUserName = loginUser, - content = content, - registeredDate = currentDate, - updatedDate = currentDate) - - def updateIssue(owner: String, repository: String, issueId: Int, - title: String, content: Option[String])(implicit s: Session) = - Issues - .filter (_.byPrimaryKey(owner, repository, issueId)) - .map { t => - (t.title, t.content.?, t.updatedDate) - } - .update (title, content, currentDate) - - def updateAssignedUserName(owner: String, repository: String, issueId: Int, - assignedUserName: Option[String])(implicit s: Session) = - Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName) - - def updateMilestoneId(owner: String, repository: String, issueId: Int, - milestoneId: Option[Int])(implicit s: Session) = - Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId) - - def updateComment(commentId: Int, content: String)(implicit s: Session) = - IssueComments - .filter (_.byPrimaryKey(commentId)) - .map { t => - t.content -> t.updatedDate - } - .update (content, currentDate) - - def deleteComment(commentId: Int)(implicit s: Session) = - IssueComments filter (_.byPrimaryKey(commentId)) delete - - def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean)(implicit s: Session) = - Issues - .filter (_.byPrimaryKey(owner, repository, issueId)) - .map { t => - t.closed -> t.updatedDate - } - .update (closed, currentDate) - - /** - * Search issues by keyword. - * - * @param owner the repository owner - * @param repository the repository name - * @param query the keywords separated by whitespace. - * @return issues with comment count and matched content of issue or comment - */ - def searchIssuesByKeyword(owner: String, repository: String, query: String) - (implicit s: Session): List[(Issue, Int, String)] = { - import slick.driver.JdbcDriver.likeEncode - val keywords = splitWords(query.toLowerCase) - - // Search Issue - val issues = Issues - .filter(_.byRepository(owner, repository)) - .innerJoin(IssueOutline).on { case (t1, t2) => - t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) - } - .filter { case (t1, t2) => - keywords.map { keyword => - (t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) || - (t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) - } .reduceLeft(_ && _) - } - .map { case (t1, t2) => - (t1, 0, t1.content.?, t2.commentCount) - } - - // Search IssueComment - val comments = IssueComments - .filter(_.byRepository(owner, repository)) - .innerJoin(Issues).on { case (t1, t2) => - t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) - } - .innerJoin(IssueOutline).on { case ((t1, t2), t3) => - t2.byIssue(t3.userName, t3.repositoryName, t3.issueId) - } - .filter { case ((t1, t2), t3) => - keywords.map { query => - t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^') - }.reduceLeft(_ && _) - } - .map { case ((t1, t2), t3) => - (t2, t1.commentId, t1.content.?, t3.commentCount) - } - - issues.union(comments).sortBy { case (issue, commentId, _, _) => - issue.issueId -> commentId - }.list.splitWith { case ((issue1, _, _, _), (issue2, _, _, _)) => - issue1.issueId == issue2.issueId - }.map { _.head match { - case (issue, _, content, commentCount) => (issue, commentCount, content.getOrElse("")) - } - }.toList - } - - def closeIssuesFromMessage(message: String, userName: String, owner: String, repository: String)(implicit s: Session) = { - extractCloseId(message).foreach { issueId => - for(issue <- getIssue(owner, repository, issueId) if !issue.closed){ - createComment(owner, repository, userName, issue.issueId, "Close", "close") - updateClosed(owner, repository, issue.issueId, true) - } - } - } -} - -object IssuesService { - import javax.servlet.http.HttpServletRequest - - val IssueLimit = 30 - - case class IssueSearchCondition( - labels: Set[String] = Set.empty, - milestone: Option[Option[String]] = None, - author: Option[String] = None, - assigned: Option[String] = None, - mentioned: Option[String] = None, - state: String = "open", - sort: String = "created", - direction: String = "desc", - visibility: Option[String] = None, - groups: Set[String] = Set.empty){ - - def isEmpty: Boolean = { - labels.isEmpty && milestone.isEmpty && author.isEmpty && assigned.isEmpty && - state == "open" && sort == "created" && direction == "desc" && visibility.isEmpty - } - - def nonEmpty: Boolean = !isEmpty - - def toFilterString: String = ( - List( - Some(s"is:${state}"), - author.map(author => s"author:${author}"), - assigned.map(assignee => s"assignee:${assignee}"), - mentioned.map(mentioned => s"mentions:${mentioned}") - ).flatten ++ - labels.map(label => s"label:${label}") ++ - List( - milestone.map { _ match { - case Some(x) => s"milestone:${x}" - case None => "no:milestone" - }}, - (sort, direction) match { - case ("created" , "desc") => None - case ("created" , "asc" ) => Some("sort:created-asc") - case ("comments", "desc") => Some("sort:comments-desc") - case ("comments", "asc" ) => Some("sort:comments-asc") - case ("updated" , "desc") => Some("sort:updated-desc") - case ("updated" , "asc" ) => Some("sort:updated-asc") - }, - visibility.map(visibility => s"visibility:${visibility}") - ).flatten ++ - groups.map(group => s"group:${group}") - ).mkString(" ") - - def toURL: String = - "?" + List( - if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))), - milestone.map { _ match { - case Some(x) => "milestone=" + urlEncode(x) - case None => "milestone=none" - }}, - author .map(x => "author=" + urlEncode(x)), - assigned .map(x => "assigned=" + urlEncode(x)), - mentioned.map(x => "mentioned=" + urlEncode(x)), - Some("state=" + urlEncode(state)), - Some("sort=" + urlEncode(sort)), - Some("direction=" + urlEncode(direction)), - visibility.map(x => "visibility=" + urlEncode(x)), - if(groups.isEmpty) None else Some("groups=" + urlEncode(groups.mkString(","))) - ).flatten.mkString("&") - - } - - object IssueSearchCondition { - - private def param(request: HttpServletRequest, name: String, allow: Seq[String] = Nil): Option[String] = { - val value = request.getParameter(name) - if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value) - } - - /** - * Restores IssueSearchCondition instance from filter query. - */ - def apply(filter: String, milestones: Map[String, Int]): IssueSearchCondition = { - val conditions = filter.split("[  \t]+").map { x => - val dim = x.split(":") - dim(0) -> dim(1) - }.groupBy(_._1).map { case (key, values) => - key -> values.map(_._2).toSeq - } - - val (sort, direction) = conditions.get("sort").flatMap(_.headOption).getOrElse("created-desc") match { - case "created-asc" => ("created" , "asc" ) - case "comments-desc" => ("comments", "desc") - case "comments-asc" => ("comments", "asc" ) - case "updated-desc" => ("comments", "desc") - case "updated-asc" => ("comments", "asc" ) - case _ => ("created" , "desc") - } - - IssueSearchCondition( - conditions.get("label").map(_.toSet).getOrElse(Set.empty), - conditions.get("milestone").flatMap(_.headOption) match { - case None => None - case Some("none") => Some(None) - case Some(x) => Some(Some(x)) //milestones.get(x).map(x => Some(x)) - }, - conditions.get("author").flatMap(_.headOption), - conditions.get("assignee").flatMap(_.headOption), - conditions.get("mentions").flatMap(_.headOption), - conditions.get("is").getOrElse(Seq.empty).find(x => x == "open" || x == "closed").getOrElse("open"), - sort, - direction, - conditions.get("visibility").flatMap(_.headOption), - conditions.get("group").map(_.toSet).getOrElse(Set.empty) - ) - } - - /** - * Restores IssueSearchCondition instance from request parameters. - */ - def apply(request: HttpServletRequest): IssueSearchCondition = - IssueSearchCondition( - param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty), - param(request, "milestone").map { - case "none" => None - case x => Some(x) - }, - param(request, "author"), - param(request, "assigned"), - param(request, "mentioned"), - param(request, "state", Seq("open", "closed")).getOrElse("open"), - param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"), - param(request, "direction", Seq("asc", "desc")).getOrElse("desc"), - param(request, "visibility"), - param(request, "groups").map(_.split(",").toSet).getOrElse(Set.empty) - ) - - def page(request: HttpServletRequest) = try { - val i = param(request, "page").getOrElse("1").toInt - if(i <= 0) 1 else i - } catch { - case e: NumberFormatException => 1 - } - } - - case class IssueInfo(issue: Issue, labels: List[Label], milestone: Option[String], commentCount: Int) - -} diff --git a/src/main/scala/service/LabelsService.scala b/src/main/scala/service/LabelsService.scala deleted file mode 100644 index de1dcb8..0000000 --- a/src/main/scala/service/LabelsService.scala +++ /dev/null @@ -1,34 +0,0 @@ -package service - -import model.Profile._ -import profile.simple._ -import model.Label - -trait LabelsService { - - def getLabels(owner: String, repository: String)(implicit s: Session): List[Label] = - Labels.filter(_.byRepository(owner, repository)).sortBy(_.labelName asc).list - - def getLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Option[Label] = - Labels.filter(_.byPrimaryKey(owner, repository, labelId)).firstOption - - def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Int = - Labels returning Labels.map(_.labelId) += Label( - userName = owner, - repositoryName = repository, - labelName = labelName, - color = color - ) - - def updateLabel(owner: String, repository: String, labelId: Int, labelName: String, color: String) - (implicit s: Session): Unit = - Labels.filter(_.byPrimaryKey(owner, repository, labelId)) - .map(t => t.labelName -> t.color) - .update(labelName, color) - - def deleteLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Unit = { - IssueLabels.filter(_.byLabel(owner, repository, labelId)).delete - Labels.filter(_.byPrimaryKey(owner, repository, labelId)).delete - } - -} diff --git a/src/main/scala/service/MilestonesService.scala b/src/main/scala/service/MilestonesService.scala deleted file mode 100644 index 476e0c4..0000000 --- a/src/main/scala/service/MilestonesService.scala +++ /dev/null @@ -1,57 +0,0 @@ -package service - -import model.Profile._ -import profile.simple._ -import model.Milestone -// TODO [Slick 2.0]NOT import directly? -import model.Profile.dateColumnType - -trait MilestonesService { - - def createMilestone(owner: String, repository: String, title: String, description: Option[String], - dueDate: Option[java.util.Date])(implicit s: Session): Unit = - Milestones insert Milestone( - userName = owner, - repositoryName = repository, - title = title, - description = description, - dueDate = dueDate, - closedDate = None - ) - - def updateMilestone(milestone: Milestone)(implicit s: Session): Unit = - Milestones - .filter (t => t.byPrimaryKey(milestone.userName, milestone.repositoryName, milestone.milestoneId)) - .map (t => (t.title, t.description.?, t.dueDate.?, t.closedDate.?)) - .update (milestone.title, milestone.description, milestone.dueDate, milestone.closedDate) - - def openMilestone(milestone: Milestone)(implicit s: Session): Unit = - updateMilestone(milestone.copy(closedDate = None)) - - def closeMilestone(milestone: Milestone)(implicit s: Session): Unit = - updateMilestone(milestone.copy(closedDate = Some(currentDate))) - - def deleteMilestone(owner: String, repository: String, milestoneId: Int)(implicit s: Session): Unit = { - Issues.filter(_.byMilestone(owner, repository, milestoneId)).map(_.milestoneId.?).update(None) - Milestones.filter(_.byPrimaryKey(owner, repository, milestoneId)).delete - } - - def getMilestone(owner: String, repository: String, milestoneId: Int)(implicit s: Session): Option[Milestone] = - Milestones.filter(_.byPrimaryKey(owner, repository, milestoneId)).firstOption - - def getMilestonesWithIssueCount(owner: String, repository: String)(implicit s: Session): List[(Milestone, Int, Int)] = { - val counts = Issues - .filter { t => (t.byRepository(owner, repository)) && (t.milestoneId.? isDefined) } - .groupBy { t => t.milestoneId -> t.closed } - .map { case (t1, t2) => t1._1 -> t1._2 -> t2.length } - .toMap - - getMilestones(owner, repository).map { milestone => - (milestone, counts.getOrElse((milestone.milestoneId, false), 0), counts.getOrElse((milestone.milestoneId, true), 0)) - } - } - - def getMilestones(owner: String, repository: String)(implicit s: Session): List[Milestone] = - Milestones.filter(_.byRepository(owner, repository)).sortBy(_.milestoneId asc).list - -} diff --git a/src/main/scala/service/PluginService.scala b/src/main/scala/service/PluginService.scala deleted file mode 100644 index d1bb9d8..0000000 --- a/src/main/scala/service/PluginService.scala +++ /dev/null @@ -1,24 +0,0 @@ -package service - -import model.Profile._ -import profile.simple._ -import model.Plugin - -trait PluginService { - - def getPlugins()(implicit s: Session): List[Plugin] = - Plugins.sortBy(_.pluginId).list - - def registerPlugin(plugin: Plugin)(implicit s: Session): Unit = - Plugins.insert(plugin) - - def updatePlugin(plugin: Plugin)(implicit s: Session): Unit = - Plugins.filter(_.pluginId === plugin.pluginId.bind).map(_.version).update(plugin.version) - - def deletePlugin(pluginId: String)(implicit s: Session): Unit = - Plugins.filter(_.pluginId === pluginId.bind).delete - - def getPlugin(pluginId: String)(implicit s: Session): Option[Plugin] = - Plugins.filter(_.pluginId === pluginId.bind).firstOption - -} diff --git a/src/main/scala/service/PullRequestService.scala b/src/main/scala/service/PullRequestService.scala deleted file mode 100644 index 1f09d7f..0000000 --- a/src/main/scala/service/PullRequestService.scala +++ /dev/null @@ -1,125 +0,0 @@ -package service - -import model.Profile._ -import profile.simple._ -import model.{PullRequest, Issue} -import util.JGitUtil - -trait PullRequestService { self: IssuesService => - import PullRequestService._ - - def getPullRequest(owner: String, repository: String, issueId: Int) - (implicit s: Session): Option[(Issue, PullRequest)] = - getIssue(owner, repository, issueId.toString).flatMap{ issue => - PullRequests.filter(_.byPrimaryKey(owner, repository, issueId)).firstOption.map{ - pullreq => (issue, pullreq) - } - } - - def updateCommitId(owner: String, repository: String, issueId: Int, commitIdTo: String, commitIdFrom: String) - (implicit s: Session): Unit = - PullRequests.filter(_.byPrimaryKey(owner, repository, issueId)) - .map(pr => pr.commitIdTo -> pr.commitIdFrom) - .update((commitIdTo, commitIdFrom)) - - def getPullRequestCountGroupByUser(closed: Boolean, owner: Option[String], repository: Option[String]) - (implicit s: Session): List[PullRequestCount] = - PullRequests - .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) } - .filter { case (t1, t2) => - (t2.closed === closed.bind) && - (t1.userName === owner.get.bind, owner.isDefined) && - (t1.repositoryName === repository.get.bind, repository.isDefined) - } - .groupBy { case (t1, t2) => t2.openedUserName } - .map { case (userName, t) => userName -> t.length } - .sortBy(_._2 desc) - .list - .map { x => PullRequestCount(x._1, x._2) } - -// def getAllPullRequestCountGroupByUser(closed: Boolean, userName: String)(implicit s: Session): List[PullRequestCount] = -// PullRequests -// .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) } -// .innerJoin(Repositories).on { case ((t1, t2), t3) => t2.byRepository(t3.userName, t3.repositoryName) } -// .filter { case ((t1, t2), t3) => -// (t2.closed === closed.bind) && -// ( -// (t3.isPrivate === false.bind) || -// (t3.userName === userName.bind) || -// (Collaborators.filter { t4 => t4.byRepository(t3.userName, t3.repositoryName) && (t4.collaboratorName === userName.bind)} exists) -// ) -// } -// .groupBy { case ((t1, t2), t3) => t2.openedUserName } -// .map { case (userName, t) => userName -> t.length } -// .sortBy(_._2 desc) -// .list -// .map { x => PullRequestCount(x._1, x._2) } - - def createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int, - originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String, - commitIdFrom: String, commitIdTo: String)(implicit s: Session): Unit = - PullRequests insert PullRequest( - originUserName, - originRepositoryName, - issueId, - originBranch, - requestUserName, - requestRepositoryName, - requestBranch, - commitIdFrom, - commitIdTo) - - def getPullRequestsByRequest(userName: String, repositoryName: String, branch: String, closed: Boolean) - (implicit s: Session): List[PullRequest] = - PullRequests - .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) } - .filter { case (t1, t2) => - (t1.requestUserName === userName.bind) && - (t1.requestRepositoryName === repositoryName.bind) && - (t1.requestBranch === branch.bind) && - (t2.closed === closed.bind) - } - .map { case (t1, t2) => t1 } - .list - - /** - * Fetch pull request contents into refs/pull/${issueId}/head and update pull request table. - */ - def updatePullRequests(owner: String, repository: String, branch: String)(implicit s: Session): Unit = - getPullRequestsByRequest(owner, repository, branch, false).foreach { pullreq => - if(Repositories.filter(_.byRepository(pullreq.userName, pullreq.repositoryName)).exists.run){ - val (commitIdTo, commitIdFrom) = JGitUtil.updatePullRequest( - pullreq.userName, pullreq.repositoryName, pullreq.branch, pullreq.issueId, - pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch) - updateCommitId(pullreq.userName, pullreq.repositoryName, pullreq.issueId, commitIdTo, commitIdFrom) - } - } - - def getPullRequestByRequestCommit(userName: String, repositoryName: String, toBranch:String, fromBranch: String, commitId: String) - (implicit s: Session): Option[(PullRequest, Issue)] = { - if(toBranch == fromBranch){ - None - } else { - PullRequests - .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) } - .filter { case (t1, t2) => - (t1.userName === userName.bind) && - (t1.repositoryName === repositoryName.bind) && - (t1.branch === toBranch.bind) && - (t1.requestUserName === userName.bind) && - (t1.requestRepositoryName === repositoryName.bind) && - (t1.requestBranch === fromBranch.bind) && - (t1.commitIdTo === commitId.bind) - } - .firstOption - } - } -} - -object PullRequestService { - - val PullRequestLimit = 25 - - case class PullRequestCount(userName: String, count: Int) - -} diff --git a/src/main/scala/service/RepositorySearchService.scala b/src/main/scala/service/RepositorySearchService.scala deleted file mode 100644 index f727af1..0000000 --- a/src/main/scala/service/RepositorySearchService.scala +++ /dev/null @@ -1,128 +0,0 @@ -package service - -import util.{FileUtil, StringUtil, JGitUtil} -import util.Directory._ -import util.ControlUtil._ -import org.eclipse.jgit.revwalk.RevWalk -import org.eclipse.jgit.treewalk.TreeWalk -import org.eclipse.jgit.lib.FileMode -import org.eclipse.jgit.api.Git -import model.Profile._ -import profile.simple._ - -trait RepositorySearchService { self: IssuesService => - import RepositorySearchService._ - - def countIssues(owner: String, repository: String, query: String)(implicit session: Session): Int = - searchIssuesByKeyword(owner, repository, query).length - - def searchIssues(owner: String, repository: String, query: String)(implicit session: Session): List[IssueSearchResult] = - searchIssuesByKeyword(owner, repository, query).map { case (issue, commentCount, content) => - IssueSearchResult( - issue.issueId, - issue.isPullRequest, - issue.title, - issue.openedUserName, - issue.registeredDate, - commentCount, - getHighlightText(content, query)._1) - } - - def countFiles(owner: String, repository: String, query: String): Int = - using(Git.open(getRepositoryDir(owner, repository))){ git => - if(JGitUtil.isEmpty(git)) 0 else searchRepositoryFiles(git, query).length - } - - def searchFiles(owner: String, repository: String, query: String): List[FileSearchResult] = - using(Git.open(getRepositoryDir(owner, repository))){ git => - if(JGitUtil.isEmpty(git)){ - Nil - } else { - val files = searchRepositoryFiles(git, query) - val commits = JGitUtil.getLatestCommitFromPaths(git, files.map(_._1), "HEAD") - files.map { case (path, text) => - val (highlightText, lineNumber) = getHighlightText(text, query) - FileSearchResult( - path, - commits(path).getCommitterIdent.getWhen, - highlightText, - lineNumber) - } - } - } - - private def searchRepositoryFiles(git: Git, query: String): List[(String, String)] = { - val revWalk = new RevWalk(git.getRepository) - val objectId = git.getRepository.resolve("HEAD") - val revCommit = revWalk.parseCommit(objectId) - val treeWalk = new TreeWalk(git.getRepository) - treeWalk.setRecursive(true) - treeWalk.addTree(revCommit.getTree) - - val keywords = StringUtil.splitWords(query.toLowerCase) - val list = new scala.collection.mutable.ListBuffer[(String, String)] - - while (treeWalk.next()) { - val mode = treeWalk.getFileMode(0) - if(mode == FileMode.REGULAR_FILE || mode == FileMode.EXECUTABLE_FILE){ - JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).foreach { bytes => - if(FileUtil.isText(bytes)){ - val text = StringUtil.convertFromByteArray(bytes) - val lowerText = text.toLowerCase - val indices = keywords.map(lowerText.indexOf _) - if(!indices.exists(_ < 0)){ - list.append((treeWalk.getPathString, text)) - } - } - } - } - } - treeWalk.release - revWalk.release - - list.toList - } - -} - -object RepositorySearchService { - - val CodeLimit = 10 - val IssueLimit = 10 - - def getHighlightText(content: String, query: String): (String, Int) = { - val keywords = StringUtil.splitWords(query.toLowerCase) - val lowerText = content.toLowerCase - val indices = keywords.map(lowerText.indexOf _) - - if(!indices.exists(_ < 0)){ - val lineNumber = content.substring(0, indices.min).split("\n").size - 1 - val highlightText = StringUtil.escapeHtml(content.split("\n").drop(lineNumber).take(5).mkString("\n")) - .replaceAll("(?i)(" + keywords.map("\\Q" + _ + "\\E").mkString("|") + ")", - "$1") - (highlightText, lineNumber + 1) - } else { - (content.split("\n").take(5).mkString("\n"), 1) - } - } - - case class SearchResult( - files : List[(String, String)], - issues: List[(model.Issue, Int, String)]) - - case class IssueSearchResult( - issueId: Int, - isPullRequest: Boolean, - title: String, - openedUserName: String, - registeredDate: java.util.Date, - commentCount: Int, - highlightText: String) - - case class FileSearchResult( - path: String, - lastModified: java.util.Date, - highlightText: String, - highlightLineNumber: Int) - -} diff --git a/src/main/scala/service/RepositoryService.scala b/src/main/scala/service/RepositoryService.scala deleted file mode 100644 index f54291b..0000000 --- a/src/main/scala/service/RepositoryService.scala +++ /dev/null @@ -1,399 +0,0 @@ -package service - -import model.Profile._ -import profile.simple._ -import model.{Repository, Account, Collaborator, Label} -import util.JGitUtil - -trait RepositoryService { self: AccountService => - import RepositoryService._ - - /** - * Creates a new repository. - * - * @param repositoryName the repository name - * @param userName the user name of the repository owner - * @param description the repository description - * @param isPrivate the repository type (private is true, otherwise false) - * @param originRepositoryName specify for the forked repository. (default is None) - * @param originUserName specify for the forked repository. (default is None) - */ - def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean, - originRepositoryName: Option[String] = None, originUserName: Option[String] = None, - parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None) - (implicit s: Session): Unit = { - Repositories insert - Repository( - userName = userName, - repositoryName = repositoryName, - isPrivate = isPrivate, - description = description, - defaultBranch = "master", - registeredDate = currentDate, - updatedDate = currentDate, - lastActivityDate = currentDate, - originUserName = originUserName, - originRepositoryName = originRepositoryName, - parentUserName = parentUserName, - parentRepositoryName = parentRepositoryName) - - IssueId insert (userName, repositoryName, 0) - } - - def renameRepository(oldUserName: String, oldRepositoryName: String, newUserName: String, newRepositoryName: String) - (implicit s: Session): Unit = { - getAccountByUserName(newUserName).foreach { account => - (Repositories filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository => - Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName) - - val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list - val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list - val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list - val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list - val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list - val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list - val issueComments = IssueComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list - val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list - val commitComments = CommitComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list - val collaborators = Collaborators .filter(_.byRepository(oldUserName, oldRepositoryName)).list - - Repositories.filter { t => - (t.originUserName === oldUserName.bind) && (t.originRepositoryName === oldRepositoryName.bind) - }.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName) - - Repositories.filter { t => - (t.parentUserName === oldUserName.bind) && (t.parentRepositoryName === oldRepositoryName.bind) - }.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName) - - PullRequests.filter { t => - t.requestRepositoryName === oldRepositoryName.bind - }.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName) - - // Updates activity fk before deleting repository because activity is sorted by activityId - // and it can't be changed by deleting-and-inserting record. - Activities.filter(_.byRepository(oldUserName, oldRepositoryName)).list.foreach { activity => - Activities.filter(_.activityId === activity.activityId.bind) - .map(x => (x.userName, x.repositoryName)).update(newUserName, newRepositoryName) - } - - deleteRepository(oldUserName, oldRepositoryName) - - WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) - Milestones.insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) - IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*) - - val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list - Issues.insertAll(issues.map { x => x.copy( - userName = newUserName, - repositoryName = newRepositoryName, - milestoneId = x.milestoneId.map { id => - newMilestones.find(_.title == milestones.find(_.milestoneId == id).get.title).get.milestoneId - } - )} :_*) - - PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) - IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) - Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) - CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) - - // Convert labelId - val oldLabelMap = labels.map(x => (x.labelId, x.labelName)).toMap - val newLabelMap = Labels.filter(_.byRepository(newUserName, newRepositoryName)).map(x => (x.labelName, x.labelId)).list.toMap - IssueLabels.insertAll(issueLabels.map(x => x.copy( - labelId = newLabelMap(oldLabelMap(x.labelId)), - userName = newUserName, - repositoryName = newRepositoryName - )) :_*) - - if(account.isGroupAccount){ - Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*) - } else { - Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) - } - - // Update activity messages - Activities.filter { t => - (t.message like s"%:${oldUserName}/${oldRepositoryName}]%") || - (t.message like s"%:${oldUserName}/${oldRepositoryName}#%") || - (t.message like s"%:${oldUserName}/${oldRepositoryName}@%") - }.map { t => t.activityId -> t.message }.list.foreach { case (activityId, message) => - Activities.filter(_.activityId === activityId.bind).map(_.message).update( - message - .replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]") - .replace(s"[branch:${oldUserName}/${oldRepositoryName}#" ,s"[branch:${newUserName}/${newRepositoryName}#") - .replace(s"[tag:${oldUserName}/${oldRepositoryName}#" ,s"[tag:${newUserName}/${newRepositoryName}#") - .replace(s"[pullreq:${oldUserName}/${oldRepositoryName}#",s"[pullreq:${newUserName}/${newRepositoryName}#") - .replace(s"[issue:${oldUserName}/${oldRepositoryName}#" ,s"[issue:${newUserName}/${newRepositoryName}#") - .replace(s"[commit:${oldUserName}/${oldRepositoryName}@" ,s"[commit:${newUserName}/${newRepositoryName}@") - ) - } - } - } - } - - def deleteRepository(userName: String, repositoryName: String)(implicit s: Session): Unit = { - Activities .filter(_.byRepository(userName, repositoryName)).delete - Collaborators .filter(_.byRepository(userName, repositoryName)).delete - CommitComments.filter(_.byRepository(userName, repositoryName)).delete - IssueLabels .filter(_.byRepository(userName, repositoryName)).delete - Labels .filter(_.byRepository(userName, repositoryName)).delete - IssueComments .filter(_.byRepository(userName, repositoryName)).delete - PullRequests .filter(_.byRepository(userName, repositoryName)).delete - Issues .filter(_.byRepository(userName, repositoryName)).delete - IssueId .filter(_.byRepository(userName, repositoryName)).delete - Milestones .filter(_.byRepository(userName, repositoryName)).delete - WebHooks .filter(_.byRepository(userName, repositoryName)).delete - Repositories .filter(_.byRepository(userName, repositoryName)).delete - - // Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME - Repositories - .filter { x => (x.originUserName === userName.bind) && (x.originRepositoryName === repositoryName.bind) } - .map { x => (x.userName, x.repositoryName) } - .list - .foreach { case (userName, repositoryName) => - Repositories - .filter(_.byRepository(userName, repositoryName)) - .map(x => (x.originUserName?, x.originRepositoryName?)) - .update(None, None) - } - - // Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME - Repositories - .filter { x => (x.parentUserName === userName.bind) && (x.parentRepositoryName === repositoryName.bind) } - .map { x => (x.userName, x.repositoryName) } - .list - .foreach { case (userName, repositoryName) => - Repositories - .filter(_.byRepository(userName, repositoryName)) - .map(x => (x.parentUserName?, x.parentRepositoryName?)) - .update(None, None) - } - } - - /** - * Returns the repository names of the specified user. - * - * @param userName the user name of repository owner - * @return the list of repository names - */ - def getRepositoryNamesOfUser(userName: String)(implicit s: Session): List[String] = - Repositories filter(_.userName === userName.bind) map (_.repositoryName) list - - /** - * Returns the specified repository information. - * - * @param userName the user name of the repository owner - * @param repositoryName the repository name - * @param baseUrl the base url of this application - * @return the repository information - */ - def getRepository(userName: String, repositoryName: String, baseUrl: String)(implicit s: Session): Option[RepositoryInfo] = { - (Repositories filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository => - // for getting issue count and pull request count - val issues = Issues.filter { t => - t.byRepository(repository.userName, repository.repositoryName) && (t.closed === false.bind) - }.map(_.pullRequest).list - - new RepositoryInfo( - JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl), - repository, - issues.count(_ == false), - issues.count(_ == true), - getForkedCount( - repository.originUserName.getOrElse(repository.userName), - repository.originRepositoryName.getOrElse(repository.repositoryName) - ), - getRepositoryManagers(repository.userName)) - } - } - - /** - * Returns the repositories without private repository that user does not have access right. - * Include public repository, private own repository and private but collaborator repository. - * - * @param userName the user name of collaborator - * @return the repository infomation list - */ - def getAllRepositories(userName: String)(implicit s: Session): List[(String, String)] = { - Repositories.filter { t1 => - (t1.isPrivate === false.bind) || - (t1.userName === userName.bind) || - (Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName === userName.bind)} exists) - }.sortBy(_.lastActivityDate desc).map{ t => - (t.userName, t.repositoryName) - }.list - } - - def getUserRepositories(userName: String, baseUrl: String, withoutPhysicalInfo: Boolean = false) - (implicit s: Session): List[RepositoryInfo] = { - Repositories.filter { t1 => - (t1.userName === userName.bind) || - (Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName === userName.bind)} exists) - }.sortBy(_.lastActivityDate desc).list.map{ repository => - new RepositoryInfo( - if(withoutPhysicalInfo){ - new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl) - } else { - JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl) - }, - repository, - getForkedCount( - repository.originUserName.getOrElse(repository.userName), - repository.originRepositoryName.getOrElse(repository.repositoryName) - ), - getRepositoryManagers(repository.userName)) - } - } - - /** - * Returns the list of visible repositories for the specified user. - * If repositoryUserName is given then filters results by repository owner. - * - * @param loginAccount the logged in account - * @param baseUrl the base url of this application - * @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user) - * @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count, - * branches and tags - * @return the repository information which is sorted in descending order of lastActivityDate. - */ - def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None, - withoutPhysicalInfo: Boolean = false) - (implicit s: Session): List[RepositoryInfo] = { - (loginAccount match { - // for Administrators - case Some(x) if(x.isAdmin) => Repositories - // for Normal Users - case Some(x) if(!x.isAdmin) => - Repositories filter { t => (t.isPrivate === false.bind) || (t.userName === x.userName) || - (Collaborators.filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName === x.userName.bind)} exists) - } - // for Guests - case None => Repositories filter(_.isPrivate === false.bind) - }).filter { t => - repositoryUserName.map { userName => t.userName === userName.bind } getOrElse LiteralColumn(true) - }.sortBy(_.lastActivityDate desc).list.map{ repository => - new RepositoryInfo( - if(withoutPhysicalInfo){ - new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl) - } else { - JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl) - }, - repository, - getForkedCount( - repository.originUserName.getOrElse(repository.userName), - repository.originRepositoryName.getOrElse(repository.repositoryName) - ), - getRepositoryManagers(repository.userName)) - } - } - - private def getRepositoryManagers(userName: String)(implicit s: Session): Seq[String] = - if(getAccountByUserName(userName).exists(_.isGroupAccount)){ - getGroupMembers(userName).collect { case x if(x.isManager) => x.userName } - } else { - Seq(userName) - } - - /** - * Updates the last activity date of the repository. - */ - def updateLastActivityDate(userName: String, repositoryName: String)(implicit s: Session): Unit = - Repositories.filter(_.byRepository(userName, repositoryName)).map(_.lastActivityDate).update(currentDate) - - /** - * Save repository options. - */ - def saveRepositoryOptions(userName: String, repositoryName: String, - description: Option[String], defaultBranch: String, isPrivate: Boolean)(implicit s: Session): Unit = - Repositories.filter(_.byRepository(userName, repositoryName)) - .map { r => (r.description.?, r.defaultBranch, r.isPrivate, r.updatedDate) } - .update (description, defaultBranch, isPrivate, currentDate) - - /** - * Add collaborator to the repository. - * - * @param userName the user name of the repository owner - * @param repositoryName the repository name - * @param collaboratorName the collaborator name - */ - def addCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit = - Collaborators insert Collaborator(userName, repositoryName, collaboratorName) - - /** - * Remove collaborator from the repository. - * - * @param userName the user name of the repository owner - * @param repositoryName the repository name - * @param collaboratorName the collaborator name - */ - def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit = - Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete - - /** - * Remove all collaborators from the repository. - * - * @param userName the user name of the repository owner - * @param repositoryName the repository name - */ - def removeCollaborators(userName: String, repositoryName: String)(implicit s: Session): Unit = - Collaborators.filter(_.byRepository(userName, repositoryName)).delete - - /** - * Returns the list of collaborators name which is sorted with ascending order. - * - * @param userName the user name of the repository owner - * @param repositoryName the repository name - * @return the list of collaborators name - */ - def getCollaborators(userName: String, repositoryName: String)(implicit s: Session): List[String] = - Collaborators.filter(_.byRepository(userName, repositoryName)).sortBy(_.collaboratorName).map(_.collaboratorName).list - - def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = { - loginAccount match { - case Some(a) if(a.isAdmin) => true - case Some(a) if(a.userName == owner) => true - case Some(a) if(getCollaborators(owner, repository).contains(a.userName)) => true - case _ => false - } - } - - private def getForkedCount(userName: String, repositoryName: String)(implicit s: Session): Int = - Query(Repositories.filter { t => - (t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind) - }.length).first - - - def getForkedRepositories(userName: String, repositoryName: String)(implicit s: Session): List[(String, String)] = - Repositories.filter { t => - (t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind) - } - .sortBy(_.userName asc).map(t => t.userName -> t.repositoryName).list - -} - -object RepositoryService { - - case class RepositoryInfo(owner: String, name: String, httpUrl: String, repository: Repository, - issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int, - branchList: Seq[String], tags: Seq[util.JGitUtil.TagInfo], managers: Seq[String]){ - - lazy val host = """^https?://(.+?)(:\d+)?/""".r.findFirstMatchIn(httpUrl).get.group(1) - - def sshUrl(port: Int, userName: String) = s"ssh://${userName}@${host}:${port}/${owner}/${name}.git" - - /** - * Creates instance with issue count and pull request count. - */ - def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) = - this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers) - - /** - * Creates instance without issue count and pull request count. - */ - def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) = - this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers) - } - - case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode]) - -} diff --git a/src/main/scala/service/RequestCache.scala b/src/main/scala/service/RequestCache.scala deleted file mode 100644 index 4ff502b..0000000 --- a/src/main/scala/service/RequestCache.scala +++ /dev/null @@ -1,37 +0,0 @@ -package service - -import model.{Account, Issue, Session} -import util.Implicits.request2Session - -/** - * This service is used for a view helper mainly. - * - * It may be called many times in one request, so each method stores - * its result into the cache which available during a request. - */ -trait RequestCache extends SystemSettingsService with AccountService with IssuesService { - - private implicit def context2Session(implicit context: app.Context): Session = - request2Session(context.request) - - def getIssue(userName: String, repositoryName: String, issueId: String) - (implicit context: app.Context): Option[Issue] = { - context.cache(s"issue.${userName}/${repositoryName}#${issueId}"){ - super.getIssue(userName, repositoryName, issueId) - } - } - - def getAccountByUserName(userName: String) - (implicit context: app.Context): Option[Account] = { - context.cache(s"account.${userName}"){ - super.getAccountByUserName(userName) - } - } - - def getAccountByMailAddress(mailAddress: String) - (implicit context: app.Context): Option[Account] = { - context.cache(s"account.${mailAddress}"){ - super.getAccountByMailAddress(mailAddress) - } - } -} diff --git a/src/main/scala/service/SshKeyService.scala b/src/main/scala/service/SshKeyService.scala deleted file mode 100644 index 4446084..0000000 --- a/src/main/scala/service/SshKeyService.scala +++ /dev/null @@ -1,18 +0,0 @@ -package service - -import model.Profile._ -import profile.simple._ -import model.SshKey - -trait SshKeyService { - - def addPublicKey(userName: String, title: String, publicKey: String)(implicit s: Session): Unit = - SshKeys insert SshKey(userName = userName, title = title, publicKey = publicKey) - - def getPublicKeys(userName: String)(implicit s: Session): List[SshKey] = - SshKeys.filter(_.userName === userName.bind).sortBy(_.sshKeyId).list - - def deletePublicKey(userName: String, sshKeyId: Int)(implicit s: Session): Unit = - SshKeys filter (_.byPrimaryKey(userName, sshKeyId)) delete - -} diff --git a/src/main/scala/service/SystemSettingsService.scala b/src/main/scala/service/SystemSettingsService.scala deleted file mode 100644 index 156a23b..0000000 --- a/src/main/scala/service/SystemSettingsService.scala +++ /dev/null @@ -1,213 +0,0 @@ -package service - -import util.Directory._ -import util.ControlUtil._ -import SystemSettingsService._ -import javax.servlet.http.HttpServletRequest - -trait SystemSettingsService { - - def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings().baseUrl(request) - - def saveSystemSettings(settings: SystemSettings): Unit = { - defining(new java.util.Properties()){ props => - settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", ""))) - settings.information.foreach(x => props.setProperty(Information, x)) - props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString) - props.setProperty(AllowAnonymousAccess, settings.allowAnonymousAccess.toString) - props.setProperty(IsCreateRepoOptionPublic, settings.isCreateRepoOptionPublic.toString) - props.setProperty(Gravatar, settings.gravatar.toString) - props.setProperty(Notification, settings.notification.toString) - props.setProperty(Ssh, settings.ssh.toString) - settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString)) - if(settings.notification) { - settings.smtp.foreach { smtp => - props.setProperty(SmtpHost, smtp.host) - smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString)) - smtp.user.foreach(props.setProperty(SmtpUser, _)) - smtp.password.foreach(props.setProperty(SmtpPassword, _)) - smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString)) - smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _)) - smtp.fromName.foreach(props.setProperty(SmtpFromName, _)) - } - } - props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString) - if(settings.ldapAuthentication){ - settings.ldap.map { ldap => - props.setProperty(LdapHost, ldap.host) - ldap.port.foreach(x => props.setProperty(LdapPort, x.toString)) - ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x)) - ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x)) - props.setProperty(LdapBaseDN, ldap.baseDN) - props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute) - ldap.additionalFilterCondition.foreach(x => props.setProperty(LdapAdditionalFilterCondition, x)) - ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x)) - ldap.mailAttribute.foreach(x => props.setProperty(LdapMailAddressAttribute, x)) - ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString)) - ldap.ssl.foreach(x => props.setProperty(LdapSsl, x.toString)) - ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x)) - } - } - using(new java.io.FileOutputStream(GitBucketConf)){ out => - props.store(out, null) - } - } - } - - - def loadSystemSettings(): SystemSettings = { - defining(new java.util.Properties()){ props => - if(GitBucketConf.exists){ - using(new java.io.FileInputStream(GitBucketConf)){ in => - props.load(in) - } - } - SystemSettings( - getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")), - getOptionValue[String](props, Information, None), - getValue(props, AllowAccountRegistration, false), - getValue(props, AllowAnonymousAccess, true), - getValue(props, IsCreateRepoOptionPublic, true), - getValue(props, Gravatar, true), - getValue(props, Notification, false), - getValue(props, Ssh, false), - getOptionValue(props, SshPort, Some(DefaultSshPort)), - if(getValue(props, Notification, false)){ - Some(Smtp( - getValue(props, SmtpHost, ""), - getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)), - getOptionValue(props, SmtpUser, None), - getOptionValue(props, SmtpPassword, None), - getOptionValue[Boolean](props, SmtpSsl, None), - getOptionValue(props, SmtpFromAddress, None), - getOptionValue(props, SmtpFromName, None))) - } else { - None - }, - getValue(props, LdapAuthentication, false), - if(getValue(props, LdapAuthentication, false)){ - Some(Ldap( - getValue(props, LdapHost, ""), - getOptionValue(props, LdapPort, Some(DefaultLdapPort)), - getOptionValue(props, LdapBindDN, None), - getOptionValue(props, LdapBindPassword, None), - getValue(props, LdapBaseDN, ""), - getValue(props, LdapUserNameAttribute, ""), - getOptionValue(props, LdapAdditionalFilterCondition, None), - getOptionValue(props, LdapFullNameAttribute, None), - getOptionValue(props, LdapMailAddressAttribute, None), - getOptionValue[Boolean](props, LdapTls, None), - getOptionValue[Boolean](props, LdapSsl, None), - getOptionValue(props, LdapKeystore, None))) - } else { - None - } - ) - } - } - -} - -object SystemSettingsService { - import scala.reflect.ClassTag - - case class SystemSettings( - baseUrl: Option[String], - information: Option[String], - allowAccountRegistration: Boolean, - allowAnonymousAccess: Boolean, - isCreateRepoOptionPublic: Boolean, - gravatar: Boolean, - notification: Boolean, - ssh: Boolean, - sshPort: Option[Int], - smtp: Option[Smtp], - ldapAuthentication: Boolean, - 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)) - } - }.stripSuffix("/") - } - - case class Ldap( - host: String, - port: Option[Int], - bindDN: Option[String], - bindPassword: Option[String], - baseDN: String, - userNameAttribute: String, - additionalFilterCondition: Option[String], - fullNameAttribute: Option[String], - mailAttribute: Option[String], - tls: Option[Boolean], - ssl: Option[Boolean], - keystore: Option[String]) - - case class Smtp( - host: String, - port: Option[Int], - user: Option[String], - password: Option[String], - ssl: Option[Boolean], - fromAddress: Option[String], - fromName: Option[String]) - - val DefaultSshPort = 29418 - val DefaultSmtpPort = 25 - val DefaultLdapPort = 389 - - private val BaseURL = "base_url" - private val Information = "information" - private val AllowAccountRegistration = "allow_account_registration" - private val AllowAnonymousAccess = "allow_anonymous_access" - private val IsCreateRepoOptionPublic = "is_create_repository_option_public" - private val Gravatar = "gravatar" - private val Notification = "notification" - private val Ssh = "ssh" - private val SshPort = "ssh.port" - private val SmtpHost = "smtp.host" - private val SmtpPort = "smtp.port" - private val SmtpUser = "smtp.user" - private val SmtpPassword = "smtp.password" - private val SmtpSsl = "smtp.ssl" - private val SmtpFromAddress = "smtp.from_address" - private val SmtpFromName = "smtp.from_name" - private val LdapAuthentication = "ldap_authentication" - private val LdapHost = "ldap.host" - private val LdapPort = "ldap.port" - private val LdapBindDN = "ldap.bindDN" - private val LdapBindPassword = "ldap.bind_password" - private val LdapBaseDN = "ldap.baseDN" - private val LdapUserNameAttribute = "ldap.username_attribute" - private val LdapAdditionalFilterCondition = "ldap.additional_filter_condition" - private val LdapFullNameAttribute = "ldap.fullname_attribute" - private val LdapMailAddressAttribute = "ldap.mail_attribute" - private val LdapTls = "ldap.tls" - private val LdapSsl = "ldap.ssl" - private val LdapKeystore = "ldap.keystore" - - private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = - defining(props.getProperty(key)){ value => - if(value == null || value.isEmpty) default - else convertType(value).asInstanceOf[A] - } - - private def getOptionValue[A: ClassTag](props: java.util.Properties, key: String, default: Option[A]): Option[A] = - defining(props.getProperty(key)){ value => - if(value == null || value.isEmpty) default - else Some(convertType(value)).asInstanceOf[Option[A]] - } - - private def convertType[A: ClassTag](value: String) = - defining(implicitly[ClassTag[A]].runtimeClass){ c => - if(c == classOf[Boolean]) value.toBoolean - else if(c == classOf[Int]) value.toInt - else value - } - -// // TODO temporary flag -// val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean - -} diff --git a/src/main/scala/service/WebHookService.scala b/src/main/scala/service/WebHookService.scala deleted file mode 100644 index a2dafbf..0000000 --- a/src/main/scala/service/WebHookService.scala +++ /dev/null @@ -1,143 +0,0 @@ -package service - -import model.Profile._ -import profile.simple._ -import model.{WebHook, Account} -import org.slf4j.LoggerFactory -import service.RepositoryService.RepositoryInfo -import util.JGitUtil -import org.eclipse.jgit.diff.DiffEntry -import util.JGitUtil.CommitInfo -import org.eclipse.jgit.api.Git -import org.apache.http.message.BasicNameValuePair -import org.apache.http.client.entity.UrlEncodedFormEntity -import org.apache.http.NameValuePair - -trait WebHookService { - import WebHookService._ - - private val logger = LoggerFactory.getLogger(classOf[WebHookService]) - - def getWebHookURLs(owner: String, repository: String)(implicit s: Session): List[WebHook] = - WebHooks.filter(_.byRepository(owner, repository)).sortBy(_.url).list - - def addWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit = - WebHooks insert WebHook(owner, repository, url) - - def deleteWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit = - WebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete - - def callWebHook(owner: String, repository: String, webHookURLs: List[WebHook], payload: WebHookPayload): Unit = { - import org.json4s._ - import org.json4s.jackson.Serialization - import org.json4s.jackson.Serialization.{read, write} - import org.apache.http.client.methods.HttpPost - import org.apache.http.impl.client.HttpClientBuilder - import scala.concurrent._ - import ExecutionContext.Implicits.global - - logger.debug("start callWebHook") - implicit val formats = Serialization.formats(NoTypeHints) - - if(webHookURLs.nonEmpty){ - val json = write(payload) - val httpClient = HttpClientBuilder.create.build - - webHookURLs.foreach { webHookUrl => - val f = Future { - logger.debug(s"start web hook invocation for ${webHookUrl}") - val httpPost = new HttpPost(webHookUrl.url) - - val params: java.util.List[NameValuePair] = new java.util.ArrayList() - params.add(new BasicNameValuePair("payload", json)) - httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8")) - - httpClient.execute(httpPost) - httpPost.releaseConnection() - logger.debug(s"end web hook invocation for ${webHookUrl}") - } - f.onSuccess { - case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}") - } - f.onFailure { - case t => logger.error(s"Failed: web hook request to ${webHookUrl.url}", t) - } - } - } - logger.debug("end callWebHook") - } - -} - -object WebHookService { - - case class WebHookPayload( - pusher: WebHookUser, - ref: String, - commits: List[WebHookCommit], - repository: WebHookRepository) - - object WebHookPayload { - def apply(git: Git, pusher: Account, refName: String, repositoryInfo: RepositoryInfo, - commits: List[CommitInfo], repositoryOwner: Account): WebHookPayload = - WebHookPayload( - WebHookUser(pusher.fullName, pusher.mailAddress), - refName, - commits.map { commit => - val diffs = JGitUtil.getDiffs(git, commit.id, false) - val commitUrl = repositoryInfo.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/commit/" + commit.id - - WebHookCommit( - id = commit.id, - message = commit.fullMessage, - timestamp = commit.commitTime.toString, - url = commitUrl, - added = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.ADD) => x.newPath }, - removed = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.DELETE) => x.oldPath }, - modified = diffs._1.collect { case x if(x.changeType != DiffEntry.ChangeType.ADD && - x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath }, - author = WebHookUser( - name = commit.committerName, - email = commit.committerEmailAddress - ) - ) - }, - WebHookRepository( - name = repositoryInfo.name, - url = repositoryInfo.httpUrl, - description = repositoryInfo.repository.description.getOrElse(""), - watchers = 0, - forks = repositoryInfo.forkedCount, - `private` = repositoryInfo.repository.isPrivate, - owner = WebHookUser( - name = repositoryOwner.userName, - email = repositoryOwner.mailAddress - ) - ) - ) - } - - case class WebHookCommit( - id: String, - message: String, - timestamp: String, - url: String, - added: List[String], - removed: List[String], - modified: List[String], - author: WebHookUser) - - case class WebHookRepository( - name: String, - url: String, - description: String, - watchers: Int, - forks: Int, - `private`: Boolean, - owner: WebHookUser) - - case class WebHookUser( - name: String, - email: String) - -} diff --git a/src/main/scala/service/WikiService.scala b/src/main/scala/service/WikiService.scala deleted file mode 100644 index add5021..0000000 --- a/src/main/scala/service/WikiService.scala +++ /dev/null @@ -1,281 +0,0 @@ -package service - -import java.util.Date -import org.eclipse.jgit.api.Git -import util._ -import _root_.util.ControlUtil._ -import org.eclipse.jgit.treewalk.CanonicalTreeParser -import org.eclipse.jgit.lib._ -import org.eclipse.jgit.dircache.DirCache -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._ -import service.RepositoryService.RepositoryInfo - -object WikiService { - - /** - * The model for wiki page. - * - * @param name the page name - * @param content the page content - * @param committer the last committer - * @param time the last modified time - * @param id the latest commit id - */ - case class WikiPageInfo(name: String, content: String, committer: String, time: Date, id: String) - - /** - * The model for wiki page history. - * - * @param name the page name - * @param committer the committer the committer - * @param message the commit message - * @param date the commit date - */ - case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date) - - def httpUrl(repository: RepositoryInfo) = repository.httpUrl.replaceFirst("\\.git\\Z", ".wiki.git") - - def sshUrl(repository: RepositoryInfo, settings: SystemSettingsService.SystemSettings, userName: String) = - repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), userName).replaceFirst("\\.git\\Z", ".wiki.git") -} - -trait WikiService { - import WikiService._ - - def createWikiRepository(loginAccount: model.Account, owner: String, repository: String): Unit = - LockUtil.lock(s"${owner}/${repository}/wiki"){ - defining(Directory.getWikiRepositoryDir(owner, repository)){ dir => - if(!dir.exists){ - JGitUtil.initRepository(dir) - saveWikiPage(owner, repository, "Home", "Home", s"Welcome to the ${repository} wiki!!", loginAccount, "Initial Commit", None) - } - } - } - - /** - * Returns the wiki page. - */ - def getWikiPage(owner: String, repository: String, pageName: String): Option[WikiPageInfo] = { - using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => - if(!JGitUtil.isEmpty(git)){ - JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file => - WikiPageInfo(file.name, StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes), - file.author, file.time, file.commitId) - } - } else None - } - } - - /** - * Returns the content of the specified file. - */ - def getFileContent(owner: String, repository: String, path: String): Option[Array[Byte]] = - using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => - if(!JGitUtil.isEmpty(git)){ - val index = path.lastIndexOf('/') - val parentPath = if(index < 0) "." else path.substring(0, index) - val fileName = if(index < 0) path else path.substring(index + 1) - - JGitUtil.getFileList(git, "master", parentPath).find(_.name == fileName).map { file => - git.getRepository.open(file.id).getBytes - } - } else None - } - - /** - * Returns the list of wiki page names. - */ - def getWikiPageList(owner: String, repository: String): List[String] = { - using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git => - JGitUtil.getFileList(git, "master", ".") - .filter(_.name.endsWith(".md")) - .map(_.name.stripSuffix(".md")) - .sortBy(x => x) - } - } - - /** - * Reverts specified changes. - */ - def revertWikiPage(owner: String, repository: String, from: String, to: String, - committer: model.Account, pageName: Option[String]): Boolean = { - - 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}")) - - val newTreeIter = new CanonicalTreeParser - newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}")) - - val diffs = git.diff.setNewTree(oldTreeIter).setOldTree(newTreeIter).call.asScala.filter { diff => - pageName match { - case Some(x) => diff.getNewPath == x + ".md" - case None => true - } - } - - val patch = using(new java.io.ByteArrayOutputStream()){ out => - val formatter = new DiffFormatter(out) - formatter.setRepository(git.getRepository) - formatter.format(diffs.asJava) - new String(out.toByteArray, "UTF-8") - } - - 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.stripSuffix(".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}") - - JGitUtil.processTree(git, headId){ (path, tree) => - 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), - Constants.HEAD, 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"){ - 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 - - if(headId != null){ - 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) - } - } - } - - if(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), - Constants.HEAD, 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.getName) - } else None - } - } - } - - /** - * Delete the wiki page. - */ - def deleteWikiPage(owner: String, repository: String, pageName: String, - committer: String, mailAddress: String, message: String): Unit = { - 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 - - 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), - Constants.HEAD, committer, mailAddress, message) - } - } - } - } - -} diff --git a/src/main/scala/servlet/BasicAuthenticationFilter.scala b/src/main/scala/servlet/BasicAuthenticationFilter.scala deleted file mode 100644 index cbecfc1..0000000 --- a/src/main/scala/servlet/BasicAuthenticationFilter.scala +++ /dev/null @@ -1,92 +0,0 @@ -package servlet - -import javax.servlet._ -import javax.servlet.http._ -import service.{SystemSettingsService, AccountService, RepositoryService} -import model._ -import org.slf4j.LoggerFactory -import util.Implicits._ -import util.ControlUtil._ -import util.Keys - -/** - * Provides BASIC Authentication for [[servlet.GitRepositoryServlet]]. - */ -class BasicAuthenticationFilter extends Filter with RepositoryService with AccountService with SystemSettingsService { - - private val logger = LoggerFactory.getLogger(classOf[BasicAuthenticationFilter]) - - def init(config: FilterConfig) = {} - - def destroy(): Unit = {} - - def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = { - implicit val request = req.asInstanceOf[HttpServletRequest] - val response = res.asInstanceOf[HttpServletResponse] - - val wrappedResponse = new HttpServletResponseWrapper(response){ - override def setCharacterEncoding(encoding: String) = {} - } - - val isUpdating = request.getRequestURI.endsWith("/git-receive-pack") || "service=git-receive-pack".equals(request.getQueryString) - - val settings = loadSystemSettings() - - try { - defining(request.paths){ - case Array(_, repositoryOwner, repositoryName, _*) => - getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match { - case Some(repository) => { - if(!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess){ - chain.doFilter(req, wrappedResponse) - } else { - request.getHeader("Authorization") match { - case null => requireAuth(response) - case auth => decodeAuthHeader(auth).split(":") match { - case Array(username, password) => { - authenticate(settings, username, password) match { - case Some(account) => { - if(isUpdating && hasWritePermission(repository.owner, repository.name, Some(account))){ - request.setAttribute(Keys.Request.UserName, account.userName) - } - chain.doFilter(req, wrappedResponse) - } - case None => requireAuth(response) - } - } - case _ => requireAuth(response) - } - } - } - } - case None => { - logger.debug(s"Repository ${repositoryOwner}/${repositoryName} is not found.") - response.sendError(HttpServletResponse.SC_NOT_FOUND) - } - } - case _ => { - logger.debug(s"Not enough path arguments: ${request.paths}") - response.sendError(HttpServletResponse.SC_NOT_FOUND) - } - } - } catch { - case ex: Exception => { - logger.error("error", ex) - requireAuth(response) - } - } - } - - private def requireAuth(response: HttpServletResponse): Unit = { - response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"") - response.sendError(HttpServletResponse.SC_UNAUTHORIZED) - } - - private def decodeAuthHeader(header: String): String = { - try { - new String(new sun.misc.BASE64Decoder().decodeBuffer(header.substring(6))) - } catch { - case _: Throwable => "" - } - } -} \ No newline at end of file diff --git a/src/main/scala/servlet/GitRepositoryServlet.scala b/src/main/scala/servlet/GitRepositoryServlet.scala deleted file mode 100644 index 7fde407..0000000 --- a/src/main/scala/servlet/GitRepositoryServlet.scala +++ /dev/null @@ -1,214 +0,0 @@ -package servlet - -import org.eclipse.jgit.http.server.GitServlet -import org.eclipse.jgit.lib._ -import org.eclipse.jgit.transport._ -import org.eclipse.jgit.transport.resolver._ -import org.slf4j.LoggerFactory - -import javax.servlet.ServletConfig -import javax.servlet.ServletContext -import javax.servlet.http.{HttpServletResponse, HttpServletRequest} -import util.{StringUtil, Keys, JGitUtil, Directory} -import util.ControlUtil._ -import util.Implicits._ -import service._ -import WebHookService._ -import org.eclipse.jgit.api.Git -import util.JGitUtil.CommitInfo -import service.IssuesService.IssueSearchCondition -import model.Session - -/** - * Provides Git repository via HTTP. - * - * This servlet provides only Git repository functionality. - * Authentication is provided by [[servlet.BasicAuthenticationFilter]]. - */ -class GitRepositoryServlet extends GitServlet with SystemSettingsService { - - private val logger = LoggerFactory.getLogger(classOf[GitRepositoryServlet]) - - override def init(config: ServletConfig): Unit = { - setReceivePackFactory(new GitBucketReceivePackFactory()) - - // TODO are there any other ways...? - super.init(new ServletConfig(){ - def getInitParameter(name: String): String = name match { - case "base-path" => Directory.RepositoryHome - case "export-all" => "true" - case name => config.getInitParameter(name) - } - def getInitParameterNames(): java.util.Enumeration[String] = { - config.getInitParameterNames - } - - def getServletContext(): ServletContext = config.getServletContext - def getServletName(): String = config.getServletName - }) - - super.init(config) - } - - override def service(req: HttpServletRequest, res: HttpServletResponse): Unit = { - val agent = req.getHeader("USER-AGENT") - val index = req.getRequestURI.indexOf(".git") - if(index >= 0 && (agent == null || agent.toLowerCase.indexOf("git/") < 0)){ - // redirect for browsers - val paths = req.getRequestURI.substring(0, index).split("/") - res.sendRedirect(baseUrl(req) + "/" + paths.dropRight(1).last + "/" + paths.last) - } else { - // response for git client - super.service(req, res) - } - } -} - -class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest] with SystemSettingsService { - - private val logger = LoggerFactory.getLogger(classOf[GitBucketReceivePackFactory]) - - override def create(request: HttpServletRequest, db: Repository): ReceivePack = { - val receivePack = new ReceivePack(db) - val pusher = request.getAttribute(Keys.Request.UserName).asInstanceOf[String] - - logger.debug("requestURI: " + request.getRequestURI) - logger.debug("pusher:" + pusher) - - defining(request.paths){ paths => - val owner = paths(1) - val repository = paths(2).stripSuffix(".git") - - logger.debug("repository:" + owner + "/" + repository) - - if(!repository.endsWith(".wiki")){ - defining(request) { implicit r => - val hook = new CommitLogHook(owner, repository, pusher, baseUrl) - receivePack.setPreReceiveHook(hook) - receivePack.setPostReceiveHook(hook) - } - } - receivePack - } - } -} - -import scala.collection.JavaConverters._ - -class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: Session) - 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 => - val pushedIds = scala.collection.mutable.Set[String]() - commands.asScala.foreach { command => - logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}") - val refName = command.getRefName.split("/") - val branchName = refName.drop(2).mkString("/") - val commits = if (refName(1) == "tags") { - Nil - } else { - command.getType match { - case ReceiveCommand.Type.DELETE => Nil - case _ => JGitUtil.getCommitLog(git, command.getOldId.name, command.getNewId.name) - } - } - - // Retrieve all issue count in the repository - val issueCount = - countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) + - countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository) - - // Extract new commit and apply issue comment - val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch - val newCommits = commits.flatMap { commit => - if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) { - if (issueCount > 0) { - pushedIds.add(commit.id) - createIssueComment(commit) - // close issues - if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){ - closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository) - } - } - Some(commit) - } else None - } - - // record activity - if(refName(1) == "heads"){ - command.getType match { - case ReceiveCommand.Type.CREATE => recordCreateBranchActivity(owner, repository, pusher, branchName) - case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, pusher, branchName, newCommits) - case ReceiveCommand.Type.DELETE => recordDeleteBranchActivity(owner, repository, pusher, branchName) - case _ => - } - } else if(refName(1) == "tags"){ - command.getType match { - case ReceiveCommand.Type.CREATE => recordCreateTagActivity(owner, repository, pusher, branchName, newCommits) - case ReceiveCommand.Type.DELETE => recordDeleteTagActivity(owner, repository, pusher, branchName, newCommits) - case _ => - } - } - - if(refName(1) == "heads"){ - command.getType match { - case ReceiveCommand.Type.CREATE | - ReceiveCommand.Type.UPDATE | - ReceiveCommand.Type.UPDATE_NONFASTFORWARD => - updatePullRequests(owner, repository, branchName) - case _ => - } - } - - // call web hook - getWebHookURLs(owner, repository) match { - case webHookURLs if(webHookURLs.nonEmpty) => - for(pusherAccount <- getAccountByUserName(pusher); - ownerAccount <- getAccountByUserName(owner); - repositoryInfo <- getRepository(owner, repository, baseUrl)){ - callWebHook(owner, repository, webHookURLs, - WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount)) - } - case _ => - } - } - } - // update repository last modified time. - updateLastActivityDate(owner, repository) - } catch { - case ex: Exception => { - logger.error(ex.toString, ex) - throw ex - } - } - } - - private def createIssueComment(commit: CommitInfo) = { - StringUtil.extractIssueId(commit.fullMessage).foreach { issueId => - if(getIssue(owner, repository, issueId).isDefined){ - getAccountByMailAddress(commit.committerEmailAddress).foreach { account => - createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit") - } - } - } - } -} diff --git a/src/main/scala/servlet/InitializeListener.scala b/src/main/scala/servlet/InitializeListener.scala deleted file mode 100644 index fdc3c48..0000000 --- a/src/main/scala/servlet/InitializeListener.scala +++ /dev/null @@ -1,204 +0,0 @@ -package servlet - -import java.io.File -import java.sql.{DriverManager, Connection} -import org.apache.commons.io.FileUtils -import javax.servlet.{ServletContextListener, ServletContextEvent} -import org.slf4j.LoggerFactory -import util.Directory._ -import util.ControlUtil._ -import util.JDBCUtil._ -import org.eclipse.jgit.api.Git -import util.{Version, Versions} -import plugin._ -import util.{DatabaseConfig, Directory} - -object AutoUpdate { - - /** - * The history of versions. A head of this sequence is the current BitBucket version. - */ - val versions = Seq( - new Version(2, 8), - new Version(2, 7) { - override def update(conn: Connection, cl: ClassLoader): Unit = { - super.update(conn, cl) - conn.select("SELECT * FROM REPOSITORY"){ rs => - // Rename attached files directory from /issues to /comments - val userName = rs.getString("USER_NAME") - val repoName = rs.getString("REPOSITORY_NAME") - defining(Directory.getAttachedDir(userName, repoName)){ newDir => - val oldDir = new File(newDir.getParentFile, "issues") - if(oldDir.exists && oldDir.isDirectory){ - oldDir.renameTo(newDir) - } - } - // Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME if it does not exist - val originalUserName = rs.getString("ORIGIN_USER_NAME") - val originalRepoName = rs.getString("ORIGIN_REPOSITORY_NAME") - if(originalUserName != null && originalRepoName != null){ - if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", - originalUserName, originalRepoName) == 0){ - conn.update("UPDATE REPOSITORY SET ORIGIN_USER_NAME = NULL, ORIGIN_REPOSITORY_NAME = NULL " + - "WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName) - } - } - // Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME if it does not exist - val parentUserName = rs.getString("PARENT_USER_NAME") - val parentRepoName = rs.getString("PARENT_REPOSITORY_NAME") - if(parentUserName != null && parentRepoName != null){ - if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", - parentUserName, parentRepoName) == 0){ - conn.update("UPDATE REPOSITORY SET PARENT_USER_NAME = NULL, PARENT_REPOSITORY_NAME = NULL " + - "WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName) - } - } - } - } - }, - new Version(2, 6), - new Version(2, 5), - new Version(2, 4), - new Version(2, 3) { - override def update(conn: Connection, cl: ClassLoader): Unit = { - super.update(conn, cl) - conn.select("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'"){ rs => - val curInfo = rs.getString("ADDITIONAL_INFO") - val newInfo = curInfo.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n") - if (curInfo != newInfo) { - conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID")) - } - } - ignore { - FileUtils.deleteDirectory(Directory.getPluginCacheDir()) - //FileUtils.deleteDirectory(new File(Directory.PluginHome)) - } - } - }, - new Version(2, 2), - new Version(2, 1), - new Version(2, 0){ - override def update(conn: Connection, cl: ClassLoader): Unit = { - import eu.medsea.mimeutil.{MimeUtil2, MimeType} - - val mimeUtil = new MimeUtil2() - mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector") - - super.update(conn, cl) - conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs => - defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir => - if(dir.exists && dir.isDirectory){ - dir.listFiles.foreach { file => - if(file.getName.indexOf('.') < 0){ - val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString - if(mimeType.startsWith("image/")){ - file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1))) - } - } - } - } - } - } - } - }, - Version(1, 13), - Version(1, 12), - Version(1, 11), - Version(1, 10), - Version(1, 9), - Version(1, 8), - Version(1, 7), - Version(1, 6), - Version(1, 5), - Version(1, 4), - new Version(1, 3){ - override def update(conn: Connection, cl: ClassLoader): Unit = { - super.update(conn, cl) - // Fix wiki repository configuration - conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs => - using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git => - defining(git.getRepository.getConfig){ config => - if(!config.getBoolean("http", "receivepack", false)){ - config.setBoolean("http", null, "receivepack", true) - config.save - } - } - } - } - } - }, - Version(1, 2), - Version(1, 1), - Version(1, 0), - Version(0, 0) - ) - - /** - * The head version of BitBucket. - */ - val headVersion = versions.head - - /** - * The version file (GITBUCKET_HOME/version). - */ - lazy val versionFile = new File(GitBucketHome, "version") - - /** - * Returns the current version from the version file. - */ - def getCurrentVersion(): Version = { - if(versionFile.exists){ - FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match { - case Array(majorVersion, minorVersion) => { - versions.find { v => - v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt - }.getOrElse(Version(0, 0)) - } - case _ => Version(0, 0) - } - } else Version(0, 0) - } - -} - -/** - * Initialize GitBucket system. - * Update database schema and load plug-ins automatically in the context initializing. - */ -class InitializeListener extends ServletContextListener { - import AutoUpdate._ - - private val logger = LoggerFactory.getLogger(classOf[InitializeListener]) - - override def contextInitialized(event: ServletContextEvent): Unit = { - val dataDir = event.getServletContext.getInitParameter("gitbucket.home") - if(dataDir != null){ - System.setProperty("gitbucket.home", dataDir) - } - org.h2.Driver.load() - - defining(getConnection()){ conn => - // Migration - logger.debug("Start schema update") - Versions.update(conn, headVersion, getCurrentVersion(), versions, Thread.currentThread.getContextClassLoader){ conn => - FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8") - } - // Load plugins - logger.debug("Initialize plugins") - PluginRegistry.initialize(event.getServletContext, conn) - } - - } - - def contextDestroyed(event: ServletContextEvent): Unit = { - // Shutdown plugins - PluginRegistry.shutdown(event.getServletContext) - } - - private def getConnection(): Connection = - DriverManager.getConnection( - DatabaseConfig.url, - DatabaseConfig.user, - DatabaseConfig.password) - -} diff --git a/src/main/scala/servlet/SessionCleanupListener.scala b/src/main/scala/servlet/SessionCleanupListener.scala deleted file mode 100644 index ee4ea7b..0000000 --- a/src/main/scala/servlet/SessionCleanupListener.scala +++ /dev/null @@ -1,16 +0,0 @@ -package servlet - -import javax.servlet.http.{HttpSessionEvent, HttpSessionListener} -import org.apache.commons.io.FileUtils -import util.Directory._ - -/** - * Removes session associated temporary files when session is destroyed. - */ -class SessionCleanupListener extends HttpSessionListener { - - def sessionCreated(se: HttpSessionEvent): Unit = {} - - def sessionDestroyed(se: HttpSessionEvent): Unit = FileUtils.deleteDirectory(getTemporaryDir(se.getSession.getId)) - -} diff --git a/src/main/scala/servlet/TransactionFilter.scala b/src/main/scala/servlet/TransactionFilter.scala deleted file mode 100644 index 20d37f5..0000000 --- a/src/main/scala/servlet/TransactionFilter.scala +++ /dev/null @@ -1,59 +0,0 @@ -package servlet - -import javax.servlet._ -import javax.servlet.http.HttpServletRequest -import com.mchange.v2.c3p0.ComboPooledDataSource -import org.slf4j.LoggerFactory -import slick.jdbc.JdbcBackend.{Database => SlickDatabase, Session} -import util.{DatabaseConfig, Keys} - -/** - * Controls the transaction with the open session in view pattern. - */ -class TransactionFilter extends Filter { - - private val logger = LoggerFactory.getLogger(classOf[TransactionFilter]) - - def init(config: FilterConfig) = {} - - def destroy(): Unit = {} - - def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = { - if(req.asInstanceOf[HttpServletRequest].getRequestURI().startsWith("/assets/")){ - // assets don't need transaction - chain.doFilter(req, res) - } else { - Database() withTransaction { session => - logger.debug("begin transaction") - req.setAttribute(Keys.Request.DBSession, session) - chain.doFilter(req, res) - logger.debug("end transaction") - } - } - } - -} - -object Database { - - private val logger = LoggerFactory.getLogger(Database.getClass) - - private val db: SlickDatabase = { - val datasource = new ComboPooledDataSource - - datasource.setDriverClass(DatabaseConfig.driver) - datasource.setJdbcUrl(DatabaseConfig.url) - datasource.setUser(DatabaseConfig.user) - datasource.setPassword(DatabaseConfig.password) - - logger.debug("load database connection pool") - - SlickDatabase.forDataSource(datasource) - } - - def apply(): SlickDatabase = db - - def getSession(req: ServletRequest): Session = - req.getAttribute(Keys.Request.DBSession).asInstanceOf[Session] - -} diff --git a/src/main/scala/ssh/GitCommand.scala b/src/main/scala/ssh/GitCommand.scala deleted file mode 100644 index 35fb67b..0000000 --- a/src/main/scala/ssh/GitCommand.scala +++ /dev/null @@ -1,132 +0,0 @@ -package ssh - -import org.apache.sshd.server.{CommandFactory, Environment, ExitCallback, Command} -import org.slf4j.LoggerFactory -import java.io.{InputStream, OutputStream} -import util.ControlUtil._ -import org.eclipse.jgit.api.Git -import util.Directory._ -import org.eclipse.jgit.transport.{ReceivePack, UploadPack} -import org.apache.sshd.server.command.UnknownCommand -import servlet.{Database, CommitLogHook} -import service.{AccountService, RepositoryService, SystemSettingsService} -import org.eclipse.jgit.errors.RepositoryNotFoundException -import model.Session - -object GitCommand { - val CommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+).git'\Z""".r -} - -abstract class GitCommand(val owner: String, val repoName: String) extends Command { - self: RepositoryService with AccountService => - - private val logger = LoggerFactory.getLogger(classOf[GitCommand]) - protected var err: OutputStream = null - protected var in: InputStream = null - protected var out: OutputStream = null - protected var callback: ExitCallback = null - - protected def runTask(user: String)(implicit session: Session): Unit - - private def newTask(user: String): Runnable = new Runnable { - override def run(): Unit = { - Database() withSession { implicit session => - try { - runTask(user) - callback.onExit(0) - } catch { - case e: RepositoryNotFoundException => - logger.info(e.getMessage) - callback.onExit(1, "Repository Not Found") - case e: Throwable => - logger.error(e.getMessage, e) - callback.onExit(1) - } - } - } - } - - override def start(env: Environment): Unit = { - val user = env.getEnv.get("USER") - val thread = new Thread(newTask(user)) - thread.start() - } - - override def destroy(): Unit = {} - - override def setExitCallback(callback: ExitCallback): Unit = { - this.callback = callback - } - - override def setErrorStream(err: OutputStream): Unit = { - this.err = err - } - - override def setOutputStream(out: OutputStream): Unit = { - this.out = out - } - - override def setInputStream(in: InputStream): Unit = { - this.in = in - } - - protected def isWritableUser(username: String, repositoryInfo: RepositoryService.RepositoryInfo) - (implicit session: Session): Boolean = - getAccountByUserName(username) match { - case Some(account) => hasWritePermission(repositoryInfo.owner, repositoryInfo.name, Some(account)) - case None => false - } - -} - -class GitUploadPack(owner: String, repoName: String, baseUrl: String) extends GitCommand(owner, repoName) - with RepositoryService with AccountService { - - override protected def runTask(user: String)(implicit session: Session): Unit = { - getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo => - if(!repositoryInfo.repository.isPrivate || isWritableUser(user, repositoryInfo)){ - using(Git.open(getRepositoryDir(owner, repoName))) { git => - val repository = git.getRepository - val upload = new UploadPack(repository) - upload.upload(in, out, err) - } - } - } - } - -} - -class GitReceivePack(owner: String, repoName: String, baseUrl: String) extends GitCommand(owner, repoName) - with SystemSettingsService with RepositoryService with AccountService { - - override protected def runTask(user: String)(implicit session: Session): Unit = { - getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo => - if(isWritableUser(user, repositoryInfo)){ - using(Git.open(getRepositoryDir(owner, repoName))) { git => - val repository = git.getRepository - val receive = new ReceivePack(repository) - if(!repoName.endsWith(".wiki")){ - val hook = new CommitLogHook(owner, repoName, user, baseUrl) - receive.setPreReceiveHook(hook) - receive.setPostReceiveHook(hook) - } - receive.receive(in, out, err) - } - } - } - } - -} - -class GitCommandFactory(baseUrl: String) extends CommandFactory { - private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory]) - - override def createCommand(command: String): Command = { - logger.debug(s"command: $command") - command match { - case GitCommand.CommandRegex("upload", owner, repoName) => new GitUploadPack(owner, repoName, baseUrl) - case GitCommand.CommandRegex("receive", owner, repoName) => new GitReceivePack(owner, repoName, baseUrl) - case _ => new UnknownCommand(command) - } - } -} diff --git a/src/main/scala/ssh/NoShell.scala b/src/main/scala/ssh/NoShell.scala deleted file mode 100644 index c107be6..0000000 --- a/src/main/scala/ssh/NoShell.scala +++ /dev/null @@ -1,62 +0,0 @@ -package ssh - -import org.apache.sshd.common.Factory -import org.apache.sshd.server.{Environment, ExitCallback, Command} -import java.io.{OutputStream, InputStream} -import org.eclipse.jgit.lib.Constants -import service.SystemSettingsService - -class NoShell extends Factory[Command] with SystemSettingsService { - override def create(): Command = new Command() { - private var in: InputStream = null - private var out: OutputStream = null - private var err: OutputStream = null - private var callback: ExitCallback = null - - override def start(env: Environment): Unit = { - val user = env.getEnv.get("USER") - val port = loadSystemSettings().sshPort.getOrElse(SystemSettingsService.DefaultSshPort) - val message = - """ - | Welcome to - | _____ _ _ ____ _ _ - | / ____| (_) | | | _ \ | | | | - | | | __ _ | |_ | |_) | _ _ ___ | | __ ___ | |_ - | | | |_ | | | | __| | _ < | | | | / __| | |/ / / _ \ | __| - | | |__| | | | | |_ | |_) | | |_| | | (__ | < | __/ | |_ - | \_____| |_| \__| |____/ \__,_| \___| |_|\_\ \___| \__| - | - | Successfully SSH Access. - | But interactive shell is disabled. - | - | Please use: - | - | git clone ssh://%s@GITBUCKET_HOST:%d/OWNER/REPOSITORY_NAME.git - """.stripMargin.format(user, port).replace("\n", "\r\n") + "\r\n" - err.write(Constants.encode(message)) - err.flush() - in.close() - out.close() - err.close() - callback.onExit(127) - } - - override def destroy(): Unit = {} - - override def setInputStream(in: InputStream): Unit = { - this.in = in - } - - override def setOutputStream(out: OutputStream): Unit = { - this.out = out - } - - override def setErrorStream(err: OutputStream): Unit = { - this.err = err - } - - override def setExitCallback(callback: ExitCallback): Unit = { - this.callback = callback - } - } -} diff --git a/src/main/scala/ssh/PublicKeyAuthenticator.scala b/src/main/scala/ssh/PublicKeyAuthenticator.scala deleted file mode 100644 index ddcc724..0000000 --- a/src/main/scala/ssh/PublicKeyAuthenticator.scala +++ /dev/null @@ -1,22 +0,0 @@ -package ssh - -import org.apache.sshd.server.PublickeyAuthenticator -import org.apache.sshd.server.session.ServerSession -import java.security.PublicKey -import service.SshKeyService -import servlet.Database - -class PublicKeyAuthenticator extends PublickeyAuthenticator with SshKeyService { - - override def authenticate(username: String, key: PublicKey, session: ServerSession): Boolean = { - Database() withSession { implicit session => - getPublicKeys(username).exists { sshKey => - SshUtil.str2PublicKey(sshKey.publicKey) match { - case Some(publicKey) => key.equals(publicKey) - case _ => false - } - } - } - } - -} diff --git a/src/main/scala/ssh/SshServerListener.scala b/src/main/scala/ssh/SshServerListener.scala deleted file mode 100644 index b441cae..0000000 --- a/src/main/scala/ssh/SshServerListener.scala +++ /dev/null @@ -1,69 +0,0 @@ -package ssh - -import javax.servlet.{ServletContextEvent, ServletContextListener} -import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider -import org.slf4j.LoggerFactory -import util.Directory -import service.SystemSettingsService -import java.util.concurrent.atomic.AtomicBoolean - -object SshServer { - private val logger = LoggerFactory.getLogger(SshServer.getClass) - private val server = org.apache.sshd.SshServer.setUpDefaultServer() - private val active = new AtomicBoolean(false) - - private def configure(port: Int, baseUrl: String) = { - server.setPort(port) - server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(s"${Directory.GitBucketHome}/gitbucket.ser")) - server.setPublickeyAuthenticator(new PublicKeyAuthenticator) - server.setCommandFactory(new GitCommandFactory(baseUrl)) - server.setShellFactory(new NoShell) - } - - def start(port: Int, baseUrl: String) = { - if(active.compareAndSet(false, true)){ - configure(port, baseUrl) - server.start() - logger.info(s"Start SSH Server Listen on ${server.getPort}") - } - } - - def stop() = { - if(active.compareAndSet(true, false)){ - server.stop(true) - logger.info("SSH Server is stopped.") - } - } - - def isActive = active.get -} - -/* - * Start a SSH Server Daemon - * - * How to use: - * git clone ssh://username@host_or_ip:29418/owner/repository_name.git - */ -class SshServerListener extends ServletContextListener with SystemSettingsService { - - private val logger = LoggerFactory.getLogger(classOf[SshServerListener]) - - override def contextInitialized(sce: ServletContextEvent): Unit = { - val settings = loadSystemSettings() - if(settings.ssh){ - settings.baseUrl match { - case None => - logger.error("Could not start SshServer because the baseUrl is not configured.") - case Some(baseUrl) => - SshServer.start(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), baseUrl) - } - } - } - - override def contextDestroyed(sce: ServletContextEvent): Unit = { - if(loadSystemSettings().ssh){ - SshServer.stop() - } - } - -} diff --git a/src/main/scala/ssh/SshUtil.scala b/src/main/scala/ssh/SshUtil.scala deleted file mode 100644 index 9d6484c..0000000 --- a/src/main/scala/ssh/SshUtil.scala +++ /dev/null @@ -1,36 +0,0 @@ -package ssh - -import java.security.PublicKey -import org.slf4j.LoggerFactory -import org.apache.commons.codec.binary.Base64 -import org.eclipse.jgit.lib.Constants -import org.apache.sshd.common.util.{KeyUtils, Buffer} - -object SshUtil { - - private val logger = LoggerFactory.getLogger(SshUtil.getClass) - - def str2PublicKey(key: String): Option[PublicKey] = { - // TODO RFC 4716 Public Key is not supported... - val parts = key.split(" ") - if (parts.size < 2) { - logger.debug(s"Invalid PublicKey Format: ${key}") - return None - } - try { - val encodedKey = parts(1) - val decode = Base64.decodeBase64(Constants.encodeASCII(encodedKey)) - Some(new Buffer(decode).getRawPublicKey) - } catch { - case e: Throwable => - logger.debug(e.getMessage, e) - None - } - } - - def fingerPrint(key: String): Option[String] = str2PublicKey(key) match { - case Some(publicKey) => Some(KeyUtils.getFingerPrint(publicKey)) - case None => None - } - -} diff --git a/src/main/scala/util/Authenticator.scala b/src/main/scala/util/Authenticator.scala deleted file mode 100644 index f40af7b..0000000 --- a/src/main/scala/util/Authenticator.scala +++ /dev/null @@ -1,181 +0,0 @@ -package util - -import app.ControllerBase -import service._ -import RepositoryService.RepositoryInfo -import util.Implicits._ -import util.ControlUtil._ - -/** - * Allows only oneself and administrators. - */ -trait OneselfAuthenticator { self: ControllerBase => - protected def oneselfOnly(action: => Any) = { authenticate(action) } - protected def oneselfOnly[T](action: T => Any) = (form: T) => { authenticate(action(form)) } - - private def authenticate(action: => Any) = { - { - defining(request.paths){ paths => - context.loginAccount match { - case Some(x) if(x.isAdmin) => action - case Some(x) if(paths(0) == x.userName) => action - case _ => Unauthorized() - } - } - } - } -} - -/** - * Allows only the repository owner and administrators. - */ -trait OwnerAuthenticator { self: ControllerBase with RepositoryService with AccountService => - protected def ownerOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } - protected def ownerOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } - - private def authenticate(action: (RepositoryInfo) => Any) = { - { - defining(request.paths){ paths => - getRepository(paths(0), paths(1), baseUrl).map { repository => - context.loginAccount match { - case Some(x) if(x.isAdmin) => action(repository) - case Some(x) if(repository.owner == x.userName) => action(repository) - case Some(x) if(getGroupMembers(repository.owner).exists { member => - member.userName == x.userName && member.isManager == true - }) => action(repository) - case _ => Unauthorized() - } - } getOrElse NotFound() - } - } - } -} - -/** - * Allows only signed in users. - */ -trait UsersAuthenticator { self: ControllerBase => - protected def usersOnly(action: => Any) = { authenticate(action) } - protected def usersOnly[T](action: T => Any) = (form: T) => { authenticate(action(form)) } - - private def authenticate(action: => Any) = { - { - context.loginAccount match { - case Some(x) => action - case None => Unauthorized() - } - } - } -} - -/** - * Allows only administrators. - */ -trait AdminAuthenticator { self: ControllerBase => - protected def adminOnly(action: => Any) = { authenticate(action) } - protected def adminOnly[T](action: T => Any) = (form: T) => { authenticate(action(form)) } - - private def authenticate(action: => Any) = { - { - context.loginAccount match { - case Some(x) if(x.isAdmin) => action - case _ => Unauthorized() - } - } - } -} - -/** - * Allows only collaborators and administrators. - */ -trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService => - protected def collaboratorsOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } - protected def collaboratorsOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } - - private def authenticate(action: (RepositoryInfo) => Any) = { - { - defining(request.paths){ paths => - getRepository(paths(0), paths(1), baseUrl).map { repository => - context.loginAccount match { - case Some(x) if(x.isAdmin) => action(repository) - case Some(x) if(paths(0) == x.userName) => action(repository) - case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository) - case _ => Unauthorized() - } - } getOrElse NotFound() - } - } - } -} - -/** - * Allows only the repository owner (or manager for group repository) and administrators. - */ -trait ReferrerAuthenticator { self: ControllerBase with RepositoryService => - protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } - protected def referrersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } - - private def authenticate(action: (RepositoryInfo) => Any) = { - { - defining(request.paths){ paths => - getRepository(paths(0), paths(1), baseUrl).map { repository => - if(!repository.repository.isPrivate){ - action(repository) - } else { - context.loginAccount match { - case Some(x) if(x.isAdmin) => action(repository) - case Some(x) if(paths(0) == x.userName) => action(repository) - case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository) - case _ => Unauthorized() - } - } - } getOrElse NotFound() - } - } - } -} - -/** - * Allows only signed in users which can access the repository. - */ -trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService => - protected def readableUsersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } - protected def readableUsersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } - - private def authenticate(action: (RepositoryInfo) => Any) = { - { - defining(request.paths){ paths => - getRepository(paths(0), paths(1), baseUrl).map { repository => - context.loginAccount match { - case Some(x) if(x.isAdmin) => action(repository) - case Some(x) if(!repository.repository.isPrivate) => action(repository) - case Some(x) if(paths(0) == x.userName) => action(repository) - case Some(x) if(getCollaborators(paths(0), paths(1)).contains(x.userName)) => action(repository) - case _ => Unauthorized() - } - } getOrElse NotFound() - } - } - } -} - -/** - * Allows only the group managers. - */ -trait GroupManagerAuthenticator { self: ControllerBase with AccountService => - protected def managersOnly(action: => Any) = { authenticate(action) } - protected def managersOnly[T](action: T => Any) = (form: T) => { authenticate(action(form)) } - - private def authenticate(action: => Any) = { - { - defining(request.paths){ paths => - context.loginAccount match { - case Some(x) if(getGroupMembers(paths(0)).exists { member => - member.userName == x.userName && member.isManager - }) => action - case _ => Unauthorized() - } - } - } - } -} diff --git a/src/main/scala/util/ControlUtil.scala b/src/main/scala/util/ControlUtil.scala deleted file mode 100644 index 7945f32..0000000 --- a/src/main/scala/util/ControlUtil.scala +++ /dev/null @@ -1,46 +0,0 @@ -package util - -import org.eclipse.jgit.api.Git -import org.eclipse.jgit.revwalk.RevWalk -import org.eclipse.jgit.treewalk.TreeWalk -import scala.util.control.Exception._ -import scala.language.reflectiveCalls - -/** - * Provides control facilities. - */ -object ControlUtil { - - def defining[A, B](value: A)(f: A => B): B = f(value) - - def using[A <% { def close(): Unit }, B](resource: A)(f: A => B): B = - try f(resource) finally { - if(resource != null){ - ignoring(classOf[Throwable]) { - resource.close() - } - } - } - - def using[T](git: Git)(f: Git => T): T = - try f(git) finally git.getRepository.close() - - def using[T](git1: Git, git2: Git)(f: (Git, Git) => T): T = - try f(git1, git2) finally { - git1.getRepository.close() - git2.getRepository.close() - } - - def using[T](revWalk: RevWalk)(f: RevWalk => T): T = - try f(revWalk) finally revWalk.release() - - def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T = - try f(treeWalk) finally treeWalk.release() - - def ignore[T](f: => Unit): Unit = try { - f - } catch { - case e: Exception => () - } - -} diff --git a/src/main/scala/util/DatabaseConfig.scala b/src/main/scala/util/DatabaseConfig.scala deleted file mode 100644 index 86453e3..0000000 --- a/src/main/scala/util/DatabaseConfig.scala +++ /dev/null @@ -1,19 +0,0 @@ -package util - -import com.typesafe.config.ConfigFactory -import util.Directory.DatabaseHome - -object DatabaseConfig { - - private val config = ConfigFactory.load("database") - private val dbUrl = config.getString("db.url") - - def url(directory: Option[String]): String = - dbUrl.replace("${DatabaseHome}", directory.getOrElse(DatabaseHome)) - - val url: String = url(None) - val user: String = config.getString("db.user") - val password: String = config.getString("db.password") - val driver: String = config.getString("db.driver") - -} diff --git a/src/main/scala/util/Directory.scala b/src/main/scala/util/Directory.scala deleted file mode 100644 index 9008171..0000000 --- a/src/main/scala/util/Directory.scala +++ /dev/null @@ -1,87 +0,0 @@ -package util - -import java.io.File -import util.ControlUtil._ -import org.apache.commons.io.FileUtils - -/** - * Provides directories used by GitBucket. - */ -object Directory { - - val GitBucketHome = (System.getProperty("gitbucket.home") match { - // -Dgitbucket.home= - case path if(path != null) => new File(path) - case _ => scala.util.Properties.envOrNone("GITBUCKET_HOME") match { - // environment variable GITBUCKET_HOME - case Some(env) => new File(env) - // default is HOME/.gitbucket - case None => { - val oldHome = new File(System.getProperty("user.home"), "gitbucket") - if(oldHome.exists && oldHome.isDirectory && new File(oldHome, "version").exists){ - //FileUtils.moveDirectory(oldHome, newHome) - oldHome - } else { - new File(System.getProperty("user.home"), ".gitbucket") - } - } - } - }).getAbsolutePath - - val GitBucketConf = new File(GitBucketHome, "gitbucket.conf") - - val RepositoryHome = s"${GitBucketHome}/repositories" - - val DatabaseHome = s"${GitBucketHome}/data" - - val PluginHome = s"${GitBucketHome}/plugins" - - val TemporaryHome = s"${GitBucketHome}/tmp" - - /** - * Substance directory of the repository. - */ - def getRepositoryDir(owner: String, repository: String): File = - 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}/comments") - - /** - * 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"${TemporaryHome}/_upload/${sessionId}") - - /** - * Root of temporary directories for the specified repository. - */ - def getTemporaryDir(owner: String, repository: String): File = - new File(s"${TemporaryHome}/${owner}/${repository}") - - /** - * Root of plugin cache directory. Plugin repositories are cloned into this directory. - */ - def getPluginCacheDir(): File = new File(s"${TemporaryHome}/_plugins") - - /** - * Temporary directory which is used to create an archive to download repository contents. - */ - def getDownloadWorkDir(owner: String, repository: String, sessionId: String): File = - new File(getTemporaryDir(owner, repository), s"download/${sessionId}") - - /** - * Substance directory of the wiki repository. - */ - def getWikiRepositoryDir(owner: String, repository: String): File = - new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git") - -} \ No newline at end of file diff --git a/src/main/scala/util/FileUtil.scala b/src/main/scala/util/FileUtil.scala deleted file mode 100644 index 0145db4..0000000 --- a/src/main/scala/util/FileUtil.scala +++ /dev/null @@ -1,53 +0,0 @@ -package util - -import org.apache.commons.io.FileUtils -import java.net.URLConnection -import java.io.File -import util.ControlUtil._ -import scala.util.Random - -object FileUtil { - - def getMimeType(name: String): String = - defining(URLConnection.getFileNameMap()){ fileNameMap => - fileNameMap.getContentTypeFor(name) match { - case null => "application/octet-stream" - case mimeType => mimeType - } - } - - def getContentType(name: String, bytes: Array[Byte]): String = { - defining(getMimeType(name)){ mimeType => - if(mimeType == "application/octet-stream" && isText(bytes)){ - "text/plain" - } else { - mimeType - } - } - } - - 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 generateFileId: String = System.currentTimeMillis + Random.alphanumeric.take(10).mkString - - def getExtension(name: String): String = - name.lastIndexOf('.') match { - case i if(i >= 0) => name.substring(i + 1) - case _ => "" - } - - def withTmpDir[A](dir: File)(action: File => A): A = { - if(dir.exists()){ - FileUtils.deleteDirectory(dir) - } - try { - action(dir) - } finally { - FileUtils.deleteDirectory(dir) - } - } -} diff --git a/src/main/scala/util/Implicits.scala b/src/main/scala/util/Implicits.scala deleted file mode 100644 index f1b1115..0000000 --- a/src/main/scala/util/Implicits.scala +++ /dev/null @@ -1,82 +0,0 @@ -package util - -import scala.util.matching.Regex -import scala.util.control.Exception._ -import slick.jdbc.JdbcBackend -import servlet.Database -import javax.servlet.http.{HttpSession, HttpServletRequest} - -/** - * Provides some usable implicit conversions. - */ -object Implicits { - - // Convert to slick session. - implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request) - - implicit class RichSeq[A](seq: Seq[A]) { - - def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition) - - @scala.annotation.tailrec - private def split[A](list: Seq[A], result: Seq[Seq[A]] = Nil)(condition: (A, A) => Boolean): Seq[Seq[A]] = - list match { - case x :: xs => { - xs.span(condition(x, _)) match { - case (matched, remained) => split(remained, result :+ (x :: matched))(condition) - } - } - case Nil => result - } - } - - implicit class RichString(value: String){ - def replaceBy(regex: Regex)(replace: Regex.MatchData => Option[String]): String = { - val sb = new StringBuilder() - var i = 0 - regex.findAllIn(value).matchData.foreach { m => - sb.append(value.substring(i, m.start)) - i = m.end - replace(m) match { - case Some(s) => sb.append(s) - case None => sb.append(m.matched) - } - } - if(i < value.length){ - sb.append(value.substring(i)) - } - sb.toString - } - - def toIntOpt: Option[Int] = catching(classOf[NumberFormatException]) opt { - Integer.parseInt(value) - } - } - - implicit class RichRequest(request: HttpServletRequest){ - - def paths: Array[String] = request.getRequestURI.substring(request.getContextPath.length + 1).split("/") - - def hasQueryString: Boolean = request.getQueryString != null - - def hasAttribute(name: String): Boolean = request.getAttribute(name) != null - - } - - implicit class RichSession(session: HttpSession){ - - def putAndGet[T](key: String, value: T): T = { - session.setAttribute(key, value) - value - } - - def getAndRemove[T](key: String): Option[T] = { - val value = session.getAttribute(key).asInstanceOf[T] - if(value == null){ - session.removeAttribute(key) - } - Option(value) - } - } - -} diff --git a/src/main/scala/util/JDBCUtil.scala b/src/main/scala/util/JDBCUtil.scala deleted file mode 100644 index 5d880d0..0000000 --- a/src/main/scala/util/JDBCUtil.scala +++ /dev/null @@ -1,63 +0,0 @@ -package util - -import java.sql._ -import util.ControlUtil._ -import scala.collection.mutable.ListBuffer - -/** - * Provides implicit class which extends java.sql.Connection. - * This is used in automatic migration in [[servlet.AutoUpdateListener]]. - */ -object JDBCUtil { - - implicit class RichConnection(conn: Connection){ - - def update(sql: String, params: Any*): Int = { - execute(sql, params: _*){ stmt => - stmt.executeUpdate() - } - } - - def find[T](sql: String, params: Any*)(f: ResultSet => T): Option[T] = { - execute(sql, params: _*){ stmt => - using(stmt.executeQuery()){ rs => - if(rs.next) Some(f(rs)) else None - } - } - } - - def select[T](sql: String, params: Any*)(f: ResultSet => T): Seq[T] = { - execute(sql, params: _*){ stmt => - using(stmt.executeQuery()){ rs => - val list = new ListBuffer[T] - while(rs.next){ - list += f(rs) - } - list.toSeq - } - } - } - - def selectInt(sql: String, params: Any*): Int = { - execute(sql, params: _*){ stmt => - using(stmt.executeQuery()){ rs => - if(rs.next) rs.getInt(1) else 0 - } - } - } - - private def execute[T](sql: String, params: Any*)(f: (PreparedStatement) => T): T = { - using(conn.prepareStatement(sql)){ stmt => - params.zipWithIndex.foreach { case (p, i) => - p match { - case x: Int => stmt.setInt(i + 1, x) - case x: String => stmt.setString(i + 1, x) - } - } - f(stmt) - } - } - - } - -} diff --git a/src/main/scala/util/JGitUtil.scala b/src/main/scala/util/JGitUtil.scala deleted file mode 100644 index 5c7be3f..0000000 --- a/src/main/scala/util/JGitUtil.scala +++ /dev/null @@ -1,751 +0,0 @@ -package util - -import org.eclipse.jgit.api.Git -import util.Directory._ -import util.StringUtil._ -import util.ControlUtil._ -import scala.annotation.tailrec -import scala.collection.JavaConverters._ -import org.eclipse.jgit.lib._ -import org.eclipse.jgit.revwalk._ -import org.eclipse.jgit.revwalk.filter._ -import org.eclipse.jgit.treewalk._ -import org.eclipse.jgit.treewalk.filter._ -import org.eclipse.jgit.diff.DiffEntry.ChangeType -import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException} -import org.eclipse.jgit.transport.RefSpec -import java.util.Date -import org.eclipse.jgit.api.errors.{JGitInternalException, InvalidRefNameException, RefAlreadyExistsException, NoHeadException} -import service.RepositoryService -import org.eclipse.jgit.dircache.DirCacheEntry -import org.slf4j.LoggerFactory - -/** - * Provides complex JGit operations. - */ -object JGitUtil { - - private val logger = LoggerFactory.getLogger(JGitUtil.getClass) - - /** - * The repository data. - * - * @param owner the user name of the repository owner - * @param name the repository name - * @param url the repository URL - * @param commitCount the commit count. If the repository has over 1000 commits then this property is 1001. - * @param branchList the list of branch names - * @param tags the list of tags - */ - case class RepositoryInfo(owner: String, name: String, url: String, commitCount: Int, branchList: List[String], tags: List[TagInfo]){ - def this(owner: String, name: String, baseUrl: String) = { - this(owner, name, s"${baseUrl}/git/${owner}/${name}.git", 0, Nil, Nil) - } - } - - /** - * The file data for the file list of the repository viewer. - * - * @param id the object id - * @param isDirectory whether is it directory - * @param name the file (or directory) name - * @param message the last commit message - * @param commitId the last commit id - * @param time the last modified time - * @param author the last committer name - * @param mailAddress the committer's mail address - * @param linkUrl the url of submodule - */ - case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, message: String, commitId: String, - time: Date, author: String, mailAddress: String, linkUrl: Option[String]) - - /** - * The commit data. - * - * @param id the commit id - * @param shortMessage the short message - * @param fullMessage the full message - * @param parents the list of parent commit id - * @param authorTime the author time - * @param authorName the author name - * @param authorEmailAddress the mail address of the author - * @param commitTime the commit time - * @param committerName the committer name - * @param committerEmailAddress the mail address of the committer - */ - case class CommitInfo(id: String, shortMessage: String, fullMessage: String, parents: List[String], - authorTime: Date, authorName: String, authorEmailAddress: String, - commitTime: Date, committerName: String, committerEmailAddress: String){ - - def this(rev: org.eclipse.jgit.revwalk.RevCommit) = this( - rev.getName, - rev.getShortMessage, - rev.getFullMessage, - rev.getParents().map(_.name).toList, - rev.getAuthorIdent.getWhen, - rev.getAuthorIdent.getName, - rev.getAuthorIdent.getEmailAddress, - rev.getCommitterIdent.getWhen, - rev.getCommitterIdent.getName, - rev.getCommitterIdent.getEmailAddress) - - val summary = getSummaryMessage(fullMessage, shortMessage) - - val description = defining(fullMessage.trim.indexOf("\n")){ i => - if(i >= 0){ - Some(fullMessage.trim.substring(i).trim) - } else None - } - - def isDifferentFromAuthor: Boolean = authorName != committerName || authorEmailAddress != committerEmailAddress - } - - case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String]) - - /** - * The file content data for the file content view of the repository viewer. - * - * @param viewType "image", "large" or "other" - * @param content the string content - * @param charset the character encoding - */ - case class ContentInfo(viewType: String, content: Option[String], charset: Option[String]){ - /** - * the line separator of this content ("LF" or "CRLF") - */ - val lineSeparator: String = if(content.exists(_.indexOf("\r\n") >= 0)) "CRLF" else "LF" - } - - /** - * The tag data. - * - * @param name the tag name - * @param time the tagged date - * @param id the commit id - */ - case class TagInfo(name: String, time: Date, id: String) - - /** - * The submodule data - * - * @param name the module name - * @param path the path in the repository - * @param url the repository url of this module - */ - case class SubmoduleInfo(name: String, path: String, url: String) - - case class BranchMergeInfo(ahead: Int, behind: Int, isMerged: Boolean) - - case class BranchInfo(name: String, committerName: String, commitTime: Date, committerEmailAddress:String, mergeInfo: Option[BranchMergeInfo], commitId: String) - - /** - * Returns RevCommit from the commit or tag id. - * - * @param git the Git object - * @param objectId the ObjectId of the commit or tag - * @return the RevCommit for the specified commit or tag - */ - def getRevCommitFromId(git: Git, objectId: ObjectId): RevCommit = { - val revWalk = new RevWalk(git.getRepository) - val revCommit = revWalk.parseAny(objectId) match { - case r: RevTag => revWalk.parseCommit(r.getObject) - case _ => revWalk.parseCommit(objectId) - } - revWalk.dispose - revCommit - } - - /** - * Returns the repository information. It contains branch names and tag names. - */ - def getRepositoryInfo(owner: String, repository: String, baseUrl: String): RepositoryInfo = { - using(Git.open(getRepositoryDir(owner, repository))){ git => - try { - // get commit count - val commitCount = git.log.all.call.iterator.asScala.map(_ => 1).take(10001).sum - - RepositoryInfo( - owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", - // commit count - commitCount, - // branches - git.branchList.call.asScala.map { ref => - ref.getName.stripPrefix("refs/heads/") - }.toList, - // tags - git.tagList.call.asScala.map { ref => - val revCommit = getRevCommitFromId(git, ref.getObjectId) - TagInfo(ref.getName.stripPrefix("refs/tags/"), revCommit.getCommitterIdent.getWhen, revCommit.getName) - }.toList - ) - } catch { - // not initialized - case e: NoHeadException => RepositoryInfo( - owner, repository, s"${baseUrl}/git/${owner}/${repository}.git", 0, Nil, Nil) - - } - } - } - - /** - * Returns the file list of the specified path. - * - * @param git the Git object - * @param revision the branch name or commit id - * @param path the directory path (optional) - * @return HTML of the file list - */ - def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = { - var list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])] - - using(new RevWalk(git.getRepository)){ revWalk => - val objectId = git.getRepository.resolve(revision) - val revCommit = revWalk.parseCommit(objectId) - - val treeWalk = if (path == ".") { - val treeWalk = new TreeWalk(git.getRepository) - treeWalk.addTree(revCommit.getTree) - treeWalk - } else { - val treeWalk = TreeWalk.forPath(git.getRepository, path, revCommit.getTree) - treeWalk.enterSubtree() - treeWalk - } - - using(treeWalk) { treeWalk => - while (treeWalk.next()) { - // submodule - val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){ - getSubmodules(git, revCommit.getTree).find(_.path == treeWalk.getPathString).map(_.url) - } else None - - list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl)) - } - - list.transform(tuple => - if (tuple._2 != FileMode.TREE) - tuple - else - simplifyPath(tuple) - ) - - @tailrec - def simplifyPath(tuple: (ObjectId, FileMode, String, String, Option[String])): (ObjectId, FileMode, String, String, Option[String]) = { - val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])] - using(new TreeWalk(git.getRepository)) { walk => - walk.addTree(tuple._1) - while (walk.next() && list.size < 2) { - val linkUrl = if (walk.getFileMode(0) == FileMode.GITLINK) { - getSubmodules(git, revCommit.getTree).find(_.path == walk.getPathString).map(_.url) - } else None - list.append((walk.getObjectId(0), walk.getFileMode(0), tuple._3 + "/" + walk.getPathString, tuple._4 + "/" + walk.getNameString, linkUrl)) - } - } - if (list.size != 1 || list.exists(_._2 != FileMode.TREE)) - tuple - else - simplifyPath(list(0)) - } - } - } - - val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision) - list.map { case (objectId, fileMode, path, name, linkUrl) => - defining(commits(path)){ commit => - FileInfo( - objectId, - fileMode == FileMode.TREE || fileMode == FileMode.GITLINK, - name, - getSummaryMessage(commit.getFullMessage, commit.getShortMessage), - commit.getName, - commit.getAuthorIdent.getWhen, - commit.getAuthorIdent.getName, - commit.getAuthorIdent.getEmailAddress, - linkUrl) - } - }.sortWith { (file1, file2) => - (file1.isDirectory, file2.isDirectory) match { - case (true , false) => true - case (false, true ) => false - case _ => file1.name.compareTo(file2.name) < 0 - } - }.toList - } - - /** - * Returns the first line of the commit message. - */ - private def getSummaryMessage(fullMessage: String, shortMessage: String): String = { - defining(fullMessage.trim.indexOf("\n")){ i => - defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine => - if(firstLine.length > shortMessage.length) shortMessage else firstLine - } - } - } - - /** - * Returns the commit list of the specified branch. - * - * @param git the Git object - * @param revision the branch name or commit id - * @param page the page number (1-) - * @param limit the number of commit info per page. 0 (default) means unlimited. - * @param path filters by this path. default is no filter. - * @return a tuple of the commit list and whether has next, or the error message - */ - def getCommitLog(git: Git, revision: String, page: Int = 1, limit: Int = 0, path: String = ""): Either[String, (List[CommitInfo], Boolean)] = { - val fixedPage = if(page <= 0) 1 else page - - @scala.annotation.tailrec - def getCommitLog(i: java.util.Iterator[RevCommit], count: Int, logs: List[CommitInfo]): (List[CommitInfo], Boolean) = - i.hasNext match { - case true if(limit <= 0 || logs.size < limit) => { - val commit = i.next - getCommitLog(i, count + 1, if(limit <= 0 || (fixedPage - 1) * limit <= count) logs :+ new CommitInfo(commit) else logs) - } - case _ => (logs, i.hasNext) - } - - using(new RevWalk(git.getRepository)){ revWalk => - defining(git.getRepository.resolve(revision)){ objectId => - if(objectId == null){ - Left(s"${revision} can't be resolved.") - } else { - revWalk.markStart(revWalk.parseCommit(objectId)) - if(path.nonEmpty){ - revWalk.setRevFilter(new RevFilter(){ - def include(walk: RevWalk, commit: RevCommit): Boolean = { - getDiffs(git, commit.getName, false)._1.find(_.newPath == path).nonEmpty - } - override def clone(): RevFilter = this - }) - } - Right(getCommitLog(revWalk.iterator, 0, Nil)) - } - } - } - } - - def getCommitLogs(git: Git, begin: String, includesLastCommit: Boolean = false) - (endCondition: RevCommit => Boolean): List[CommitInfo] = { - @scala.annotation.tailrec - def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[CommitInfo]): List[CommitInfo] = - i.hasNext match { - case true => { - val revCommit = i.next - if(endCondition(revCommit)){ - if(includesLastCommit) logs :+ new CommitInfo(revCommit) else logs - } else { - getCommitLog(i, logs :+ new CommitInfo(revCommit)) - } - } - case false => logs - } - - using(new RevWalk(git.getRepository)){ revWalk => - revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(begin))) - getCommitLog(revWalk.iterator, Nil).reverse - } - } - - - /** - * Returns the commit list between two revisions. - * - * @param git the Git object - * @param from the from revision - * @param to the to revision - * @return the commit list - */ - // TODO swap parameters 'from' and 'to'!? - def getCommitLog(git: Git, from: String, to: String): List[CommitInfo] = - getCommitLogs(git, to)(_.getName == from) - - /** - * Returns the latest RevCommit of the specified path. - * - * @param git the Git object - * @param path the path - * @param revision the branch name or commit id - * @return the latest commit - */ - def getLatestCommitFromPath(git: Git, path: String, revision: String): Option[RevCommit] = - getLatestCommitFromPaths(git, List(path), revision).get(path) - - /** - * Returns the list of latest RevCommit of the specified paths. - * - * @param git the Git object - * @param paths the list of paths - * @param revision the branch name or commit id - * @return the list of latest commit - */ - def getLatestCommitFromPaths(git: Git, paths: List[String], revision: String): Map[String, RevCommit] = { - val start = getRevCommitFromId(git, git.getRepository.resolve(revision)) - paths.map { path => - val commit = git.log.add(start).addPath(path).setMaxCount(1).call.iterator.next - (path, commit) - }.toMap - } - - /** - * Returns the tuple of diff of the given commit and the previous commit id. - */ - def getDiffs(git: Git, id: String, fetchContent: Boolean = true): (List[DiffInfo], Option[String]) = { - @scala.annotation.tailrec - def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[RevCommit]): List[RevCommit] = - i.hasNext match { - case true if(logs.size < 2) => getCommitLog(i, logs :+ i.next) - case _ => logs - } - - using(new RevWalk(git.getRepository)){ revWalk => - revWalk.markStart(revWalk.parseCommit(git.getRepository.resolve(id))) - val commits = getCommitLog(revWalk.iterator, Nil) - val revCommit = commits(0) - - if(commits.length >= 2){ - // not initial commit - val oldCommit = if(revCommit.getParentCount >= 2) { - // merge commit - revCommit.getParents.head - } else { - commits(1) - } - (getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName)) - - } else { - // initial commit - using(new TreeWalk(git.getRepository)){ treeWalk => - treeWalk.addTree(revCommit.getTree) - val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]() - while(treeWalk.next){ - buffer.append((if(!fetchContent){ - DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None) - } else { - DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, - JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray)) - })) - } - (buffer.toList, None) - } - } - } - } - - def getDiffs(git: Git, from: String, to: String, fetchContent: Boolean): List[DiffInfo] = { - val reader = git.getRepository.newObjectReader - val oldTreeIter = new CanonicalTreeParser - oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}")) - - val newTreeIter = new CanonicalTreeParser - newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}")) - - import scala.collection.JavaConverters._ - git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff => - if(!fetchContent || FileUtil.isImage(diff.getOldPath) || FileUtil.isImage(diff.getNewPath)){ - DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None) - } else { - DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, - JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray), - JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray)) - } - }.toList - } - - - /** - * Returns the list of branch names of the specified commit. - */ - def getBranchesOfCommit(git: Git, commitId: String): List[String] = - using(new RevWalk(git.getRepository)){ revWalk => - defining(revWalk.parseCommit(git.getRepository.resolve(commitId + "^0"))){ commit => - git.getRepository.getAllRefs.entrySet.asScala.filter { e => - (e.getKey.startsWith(Constants.R_HEADS) && revWalk.isMergedInto(commit, revWalk.parseCommit(e.getValue.getObjectId))) - }.map { e => - e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_HEADS.length) - }.toList.sorted - } - } - - /** - * Returns the list of tags of the specified commit. - */ - def getTagsOfCommit(git: Git, commitId: String): List[String] = - using(new RevWalk(git.getRepository)){ revWalk => - defining(revWalk.parseCommit(git.getRepository.resolve(commitId + "^0"))){ commit => - git.getRepository.getAllRefs.entrySet.asScala.filter { e => - (e.getKey.startsWith(Constants.R_TAGS) && revWalk.isMergedInto(commit, revWalk.parseCommit(e.getValue.getObjectId))) - }.map { e => - e.getValue.getName.substring(org.eclipse.jgit.lib.Constants.R_TAGS.length) - }.toList.sorted.reverse - } - } - - def initRepository(dir: java.io.File): Unit = - using(new RepositoryBuilder().setGitDir(dir).setBare.build){ repository => - repository.create - setReceivePack(repository) - } - - def cloneRepository(from: java.io.File, to: java.io.File): Unit = - using(Git.cloneRepository.setURI(from.toURI.toString).setDirectory(to).setBare(true).call){ git => - setReceivePack(git.getRepository) - } - - def isEmpty(git: Git): Boolean = git.getRepository.resolve(Constants.HEAD) == null - - private def setReceivePack(repository: org.eclipse.jgit.lib.Repository): Unit = - defining(repository.getConfig){ config => - config.setBoolean("http", null, "receivepack", true) - config.save - } - - def getDefaultBranch(git: Git, repository: RepositoryService.RepositoryInfo, - revstr: String = ""): Option[(ObjectId, String)] = { - Seq( - Some(if(revstr.isEmpty) repository.repository.defaultBranch else revstr), - repository.branchList.headOption - ).flatMap { - case Some(rev) => Some((git.getRepository.resolve(rev), rev)) - case None => None - }.find(_._1 != null) - } - - def createBranch(git: Git, fromBranch: String, newBranch: String) = { - try { - git.branchCreate().setStartPoint(fromBranch).setName(newBranch).call() - Right("Branch created.") - } catch { - case e: RefAlreadyExistsException => Left("Sorry, that branch already exists.") - // JGitInternalException occurs when new branch name is 'a' and the branch whose name is 'a/*' exists. - case _: InvalidRefNameException | _: JGitInternalException => Left("Sorry, that name is invalid.") - } - } - - 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, - ref: String, fullName: String, mailAddress: String, message: String): ObjectId = { - 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() - inserter.release() - - val refUpdate = git.getRepository.updateRef(ref) - refUpdate.setNewObjectId(newHeadId) - refUpdate.update() - - newHeadId - } - - /** - * Read submodule information from .gitmodules - */ - def getSubmodules(git: Git, tree: RevTree): List[SubmoduleInfo] = { - val repository = git.getRepository - getContentFromPath(git, tree, ".gitmodules", true).map { bytes => - (try { - val config = new BlobBasedConfig(repository.getConfig(), bytes) - config.getSubsections("submodule").asScala.map { module => - val path = config.getString("submodule", module, "path") - val url = config.getString("submodule", module, "url") - SubmoduleInfo(module, path, url) - } - } catch { - case e: ConfigInvalidException => { - logger.error("Failed to load .gitmodules file for " + repository.getDirectory(), e) - Nil - } - }).toList - } getOrElse Nil - } - - /** - * Get object content of the given path as byte array from the Git repository. - * - * @param git the Git object - * @param revTree the rev tree - * @param path the path - * @param fetchLargeFile if false then returns None for the large file - * @return the byte array of content or None if object does not exist - */ - def getContentFromPath(git: Git, revTree: RevTree, path: String, fetchLargeFile: Boolean): Option[Array[Byte]] = { - @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(revTree) - treeWalk.setRecursive(true) - getPathObjectId(path, treeWalk) - } flatMap { objectId => - getContentFromId(git, objectId, fetchLargeFile) - } - } - - 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. - * - * @param git the Git object - * @param id the object id - * @param fetchLargeFile if false then returns None for the large file - * @return the byte array of content or None if object does not exist - */ - def getContentFromId(git: Git, id: ObjectId, fetchLargeFile: Boolean): Option[Array[Byte]] = try { - val loader = git.getRepository.getObjectDatabase.open(id) - if(fetchLargeFile == false && FileUtil.isLarge(loader.getSize)){ - None - } else { - using(git.getRepository.getObjectDatabase){ db => - Some(db.open(id).getBytes) - } - } - } catch { - 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 - } - - /** - * Fetch pull request contents into refs/pull/${issueId}/head and return (commitIdTo, commitIdFrom) - */ - def updatePullRequest(userName: String, repositoryName:String, branch: String, issueId: Int, - requestUserName: String, requestRepositoryName: String, requestBranch: String):(String, String) = - using(Git.open(Directory.getRepositoryDir(userName, repositoryName)), - Git.open(Directory.getRepositoryDir(requestUserName, requestRepositoryName))){ (oldGit, newGit) => - oldGit.fetch - .setRemote(Directory.getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString) - .setRefSpecs(new RefSpec(s"refs/heads/${requestBranch}:refs/pull/${issueId}/head").setForceUpdate(true)) - .call - - val commitIdTo = oldGit.getRepository.resolve(s"refs/pull/${issueId}/head").getName - val commitIdFrom = getForkedCommitId(oldGit, newGit, - userName, repositoryName, branch, - requestUserName, requestRepositoryName, requestBranch) - (commitIdTo, commitIdFrom) - } - - /** - * Returns the last modified commit of specified path - * @param git the Git object - * @param startCommit the search base commit id - * @param path the path of target file or directory - * @return the last modified commit of specified path - */ - def getLastModifiedCommit(git: Git, startCommit: RevCommit, path: String): RevCommit = { - return git.log.add(startCommit).addPath(path).setMaxCount(1).call.iterator.next - } - - def getBranches(owner: String, name: String, defaultBranch: String): Seq[BranchInfo] = { - using(Git.open(getRepositoryDir(owner, name))){ git => - val repo = git.getRepository - val defaultObject = repo.resolve(defaultBranch) - git.branchList.call.asScala.map { ref => - val walk = new RevWalk(repo) - try{ - val defaultCommit = walk.parseCommit(defaultObject) - val branchName = ref.getName.stripPrefix("refs/heads/") - val branchCommit = if(branchName == defaultBranch){ - defaultCommit - }else{ - walk.parseCommit(ref.getObjectId) - } - val when = branchCommit.getCommitterIdent.getWhen - val committer = branchCommit.getCommitterIdent.getName - val committerEmail = branchCommit.getCommitterIdent.getEmailAddress - val mergeInfo = if(branchName==defaultBranch){ - None - }else{ - walk.reset() - walk.setRevFilter( RevFilter.MERGE_BASE ) - walk.markStart(branchCommit) - walk.markStart(defaultCommit) - val mergeBase = walk.next() - walk.reset() - walk.setRevFilter(RevFilter.ALL) - Some(BranchMergeInfo( - ahead = RevWalkUtils.count(walk, branchCommit, mergeBase), - behind = RevWalkUtils.count(walk, defaultCommit, mergeBase), - isMerged = walk.isMergedInto(branchCommit, defaultCommit))) - } - BranchInfo(branchName, committer, when, committerEmail, mergeInfo, ref.getObjectId.name) - } finally { - walk.dispose(); - } - } - } - } -} diff --git a/src/main/scala/util/Keys.scala b/src/main/scala/util/Keys.scala deleted file mode 100644 index a934058..0000000 --- a/src/main/scala/util/Keys.scala +++ /dev/null @@ -1,86 +0,0 @@ -package util - -/** - * Define key strings for request attributes, session attributes or flash attributes. - */ -object Keys { - - /** - * Define session keys. - */ - object Session { - - /** - * Session key for the logged in account information. - */ - val LoginAccount = "loginAccount" - - /** - * Session key for the issue search condition in dashboard. - */ - val DashboardIssues = "dashboard/issues" - - /** - * Session key for the pull request search condition in dashboard. - */ - val DashboardPulls = "dashboard/pulls" - - /** - * Generate session key for the issue search condition. - */ - def Issues(owner: String, name: String) = s"${owner}/${name}/issues" - - /** - * Generate session key for the pull request search condition. - */ - def Pulls(owner: String, name: String) = s"${owner}/${name}/pulls" - - /** - * Generate session key for the upload filename. - */ - def Upload(fileId: String) = s"upload_${fileId}" - - } - - object Flash { - - /** - * Flash key for the redirect URL. - */ - val Redirect = "redirect" - - /** - * Flash key for the information message. - */ - val Info = "info" - - } - - /** - * Define request keys. - */ - object Request { - - /** - * Request key for the Slick Session. - */ - val DBSession = "DB_SESSION" - - /** - * Request key for the Ajax request flag. - */ - val Ajax = "AJAX" - - /** - * Request key for the username which is used during Git repository access. - */ - val UserName = "USER_NAME" - - /** - * Generate request key for the request cache. - */ - def Cache(key: String) = s"cache.${key}" - - } - -} diff --git a/src/main/scala/util/LDAPUtil.scala b/src/main/scala/util/LDAPUtil.scala deleted file mode 100644 index c8d741f..0000000 --- a/src/main/scala/util/LDAPUtil.scala +++ /dev/null @@ -1,193 +0,0 @@ -package util - -import util.ControlUtil._ -import service.SystemSettingsService -import com.novell.ldap._ -import java.security.Security -import org.slf4j.LoggerFactory -import service.SystemSettingsService.Ldap -import scala.annotation.tailrec -import model.Account - -/** - * Utility for LDAP authentication. - */ -object LDAPUtil { - - private val LDAP_VERSION: Int = LDAPConnection.LDAP_V3 - private val logger = LoggerFactory.getLogger(getClass().getName()) - - private val LDAP_DUMMY_MAL = "@ldap-devnull" - - /** - * Returns true if mail address ends with "@ldap-devnull" - */ - def isDummyMailAddress(account: Account): Boolean = { - account.mailAddress.endsWith(LDAP_DUMMY_MAL) - } - - /** - * Creates dummy address (userName@ldap-devnull) for LDAP login. - * - * If mail address is not managed in LDAP server, GitBucket stores this dummy address in first LDAP login. - * GitBucket does not send any mails to this dummy address. And these users must input their mail address - * at the first step after LDAP authentication. - */ - def createDummyMailAddress(userName: String): String = { - userName + LDAP_DUMMY_MAL - } - - /** - * Try authentication by LDAP using given configuration. - * Returns Right(LDAPUserInfo) if authentication is successful, otherwise Left(errorMessage). - */ - def authenticate(ldapSettings: Ldap, userName: String, password: String): Either[String, LDAPUserInfo] = { - bind( - host = ldapSettings.host, - port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort), - dn = ldapSettings.bindDN.getOrElse(""), - password = ldapSettings.bindPassword.getOrElse(""), - tls = ldapSettings.tls.getOrElse(false), - ssl = ldapSettings.ssl.getOrElse(false), - keystore = ldapSettings.keystore.getOrElse(""), - error = "System LDAP authentication failed." - ){ conn => - findUser(conn, userName, ldapSettings.baseDN, ldapSettings.userNameAttribute, ldapSettings.additionalFilterCondition) match { - case Some(userDN) => userAuthentication(ldapSettings, userDN, userName, password) - case None => Left("User does not exist.") - } - } - } - - private def userAuthentication(ldapSettings: Ldap, userDN: String, userName: String, password: String): Either[String, LDAPUserInfo] = { - bind( - host = ldapSettings.host, - port = ldapSettings.port.getOrElse(SystemSettingsService.DefaultLdapPort), - dn = userDN, - password = password, - tls = ldapSettings.tls.getOrElse(false), - ssl = ldapSettings.ssl.getOrElse(false), - keystore = ldapSettings.keystore.getOrElse(""), - error = "User LDAP Authentication Failed." - ){ conn => - if(ldapSettings.mailAttribute.getOrElse("").isEmpty) { - Right(LDAPUserInfo( - userName = userName, - fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute => - findFullName(conn, userDN, ldapSettings.userNameAttribute, userName, fullNameAttribute) - }.getOrElse(userName), - mailAddress = createDummyMailAddress(userName))) - } else { - findMailAddress(conn, userDN, ldapSettings.userNameAttribute, userName, ldapSettings.mailAttribute.get) match { - case Some(mailAddress) => Right(LDAPUserInfo( - userName = getUserNameFromMailAddress(userName), - fullName = ldapSettings.fullNameAttribute.flatMap { fullNameAttribute => - findFullName(conn, userDN, ldapSettings.userNameAttribute, userName, fullNameAttribute) - }.getOrElse(userName), - mailAddress = mailAddress)) - case None => Left("Can't find mail address.") - } - } - } - } - - private def getUserNameFromMailAddress(userName: String): String = { - (userName.indexOf('@') match { - case i if i >= 0 => userName.substring(0, i) - case i => userName - }).replaceAll("[^a-zA-Z0-9\\-_.]", "").replaceAll("^[_\\-]", "") - } - - private def bind[A](host: String, port: Int, dn: String, password: String, tls: Boolean, ssl: Boolean, keystore: String, error: String) - (f: LDAPConnection => Either[String, A]): Either[String, A] = { - if (tls) { - // Dynamically set Sun as the security provider - Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider()) - - if (keystore.compareTo("") != 0) { - // Dynamically set the property that JSSE uses to identify - // the keystore that holds trusted root certificates - System.setProperty("javax.net.ssl.trustStore", keystore) - } - } - - val conn: LDAPConnection = - if(ssl) { - new LDAPConnection(new LDAPJSSESecureSocketFactory()) - }else { - new LDAPConnection(new LDAPJSSEStartTLSFactory()) - } - - try { - // Connect to the server - conn.connect(host, port) - - if (tls) { - // Secure the connection - conn.startTLS() - } - - // Bind to the server - conn.bind(LDAP_VERSION, dn, password.getBytes) - - // Execute a given function and returns a its result - f(conn) - - } catch { - case e: Exception => { - // Provide more information if something goes wrong - logger.info("" + e) - - if (conn.isConnected) { - conn.disconnect() - } - // Returns an error message - Left(error) - } - } - } - - /** - * Search a specified user and returns userDN if exists. - */ - private def findUser(conn: LDAPConnection, userName: String, baseDN: String, userNameAttribute: String, additionalFilterCondition: Option[String]): Option[String] = { - @tailrec - def getEntries(results: LDAPSearchResults, entries: List[Option[LDAPEntry]] = Nil): List[LDAPEntry] = { - if(results.hasMore){ - getEntries(results, entries :+ (try { - Option(results.next) - } catch { - case ex: LDAPReferralException => None // NOTE(tanacasino): Referral follow is off. so ignores it.(for AD) - })) - } else { - entries.flatten - } - } - - val filterCond = additionalFilterCondition.getOrElse("") match { - case "" => userNameAttribute + "=" + userName - case x => "(&(" + x + ")(" + userNameAttribute + "=" + userName + "))" - } - - getEntries(conn.search(baseDN, LDAPConnection.SCOPE_SUB, filterCond, null, false)).collectFirst { - case x => x.getDN - } - } - - private def findMailAddress(conn: LDAPConnection, userDN: String, userNameAttribute: String, userName: String, mailAttribute: String): Option[String] = - defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, userNameAttribute + "=" + userName, Array[String](mailAttribute), false)){ results => - if(results.hasMore) { - Option(results.next.getAttribute(mailAttribute)).map(_.getStringValue) - } else None - } - - private def findFullName(conn: LDAPConnection, userDN: String, userNameAttribute: String, userName: String, nameAttribute: String): Option[String] = - defining(conn.search(userDN, LDAPConnection.SCOPE_BASE, userNameAttribute + "=" + userName, Array[String](nameAttribute), false)){ results => - if(results.hasMore) { - Option(results.next.getAttribute(nameAttribute)).map(_.getStringValue) - } else None - } - - case class LDAPUserInfo(userName: String, fullName: String, mailAddress: String) - -} diff --git a/src/main/scala/util/LockUtil.scala b/src/main/scala/util/LockUtil.scala deleted file mode 100644 index 267b28b..0000000 --- a/src/main/scala/util/LockUtil.scala +++ /dev/null @@ -1,36 +0,0 @@ -package util - -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.locks.{ReentrantLock, Lock} -import util.ControlUtil._ - -object LockUtil { - - /** - * lock objects - */ - private val locks = new ConcurrentHashMap[String, Lock]() - - /** - * Returns the lock object for the specified repository. - */ - private def getLockObject(key: String): Lock = synchronized { - if(!locks.containsKey(key)){ - locks.put(key, new ReentrantLock()) - } - locks.get(key) - } - - /** - * Synchronizes a given function which modifies the working copy of the wiki repository. - */ - def lock[T](key: String)(f: => T): T = defining(getLockObject(key)){ lock => - try { - lock.lock() - f - } finally { - lock.unlock() - } - } - -} diff --git a/src/main/scala/util/Notifier.scala b/src/main/scala/util/Notifier.scala deleted file mode 100644 index f9e4348..0000000 --- a/src/main/scala/util/Notifier.scala +++ /dev/null @@ -1,116 +0,0 @@ -package util - -import scala.concurrent._ -import ExecutionContext.Implicits.global -import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail} -import org.slf4j.LoggerFactory - -import app.Context -import model.Session -import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService} -import servlet.Database -import SystemSettingsService.Smtp -import _root_.util.ControlUtil.defining - -trait Notifier extends RepositoryService with AccountService with IssuesService { - def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) - (msg: String => String)(implicit context: Context): Unit - - protected def recipients(issue: model.Issue)(notify: String => Unit)(implicit session: Session, context: Context) = - ( - // individual repository's owner - issue.userName :: - // collaborators - getCollaborators(issue.userName, issue.repositoryName) ::: - // participants - issue.openedUserName :: - getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName) - ) - .distinct - .withFilter ( _ != context.loginAccount.get.userName ) // the operation in person is excluded - .foreach ( getAccountByUserName(_) filterNot (_.isGroupAccount) filterNot (LDAPUtil.isDummyMailAddress(_)) foreach (x => notify(x.mailAddress)) ) - -} - -object Notifier { - // TODO We want to be able to switch to mock. - def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match { - case settings if settings.notification => new Mailer(settings.smtp.get) - case _ => new MockMailer - } - - def msgIssue(url: String) = (content: String) => s""" - |${content}
    - |--
    - |View it on GitBucket - """.stripMargin - - def msgPullRequest(url: String) = (content: String) => s""" - |${content}
    - |View, comment on, or merge it at:
    - |${url} - """.stripMargin - - def msgComment(url: String) = (content: String) => s""" - |${content}
    - |--
    - |View it on GitBucket - """.stripMargin - - def msgStatus(url: String) = (content: String) => s""" - |${content} #${url split('/') last} - """.stripMargin -} - -class Mailer(private val smtp: Smtp) extends Notifier { - private val logger = LoggerFactory.getLogger(classOf[Mailer]) - - def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) - (msg: String => String)(implicit context: Context) = { - val database = Database() - - val f = Future { - database withSession { implicit session => - getIssue(r.owner, r.name, issueId.toString) foreach { issue => - defining( - s"[${r.name}] ${issue.title} (#${issueId})" -> - msg(view.Markdown.toHtml(content, r, false, true))) { case (subject, msg) => - recipients(issue) { to => - val email = new HtmlEmail - email.setHostName(smtp.host) - email.setSmtpPort(smtp.port.get) - smtp.user.foreach { user => - email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse(""))) - } - smtp.ssl.foreach { ssl => - email.setSSLOnConnect(ssl) - } - smtp.fromAddress - .map (_ -> smtp.fromName.orNull) - .orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName)) - .foreach { case (address, name) => - email.setFrom(address, name) - } - email.setCharset("UTF-8") - email.setSubject(subject) - email.setHtmlMsg(msg) - - email.addTo(to).send - } - } - } - } - "Notifications Successful." - } - f onSuccess { - case s => logger.debug(s) - } - f onFailure { - case t => logger.error("Notifications Failed.", t) - } - } -} -class MockMailer extends Notifier { - def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) - (msg: String => String)(implicit context: Context): Unit = {} -} diff --git a/src/main/scala/util/StringUtil.scala b/src/main/scala/util/StringUtil.scala deleted file mode 100644 index dac3de3..0000000 --- a/src/main/scala/util/StringUtil.scala +++ /dev/null @@ -1,83 +0,0 @@ -package util - -import java.net.{URLDecoder, URLEncoder} -import org.mozilla.universalchardet.UniversalDetector -import util.ControlUtil._ -import org.apache.commons.io.input.BOMInputStream -import org.apache.commons.io.IOUtils - -object StringUtil { - - def sha1(value: String): String = - defining(java.security.MessageDigest.getInstance("SHA-1")){ md => - md.update(value.getBytes) - md.digest.map(b => "%02x".format(b)).mkString - } - - def md5(value: String): String = { - val md = java.security.MessageDigest.getInstance("MD5") - md.update(value.getBytes) - md.digest.map(b => "%02x".format(b)).mkString - } - - def urlEncode(value: String): String = URLEncoder.encode(value, "UTF-8") - - def urlDecode(value: String): String = URLDecoder.decode(value, "UTF-8") - - def splitWords(value: String): Array[String] = value.split("[ \\t ]+") - - def escapeHtml(value: String): String = - value.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """) - - /** - * Make string from byte array. Character encoding is detected automatically by [[util.StringUtil.detectEncoding]]. - * And if given bytes contains UTF-8 BOM, it's removed from returned string. - */ - def convertFromByteArray(content: Array[Byte]): String = - IOUtils.toString(new BOMInputStream(new java.io.ByteArrayInputStream(content)), detectEncoding(content)) - - def detectEncoding(content: Array[Byte]): String = - defining(new UniversalDetector(null)){ detector => - detector.handleData(content, 0, content.length) - detector.dataEnd() - detector.getDetectedCharset match { - case null => "UTF-8" - case e => e - } - } - - /** - * Converts line separator in the given content. - * - * @param content the content - * @param lineSeparator "LF" or "CRLF" - * @return the converted content - */ - def convertLineSeparator(content: String, lineSeparator: String): String = { - val lf = content.replace("\r\n", "\n").replace("\r", "\n") - if(lineSeparator == "CRLF"){ - lf.replace("\n", "\r\n") - } else { - lf - } - } - - /** - * Extract issue id like ```#issueId``` from the given message. - * - *@param message the message which may contains issue id - * @return the iterator of issue id - */ - def extractIssueId(message: String): Iterator[String] = - "(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(message).matchData.map(_.group(2)) - - /** - * Extract close issue id like ```close #issueId ``` from the given message. - * - * @param message the message which may contains close command - * @return the iterator of issue id - */ - def extractCloseId(message: String): Iterator[String] = - "(?i)(? - if(in != null){ - val sql = IOUtils.toString(in, "UTF-8") - using(conn.createStatement()){ stmt => - logger.debug(sqlPath + "=" + sql) - stmt.executeUpdate(sql) - } - } - } - } - - - /** - * MAJOR.MINOR - */ - val versionString = s"${majorVersion}.${minorVersion}" - -} - -object Versions { - - private val logger = LoggerFactory.getLogger(Versions.getClass) - - def update(conn: Connection, headVersion: Version, currentVersion: Version, versions: Seq[Version], cl: ClassLoader) - (save: Connection => Unit): Unit = { - logger.debug("Start schema update") - try { - if(currentVersion == headVersion){ - logger.debug("No update") - } else if(currentVersion.versionString != "0.0" && !versions.contains(currentVersion)){ - logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.") - } else { - versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn, cl)) - save(conn) - logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}") - } - } catch { - case ex: Throwable => { - logger.error("Failed to schema update", ex) - ex.printStackTrace() - conn.rollback() - } - } - logger.debug("End schema update") - } - -} - diff --git a/src/main/scala/view/AvatarImageProvider.scala b/src/main/scala/view/AvatarImageProvider.scala deleted file mode 100644 index 8e610af..0000000 --- a/src/main/scala/view/AvatarImageProvider.scala +++ /dev/null @@ -1,51 +0,0 @@ -package view - -import service.RequestCache -import play.twirl.api.Html -import util.StringUtil - -trait AvatarImageProvider { self: RequestCache => - - /** - * Returns <img> which displays the avatar icon. - * Looks up Gravatar if avatar icon has not been configured in user settings. - */ - protected def getAvatarImageHtml(userName: String, size: Int, - mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html = { - - val src = if(mailAddress.isEmpty){ - // 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}&d=retro&r=g""" - } else { - s"""${context.path}/${account.userName}/_avatar""" - } - } getOrElse { - s"""${context.path}/_unknown/_avatar""" - } - } else { - // 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}&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}&d=retro&r=g""" - } else { - s"""${context.path}/_unknown/_avatar""" - } - } - } - - if(tooltip){ - Html(s"""""") - } else { - Html(s"""""") - } - } - -} \ No newline at end of file diff --git a/src/main/scala/view/LinkConverter.scala b/src/main/scala/view/LinkConverter.scala deleted file mode 100644 index 03f6009..0000000 --- a/src/main/scala/view/LinkConverter.scala +++ /dev/null @@ -1,34 +0,0 @@ -package view - -import service.RequestCache -import util.Implicits.RichString - -trait LinkConverter { self: RequestCache => - - /** - * Converts issue id, username and commit id to link. - */ - protected def convertRefsLinks(value: String, repository: service.RepositoryService.RepositoryInfo, - issueIdPrefix: String = "#")(implicit context: app.Context): String = { - value - // escape HTML tags - .replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """) - // convert issue id to link - .replaceBy(("(?<=(^|\\W))" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r){ m => - getIssue(repository.owner, repository.name, m.group(2)) match { - case Some(issue) if(issue.isPullRequest) - => Some(s"""#${m.group(2)}""") - case Some(_) => Some(s"""#${m.group(2)}""") - case None => Some(s"""#${m.group(2)}""") - } - } - // convert @username to link - .replaceBy("(?<=(^|\\W))@([a-zA-Z0-9\\-_]+)(?=(\\W|$))".r){ m => - getAccountByUserName(m.group(2)).map { _ => - s"""@${m.group(2)}""" - } - } - // convert commit id to link - .replaceAll("(?<=(^|\\W))([a-f0-9]{40})(?=(\\W|$))", s"""$$2""") - } -} diff --git a/src/main/scala/view/Markdown.scala b/src/main/scala/view/Markdown.scala deleted file mode 100644 index 836e5c9..0000000 --- a/src/main/scala/view/Markdown.scala +++ /dev/null @@ -1,246 +0,0 @@ -package view - -import util.StringUtil -import util.ControlUtil._ -import util.Directory._ -import org.parboiled.common.StringUtils -import org.pegdown._ -import org.pegdown.ast._ -import org.pegdown.LinkRenderer.Rendering -import java.text.Normalizer -import java.util.Locale -import java.util.regex.Pattern -import scala.collection.JavaConverters._ -import service.{RequestCache, WikiService} - -object Markdown { - - /** - * Converts Markdown of Wiki pages to HTML. - */ - def toHtml(markdown: String, - repository: service.RepositoryService.RepositoryInfo, - enableWikiLink: Boolean, - enableRefsLink: Boolean, - enableTaskList: Boolean = false, - hasWritePermission: Boolean = false, - pages: List[String] = Nil)(implicit context: app.Context): String = { - - // escape issue id - val s = if(enableRefsLink){ - markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2") - } else markdown - - // escape task list - val source = if(enableTaskList){ - GitBucketHtmlSerializer.escapeTaskList(s) - } else s - - val rootNode = new PegDownProcessor( - Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS | Extensions.SUPPRESS_ALL_HTML - ).parseMarkdown(source.toCharArray) - - new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission, pages).toHtml(rootNode) - } -} - -class GitBucketLinkRender( - context: app.Context, - repository: service.RepositoryService.RepositoryInfo, - enableWikiLink: Boolean, - pages: List[String]) extends LinkRenderer with WikiService { - - override def render(node: WikiLinkNode): Rendering = { - if(enableWikiLink){ - try { - val text = node.getText - val (label, page) = if(text.contains('|')){ - val i = text.indexOf('|') - (text.substring(0, i), text.substring(i + 1)) - } else { - (text, text) - } - - val url = repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/" + StringUtil.urlEncode(page) - - if(pages.contains(page)){ - new Rendering(url, label) - } else { - new Rendering(url, label).withAttribute("class", "absent") - } - } catch { - case e: java.io.UnsupportedEncodingException => throw new IllegalStateException - } - } else { - super.render(node) - } - } -} - -class GitBucketVerbatimSerializer extends VerbatimSerializer { - def serialize(node: VerbatimNode, printer: Printer): Unit = { - printer.println.print("") - var text: String = node.getText - while (text.charAt(0) == '\n') { - printer.print("
    ") - text = text.substring(1) - } - printer.printEncoded(text) - printer.print("") - } -} - -class GitBucketHtmlSerializer( - markdown: String, - repository: service.RepositoryService.RepositoryInfo, - enableWikiLink: Boolean, - enableRefsLink: Boolean, - enableTaskList: Boolean, - hasWritePermission: Boolean, - pages: List[String] - )(implicit val context: app.Context) extends ToHtmlSerializer( - new GitBucketLinkRender(context, repository, enableWikiLink, pages), - Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava - ) with LinkConverter with RequestCache { - - override protected def printImageTag(imageNode: SuperNode, url: String): Unit = { - printer.print("") - .print("\"").printEncoded(printChildrenToString(imageNode)).print("\"/") - } - - override protected def printLink(rendering: LinkRenderer.Rendering): Unit = { - printer.print('<').print('a') - printAttribute("href", fixUrl(rendering.href)) - for (attr <- rendering.attributes.asScala) { - printAttribute(attr.name, attr.value) - } - printer.print('>').print(rendering.text).print("") - } - - private def fixUrl(url: String, isImage: Boolean = false): String = { - if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#") || url.startsWith("/")){ - url - } else if(!enableWikiLink){ - if(context.currentPath.contains("/blob/")){ - url + (if(isImage) "?raw=true" else "") - } else if(context.currentPath.contains("/tree/")){ - val paths = context.currentPath.split("/") - val branch = if(paths.length > 3) paths.drop(4).mkString("/") else repository.repository.defaultBranch - repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "") - } else { - val paths = context.currentPath.split("/") - val branch = if(paths.length > 3) paths.last else repository.repository.defaultBranch - repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "") - } - } else { - repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url - } - } - - private def printAttribute(name: String, value: String): Unit = { - printer.print(' ').print(name).print('=').print('"').print(value).print('"') - } - - private def printHeaderTag(node: HeaderNode): Unit = { - val tag = s"h${node.getLevel}" - val headerTextString = printChildrenToString(node) - val anchorName = GitBucketHtmlSerializer.generateAnchorName(headerTextString) - printer.print(s"""<$tag class="markdown-head">""") - printer.print(s"""""") - printer.print(s"""""") - visitChildren(node) - printer.print(s"") - } - - override def visit(node: HeaderNode): Unit = { - printHeaderTag(node) - } - - override def visit(node: TextNode): Unit = { - // convert commit id and username to link. - val t = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText - - // convert task list to checkbox. - val text = if(enableTaskList) GitBucketHtmlSerializer.convertCheckBox(t, hasWritePermission) else t - - if (abbreviations.isEmpty) { - printer.print(text) - } else { - printWithAbbreviations(text) - } - } - - override def visit(node: BulletListNode): Unit = { - if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) { - printer.println().print("""
      """).indent(+2) - visitChildren(node) - printer.indent(-2).println().print("
    ") - } else { - printIndentedTag(node, "ul") - } - } - - override def visit(node: ListItemNode): Unit = { - if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) { - printer.println() - printer.print("""
  • """) - visitChildren(node) - printer.print("
  • ") - } else { - printer.println() - printTag(node, "li") - } - } - - override def visit(node: ExpLinkNode) { - printLink(linkRenderer.render(node, printLinkChildrenToString(node))) - } - - def printLinkChildrenToString(node: SuperNode) = { - val priorPrinter = printer - printer = new Printer() - visitLinkChildren(node) - val result = printer.getString() - printer = priorPrinter - result - } - - def visitLinkChildren(node: SuperNode) { - import scala.collection.JavaConversions._ - node.getChildren.foreach(child => child match { - case node: ExpImageNode => visitLinkChild(node) - case node: SuperNode => visitLinkChildren(node) - case _ => child.accept(this) - }) - } - - def visitLinkChild(node: ExpImageNode) { - printer.print("\"").printEncoded(printChildrenToString(node)).print("\"/") - } -} - -object GitBucketHtmlSerializer { - - private val Whitespace = "[\\s]".r - - def generateAnchorName(text: String): String = { - val noWhitespace = Whitespace.replaceAllIn(text, "-") - val normalized = Normalizer.normalize(noWhitespace, Normalizer.Form.NFD) - val noSpecialChars = StringUtil.urlEncode(normalized) - noSpecialChars.toLowerCase(Locale.ENGLISH) - } - - def escapeTaskList(text: String): String = { - Pattern.compile("""^( *)- \[([x| ])\] """, Pattern.MULTILINE).matcher(text).replaceAll("$1* task:$2: ") - } - - def convertCheckBox(text: String, hasWritePermission: Boolean): String = { - val disabled = if (hasWritePermission) "" else "disabled" - text.replaceAll("task:x:", """") - .replaceAll("task: :", """") - } -} diff --git a/src/main/scala/view/Pagination.scala b/src/main/scala/view/Pagination.scala deleted file mode 100644 index 8f2b62d..0000000 --- a/src/main/scala/view/Pagination.scala +++ /dev/null @@ -1,50 +0,0 @@ -package view - -/** - * Provides control information for pagination. - * This class is used by paginator.scala.html. - * - * @param page the current page number - * @param count the total record count - * @param limit the limit record count per one page - * @param width the width (number of cells) of the paginator - */ -case class Pagination(page: Int, count: Int, limit: Int, width: Int){ - - /** - * max page number - */ - val max = (count - 1) / limit + 1 - - /** - * whether to omit the left side - */ - val omitLeft = width / 2 < page - - /** - * whether to omit the right side - */ - val omitRight = max - width / 2 > page - - /** - * Returns true if given page number is visible. - */ - def visibleFor(i: Int): Boolean = { - if(i == 1 || i == max){ - true - } else { - val leftRange = page - width / 2 + (if(omitLeft) 2 else 0) - val rightRange = page + width / 2 - (if(omitRight) 2 else 0) - - val fixedRange = if(leftRange < 1){ - (1, rightRange + (leftRange * -1) + 1) - } else if(rightRange > max){ - (leftRange - (rightRange - max), max) - } else { - (leftRange, rightRange) - } - - (i >= fixedRange._1 && i <= fixedRange._2) - } - } -} diff --git a/src/main/scala/view/helpers.scala b/src/main/scala/view/helpers.scala deleted file mode 100644 index caea648..0000000 --- a/src/main/scala/view/helpers.scala +++ /dev/null @@ -1,263 +0,0 @@ -package view -import java.util.{Locale, Date, TimeZone} -import java.text.SimpleDateFormat -import play.twirl.api.Html -import util.StringUtil -import service.RequestCache - -/** - * Provides helper methods for Twirl templates. - */ -object helpers extends AvatarImageProvider with LinkConverter with RequestCache { - - /** - * Format java.util.Date to "yyyy-MM-dd HH:mm:ss". - */ - def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date) - - val timeUnits = List( - (1000L, "second"), - (1000L * 60, "minute"), - (1000L * 60 * 60, "hour"), - (1000L * 60 * 60 * 24, "day"), - (1000L * 60 * 60 * 24 * 30, "month"), - (1000L * 60 * 60 * 24 * 365, "year") - ).reverse - - /** - * Format java.util.Date to "x {seconds/minutes/hours/days/months/years} ago" - */ - def datetimeAgo(date: Date): String = { - val duration = new Date().getTime - date.getTime - timeUnits.find(tuple => duration / tuple._1 > 0) match { - case Some((unitValue, unitString)) => - val value = duration / unitValue - s"${value} ${unitString}${if (value > 1) "s" else ""} ago" - case None => "just now" - } - } - - /** - * Format java.util.Date to "x {seconds/minutes/hours/days} ago" - * If duration over 1 month, format to "d MMM (yyyy)" - */ - def datetimeAgoRecentOnly(date: Date): String = { - val duration = new Date().getTime - date.getTime - timeUnits.find(tuple => duration / tuple._1 > 0) match { - case Some((_, "month")) => s"on ${new SimpleDateFormat("d MMM", Locale.ENGLISH).format(date)}" - case Some((_, "year")) => s"on ${new SimpleDateFormat("d MMM yyyy", Locale.ENGLISH).format(date)}" - case Some((unitValue, unitString)) => - val value = duration / unitValue - s"${value} ${unitString}${if (value > 1) "s" else ""} ago" - case None => "just now" - } - } - - - /** - * Format java.util.Date to "yyyy-MM-dd'T'hh:mm:ss'Z'". - */ - def datetimeRFC3339(date: Date): String = { - val sf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") - sf.setTimeZone(TimeZone.getTimeZone("UTC")) - sf.format(date) - } - - /** - * Format java.util.Date to "yyyy-MM-dd". - */ - def date(date: Date): String = new SimpleDateFormat("yyyy-MM-dd").format(date) - - /** - * Returns singular if count is 1, otherwise plural. - * If plural is not specified, returns singular + "s" as plural. - */ - def plural(count: Int, singular: String, plural: String = ""): String = - if(count == 1) singular else if(plural.isEmpty) singular + "s" else plural - - 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)) - ) - - def renderableSuffixes: Seq[String] = renderersBySuffix.map(_._1) - - /** - * Converts Markdown of Wiki pages to HTML. - */ - def markdown(value: String, - repository: service.RepositoryService.RepositoryInfo, - enableWikiLink: Boolean, - enableRefsLink: Boolean, - enableTaskList: Boolean = false, - hasWritePermission: Boolean = false, - pages: List[String] = Nil)(implicit context: app.Context): Html = - Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission, pages)) - - def renderMarkup(filePath: List[String], fileContent: String, branch: String, - repository: service.RepositoryService.RepositoryInfo, - enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = { - - val fileNameLower = filePath.reverse.head.toLowerCase - renderersBySuffix.find { case (suffix, _) => fileNameLower.endsWith(suffix) } match { - case Some((_, handler)) => handler(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, context) - case None => Html( - s"${ - fileContent.split("(\\r\\n)|\\n").map(xml.Utility.escape(_)).mkString("
    ") - }
    " - ) - } - } - - /** - * 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. - */ - def avatar(userName: String, size: Int, tooltip: Boolean = false)(implicit context: app.Context): Html = - getAvatarImageHtml(userName, size, "", tooltip) - - /** - * Returns <img> which displays the avatar icon for the given mail address. - * This method looks up Gravatar if avatar icon has not been configured in user settings. - */ - def avatar(commit: util.JGitUtil.CommitInfo, size: Int)(implicit context: app.Context): Html = - getAvatarImageHtml(commit.authorName, size, commit.authorEmailAddress) - - /** - * Converts commit id, issue id and username to the link. - */ - def link(value: String, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): Html = - Html(convertRefsLinks(value, repository)) - - def cut(value: String, length: Int): String = - if(value.length > length){ - value.substring(0, length) + "..." - } else { - value - } - - import scala.util.matching.Regex._ - implicit class RegexReplaceString(s: String) { - def replaceAll(pattern: String, replacer: (Match) => String): String = { - pattern.r.replaceAllIn(s, replacer) - } - } - - /** - * Convert link notations in the activity message. - */ - def activityMessage(message: String)(implicit context: app.Context): Html = - Html(message - .replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""$$1/$$2#$$3""") - .replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]" , s"""$$1/$$2#$$3""") - .replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]" , s"""$$1/$$2""") - .replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""${m.group(3)}""") - .replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]" , (m: Match) => s"""${m.group(3)}""") - .replaceAll("\\[user:([^\\s]+?)\\]" , (m: Match) => user(m.group(1)).body) - .replaceAll("\\[commit:([^\\s]+?)/([^\\s]+?)\\@([^\\s]+?)\\]", (m: Match) => s"""${m.group(1)}/${m.group(2)}@${m.group(3).substring(0, 7)}""") - ) - - /** - * URL encode except '/'. - */ - def encodeRefName(value: String): String = StringUtil.urlEncode(value).replace("%2F", "/") - - def urlEncode(value: String): String = StringUtil.urlEncode(value) - - def urlEncode(value: Option[String]): String = value.map(urlEncode).getOrElse("") - - /** - * Generates the url to the repository. - */ - def url(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context): String = - s"${context.path}/${repository.owner}/${repository.name}" - - /** - * Generates the url to the account page. - */ - def url(userName: String)(implicit context: app.Context): String = s"${context.path}/${userName}" - - /** - * Returns the url to the root of assets. - */ - def assets(implicit context: app.Context): String = s"${context.path}/assets" - - /** - * Generates the text link to the account page. - * If user does not exist or disabled, this method returns user name as text without link. - */ - def user(userName: String, mailAddress: String = "", styleClass: String = "")(implicit context: app.Context): Html = - userWithContent(userName, mailAddress, styleClass)(Html(userName)) - - /** - * Generates the avatar link to the account page. - * If user does not exist or disabled, this method returns avatar image without link. - */ - def avatarLink(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false)(implicit context: app.Context): Html = - userWithContent(userName, mailAddress)(avatar(userName, size, tooltip)) - - private def userWithContent(userName: String, mailAddress: String = "", styleClass: String = "")(content: Html)(implicit context: app.Context): Html = - (if(mailAddress.isEmpty){ - getAccountByUserName(userName) - } else { - getAccountByMailAddress(mailAddress) - }).map { account => - Html(s"""${content}""") - } getOrElse content - - - /** - * Test whether the given Date is past date. - */ - 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(".yml")) => "yaml" - case _ => "plain_text" - } - } - - def pre(value: Html): Html = Html(s"
    ${value.body.trim.split("\n").map(_.trim).mkString("\n")}
    ") - - /** - * Implicit conversion to add mkHtml() to Seq[Html]. - */ - implicit class RichHtmlSeq(seq: Seq[Html]) { - def mkHtml(separator: String) = Html(seq.mkString(separator)) - def mkHtml(separator: scala.xml.Elem) = Html(seq.mkString(separator.toString)) - } - -} diff --git a/src/main/twirl/account/activity.scala.html b/src/main/twirl/account/activity.scala.html deleted file mode 100644 index ca4c3c7..0000000 --- a/src/main/twirl/account/activity.scala.html +++ /dev/null @@ -1,9 +0,0 @@ -@(account: model.Account, groupNames: List[String], activities: List[model.Activity])(implicit context: app.Context) -@import context._ -@import view.helpers._ -@main(account, groupNames, "activity"){ -
    - activities -
    - @helper.html.activities(activities) -} diff --git a/src/main/twirl/account/edit.scala.html b/src/main/twirl/account/edit.scala.html deleted file mode 100644 index 32055ab..0000000 --- a/src/main/twirl/account/edit.scala.html +++ /dev/null @@ -1,71 +0,0 @@ -@(account: model.Account, info: Option[Any])(implicit context: app.Context) -@import context._ -@import view.helpers._ -@import util.LDAPUtil -@html.main("Edit your profile"){ -
    -
    -
    - @menu("profile", settings.ssh) -
    -
    - @helper.html.information(info) - @if(LDAPUtil.isDummyMailAddress(account)){
    Please register your mail address.
    } -
    -
    -
    Profile
    -
    -
    -
    - @if(account.password.nonEmpty){ -
    - - - -
    - } -
    - - - -
    -
    - - - -
    -
    - - - -
    -
    -
    -
    - - @helper.html.uploadavatar(Some(account)) -
    -
    -
    -
    - - - @if(!LDAPUtil.isDummyMailAddress(account)){Cancel} -
    -
    -
    -
    -
    -
    -} - diff --git a/src/main/twirl/account/group.scala.html b/src/main/twirl/account/group.scala.html deleted file mode 100644 index d6d7cf6..0000000 --- a/src/main/twirl/account/group.scala.html +++ /dev/null @@ -1,140 +0,0 @@ -@(account: Option[model.Account], members: List[model.GroupMember])(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(if(account.isEmpty) "Create group" else "Edit group"){ -
    -
    -
    -
    -
    - -
    - -
    - -
    -
    - -
    - -
    - -
    -
    - - @helper.html.uploadavatar(account) -
    -
    -
    -
    - -
      -
    - @helper.html.account("memberName", 200) - - -
    - -
    -
    -
    -
    -
    - @if(account.isDefined){ - - } - - @if(account.isDefined){ - Cancel - } -
    -
    -
    -} - \ No newline at end of file diff --git a/src/main/twirl/account/main.scala.html b/src/main/twirl/account/main.scala.html deleted file mode 100644 index 2e148fe..0000000 --- a/src/main/twirl/account/main.scala.html +++ /dev/null @@ -1,59 +0,0 @@ -@(account: model.Account, groupNames: List[String], active: String, - isGroupManager: Boolean = false)(body: Html)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(account.userName){ -
    -
    -
    -
    -
    - - - -
    -
    - @if(account.url.isDefined){ - - } -
    Joined on @date(account.registeredDate)
    -
    - @if(groupNames.nonEmpty){ -
    -
    Groups
    - @groupNames.map { groupName => - @avatar(groupName, 36, tooltip = true) - } -
    - } - -
    -
    - - @body -
    -
    -
    -
    -} diff --git a/src/main/twirl/account/members.scala.html b/src/main/twirl/account/members.scala.html deleted file mode 100644 index 0e21d01..0000000 --- a/src/main/twirl/account/members.scala.html +++ /dev/null @@ -1,16 +0,0 @@ -@(account: model.Account, members: List[String], isGroupManager: Boolean)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@main(account, Nil, "members", isGroupManager){ - @if(members.isEmpty){ - No members - } else { - @members.map { userName => -
    -
    - @avatar(userName, 20) @userName -
    -
    - } - } -} \ No newline at end of file diff --git a/src/main/twirl/account/menu.scala.html b/src/main/twirl/account/menu.scala.html deleted file mode 100644 index a5d9139..0000000 --- a/src/main/twirl/account/menu.scala.html +++ /dev/null @@ -1,14 +0,0 @@ -@(active: String, ssh: Boolean)(implicit context: app.Context) -@import context._ -
    - -
    diff --git a/src/main/twirl/account/newrepo.scala.html b/src/main/twirl/account/newrepo.scala.html deleted file mode 100644 index da9b068..0000000 --- a/src/main/twirl/account/newrepo.scala.html +++ /dev/null @@ -1,74 +0,0 @@ -@(groupNames: List[String], -isCreateRepoOptionPublic: Boolean)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main("Create a New Repository"){ -
    -
    -
    - -
    - - - -
    - / - - -
    -
    - - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -} - diff --git a/src/main/twirl/account/register.scala.html b/src/main/twirl/account/register.scala.html deleted file mode 100644 index 3883f50..0000000 --- a/src/main/twirl/account/register.scala.html +++ /dev/null @@ -1,50 +0,0 @@ -@()(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main("Create your account"){ -
    -

    Create your account

    -
    -
    -
    -
    - - - -
    -
    - - - -
    -
    - - - -
    -
    - - - -
    -
    - - - -
    -
    -
    -
    - - @helper.html.uploadavatar(None) -
    -
    -
    -
    - -
    -
    -
    -} diff --git a/src/main/twirl/account/repositories.scala.html b/src/main/twirl/account/repositories.scala.html deleted file mode 100644 index 8a3706b..0000000 --- a/src/main/twirl/account/repositories.scala.html +++ /dev/null @@ -1,33 +0,0 @@ -@(account: model.Account, groupNames: List[String], - repositories: List[service.RepositoryService.RepositoryInfo], - isGroupManager: Boolean)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@main(account, groupNames, "repositories", isGroupManager){ - @if(repositories.isEmpty){ - No repositories - } else { - @repositories.map { repository => -
    -
    - @helper.html.repositoryicon(repository, true) -
    -
    -
    - @repository.name - @if(repository.repository.isPrivate){ - - } -
    - @if(repository.repository.originUserName.isDefined){ - - } - @if(repository.repository.description.isDefined){ -
    @repository.repository.description
    - } -
    Updated @helper.html.datetimeago(repository.repository.lastActivityDate)
    -
    -
    - } - } -} diff --git a/src/main/twirl/account/ssh.scala.html b/src/main/twirl/account/ssh.scala.html deleted file mode 100644 index 100044e..0000000 --- a/src/main/twirl/account/ssh.scala.html +++ /dev/null @@ -1,47 +0,0 @@ -@(account: model.Account, sshKeys: List[model.SshKey])(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main("SSH Keys"){ -
    -
    -
    - @menu("ssh", settings.ssh) -
    -
    -
    -
    SSH Keys
    -
    - @if(sshKeys.isEmpty){ - No keys - } - @sshKeys.zipWithIndex.map { case (key, i) => - @if(i != 0){ -
    - } - @key.title (@_root_.ssh.SshUtil.fingerPrint(key.publicKey).getOrElse("Key is invalid.")) - Delete - } -
    -
    -
    -
    -
    Add an SSH Key
    -
    -
    - -
    - -
    -
    - -
    - -
    - -
    -
    -
    -
    -
    -
    -} diff --git a/src/main/twirl/admin/menu.scala.html b/src/main/twirl/admin/menu.scala.html deleted file mode 100644 index 09bc2de..0000000 --- a/src/main/twirl/admin/menu.scala.html +++ /dev/null @@ -1,24 +0,0 @@ -@(active: String)(body: Html)(implicit context: app.Context) -@import context._ -
    -
    -
    -
    - -
    -
    -
    - @body -
    -
    -
    \ No newline at end of file diff --git a/src/main/twirl/admin/system.scala.html b/src/main/twirl/admin/system.scala.html deleted file mode 100644 index 194a164..0000000 --- a/src/main/twirl/admin/system.scala.html +++ /dev/null @@ -1,294 +0,0 @@ -@(info: Option[Any])(implicit context: app.Context) -@import context._ -@import util.Directory._ -@import view.helpers._ -@html.main("System Settings"){ - @menu("system"){ - @helper.html.information(info) -
    -
    -
    System Settings
    -
    - - - - - @GitBucketHome - - - -
    - -
    -
    - - -
    -
    -

    - The base URL is used for redirect, notification email, git repository URL box and more. - If the base URL is empty, GitBucket generates URL from request information. - You can use this property to adjust URL difference between the reverse proxy and GitBucket. -

    - - - -
    - -
    - -
    - - - -
    - -
    - - -
    -
    - -
    - - -
    - - - -
    - -
    - - -
    - - - -
    - -
    - -
    - - - -
    - -
    - -
    -
    -
    - -
    - - -
    -
    -
    -

    - Base URL is required if SSH access is enabled. -

    - - - -
    - -
    - -
    -
    -
    - -
    - - -
    -
    -
    - -
    - - -
    -
    -
    - -
    - - -
    -
    -
    - -
    - - -
    -
    -
    - -
    - - -
    -
    -
    - -
    - - -
    -
    -
    - -
    - - -
    -
    -
    - -
    - - -
    -
    -
    - -
    - - -
    -
    -
    -
    - -
    -
    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -
    - - - -
    - -
    - -
    -
    -
    - -
    - - -
    -
    -
    - -
    - - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    -
    -
    -
    - -
    -
    - } -} - \ No newline at end of file diff --git a/src/main/twirl/admin/users/group.scala.html b/src/main/twirl/admin/users/group.scala.html deleted file mode 100644 index 8b7672d..0000000 --- a/src/main/twirl/admin/users/group.scala.html +++ /dev/null @@ -1,135 +0,0 @@ -@(account: Option[model.Account], members: List[model.GroupMember])(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(if(account.isEmpty) "New Group" else "Update Group"){ - @admin.html.menu("users"){ -
    -
    -
    -
    - -
    - -
    - - @if(account.isDefined){ - - } -
    -
    - -
    - -
    - -
    -
    - - @helper.html.uploadavatar(account) -
    -
    -
    -
    - -
      -
    - @helper.html.account("memberName", 200) - - -
    - -
    -
    -
    -
    -
    - - Cancel -
    -
    - } -} - \ No newline at end of file diff --git a/src/main/twirl/admin/users/list.scala.html b/src/main/twirl/admin/users/list.scala.html deleted file mode 100644 index 3b2029c..0000000 --- a/src/main/twirl/admin/users/list.scala.html +++ /dev/null @@ -1,71 +0,0 @@ -@(users: List[model.Account], members: Map[String, List[String]], includeRemoved: Boolean)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main("Manage Users"){ - @admin.html.menu("users"){ - - - - @users.map { account => - - - - } -
    -
    - @if(account.isGroupAccount){ - Edit - } else { - Edit - } -
    -
    - @avatar(account.userName, 20) - @account.userName - @if(account.isGroupAccount){ - (Group) - } else { - @if(account.isAdmin){ - (Administrator) - } else { - (Normal) - } - } - @if(account.isGroupAccount){ - @members(account.userName).map { userName => - @avatar(userName, 20, tooltip = true) - } - } -
    -
    -
    - @if(!account.isGroupAccount){ - @account.mailAddress - } - @account.url.map { url => - @url - } -
    -
    - Registered: @datetime(account.registeredDate) - Updated: @datetime(account.updatedDate) - @if(!account.isGroupAccount){ - Last Login: @account.lastLoginDate.map(datetime) - } -
    -
    - } -} - \ No newline at end of file diff --git a/src/main/twirl/admin/users/user.scala.html b/src/main/twirl/admin/users/user.scala.html deleted file mode 100644 index 1f25857..0000000 --- a/src/main/twirl/admin/users/user.scala.html +++ /dev/null @@ -1,83 +0,0 @@ -@(account: Option[model.Account])(implicit context: app.Context) -@import context._ -@html.main(if(account.isEmpty) "New User" else "Update User"){ - @admin.html.menu("users"){ -
    -
    -
    -
    - -
    - -
    - - @if(account.isDefined){ - -
    - -
    - } -
    - @if(account.map(_.password.nonEmpty).getOrElse(true)){ -
    - -
    - -
    - -
    - } -
    - -
    - -
    - -
    -
    - -
    - -
    - -
    -
    - - - -
    -
    - -
    - -
    - -
    -
    -
    -
    - - @helper.html.uploadavatar(account) -
    -
    -
    -
    - - Cancel -
    -
    - } -} diff --git a/src/main/twirl/dashboard/header.scala.html b/src/main/twirl/dashboard/header.scala.html deleted file mode 100644 index 01c5bfc..0000000 --- a/src/main/twirl/dashboard/header.scala.html +++ /dev/null @@ -1,74 +0,0 @@ -@(openCount: Int, - closedCount: Int, - condition: service.IssuesService.IssueSearchCondition, - groups: List[String])(implicit context: app.Context) -@import context._ -@import view.helpers._ - - - - @openCount Open -    - - - @closedCount Closed - - - \ No newline at end of file diff --git a/src/main/twirl/dashboard/issues.scala.html b/src/main/twirl/dashboard/issues.scala.html deleted file mode 100644 index 898ac6b..0000000 --- a/src/main/twirl/dashboard/issues.scala.html +++ /dev/null @@ -1,16 +0,0 @@ -@(issues: List[service.IssuesService.IssueInfo], - page: Int, - openCount: Int, - closedCount: Int, - condition: service.IssuesService.IssueSearchCondition, - filter: String, - groups: List[String])(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main("Issues"){ - @dashboard.html.tab("issues") -
    - @issuesnavi(filter, "issues", condition) - @issueslist(issues, page, openCount, closedCount, condition, filter, groups) -
    -} diff --git a/src/main/twirl/dashboard/issueslist.scala.html b/src/main/twirl/dashboard/issueslist.scala.html deleted file mode 100644 index f2afaa0..0000000 --- a/src/main/twirl/dashboard/issueslist.scala.html +++ /dev/null @@ -1,60 +0,0 @@ -@(issues: List[service.IssuesService.IssueInfo], - page: Int, - openCount: Int, - closedCount: Int, - condition: service.IssuesService.IssueSearchCondition, - filter: String, - groups: List[String])(implicit context: app.Context) -@import context._ -@import view.helpers._ -@import service.IssuesService.IssueInfo - - - - - @issues.map { case IssueInfo(issue, labels, milestone, commentCount) => - - - - } -
    - @dashboard.html.header(openCount, closedCount, condition, groups) -
    - @if(issue.isPullRequest){ - - } else { - - } - @issue.userName/@issue.repositoryName ・ - @if(issue.isPullRequest){ - @issue.title - } else { - @issue.title - } - @labels.map { label => - @label.labelName - } - - @issue.assignedUserName.map { userName => - @avatar(userName, 20, tooltip = true) - } - @if(commentCount > 0){ - - @commentCount - - } else { - - @commentCount - - } - -
    - #@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate) - @milestone.map { milestone => - @milestone - } -
    -
    -
    - @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL) -
    diff --git a/src/main/twirl/dashboard/issuesnavi.scala.html b/src/main/twirl/dashboard/issuesnavi.scala.html deleted file mode 100644 index ec15e74..0000000 --- a/src/main/twirl/dashboard/issuesnavi.scala.html +++ /dev/null @@ -1,22 +0,0 @@ -@(filter: String, - active: String, - condition: service.IssuesService.IssueSearchCondition)(implicit context: app.Context) -@import context._ -@import view.helpers._ - diff --git a/src/main/twirl/dashboard/pulls.scala.html b/src/main/twirl/dashboard/pulls.scala.html deleted file mode 100644 index d908e94..0000000 --- a/src/main/twirl/dashboard/pulls.scala.html +++ /dev/null @@ -1,16 +0,0 @@ -@(issues: List[service.IssuesService.IssueInfo], - page: Int, - openCount: Int, - closedCount: Int, - condition: service.IssuesService.IssueSearchCondition, - filter: String, - groups: List[String])(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main("Pull Requests"){ - @dashboard.html.tab("pulls") -
    - @issuesnavi(filter, "pulls", condition) - @issueslist(issues, page, openCount, closedCount, condition, filter, groups) -
    -} diff --git a/src/main/twirl/dashboard/tab.scala.html b/src/main/twirl/dashboard/tab.scala.html deleted file mode 100644 index 3cb3bf0..0000000 --- a/src/main/twirl/dashboard/tab.scala.html +++ /dev/null @@ -1,47 +0,0 @@ -@(active: String = "")(implicit context: app.Context) -@import context._ -@import view.helpers._ -
    -
    - - - News Feed - - @if(loginAccount.isDefined){ - - - Pull Requests - - - - Issues - - } -
    -
    - \ No newline at end of file diff --git a/src/main/twirl/error.scala.html b/src/main/twirl/error.scala.html deleted file mode 100644 index fc4a011..0000000 --- a/src/main/twirl/error.scala.html +++ /dev/null @@ -1,4 +0,0 @@ -@(title: String)(implicit context: app.Context) -@main("Error"){ -

    @title

    -} \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/account/activity.scala.html b/src/main/twirl/gitbucket/core/account/activity.scala.html new file mode 100644 index 0000000..24d2c28 --- /dev/null +++ b/src/main/twirl/gitbucket/core/account/activity.scala.html @@ -0,0 +1,11 @@ +@(account: gitbucket.core.model.Account, + groupNames: List[String], + activities: List[gitbucket.core.model.Activity])(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@main(account, groupNames, "activity"){ +
    + activities +
    + @helper.html.activities(activities) +} diff --git a/src/main/twirl/gitbucket/core/account/edit.scala.html b/src/main/twirl/gitbucket/core/account/edit.scala.html new file mode 100644 index 0000000..b2691a1 --- /dev/null +++ b/src/main/twirl/gitbucket/core/account/edit.scala.html @@ -0,0 +1,71 @@ +@(account: gitbucket.core.model.Account, info: Option[Any])(implicit context: gitbucket.core.controller.Context) +@import gitbucket.core.util.LDAPUtil +@import context._ +@import gitbucket.core.view.helpers._ +@html.main("Edit your profile"){ +
    +
    +
    + @menu("profile", settings.ssh) +
    +
    + @helper.html.information(info) + @if(LDAPUtil.isDummyMailAddress(account)){
    Please register your mail address.
    } +
    +
    +
    Profile
    +
    +
    +
    + @if(account.password.nonEmpty){ +
    + + + +
    + } +
    + + + +
    +
    + + + +
    +
    + + + +
    +
    +
    +
    + + @helper.html.uploadavatar(Some(account)) +
    +
    +
    +
    + + + @if(!LDAPUtil.isDummyMailAddress(account)){Cancel} +
    +
    +
    +
    +
    +
    +} + diff --git a/src/main/twirl/gitbucket/core/account/group.scala.html b/src/main/twirl/gitbucket/core/account/group.scala.html new file mode 100644 index 0000000..921beb2 --- /dev/null +++ b/src/main/twirl/gitbucket/core/account/group.scala.html @@ -0,0 +1,140 @@ +@(account: Option[gitbucket.core.model.Account], members: List[gitbucket.core.model.GroupMember])(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(if(account.isEmpty) "Create group" else "Edit group"){ +
    +
    +
    +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + + @helper.html.uploadavatar(account) +
    +
    +
    +
    + +
      +
    + @helper.html.account("memberName", 200) + + +
    + +
    +
    +
    +
    +
    + @if(account.isDefined){ + + } + + @if(account.isDefined){ + Cancel + } +
    +
    +
    +} + \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/account/main.scala.html b/src/main/twirl/gitbucket/core/account/main.scala.html new file mode 100644 index 0000000..b4467f9 --- /dev/null +++ b/src/main/twirl/gitbucket/core/account/main.scala.html @@ -0,0 +1,59 @@ +@(account: gitbucket.core.model.Account, groupNames: List[String], active: String, + isGroupManager: Boolean = false)(body: Html)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(account.userName){ +
    +
    +
    +
    +
    + + + +
    +
    + @if(account.url.isDefined){ + + } +
    Joined on @date(account.registeredDate)
    +
    + @if(groupNames.nonEmpty){ +
    +
    Groups
    + @groupNames.map { groupName => + @avatar(groupName, 36, tooltip = true) + } +
    + } + +
    +
    + + @body +
    +
    +
    +
    +} diff --git a/src/main/twirl/gitbucket/core/account/members.scala.html b/src/main/twirl/gitbucket/core/account/members.scala.html new file mode 100644 index 0000000..0291223 --- /dev/null +++ b/src/main/twirl/gitbucket/core/account/members.scala.html @@ -0,0 +1,16 @@ +@(account: gitbucket.core.model.Account, members: List[String], isGroupManager: Boolean)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@main(account, Nil, "members", isGroupManager){ + @if(members.isEmpty){ + No members + } else { + @members.map { userName => +
    +
    + @avatar(userName, 20) @userName +
    +
    + } + } +} \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/account/menu.scala.html b/src/main/twirl/gitbucket/core/account/menu.scala.html new file mode 100644 index 0000000..60f0229 --- /dev/null +++ b/src/main/twirl/gitbucket/core/account/menu.scala.html @@ -0,0 +1,14 @@ +@(active: String, ssh: Boolean)(implicit context: gitbucket.core.controller.Context) +@import context._ +
    + +
    diff --git a/src/main/twirl/gitbucket/core/account/newrepo.scala.html b/src/main/twirl/gitbucket/core/account/newrepo.scala.html new file mode 100644 index 0000000..607bd0b --- /dev/null +++ b/src/main/twirl/gitbucket/core/account/newrepo.scala.html @@ -0,0 +1,74 @@ +@(groupNames: List[String], +isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main("Create a New Repository"){ +
    +
    +
    + +
    + + + +
    + / + + +
    +
    + + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +} + diff --git a/src/main/twirl/gitbucket/core/account/register.scala.html b/src/main/twirl/gitbucket/core/account/register.scala.html new file mode 100644 index 0000000..5810d95 --- /dev/null +++ b/src/main/twirl/gitbucket/core/account/register.scala.html @@ -0,0 +1,50 @@ +@()(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main("Create your account"){ +
    +

    Create your account

    +
    +
    +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    +
    + + + +
    +
    +
    +
    + + @helper.html.uploadavatar(None) +
    +
    +
    +
    + +
    +
    +
    +} diff --git a/src/main/twirl/gitbucket/core/account/repositories.scala.html b/src/main/twirl/gitbucket/core/account/repositories.scala.html new file mode 100644 index 0000000..b515163 --- /dev/null +++ b/src/main/twirl/gitbucket/core/account/repositories.scala.html @@ -0,0 +1,33 @@ +@(account: gitbucket.core.model.Account, groupNames: List[String], + repositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo], + isGroupManager: Boolean)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@main(account, groupNames, "repositories", isGroupManager){ + @if(repositories.isEmpty){ + No repositories + } else { + @repositories.map { repository => +
    +
    + @helper.html.repositoryicon(repository, true) +
    +
    +
    + @repository.name + @if(repository.repository.isPrivate){ + + } +
    + @if(repository.repository.originUserName.isDefined){ + + } + @if(repository.repository.description.isDefined){ +
    @repository.repository.description
    + } +
    Updated @helper.html.datetimeago(repository.repository.lastActivityDate)
    +
    +
    + } + } +} diff --git a/src/main/twirl/gitbucket/core/account/ssh.scala.html b/src/main/twirl/gitbucket/core/account/ssh.scala.html new file mode 100644 index 0000000..7229082 --- /dev/null +++ b/src/main/twirl/gitbucket/core/account/ssh.scala.html @@ -0,0 +1,48 @@ +@(account: gitbucket.core.model.Account, sshKeys: List[gitbucket.core.model.SshKey])(implicit context: gitbucket.core.controller.Context) +@import gitbucket.core.ssh.SshUtil +@import context._ +@import gitbucket.core.view.helpers._ +@html.main("SSH Keys"){ +
    +
    +
    + @menu("ssh", settings.ssh) +
    +
    +
    +
    SSH Keys
    +
    + @if(sshKeys.isEmpty){ + No keys + } + @sshKeys.zipWithIndex.map { case (key, i) => + @if(i != 0){ +
    + } + @key.title (@SshUtil.fingerPrint(key.publicKey).getOrElse("Key is invalid.")) + Delete + } +
    +
    +
    +
    +
    Add an SSH Key
    +
    +
    + +
    + +
    +
    + +
    + +
    + +
    +
    +
    +
    +
    +
    +} diff --git a/src/main/twirl/gitbucket/core/admin/menu.scala.html b/src/main/twirl/gitbucket/core/admin/menu.scala.html new file mode 100644 index 0000000..e5f6f3d --- /dev/null +++ b/src/main/twirl/gitbucket/core/admin/menu.scala.html @@ -0,0 +1,24 @@ +@(active: String)(body: Html)(implicit context: gitbucket.core.controller.Context) +@import context._ +
    +
    +
    +
    + +
    +
    +
    + @body +
    +
    +
    \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/admin/system.scala.html b/src/main/twirl/gitbucket/core/admin/system.scala.html new file mode 100644 index 0000000..47e0221 --- /dev/null +++ b/src/main/twirl/gitbucket/core/admin/system.scala.html @@ -0,0 +1,294 @@ +@(info: Option[Any])(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@import gitbucket.core.util.Directory._ +@html.main("System Settings"){ + @menu("system"){ + @helper.html.information(info) +
    +
    +
    System Settings
    +
    + + + + + @GitBucketHome + + + +
    + +
    +
    + + +
    +
    +

    + The base URL is used for redirect, notification email, git repository URL box and more. + If the base URL is empty, GitBucket generates URL from request information. + You can use this property to adjust URL difference between the reverse proxy and GitBucket. +

    + + + +
    + +
    + +
    + + + +
    + +
    + + +
    +
    + +
    + + +
    + + + +
    + +
    + + +
    + + + +
    + +
    + +
    + + + +
    + +
    + +
    +
    +
    + +
    + + +
    +
    +
    +

    + Base URL is required if SSH access is enabled. +

    + + + +
    + +
    + +
    +
    +
    + +
    + + +
    +
    +
    + +
    + + +
    +
    +
    + +
    + + +
    +
    +
    + +
    + + +
    +
    +
    + +
    + + +
    +
    +
    + +
    + + +
    +
    +
    + +
    + + +
    +
    +
    + +
    + + +
    +
    +
    + +
    + + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    + + +
    +
    +
    + + + +
    + +
    + +
    +
    +
    + +
    + + +
    +
    +
    + +
    + + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    + +
    +
    + } +} + \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/admin/users/group.scala.html b/src/main/twirl/gitbucket/core/admin/users/group.scala.html new file mode 100644 index 0000000..bac702b --- /dev/null +++ b/src/main/twirl/gitbucket/core/admin/users/group.scala.html @@ -0,0 +1,135 @@ +@(account: Option[gitbucket.core.model.Account], members: List[gitbucket.core.model.GroupMember])(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(if(account.isEmpty) "New Group" else "Update Group"){ + @admin.html.menu("users"){ +
    +
    +
    +
    + +
    + +
    + + @if(account.isDefined){ + + } +
    +
    + +
    + +
    + +
    +
    + + @helper.html.uploadavatar(account) +
    +
    +
    +
    + +
      +
    + @helper.html.account("memberName", 200) + + +
    + +
    +
    +
    +
    +
    + + Cancel +
    +
    + } +} + \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/admin/users/list.scala.html b/src/main/twirl/gitbucket/core/admin/users/list.scala.html new file mode 100644 index 0000000..1c8f17b --- /dev/null +++ b/src/main/twirl/gitbucket/core/admin/users/list.scala.html @@ -0,0 +1,71 @@ +@(users: List[gitbucket.core.model.Account], members: Map[String, List[String]], includeRemoved: Boolean)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main("Manage Users"){ + @admin.html.menu("users"){ + + + + @users.map { account => + + + + } +
    +
    + @if(account.isGroupAccount){ + Edit + } else { + Edit + } +
    +
    + @avatar(account.userName, 20) + @account.userName + @if(account.isGroupAccount){ + (Group) + } else { + @if(account.isAdmin){ + (Administrator) + } else { + (Normal) + } + } + @if(account.isGroupAccount){ + @members(account.userName).map { userName => + @avatar(userName, 20, tooltip = true) + } + } +
    +
    +
    + @if(!account.isGroupAccount){ + @account.mailAddress + } + @account.url.map { url => + @url + } +
    +
    + Registered: @datetime(account.registeredDate) + Updated: @datetime(account.updatedDate) + @if(!account.isGroupAccount){ + Last Login: @account.lastLoginDate.map(datetime) + } +
    +
    + } +} + \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/admin/users/user.scala.html b/src/main/twirl/gitbucket/core/admin/users/user.scala.html new file mode 100644 index 0000000..fb54eb5 --- /dev/null +++ b/src/main/twirl/gitbucket/core/admin/users/user.scala.html @@ -0,0 +1,83 @@ +@(account: Option[gitbucket.core.model.Account])(implicit context: gitbucket.core.controller.Context) +@import context._ +@html.main(if(account.isEmpty) "New User" else "Update User"){ + @admin.html.menu("users"){ +
    +
    +
    +
    + +
    + +
    + + @if(account.isDefined){ + +
    + +
    + } +
    + @if(account.map(_.password.nonEmpty).getOrElse(true)){ +
    + +
    + +
    + +
    + } +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + + + +
    +
    + +
    + +
    + +
    +
    +
    +
    + + @helper.html.uploadavatar(account) +
    +
    +
    +
    + + Cancel +
    +
    + } +} diff --git a/src/main/twirl/gitbucket/core/dashboard/header.scala.html b/src/main/twirl/gitbucket/core/dashboard/header.scala.html new file mode 100644 index 0000000..ea14458 --- /dev/null +++ b/src/main/twirl/gitbucket/core/dashboard/header.scala.html @@ -0,0 +1,74 @@ +@(openCount: Int, + closedCount: Int, + condition: gitbucket.core.service.IssuesService.IssueSearchCondition, + groups: List[String])(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ + + + + @openCount Open +    + + + @closedCount Closed + + + \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/dashboard/issues.scala.html b/src/main/twirl/gitbucket/core/dashboard/issues.scala.html new file mode 100644 index 0000000..af395e5 --- /dev/null +++ b/src/main/twirl/gitbucket/core/dashboard/issues.scala.html @@ -0,0 +1,16 @@ +@(issues: List[gitbucket.core.service.IssuesService.IssueInfo], + page: Int, + openCount: Int, + closedCount: Int, + condition: gitbucket.core.service.IssuesService.IssueSearchCondition, + filter: String, + groups: List[String])(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main("Issues"){ + @dashboard.html.tab("issues") +
    + @issuesnavi(filter, "issues", condition) + @issueslist(issues, page, openCount, closedCount, condition, filter, groups) +
    +} diff --git a/src/main/twirl/gitbucket/core/dashboard/issueslist.scala.html b/src/main/twirl/gitbucket/core/dashboard/issueslist.scala.html new file mode 100644 index 0000000..6729173 --- /dev/null +++ b/src/main/twirl/gitbucket/core/dashboard/issueslist.scala.html @@ -0,0 +1,61 @@ +@(issues: List[gitbucket.core.service.IssuesService.IssueInfo], + page: Int, + openCount: Int, + closedCount: Int, + condition: gitbucket.core.service.IssuesService.IssueSearchCondition, + filter: String, + groups: List[String])(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@import gitbucket.core.service.IssuesService +@import gitbucket.core.service.IssuesService.IssueInfo + + + + + @issues.map { case IssueInfo(issue, labels, milestone, commentCount) => + + + + } +
    + @dashboard.html.header(openCount, closedCount, condition, groups) +
    + @if(issue.isPullRequest){ + + } else { + + } + @issue.userName/@issue.repositoryName ・ + @if(issue.isPullRequest){ + @issue.title + } else { + @issue.title + } + @labels.map { label => + @label.labelName + } + + @issue.assignedUserName.map { userName => + @avatar(userName, 20, tooltip = true) + } + @if(commentCount > 0){ + + @commentCount + + } else { + + @commentCount + + } + +
    + #@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate) + @milestone.map { milestone => + @milestone + } +
    +
    +
    + @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), IssuesService.IssueLimit, 10, condition.toURL) +
    diff --git a/src/main/twirl/gitbucket/core/dashboard/issuesnavi.scala.html b/src/main/twirl/gitbucket/core/dashboard/issuesnavi.scala.html new file mode 100644 index 0000000..5770f82 --- /dev/null +++ b/src/main/twirl/gitbucket/core/dashboard/issuesnavi.scala.html @@ -0,0 +1,22 @@ +@(filter: String, + active: String, + condition: gitbucket.core.service.IssuesService.IssueSearchCondition)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ + diff --git a/src/main/twirl/gitbucket/core/dashboard/pulls.scala.html b/src/main/twirl/gitbucket/core/dashboard/pulls.scala.html new file mode 100644 index 0000000..46dba30 --- /dev/null +++ b/src/main/twirl/gitbucket/core/dashboard/pulls.scala.html @@ -0,0 +1,16 @@ +@(issues: List[gitbucket.core.service.IssuesService.IssueInfo], + page: Int, + openCount: Int, + closedCount: Int, + condition: gitbucket.core.service.IssuesService.IssueSearchCondition, + filter: String, + groups: List[String])(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main("Pull Requests"){ + @dashboard.html.tab("pulls") +
    + @issuesnavi(filter, "pulls", condition) + @issueslist(issues, page, openCount, closedCount, condition, filter, groups) +
    +} diff --git a/src/main/twirl/gitbucket/core/dashboard/tab.scala.html b/src/main/twirl/gitbucket/core/dashboard/tab.scala.html new file mode 100644 index 0000000..6906048 --- /dev/null +++ b/src/main/twirl/gitbucket/core/dashboard/tab.scala.html @@ -0,0 +1,47 @@ +@(active: String = "")(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +
    +
    + + + News Feed + + @if(loginAccount.isDefined){ + + + Pull Requests + + + + Issues + + } +
    +
    + \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/error.scala.html b/src/main/twirl/gitbucket/core/error.scala.html new file mode 100644 index 0000000..48e5507 --- /dev/null +++ b/src/main/twirl/gitbucket/core/error.scala.html @@ -0,0 +1,4 @@ +@(title: String)(implicit context: gitbucket.core.controller.Context) +@main("Error"){ +

    @title

    +} \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/helper/account.scala.html b/src/main/twirl/gitbucket/core/helper/account.scala.html new file mode 100644 index 0000000..4b62e72 --- /dev/null +++ b/src/main/twirl/gitbucket/core/helper/account.scala.html @@ -0,0 +1,15 @@ +@(id: String, width: Int)(implicit context: gitbucket.core.controller.Context) +@import context._ + + diff --git a/src/main/twirl/gitbucket/core/helper/activities.scala.html b/src/main/twirl/gitbucket/core/helper/activities.scala.html new file mode 100644 index 0000000..18856c2 --- /dev/null +++ b/src/main/twirl/gitbucket/core/helper/activities.scala.html @@ -0,0 +1,99 @@ +@(activities: List[gitbucket.core.model.Activity])(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ + +@if(activities.isEmpty){ + No activity +} else { + @activities.map { activity => +
    + @(activity.activityType match { + case "open_issue" => detailActivity(activity, "activity-issue.png") + case "comment_issue" => detailActivity(activity, "activity-comment.png") + case "comment_commit" => detailActivity(activity, "activity-comment.png") + case "close_issue" => detailActivity(activity, "activity-issue-close.png") + case "reopen_issue" => detailActivity(activity, "activity-issue-reopen.png") + case "open_pullreq" => detailActivity(activity, "activity-merge.png") + case "merge_pullreq" => detailActivity(activity, "activity-merge.png") + case "create_repository" => simpleActivity(activity, "activity-create-repository.png") + case "create_branch" => simpleActivity(activity, "activity-branch.png") + case "delete_branch" => simpleActivity(activity, "activity-delete.png") + case "create_tag" => simpleActivity(activity, "activity-tag.png") + case "delete_tag" => simpleActivity(activity, "activity-delete.png") + case "fork" => simpleActivity(activity, "activity-fork.png") + case "push" => customActivity(activity, "activity-commit.png"){ +
    + {activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) => + if(i == 3){ +
    ...
    + } else { + if(commit.nonEmpty){ +
    + {commit.substring(0, 7)} + {commit.substring(41)} +
    + } + } + }} +
    + } + case "create_wiki" => customActivity(activity, "activity-wiki.png"){ + + } + case "edit_wiki" => customActivity(activity, "activity-wiki.png"){ + activity.additionalInfo.get.split(":") match { + case Array(pageName, commitId) => + + case Array(pageName) => +
    + Edited {pageName}. +
    + } + } + }) +
    + } +} + +@detailActivity(activity: gitbucket.core.model.Activity, image: String) = { +
    +
    +
    @helper.html.datetimeago(activity.activityDate)
    +
    + @avatar(activity.activityUserName, 16) + @activityMessage(activity.message) +
    + @activity.additionalInfo.map { additionalInfo => +
    @additionalInfo
    + } +
    +} + +@customActivity(activity: gitbucket.core.model.Activity, image: String)(additionalInfo: Any) = { +
    +
    +
    @helper.html.datetimeago(activity.activityDate)
    +
    + @avatar(activity.activityUserName, 16) + @activityMessage(activity.message) +
    + @additionalInfo +
    +} + +@simpleActivity(activity: gitbucket.core.model.Activity, image: String) = { +
    +
    +
    + @avatar(activity.activityUserName, 16) + @activityMessage(activity.message) + @helper.html.datetimeago(activity.activityDate) +
    +
    +} + diff --git a/src/main/twirl/gitbucket/core/helper/attached.scala.html b/src/main/twirl/gitbucket/core/helper/attached.scala.html new file mode 100644 index 0000000..33bd58e --- /dev/null +++ b/src/main/twirl/gitbucket/core/helper/attached.scala.html @@ -0,0 +1,33 @@ +@(owner: String, repository: String)(textarea: Html)(implicit context: gitbucket.core.controller.Context) +@import context._ +
    + @textarea +
    Attach images by dragging & dropping, or selecting them.
    +
    +@defining("(id=\")([\\w\\-]*)(\")".r.findFirstMatchIn(textarea.body).map(_.group(2))){ textareaId => + +} diff --git a/src/main/twirl/gitbucket/core/helper/branchcontrol.scala.html b/src/main/twirl/gitbucket/core/helper/branchcontrol.scala.html new file mode 100644 index 0000000..f97e166 --- /dev/null +++ b/src/main/twirl/gitbucket/core/helper/branchcontrol.scala.html @@ -0,0 +1,64 @@ +@(branch: String = "", + repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + hasWritePermission: Boolean)(body: Html)(implicit context: gitbucket.core.controller.Context) +@import gitbucket.core.service.RepositoryService +@import context._ +@import gitbucket.core._ +@import gitbucket.core.view.helpers._ +@helper.html.dropdown( + value = if(branch.length == 40) branch.substring(0, 10) else branch, + prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree", + mini = true +) { +
  • Switch branches
  • +
  • + @body + @if(hasWritePermission) { + + } +} + diff --git a/src/main/twirl/gitbucket/core/helper/checkicon.scala.html b/src/main/twirl/gitbucket/core/helper/checkicon.scala.html new file mode 100644 index 0000000..b9726a7 --- /dev/null +++ b/src/main/twirl/gitbucket/core/helper/checkicon.scala.html @@ -0,0 +1,6 @@ +@(condition: => Boolean) +@if(condition){ + +} else { + +} \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/helper/commitcomment.scala.html b/src/main/twirl/gitbucket/core/helper/commitcomment.scala.html new file mode 100644 index 0000000..0df308b --- /dev/null +++ b/src/main/twirl/gitbucket/core/helper/commitcomment.scala.html @@ -0,0 +1,36 @@ +@(comment: gitbucket.core.model.CommitComment, + hasWritePermission: Boolean, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + latestCommitId: Option[String] = None)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core._ +@import gitbucket.core.view.helpers._ +
    +
    @avatar(comment.commentedUserName, 48)
    +
    +
    + @user(comment.commentedUserName, styleClass="username strong") + + commented + @if(comment.pullRequest){ + on this Pull Request + }else{ + @if(comment.fileName.isDefined){ + on @comment.fileName.get + } + in @comment.commitId.substring(0, 7) + } + @helper.html.datetimeago(comment.registeredDate) + + + @if(hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false)){ +   + + } + +
    +
    + @markdown(comment.content, repository, false, true, true, hasWritePermission) +
    +
    +
    diff --git a/src/main/twirl/gitbucket/core/helper/copy.scala.html b/src/main/twirl/gitbucket/core/helper/copy.scala.html new file mode 100644 index 0000000..7a09a52 --- /dev/null +++ b/src/main/twirl/gitbucket/core/helper/copy.scala.html @@ -0,0 +1,55 @@ +@(id: String, value: String)(html: Html) +
    + @html + +
    + diff --git a/src/main/twirl/gitbucket/core/helper/datepicker.scala.html b/src/main/twirl/gitbucket/core/helper/datepicker.scala.html new file mode 100644 index 0000000..c221629 --- /dev/null +++ b/src/main/twirl/gitbucket/core/helper/datepicker.scala.html @@ -0,0 +1,11 @@ +@(name: String, value: Option[java.util.Date]) +@import gitbucket.core.view.helpers +
    + + +
    + diff --git a/src/main/twirl/gitbucket/core/helper/datetimeago.scala.html b/src/main/twirl/gitbucket/core/helper/datetimeago.scala.html new file mode 100644 index 0000000..ded15a4 --- /dev/null +++ b/src/main/twirl/gitbucket/core/helper/datetimeago.scala.html @@ -0,0 +1,10 @@ +@(latestUpdatedDate: java.util.Date, + recentOnly: Boolean = true) +@import gitbucket.core.view.helpers._ + + @if(recentOnly){ + @datetimeAgoRecentOnly(latestUpdatedDate) + }else{ + @datetimeAgo(latestUpdatedDate) + } + diff --git a/src/main/twirl/gitbucket/core/helper/diff.scala.html b/src/main/twirl/gitbucket/core/helper/diff.scala.html new file mode 100644 index 0000000..2877060 --- /dev/null +++ b/src/main/twirl/gitbucket/core/helper/diff.scala.html @@ -0,0 +1,251 @@ +@(diffs: Seq[gitbucket.core.util.JGitUtil.DiffInfo], + repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + newCommitId: Option[String], + oldCommitId: Option[String], + showIndex: Boolean, + issueId: Option[Int], + hasWritePermission: Boolean, + showLineNotes: Boolean)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@import org.eclipse.jgit.diff.DiffEntry.ChangeType +@if(showIndex){ + + +} +@diffs.zipWithIndex.map { case (diff, i) => + + + + + + + + +
    + @if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){ + @diff.oldPath -> @diff.newPath + @if(newCommitId.isDefined){ + + } + } + @if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){ + @if(diff.changeType == ChangeType.ADD){ + + }else{ + + } @diff.newPath + @if(newCommitId.isDefined){ + + } + } + @if(diff.changeType == ChangeType.DELETE){ + @diff.oldPath + @if(oldCommitId.isDefined){ + + } + } +
    + @if(diff.newContent != None || diff.oldContent != None){ +
    + + + } else { + Not supported + } +
    +} + + + + diff --git a/src/main/twirl/gitbucket/core/helper/dropdown.scala.html b/src/main/twirl/gitbucket/core/helper/dropdown.scala.html new file mode 100644 index 0000000..20e2f4a --- /dev/null +++ b/src/main/twirl/gitbucket/core/helper/dropdown.scala.html @@ -0,0 +1,24 @@ +@(value : String = "", + prefix: String = "", + mini : Boolean = true, + style : String = "", + right : Boolean = false, + flat : Boolean = false)(body: Html) +
    + + +
    diff --git a/src/main/twirl/gitbucket/core/helper/error.scala.html b/src/main/twirl/gitbucket/core/helper/error.scala.html new file mode 100644 index 0000000..00f43e2 --- /dev/null +++ b/src/main/twirl/gitbucket/core/helper/error.scala.html @@ -0,0 +1,7 @@ +@(error: Option[Any]) +@if(error.isDefined){ +
    + + @error +
    +} \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/helper/feed.scala.xml b/src/main/twirl/gitbucket/core/helper/feed.scala.xml new file mode 100644 index 0000000..865cee9 --- /dev/null +++ b/src/main/twirl/gitbucket/core/helper/feed.scala.xml @@ -0,0 +1,27 @@ +@(activities: List[gitbucket.core.model.Activity])(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ + + tag:@context.host,2013:gitbucket + Gitbucket's activities + + + Gitbucket + @context.baseUrl + + @datetimeRFC3339(if(activities.isEmpty) new java.util.Date else activities.map(_.activityDate).max) + @activities.map { activity => + + tag:@context.host,@date(activity.activityDate):activity:@activity.activityId + @datetimeRFC3339(activity.activityDate) + @datetimeRFC3339(activity.activityDate) + + @activity.activityType + + @activity.activityUserName + @url(activity.activityUserName) + + @activityMessage(activity.message) + + } + diff --git a/src/main/twirl/gitbucket/core/helper/forkrepository.scala.html b/src/main/twirl/gitbucket/core/helper/forkrepository.scala.html new file mode 100644 index 0000000..bd72554 --- /dev/null +++ b/src/main/twirl/gitbucket/core/helper/forkrepository.scala.html @@ -0,0 +1,18 @@ +@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + groupAndPerm: Map[String, Boolean])(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +

    Where should we fork this repository?

    +
    +
    +
    @avatar(loginAccount.get.userName, 100)@@@loginAccount.get.userName
    + @for((groupName, isManager) <- groupAndPerm) { + @if(isManager) { +
    @avatar(groupName, 100)@@@groupName
    + } else { +
    @avatar(groupName, 100)@@@groupName
    + } + } +
    + +
    \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/helper/information.scala.html b/src/main/twirl/gitbucket/core/helper/information.scala.html new file mode 100644 index 0000000..ff382a2 --- /dev/null +++ b/src/main/twirl/gitbucket/core/helper/information.scala.html @@ -0,0 +1,7 @@ +@(info: Option[Any]) +@if(info.isDefined){ +
    + + @info +
    +} diff --git a/src/main/twirl/gitbucket/core/helper/paginator.scala.html b/src/main/twirl/gitbucket/core/helper/paginator.scala.html new file mode 100644 index 0000000..609e149 --- /dev/null +++ b/src/main/twirl/gitbucket/core/helper/paginator.scala.html @@ -0,0 +1,34 @@ +@(page: Int, count: Int, limit: Int, width: Int, baseURL: String) +@defining(gitbucket.core.view.Pagination(page, count, limit, width)){ p => + @if(p.count > p.limit){ + + } +} diff --git a/src/main/twirl/gitbucket/core/helper/preview.scala.html b/src/main/twirl/gitbucket/core/helper/preview.scala.html new file mode 100644 index 0000000..86e940b --- /dev/null +++ b/src/main/twirl/gitbucket/core/helper/preview.scala.html @@ -0,0 +1,58 @@ +@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + content: String, + enableWikiLink: Boolean, + enableRefsLink: Boolean, + enableTaskList: Boolean, + hasWritePermission: Boolean, + style: String = "", + placeholder: String = "Leave a comment", + elastic: Boolean = false, + uid: Long = new java.util.Date().getTime())(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core._ +@import gitbucket.core.view.helpers._ +
    + +
    +
    + + @textarea = { + + } + @if(enableWikiLink){ + @textarea + } else { + @helper.html.attached(repository.owner, repository.name)(textarea) + } +
    +
    +
    +
    +
    +
    +
    + + + diff --git a/src/main/twirl/gitbucket/core/helper/repositoryicon.scala.html b/src/main/twirl/gitbucket/core/helper/repositoryicon.scala.html new file mode 100644 index 0000000..88462a5 --- /dev/null +++ b/src/main/twirl/gitbucket/core/helper/repositoryicon.scala.html @@ -0,0 +1,13 @@ +@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo, large: Boolean)(implicit context: gitbucket.core.controller.Context) +@import gitbucket.core.service.RepositoryService +@import context._ +@import gitbucket.core.view.helpers._ +@if(repository.repository.isPrivate){ + +} else { + @if(repository.repository.originUserName.isDefined){ + + } else { + + } +} diff --git a/src/main/twirl/gitbucket/core/helper/uploadavatar.scala.html b/src/main/twirl/gitbucket/core/helper/uploadavatar.scala.html new file mode 100644 index 0000000..6cf76e0 --- /dev/null +++ b/src/main/twirl/gitbucket/core/helper/uploadavatar.scala.html @@ -0,0 +1,51 @@ +@(account: Option[gitbucket.core.model.Account])(implicit context: gitbucket.core.controller.Context) +@import context._ +
    + @if(account.nonEmpty && account.get.image.nonEmpty){ + + } else { +
    Upload Image
    + } +
    +@if(account.nonEmpty && account.get.image.nonEmpty){ + +} + +@if(account.isEmpty || account.get.image.isEmpty){ + +} + \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/index.scala.html b/src/main/twirl/gitbucket/core/index.scala.html new file mode 100644 index 0000000..7976e2a --- /dev/null +++ b/src/main/twirl/gitbucket/core/index.scala.html @@ -0,0 +1,80 @@ +@(activities: List[gitbucket.core.model.Activity], + recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo], + userRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@main("GitBucket"){ + @dashboard.html.tab() +
    +
    +
    +
    + activities +
    + @helper.html.activities(activities) +
    + +
    + @settings.information.map { information => +
    + + @Html(information) +
    + } + @if(loginAccount.isEmpty){ + @signinform(settings) + } else { + + + + + @if(userRepositories.isEmpty){ + + + + } else { + @userRepositories.map { repository => + + + + } + } +
    + + Your repositories (@userRepositories.size) +
    No repositories
    + @helper.html.repositoryicon(repository, false) + @if(repository.owner == loginAccount.get.userName){ + @repository.name + } else { + @repository.owner/@repository.name + } +
    + } + + + + + @if(recentRepositories.isEmpty){ + + + + } else { + @recentRepositories.map { repository => + + + + } + } +
    + Recent updated repositories +
    No repositories
    + @helper.html.repositoryicon(repository, false) + @repository.owner/@repository.name +
    +
    +
    +
    +} diff --git a/src/main/twirl/gitbucket/core/issues/commentform.scala.html b/src/main/twirl/gitbucket/core/issues/commentform.scala.html new file mode 100644 index 0000000..20b4dc4 --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/commentform.scala.html @@ -0,0 +1,31 @@ +@(issue: gitbucket.core.model.Issue, + reopenable: Boolean, + hasWritePermission: Boolean, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@if(loginAccount.isDefined){ +

    +
    +
    @avatar(loginAccount.get.userName, 48)
    +
    +
    + @helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 635px; height: 100px; max-height: 150px;", elastic = true) +
    +
    +
    + + + @if((reopenable || !issue.closed) && (hasWritePermission || issue.openedUserName == loginAccount.get.userName)){ + + } +
    +
    +} + diff --git a/src/main/twirl/gitbucket/core/issues/commentlist.scala.html b/src/main/twirl/gitbucket/core/issues/commentlist.scala.html new file mode 100644 index 0000000..55ed11c --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/commentlist.scala.html @@ -0,0 +1,264 @@ +@(issue: Option[gitbucket.core.model.Issue], + comments: List[gitbucket.core.model.Comment], + hasWritePermission: Boolean, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + pullreq: Option[gitbucket.core.model.PullRequest] = None)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@import gitbucket.core.model.CommitComment +@if(issue.isDefined){ +
    @avatar(issue.get.openedUserName, 48)
    +
    +
    + @user(issue.get.openedUserName, styleClass="username strong") commented @helper.html.datetimeago(issue.get.registeredDate) + + @if(hasWritePermission || loginAccount.map(_.userName == issue.get.openedUserName).getOrElse(false)){ + + } + +
    +
    + @markdown(issue.get.content getOrElse "No description provided.", repository, false, true, true, hasWritePermission) +
    +
    +} + +@comments.map { + case comment: gitbucket.core.model.IssueComment => { + @if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){ +
    @avatar(comment.commentedUserName, 48)
    +
    +
    + @user(comment.commentedUserName, styleClass="username strong") + + @if(comment.action == "comment"){ + commented + } else { + @if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request } + } + @helper.html.datetimeago(comment.registeredDate) + + + @if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" + && (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){ +   + + } + +
    +
    + @if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){ + @defining(comment.content.substring(comment.content.length - 40)){ id => + + @markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true, true, hasWritePermission) + } + } else { + @if(comment.action == "refer"){ + @defining(comment.content.split(":")){ case Array(issueId, rest @ _*) => + Issue #@issueId: @rest.mkString(":") + } + } else { + @markdown(comment.content, repository, false, true, true, hasWritePermission) + } + } +
    +
    + } + @if(comment.action == "merge"){ +
    + Merged + @avatar(comment.commentedUserName, 20) + @user(comment.commentedUserName, styleClass="username strong") merged commit @pullreq.map(_.commitIdTo.substring(0, 7)) into + @if(pullreq.get.requestUserName == repository.owner){ + @pullreq.map(_.branch) from @pullreq.map(_.requestBranch) + } else { + @pullreq.map(_.userName):@pullreq.map(_.branch) from @pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch) + } + @helper.html.datetimeago(comment.registeredDate) +
    + } + @if(comment.action == "close" || comment.action == "close_comment"){ +
    + Closed + @avatar(comment.commentedUserName, 20) + @if(issue.isDefined && issue.get.isPullRequest){ + @user(comment.commentedUserName, styleClass="username strong") closed the pull request @helper.html.datetimeago(comment.registeredDate) + } else { + @user(comment.commentedUserName, styleClass="username strong") closed the issue @helper.html.datetimeago(comment.registeredDate) + } +
    + } + @if(comment.action == "reopen" || comment.action == "reopen_comment"){ +
    + Reopened + @avatar(comment.commentedUserName, 20) + @user(comment.commentedUserName, styleClass="username strong") reopened the issue @helper.html.datetimeago(comment.registeredDate) +
    + } + @if(comment.action == "delete_branch"){ +
    + Deleted + @avatar(comment.commentedUserName, 20) + @user(comment.commentedUserName, styleClass="username strong") deleted the @pullreq.map(_.requestBranch) branch @helper.html.datetimeago(comment.registeredDate) +
    + } + } + case comment: CommitComment => { + @helper.html.commitcomment(comment, hasWritePermission, repository, pullreq.map(_.commitIdTo)) + } +} + diff --git a/src/main/twirl/gitbucket/core/issues/create.scala.html b/src/main/twirl/gitbucket/core/issues/create.scala.html new file mode 100644 index 0000000..a45b047 --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/create.scala.html @@ -0,0 +1,148 @@ +@(collaborators: List[String], + milestones: List[gitbucket.core.model.Milestone], + labels: List[gitbucket.core.model.Label], + hasWritePermission: Boolean, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){ + @html.menu("issues", repository){ + @navigation("issues", false, repository) +


    +
    +
    +
    +
    @avatar(loginAccount.get.userName, 48)
    +
    +
    + + +
    + No one is assigned + @if(hasWritePermission){ + + @helper.html.dropdown() { +
  • Clear assignee
  • + @collaborators.map { collaborator => +
  • @avatar(collaborator, 20) @collaborator
  • + } + } + } +
    + No milestone + @if(hasWritePermission){ + + @helper.html.dropdown() { +
  • Clear this milestone
  • + @milestones.filter(_.closedDate.isEmpty).map { milestone => +
  • + + @milestone.title +
    + @milestone.dueDate.map { dueDate => + @if(isPast(dueDate)){ + Due by @date(dueDate) + } else { + Due by @date(dueDate) + } + }.getOrElse { + No due date + } +
    +
    +
  • + } + } + } +
    +
    +
    + @helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 565px; height: 200px; max-height: 250px;", elastic = true) +
    +
    +
    + +
    +
    +
    + @if(hasWritePermission){ + Labels +
    +
    + + +
    +
    + } +
    +
    +
    + } +} + diff --git a/src/main/twirl/gitbucket/core/issues/editcomment.scala.html b/src/main/twirl/gitbucket/core/issues/editcomment.scala.html new file mode 100644 index 0000000..07a815d --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/editcomment.scala.html @@ -0,0 +1,42 @@ +@(content: String, commentId: Int, owner: String, repository: String)(implicit context: gitbucket.core.controller.Context) +@import context._ + +@helper.html.attached(owner, repository){ + +} +
    + + +
    + diff --git a/src/main/twirl/gitbucket/core/issues/editissue.scala.html b/src/main/twirl/gitbucket/core/issues/editissue.scala.html new file mode 100644 index 0000000..5dfb18f --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/editissue.scala.html @@ -0,0 +1,38 @@ +@(content: Option[String], issueId: Int, owner: String, repository: String)(implicit context: gitbucket.core.controller.Context) +@import context._ +@helper.html.attached(owner, repository){ + +} +
    + + +
    + diff --git a/src/main/twirl/gitbucket/core/issues/issue.scala.html b/src/main/twirl/gitbucket/core/issues/issue.scala.html new file mode 100644 index 0000000..5ced5fc --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/issue.scala.html @@ -0,0 +1,92 @@ +@(issue: gitbucket.core.model.Issue, + comments: List[gitbucket.core.model.IssueComment], + issueLabels: List[gitbucket.core.model.Label], + collaborators: List[String], + milestones: List[(gitbucket.core.model.Milestone, Int, Int)], + labels: List[gitbucket.core.model.Label], + hasWritePermission: Boolean, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){ + @html.menu("issues", repository){ +
    +
    + @if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){ + Edit + } + New issue +
    + +

    + + @issue.title + #@issue.issueId + + +

    +
    + @if(issue.closed) { + Closed + } else { + Open + } + + @user(issue.openedUserName, styleClass="username strong") opened this issue @helper.html.datetimeago(issue.registeredDate) - @defining( + comments.count( _.action.contains("comment") ) + ){ count => + @count @plural(count, "comment") + } + +

    +
    +
    +
    + @commentlist(Some(issue), comments, hasWritePermission, repository) + @commentform(issue, true, hasWritePermission, repository) +
    +
    + @issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository) +
    +
    + } +} + diff --git a/src/main/twirl/gitbucket/core/issues/issueinfo.scala.html b/src/main/twirl/gitbucket/core/issues/issueinfo.scala.html new file mode 100644 index 0000000..84aee5e --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/issueinfo.scala.html @@ -0,0 +1,173 @@ +@(issue: gitbucket.core.model.Issue, + comments: List[gitbucket.core.model.Comment], + issueLabels: List[gitbucket.core.model.Label], + collaborators: List[String], + milestones: List[(gitbucket.core.model.Milestone, Int, Int)], + labels: List[gitbucket.core.model.Label], + hasWritePermission: Boolean, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) +@import gitbucket.core.view.helpers._ +
    + Labels + @if(hasWritePermission){ +
    + @helper.html.dropdown(right = true) { + @labels.map { label => +
  • + + @helper.html.checkicon(issueLabels.exists(_.labelId == label.labelId)) +   + @label.labelName + +
  • + } + } +
    + } +
    + +
    + +
    + @issue.milestoneId.map { milestoneId => + @milestones.collect { case (milestone, openCount, closeCount) if(milestone.milestoneId == milestoneId) => + @issues.milestones.html.progress(openCount + closeCount, closeCount) + } + } +
    + + @issue.milestoneId.map { milestoneId => + @milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) => + @milestone.title + } + }.getOrElse { + No milestone + } + +
    +
    + Assignee + @if(hasWritePermission){ +
    + @helper.html.dropdown(right = true) { +
  • Clear assignee
  • + @collaborators.map { collaborator => +
  • + + @helper.html.checkicon(Some(collaborator) == issue.assignedUserName)@avatar(collaborator, 20) @collaborator + +
  • + } + } +
    + } +
    + + @issue.assignedUserName.map { userName => + @avatar(userName, 20) @user(userName, styleClass="username strong small") + }.getOrElse{ + No one + } + +
    +
    + @defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants => +
    @participants.size @plural(participants.size, "participant")
    + @participants.map { participant => @avatarLink(participant, 20, tooltip = true) } + } +
    + diff --git a/src/main/twirl/gitbucket/core/issues/labellist.scala.html b/src/main/twirl/gitbucket/core/issues/labellist.scala.html new file mode 100644 index 0000000..38b2aa3 --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/labellist.scala.html @@ -0,0 +1,7 @@ +@(issueLabels: List[gitbucket.core.model.Label]) +@if(issueLabels.isEmpty){ +
  • None yet
  • +} +@issueLabels.map { label => +
  • @label.labelName
  • +} diff --git a/src/main/twirl/gitbucket/core/issues/labels/edit.scala.html b/src/main/twirl/gitbucket/core/issues/labels/edit.scala.html new file mode 100644 index 0000000..f590d94 --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/labels/edit.scala.html @@ -0,0 +1,62 @@ +@(label: Option[gitbucket.core.model.Label], + repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@defining(label.map(_.labelId).getOrElse("new")){ labelId => +
    +
    + +
    + + +
    + + + + + + +
    +
    + +} diff --git a/src/main/twirl/gitbucket/core/issues/labels/label.scala.html b/src/main/twirl/gitbucket/core/issues/labels/label.scala.html new file mode 100644 index 0000000..b7cee42 --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/labels/label.scala.html @@ -0,0 +1,36 @@ +@(label: gitbucket.core.model.Label, + counts: Map[String, Int], + repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ + + +
    + +
    +
    + @counts.get(label.labelName).getOrElse(0) open issues +
    +
    + @if(hasWritePermission){ +
    +
    + Edit +    + Delete +
    +
    + } +
    + + diff --git a/src/main/twirl/gitbucket/core/issues/labels/list.scala.html b/src/main/twirl/gitbucket/core/issues/labels/list.scala.html new file mode 100644 index 0000000..d82a00d --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/labels/list.scala.html @@ -0,0 +1,67 @@ +@(labels: List[gitbucket.core.model.Label], + counts: Map[String, Int], + repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(s"Labels - ${repository.owner}/${repository.name}"){ + @html.menu("issues", repository){ + @issues.html.navigation("labels", hasWritePermission, repository) +
    + + + + + + + + @labels.map { label => + @gitbucket.core.issues.labels.html.label(label, counts, repository, hasWritePermission) + } + @if(labels.isEmpty){ + + + + } +
    + @labels.size labels +
    + No labels to show. + @if(hasWritePermission){ + Create a new label. + } +
    + } +} + diff --git a/src/main/twirl/gitbucket/core/issues/list.scala.html b/src/main/twirl/gitbucket/core/issues/list.scala.html new file mode 100644 index 0000000..ea4d8d7 --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/list.scala.html @@ -0,0 +1,83 @@ +@(target: String, + issues: List[gitbucket.core.service.IssuesService.IssueInfo], + page: Int, + collaborators: List[String], + milestones: List[gitbucket.core.model.Milestone], + labels: List[gitbucket.core.model.Label], + openCount: Int, + closedCount: Int, + condition: gitbucket.core.service.IssuesService.IssueSearchCondition, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(repository)){ + @html.menu(target, repository){ + @navigation(target, true, repository, Some(condition)) + @listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission) + @if(hasWritePermission){ +
    + + + +
    + } + } +} +@if(hasWritePermission){ + +} diff --git a/src/main/twirl/gitbucket/core/issues/listparts.scala.html b/src/main/twirl/gitbucket/core/issues/listparts.scala.html new file mode 100644 index 0000000..b5c8789 --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/listparts.scala.html @@ -0,0 +1,219 @@ +@(target: String, + issues: List[gitbucket.core.service.IssuesService.IssueInfo], + page: Int, + openCount: Int, + closedCount: Int, + condition: gitbucket.core.service.IssuesService.IssueSearchCondition, + collaborators: List[String] = Nil, + milestones: List[gitbucket.core.model.Milestone] = Nil, + labels: List[gitbucket.core.model.Label] = Nil, + repository: Option[gitbucket.core.service.RepositoryService.RepositoryInfo] = None, + hasWritePermission: Boolean = false)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@import gitbucket.core.service.IssuesService +@import gitbucket.core.service.IssuesService.IssueInfo +
    +@if(condition.nonEmpty){ + +} + + + + + @if(issues.isEmpty){ + + + + } + @issues.map { case IssueInfo(issue, labels, milestone, commentCount) => + + + + } +
    + + + + + @openCount Open +    + + + @closedCount Closed + + +
    + @helper.html.dropdown("Author", flat = true) { + @collaborators.map { collaborator => +
  • + + @helper.html.checkicon(condition.author == Some(collaborator)) + @avatar(collaborator, 20) @collaborator + +
  • + } + } + @helper.html.dropdown("Label", flat = true) { + @labels.map { label => +
  • + + @helper.html.checkicon(condition.labels.contains(label.labelName)) +    + @label.labelName + +
  • + } + } + @helper.html.dropdown("Milestone", flat = true) { +
  • + + @helper.html.checkicon(condition.milestone == Some(None)) Issues with no milestone + +
  • + @milestones.filter(_.closedDate.isEmpty).map { milestone => +
  • + + @helper.html.checkicon(condition.milestone == Some(Some(milestone.title))) @milestone.title + +
  • + } + } + @helper.html.dropdown("Assignee", flat = true) { + @collaborators.map { collaborator => +
  • + + @helper.html.checkicon(condition.assigned == Some(collaborator)) + @avatar(collaborator, 20) @collaborator + +
  • + } + } + @helper.html.dropdown("Sort", flat = true){ +
  • + + @helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest + +
  • +
  • + + @helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest + +
  • +
  • + + @helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented + +
  • +
  • + + @helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented + +
  • +
  • + + @helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated + +
  • +
  • + + @helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated + +
  • + } +
    + @if(hasWritePermission){ +
    + @helper.html.dropdown("Mark as", flat = true) { +
  • Open
  • +
  • Close
  • + } + @helper.html.dropdown("Label", flat = true) { + @labels.map { label => +
  • + + +   + @label.labelName + +
  • + } + } + @helper.html.dropdown("Milestone", flat = true) { +
  • No milestone
  • + @milestones.filter(_.closedDate.isEmpty).map { milestone => +
  • @milestone.title
  • + } + } + @helper.html.dropdown("Assignee", flat = true) { +
  • Clear assignee
  • + @collaborators.map { collaborator => +
  • @avatar(collaborator, 20) @collaborator
  • + } + } +
    + } +
    + @if(target == "issues"){ + No issues to show. + } else { + No pull requests to show. + } + @if(condition.labels.nonEmpty || condition.milestone.isDefined){ + Clear active filters. + } else { + @if(repository.isDefined){ + @if(target == "issues"){ + Create a new issue. + } else { + Create a new pull request. + } + } + } +
    + @if(hasWritePermission){ + + } + + @if(repository.isEmpty){ + @issue.repositoryName ・ + } + @if(target == "issues"){ + @issue.title + } else { + @issue.title + } + @labels.map { label => + @label.labelName + } + + @issue.assignedUserName.map { userName => + @avatar(userName, 20, tooltip = true) + } + @if(commentCount > 0){ + + @commentCount + + } else { + + @commentCount + + } + +
    + #@issue.issueId opened @helper.html.datetimeago(issue.registeredDate) by @user(issue.openedUserName, styleClass="username") + @milestone.map { milestone => + @milestone + } +
    +
    +
    + @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), IssuesService.IssueLimit, 10, condition.toURL) +
    + diff --git a/src/main/twirl/gitbucket/core/issues/milestones/edit.scala.html b/src/main/twirl/gitbucket/core/issues/milestones/edit.scala.html new file mode 100644 index 0000000..dc4ef3b --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/milestones/edit.scala.html @@ -0,0 +1,47 @@ +@(milestone: Option[gitbucket.core.model.Milestone], + repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(s"Milestones - ${repository.owner}/${repository.name}"){ + @html.menu("issues", repository){ + @if(milestone.isEmpty){ +

    New milestone

    +
    Create a new milestone to help organize your issues and pull requests.
    + } else { + @issues.html.navigation("milestones", false, repository) +

    + } +
    +
    +
    + + +
    +
    + + + +
    +
    + + @helper.html.datepicker("dueDate", milestone.flatMap(_.dueDate)) + +
    +
    +
    + @if(milestone.isEmpty){ + + } else { + @if(milestone.get.closedDate.isDefined){ + + } else { + + } + + } +
    +
    + } +} diff --git a/src/main/twirl/gitbucket/core/issues/milestones/list.scala.html b/src/main/twirl/gitbucket/core/issues/milestones/list.scala.html new file mode 100644 index 0000000..fe77565 --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/milestones/list.scala.html @@ -0,0 +1,105 @@ +@(state: String, + milestones: List[(gitbucket.core.model.Milestone, Int, Int)], + repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(s"Milestones - ${repository.owner}/${repository.name}"){ + @html.menu("issues", repository){ + @issues.html.navigation("milestones", hasWritePermission, repository) +
    + + + + + @defining(milestones.filter { case (milestone, _, _) => + milestone.closedDate.map(_ => state == "closed").getOrElse(state == "open") + }){ milestones => + @milestones.map { case (milestone, openCount, closedCount) => + + + + } + @if(milestones.isEmpty){ + + + + } + } +
    + + + + @milestones.count(_._1.closedDate.isEmpty) Open +    + + + @milestones.count(_._1.closedDate.isDefined) Closed + + +
    +
    +
    + @milestone.title +
    + @if(milestone.closedDate.isDefined){ + Closed @helper.html.datetimeago(milestone.closedDate.get) + } else { + @milestone.dueDate.map { dueDate => + @if(isPast(dueDate)){ + Due by @date(dueDate) + } else { + Due by @date(dueDate) + } + }.getOrElse { + No due date + } + } +
    +
    +
    + @progress(openCount + closedCount, closedCount) +
    +
    + @if(closedCount == 0){ + 0% + } else { + @((closedCount.toDouble / (openCount + closedCount).toDouble * 100).toInt)% + } complete    + @openCount open    + @closedCount closed +
    +
    + @if(hasWritePermission){ + Edit    + @if(milestone.closedDate.isDefined){ + Open    + } else { + Close    + } + Delete + } +
    +
    +
    +
    + @if(milestone.description.isDefined){ +
    + @markdown(milestone.description.get, repository, false, false) +
    + } +
    + No milestones to show. + @if(hasWritePermission){ + Create a new milestone. + } +
    + } +} + diff --git a/src/main/twirl/gitbucket/core/issues/milestones/progress.scala.html b/src/main/twirl/gitbucket/core/issues/milestones/progress.scala.html new file mode 100644 index 0000000..d099190 --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/milestones/progress.scala.html @@ -0,0 +1,6 @@ +@(total: Int, progress: Int) +
    + @if(progress > 0){ + + } +
    diff --git a/src/main/twirl/gitbucket/core/issues/navigation.scala.html b/src/main/twirl/gitbucket/core/issues/navigation.scala.html new file mode 100644 index 0000000..eb2f077 --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/navigation.scala.html @@ -0,0 +1,58 @@ +@(active: String, + newButton: Boolean, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + condition: Option[gitbucket.core.service.IssuesService.IssueSearchCondition] = None)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ + diff --git a/src/main/twirl/gitbucket/core/main.scala.html b/src/main/twirl/gitbucket/core/main.scala.html new file mode 100644 index 0000000..1026201 --- /dev/null +++ b/src/main/twirl/gitbucket/core/main.scala.html @@ -0,0 +1,93 @@ +@(title: String, repository: Option[gitbucket.core.service.RepositoryService.RepositoryInfo] = None)(body: Html)(implicit context: gitbucket.core.controller.Context) +@import gitbucket.core.plugin.PluginRegistry +@import gitbucket.core.servlet.AutoUpdate +@import context._ +@import gitbucket.core.view.helpers._ + + + + + @title + + + + + + + + + + + + + + + + + + + + + + + + + + + @body + + @PluginRegistry().getJavaScript(request.getRequestURI).map { script => + + } + + diff --git a/src/main/twirl/gitbucket/core/menu.scala.html b/src/main/twirl/gitbucket/core/menu.scala.html new file mode 100644 index 0000000..2c5f008 --- /dev/null +++ b/src/main/twirl/gitbucket/core/menu.scala.html @@ -0,0 +1,227 @@ +@(active: String, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + id: Option[String] = None, + expand: Boolean = false, + isNoGroup: Boolean = true, + info: Option[Any] = None, + error: Option[Any] = None)(body: Html)(implicit context: gitbucket.core.controller.Context) +@import gitbucket.core.service.SystemSettingsService +@import context._ +@import gitbucket.core.view.helpers._ + +@sidemenu(path: String, name: String, label: String, count: Int = 0) = { +
  • +
    + + @if(active == name){ + + } else { + + + } + @if(expand){ @label} + @if(expand && count > 0){ +
    @count
    + } +
    +
  • +} + +@sidemenuPlugin(path: String, name: String, label: String, icon: String) = { +
  • +
    + @if(expand){ @label} +
  • +} + +
    + @helper.html.information(info) + @helper.html.error(error) + @if(repository.commitCount > 0){ +
    +
    + @if(loginAccount.isEmpty){ + Fork + } else { + @if(isNoGroup) { + Fork + } else { + Fork + } + } + @repository.forkedCount +
    +
    + } +
    + @helper.html.repositoryicon(repository, true) + @repository.owner / @repository.name + + @defining(repository.repository){ x => + @if(repository.repository.originRepositoryName.isDefined){ + + } + } +
    +
    +
    +
    +
    +
      +
    • + @sidemenu("" , "code" , "Code") + @sidemenu("/issues", "issues", "Issues", repository.issueCount) + @sidemenu("/pulls" , "pulls" , "Pull Requests", repository.pullCount) + @sidemenu("/wiki" , "wiki" , "Wiki") + @if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){ + @sidemenu("/settings", "settings", "Settings") + } +
    • +
    + @if(expand){ +
    + HTTP clone URL +
    + @helper.html.copy("repository-url-copy", repository.httpUrl){ + + } + @if(settings.ssh && loginAccount.isDefined){ +
    + You can clone HTTP or SSH. +
    + } + @id.map { id => + + + } + } +
    +
    + @if(expand){ + @repository.repository.description.map { description => +

    @description

    + } + + } + @body +
    +
    + diff --git a/src/main/twirl/gitbucket/core/pulls/commits.scala.html b/src/main/twirl/gitbucket/core/pulls/commits.scala.html new file mode 100644 index 0000000..43ed945 --- /dev/null +++ b/src/main/twirl/gitbucket/core/pulls/commits.scala.html @@ -0,0 +1,35 @@ +@(commits: Seq[Seq[gitbucket.core.util.JGitUtil.CommitInfo]], + comments: Option[List[gitbucket.core.model.Comment]] = None, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@import gitbucket.core.model._ +
    + + @commits.map { day => + + + + @day.map { commit => + + + + + + + } + } +
    @date(day.head.commitTime)
    + @avatar(commit, 20) + @user(commit.authorName, commit.authorEmailAddress, "username") + @commit.shortMessage + @if(comments.isDefined){ + @comments.get.flatMap @{ + case comment: CommitComment => Some(comment) + case other => None + }.count(t => t.commitId == commit.id && !t.pullRequest) + } + + @commit.id.substring(0, 7) +
    +
    diff --git a/src/main/twirl/gitbucket/core/pulls/compare.scala.html b/src/main/twirl/gitbucket/core/pulls/compare.scala.html new file mode 100644 index 0000000..d706075 --- /dev/null +++ b/src/main/twirl/gitbucket/core/pulls/compare.scala.html @@ -0,0 +1,155 @@ +@(commits: Seq[Seq[gitbucket.core.util.JGitUtil.CommitInfo]], + diffs: Seq[gitbucket.core.util.JGitUtil.DiffInfo], + members: List[(String, String)], + comments: List[gitbucket.core.model.Comment], + originId: String, + forkedId: String, + sourceId: String, + commitId: String, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + originRepository: gitbucket.core.service.RepositoryService.RepositoryInfo, + forkedRepository: gitbucket.core.service.RepositoryService.RepositoryInfo, + hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(s"Pull Requests - ${repository.owner}/${repository.name}", Some(repository)){ + @html.menu("pulls", repository){ +
    +
    + Edit + @originRepository.owner:@originId ... @forkedRepository.owner:@forkedId +
    + +
    + @if(commits.nonEmpty && hasWritePermission){ + + + } + @if(commits.isEmpty){ + + + + +
    +

    There isn't anything to compare.

    + @originRepository.owner:@originId and @forkedRepository.owner:@forkedId are identical. +
    + } else { + @pulls.html.commits(commits, Some(comments), repository) + @helper.html.diff(diffs, repository, Some(commitId), Some(sourceId), true, None, hasWritePermission, false) +

    Showing you all comments on commits in this comparison.

    + @issues.html.commentlist(None, comments, hasWritePermission, repository, None) + } + } +} + diff --git a/src/main/twirl/gitbucket/core/pulls/conversation.scala.html b/src/main/twirl/gitbucket/core/pulls/conversation.scala.html new file mode 100644 index 0000000..3bead01 --- /dev/null +++ b/src/main/twirl/gitbucket/core/pulls/conversation.scala.html @@ -0,0 +1,82 @@ +@(issue: gitbucket.core.model.Issue, + pullreq: gitbucket.core.model.PullRequest, + comments: List[gitbucket.core.model.Comment], + issueLabels: List[gitbucket.core.model.Label], + collaborators: List[String], + milestones: List[(gitbucket.core.model.Milestone, Int, Int)], + labels: List[gitbucket.core.model.Label], + hasWritePermission: Boolean, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@import gitbucket.core.model._ +
    +
    +
    + @issues.html.commentlist(Some(issue), comments, hasWritePermission, repository, Some(pullreq)) +
    + @defining(comments.flatMap { + case comment: IssueComment => Some(comment) + case other => None + }.exists(_.action == "merge")){ merged => + @if(hasWritePermission && !issue.closed){ +
    +
    +
    + +
    + +
    +
    + } + @if(hasWritePermission && issue.closed && pullreq.userName == pullreq.requestUserName && merged && + pullreq.repositoryName == pullreq.requestRepositoryName && repository.branchList.contains(pullreq.requestBranch)){ +
    +
    + Delete branch +
    + Pull request successfully merged and closed +
    + You're all set-the @pullreq.requestBranch branch can be safely deleted. +
    +
    + } + @issues.html.commentform(issue, !merged, hasWritePermission, repository) + } +
    +
    + @issues.html.issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository) +
    +
    + \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/pulls/mergecheck.scala.html b/src/main/twirl/gitbucket/core/pulls/mergecheck.scala.html new file mode 100644 index 0000000..dcc5af8 --- /dev/null +++ b/src/main/twirl/gitbucket/core/pulls/mergecheck.scala.html @@ -0,0 +1,9 @@ +@(hasConflict: Boolean) +@if(hasConflict){ +

    We can’t automatically merge these branches

    +

    Don't worry, you can still submit the pull request.

    +} else { +

    Able to merge

    +

    These branches can be automatically merged.

    +} + diff --git a/src/main/twirl/gitbucket/core/pulls/mergeguide.scala.html b/src/main/twirl/gitbucket/core/pulls/mergeguide.scala.html new file mode 100644 index 0000000..18f0a36 --- /dev/null +++ b/src/main/twirl/gitbucket/core/pulls/mergeguide.scala.html @@ -0,0 +1,84 @@ +@(hasConflict: Boolean, + pullreq: gitbucket.core.model.PullRequest, + requestRepositoryUrl: String)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +
    + +
    +
    + @if(hasConflict){ + We can’t automatically merge this pull request. + } else { + This pull request can be automatically merged. + } +
    +
    + @if(hasConflict){ + Use the command line to resolve conflicts before continuing. + } else { + You can also merge branches on the command line. + } +
    + + \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/pulls/pullreq.scala.html b/src/main/twirl/gitbucket/core/pulls/pullreq.scala.html new file mode 100644 index 0000000..bcfd7d8 --- /dev/null +++ b/src/main/twirl/gitbucket/core/pulls/pullreq.scala.html @@ -0,0 +1,125 @@ +@(issue: gitbucket.core.model.Issue, + pullreq: gitbucket.core.model.PullRequest, + comments: List[gitbucket.core.model.Comment], + issueLabels: List[gitbucket.core.model.Label], + collaborators: List[String], + milestones: List[(gitbucket.core.model.Milestone, Int, Int)], + labels: List[gitbucket.core.model.Label], + dayByDayCommits: Seq[Seq[gitbucket.core.util.JGitUtil.CommitInfo]], + diffs: Seq[gitbucket.core.util.JGitUtil.DiffInfo], + hasWritePermission: Boolean, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@import gitbucket.core.model._ +@html.main(s"${issue.title} - Pull Request #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){ + @html.menu("pulls", repository){ + @defining(dayByDayCommits.flatten){ commits => +
    +
    + @if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){ + Edit + } + New issue +
    + +

    + + @issue.title + #@issue.issueId + + +

    +
    + @if(issue.closed) { + @comments.flatMap @{ + case comment: IssueComment => Some(comment) + case _ => None + }.find(_.action == "merge").map{ comment => + Merged + + @user(comment.commentedUserName, styleClass="username strong") merged @commits.size @plural(commits.size, "commit") + into @pullreq.userName:@pullreq.branch from @pullreq.requestUserName:@pullreq.requestBranch + @helper.html.datetimeago(comment.registeredDate) + + }.getOrElse { + Closed + + @user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit") + into @pullreq.userName:@pullreq.branch from @pullreq.requestUserName:@pullreq.requestBranch + + } + } else { + Open + + @user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit") + into @pullreq.userName:@pullreq.branch from @pullreq.requestUserName:@pullreq.requestBranch + + } +

    + +
    +
    + @pulls.html.conversation(issue, pullreq, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository) +
    +
    + @pulls.html.commits(dayByDayCommits, Some(comments), repository) +
    +
    + @helper.html.diff(diffs, repository, Some(commits.head.id), Some(commits.last.id), true, Some(pullreq.issueId), hasWritePermission, true) +
    +
    + } + } +} + \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/repo/blob.scala.html b/src/main/twirl/gitbucket/core/repo/blob.scala.html new file mode 100644 index 0000000..99f1062 --- /dev/null +++ b/src/main/twirl/gitbucket/core/repo/blob.scala.html @@ -0,0 +1,134 @@ +@(branch: String, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + pathList: List[String], + content: gitbucket.core.util.JGitUtil.ContentInfo, + latestCommit: gitbucket.core.util.JGitUtil.CommitInfo, + hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { + @html.menu("code", repository){ +
    + @helper.html.branchcontrol( + branch, + repository, + hasWritePermission + ){ + @repository.branchList.map { x => +
  • @helper.html.checkicon(x == branch) @x
  • + } + } + @repository.name / + @pathList.zipWithIndex.map { case (section, i) => + @if(i == pathList.length - 1){ + @section + } else { + @section / + } + } +
    + + + + + + + + +
    +
    + @avatar(latestCommit, 20) + @user(latestCommit.authorName, latestCommit.authorEmailAddress, "username strong") + @helper.html.datetimeago(latestCommit.commitTime) + @link(latestCommit.summary, repository) +
    +
    + @if(hasWritePermission && content.viewType == "text" && repository.branchList.contains(branch)){ + Edit + } + Raw + History + @if(hasWritePermission){ + Delete + } +
    +
    + @if(content.viewType == "text"){ + @defining(pathList.reverse.head) { file => + @if(renderableSuffixes.find(suffix => file.toLowerCase.endsWith(suffix))) { +
    + @renderMarkup(pathList, content.content.get, branch, repository, false, false) +
    + } else { +
    @content.content.get
    + } + } + } + @if(content.viewType == "image"){ + + } + @if(content.viewType == "large" || content.viewType == "binary"){ +
    + View Raw
    +
    + (Sorry about that, but we can't show files that are this big right now) +
    + } +
    + } +} + + \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/repo/branches.scala.html b/src/main/twirl/gitbucket/core/repo/branches.scala.html new file mode 100644 index 0000000..1352fe8 --- /dev/null +++ b/src/main/twirl/gitbucket/core/repo/branches.scala.html @@ -0,0 +1,83 @@ +@(branchInfo: Seq[(gitbucket.core.util.JGitUtil.BranchInfo, Option[(gitbucket.core.model.PullRequest, gitbucket.core.model.Issue)])], + hasWritePermission: Boolean, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { + @html.menu("code", repository){ +

    Branches

    + + + + + + + + @branchInfo.map { case (branch, prs) => + + } +
    All branches
    +
    + @branch.mergeInfo.map{ info => + @prs.map{ case (pull, issue) => + #@issue.issueId + @if(issue.closed) { + @if(info.isMerged){ + Merged + }else{ + Closed + } + } else { + Open + } + }.getOrElse{ + @if(context.loginAccount.isDefined){ + New Pull Request + }else{ + Compare + } + } + @if(hasWritePermission){ + @if(prs.map(!_._2.closed).getOrElse(false)){ + + }else{ + + } + } + } +
    +
    + @branch.name + + Updated @helper.html.datetimeago(branch.commitTime, false) + by @user(branch.committerName, branch.committerEmailAddress, "muted-link") + + +
    +
    + @if(branch.mergeInfo.isEmpty){ + Default + }else{ + @branch.mergeInfo.map{ info => +
    +
    +
    @info.ahead
    +
    @info.behind
    +
    +
    +
    + } + } + +
    + } +} + diff --git a/src/main/twirl/gitbucket/core/repo/commentform.scala.html b/src/main/twirl/gitbucket/core/repo/commentform.scala.html new file mode 100644 index 0000000..824f32f --- /dev/null +++ b/src/main/twirl/gitbucket/core/repo/commentform.scala.html @@ -0,0 +1,71 @@ +@(commitId: String, + fileName: Option[String] = None, + oldLineNumber: Option[Int] = None, + newLineNumber: Option[Int] = None, + issueId: Option[Int] = None, + hasWritePermission: Boolean, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@if(loginAccount.isDefined){ + @if(!fileName.isDefined){

    } +
    + @if(!fileName.isDefined){ +
    @avatar(loginAccount.get.userName, 48)
    + } +
    +
    + @helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 635px; height: 100px; max-height: 150px;", elastic = true) +
    + @if(fileName.isDefined){ +
    + + +
    + } +
    + @if(!fileName.isDefined){ +
    + +
    + } + @issueId.map { issueId => } + @fileName.map { fileName => } + @oldLineNumber.map { oldLineNumber => } + @newLineNumber.map { newLineNumber => } +
    + +} diff --git a/src/main/twirl/gitbucket/core/repo/commit.scala.html b/src/main/twirl/gitbucket/core/repo/commit.scala.html new file mode 100644 index 0000000..4588ff7 --- /dev/null +++ b/src/main/twirl/gitbucket/core/repo/commit.scala.html @@ -0,0 +1,154 @@ +@(commitId: String, + commit: gitbucket.core.util.JGitUtil.CommitInfo, + branches: List[String], + tags: List[String], + comments: List[gitbucket.core.model.Comment], + repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + diffs: Seq[gitbucket.core.util.JGitUtil.DiffInfo], + oldCommitId: Option[String], + hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context) +@import gitbucket.core.util.Implicits._ +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(commit.shortMessage, Some(repository)){ + @html.menu("code", repository){ + + + + + + + +
    + +
    @link(commit.summary, repository)
    + @if(commit.description.isDefined){ +
    @link(commit.description.get, repository)
    + } +
    + @if(branches.nonEmpty){ + + + @branches.zipWithIndex.map { case (branch, i) => + @branch + } + + } + @if(tags.nonEmpty){ + + + @tags.zipWithIndex.map { case (tag, i) => + @tag + } + + } +
    +
    +
    +
    + @if(commit.parents.size == 0){ + 0 parent + } + @if(commit.parents.size == 1){ + 1 parent + @commit.parents(0).substring(0, 7) + } + commit @commit.id +
    + @if(commit.parents.size > 1){ +
    + @commit.parents.size parents + @commit.parents.map { parent => + @parent.substring(0, 7) + }.mkHtml(" + ") + +
    + } +
    + +
    +
    + @avatar(commit, 20) + @user(commit.authorName, commit.authorEmailAddress, "username strong") + authored @helper.html.datetimeago(commit.authorTime) +
    + @if(commit.isDifferentFromAuthor) { +
    + + @user(commit.committerName, commit.committerEmailAddress, "username strong") + committed @helper.html.datetimeago(commit.commitTime) +
    + } +
    +
    + @helper.html.diff(diffs, repository, Some(commit.id), oldCommitId, true, None, hasWritePermission, true) + +
    + @issues.html.commentlist(None, comments, hasWritePermission, repository, None) +
    + @commentform(commitId = commitId, hasWritePermission = hasWritePermission, repository = repository) + } +} + + diff --git a/src/main/twirl/gitbucket/core/repo/commits.scala.html b/src/main/twirl/gitbucket/core/repo/commits.scala.html new file mode 100644 index 0000000..01b5d2c --- /dev/null +++ b/src/main/twirl/gitbucket/core/repo/commits.scala.html @@ -0,0 +1,81 @@ +@(pathList: List[String], + branch: String, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + commits: Seq[Seq[gitbucket.core.util.JGitUtil.CommitInfo]], + page: Int, + hasNext: Boolean, + hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { + @html.menu("code", repository){ +
    + @helper.html.branchcontrol( + branch, + repository, + hasWritePermission + ){ + @repository.branchList.map { x => +
  • @helper.html.checkicon(x == branch) @x
  • + } + } + @if(pathList.isEmpty){ + @repository.name / Commit History + } + @if(pathList.nonEmpty){ + History for + @repository.name / + @pathList.zipWithIndex.map { case (section, i) => + @if(i == pathList.length - 1){ + @section + } else { + @section / + } + } + } +
    + @commits.map { day => + + + + + @day.map { commit => + + + + } +
    @date(day.head.commitTime)
    + +
    +
    @avatar(commit, 40)
    +
    + @link(commit.summary, repository) + @if(commit.description.isDefined){ + ... + } +
    + @if(commit.description.isDefined){ + + } +
    + @user(commit.authorName, commit.authorEmailAddress, "username") + authored @helper.html.datetimeago(commit.authorTime) + @if(commit.isDifferentFromAuthor) { + + @user(commit.committerName, commit.committerEmailAddress, "username") + committed @helper.html.datetimeago(commit.authorTime) + } +
    +
    +
    +
    + } +
    + + +
    + } +} diff --git a/src/main/twirl/gitbucket/core/repo/delete.scala.html b/src/main/twirl/gitbucket/core/repo/delete.scala.html new file mode 100644 index 0000000..fd96793 --- /dev/null +++ b/src/main/twirl/gitbucket/core/repo/delete.scala.html @@ -0,0 +1,61 @@ +@(branch: String, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + pathList: List[String], + fileName: String, + content: gitbucket.core.util.JGitUtil.ContentInfo)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(s"Deleting ${path} at ${fileName} - ${repository.owner}/${repository.name}", Some(repository)) { + @html.menu("code", repository){ +
    +
    + @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/gitbucket/core/repo/editcomment.scala.html b/src/main/twirl/gitbucket/core/repo/editcomment.scala.html new file mode 100644 index 0000000..5e1c4cf --- /dev/null +++ b/src/main/twirl/gitbucket/core/repo/editcomment.scala.html @@ -0,0 +1,45 @@ +@(content: String, commentId: Int, owner: String, repository: String)(implicit context: gitbucket.core.controller.Context) +@import context._ + +@helper.html.attached(owner, repository){ + +} +
    + + +
    + diff --git a/src/main/twirl/gitbucket/core/repo/editor.scala.html b/src/main/twirl/gitbucket/core/repo/editor.scala.html new file mode 100644 index 0000000..27fa4fc --- /dev/null +++ b/src/main/twirl/gitbucket/core/repo/editor.scala.html @@ -0,0 +1,147 @@ +@(branch: String, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + pathList: List[String], + fileName: Option[String], + content: gitbucket.core.util.JGitUtil.ContentInfo)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(if(fileName.isEmpty) "New File" else s"Editing ${fileName.get} at ${branch} - ${repository.owner}/${repository.name}", Some(repository)) { + @html.menu("code", repository){ +
    + +
    + @repository.name / + @pathList.zipWithIndex.map { case (section, i) => + @section / + } + + + + +
    + + + + + + + +
    +
    + +
    +
    + + +
    +
    +
    + +
    +
    @avatar(loginAccount.get.userName, 48)
    +
    +
    +
    + Commit changes +
    +
    + +
    +
    + @if(fileName.isEmpty){ + Cancel + } else { + Cancel + } + + + + + +
    +
    +
    +
    + } +} + + + + + diff --git a/src/main/twirl/gitbucket/core/repo/files.scala.html b/src/main/twirl/gitbucket/core/repo/files.scala.html new file mode 100644 index 0000000..11d5f70 --- /dev/null +++ b/src/main/twirl/gitbucket/core/repo/files.scala.html @@ -0,0 +1,123 @@ +@(branch: String, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + pathList: List[String], + groupNames: List[String], + latestCommit: gitbucket.core.util.JGitUtil.CommitInfo, + files: List[gitbucket.core.util.JGitUtil.FileInfo], + readme: Option[(List[String], String)], + hasWritePermission: Boolean, + info: Option[Any] = None, + error: Option[Any] = None)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { + @html.menu("code", repository, Some(branch), pathList.isEmpty, groupNames.isEmpty, info, error){ +
    + @helper.html.branchcontrol( + branch, + repository, + hasWritePermission + ){ + @repository.branchList.map { x => +
  • @helper.html.checkicon(x == branch) @x
  • + } + } + @repository.name / + @pathList.zipWithIndex.map { case (section, i) => + @section / + } + @if(hasWritePermission){ + + } +
    + + + + + + + + @if(pathList.size > 0){ + + + + + + + } + @files.map { file => + + + + + + + } +
    + @link(latestCommit.summary, repository) + @if(latestCommit.description.isDefined){ + ... + + } +
    +
    + +
    +
    + @avatar(latestCommit, 20) + @user(latestCommit.authorName, latestCommit.authorEmailAddress, "username strong") + authored @helper.html.datetimeago(latestCommit.authorTime) +
    + @if(latestCommit.isDifferentFromAuthor) { +
    + + @user(latestCommit.committerName, latestCommit.committerEmailAddress, "username strong") + committed @helper.html.datetimeago(latestCommit.commitTime) +
    + } +
    +
    +
    ..
    + @if(file.isDirectory){ + @if(file.linkUrl.isDefined){ + + } else { + + } + } else { + + } + + @if(file.isDirectory){ + @if(file.linkUrl.isDefined){ + + @file.name.split("/").toList.init match { + case Nil => {} + case list => {@list.mkString("", "/", "/")} + }@file.name.split("/").toList.last + + } else { + + @file.name.split("/").toList.init match { + case Nil => {} + case list => {@list.mkString("", "/", "/")} + }@file.name.split("/").toList.last + + } + } else { + @file.name + } + + @link(file.message, repository) + [@user(file.author, file.mailAddress)] + @helper.html.datetimeago(file.time, false)
    + @readme.map { case(filePath, content) => +
    +
    @filePath.reverse.head
    +
    @renderMarkup(filePath, content, branch, repository, false, false)
    +
    + } + } +} diff --git a/src/main/twirl/gitbucket/core/repo/forked.scala.html b/src/main/twirl/gitbucket/core/repo/forked.scala.html new file mode 100644 index 0000000..63dd866 --- /dev/null +++ b/src/main/twirl/gitbucket/core/repo/forked.scala.html @@ -0,0 +1,35 @@ +@(originRepository: Option[gitbucket.core.service.RepositoryService.RepositoryInfo], + members: List[(String, String)], + repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { + @html.menu("network", repository){ + +

    Members of the @repository.name Network

    +
    + @if(originRepository.isDefined){ + @avatar(originRepository.get.owner, 20) + + @originRepository.get.owner / @originRepository.get.name + + } else { + @avatar(repository.repository.originUserName.get, 20) + + @repository.repository.originUserName / @repository.repository.originRepositoryName + + } + (origin) +
    + @members.map { case (owner, name) => +
    + @avatar(owner, 20) + + @owner / @name + +
    + } + } +} diff --git a/src/main/twirl/gitbucket/core/repo/guide.scala.html b/src/main/twirl/gitbucket/core/repo/guide.scala.html new file mode 100644 index 0000000..cdb4099 --- /dev/null +++ b/src/main/twirl/gitbucket/core/repo/guide.scala.html @@ -0,0 +1,43 @@ +@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context) +@import gitbucket.core.service.SystemSettingsService +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { + @html.menu("code", repository){ + @if(!hasWritePermission){ +

    This is an empty repository

    + } else { +

    Quick setup — if you've done this kind of thing before

    +
    + via HTTP + @if(settings.ssh && loginAccount.isDefined){ + or + SSH + } +
    +

    Create a new repository on the command line

    + @pre { + touch README.md + git init + git add README.md + git commit -m "first commit" + git remote add origin @repository.httpUrl + git push -u origin master + } +

    Push an existing repository from the command line

    + @pre { + git remote add origin @repository.httpUrl + git push -u origin master + } + + } + } +} \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/repo/tags.scala.html b/src/main/twirl/gitbucket/core/repo/tags.scala.html new file mode 100644 index 0000000..75c2634 --- /dev/null +++ b/src/main/twirl/gitbucket/core/repo/tags.scala.html @@ -0,0 +1,27 @@ +@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { + @html.menu("code", repository){ +

    Tags

    + + + + + + + + @repository.tags.reverse.map { tag => + + + + + + + } +
    TagDateCommitDownload
    @tag.name@helper.html.datetimeago(tag.time, false)@tag.id.substring(0, 10) + ZIP + TAR.GZ +
    + } +} \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/search/code.scala.html b/src/main/twirl/gitbucket/core/search/code.scala.html new file mode 100644 index 0000000..4ce3450 --- /dev/null +++ b/src/main/twirl/gitbucket/core/search/code.scala.html @@ -0,0 +1,26 @@ +@(files: List[gitbucket.core.service.RepositorySearchService.FileSearchResult], + issueCount: Int, + query: String, + page: Int, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@import gitbucket.core.service.RepositorySearchService._ +@html.main("Search Results", Some(repository)){ + @menu("code", files.size, issueCount, query, repository){ + @if(files.isEmpty){ +

    We couldn't find any code matching '@query'

    + } else { +

    We've found @files.size code @plural(files.size, "result")

    + } + @files.drop((page - 1) * CodeLimit).take(CodeLimit).map { file => +
    +
    @file.path
    +
    Last committed @helper.html.datetimeago(file.lastModified)
    +
    @Html(file.highlightText)
    +
    + } + @helper.html.paginator(page, files.size, CodeLimit, 10, + s"${url(repository)}/search?q=${urlEncode(query)}&type=code") + } +} \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/search/issues.scala.html b/src/main/twirl/gitbucket/core/search/issues.scala.html new file mode 100644 index 0000000..df7baa6 --- /dev/null +++ b/src/main/twirl/gitbucket/core/search/issues.scala.html @@ -0,0 +1,35 @@ +@(issues: List[gitbucket.core.service.RepositorySearchService.IssueSearchResult], + fileCount: Int, + query: String, + page: Int, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@import gitbucket.core.service.RepositorySearchService._ +@html.main("Search Results", Some(repository)){ + @menu("issue", fileCount, issues.size, query, repository){ + @if(issues.isEmpty){ +

    We couldn't find any code matching '@query'

    + } else { +

    We've found @issues.size code @plural(issues.size, "result")

    + } + @issues.drop((page - 1) * IssueLimit).take(IssueLimit).map { issue => +
    +
    #@issue.issueId
    +

    @issue.title

    + @if(issue.highlightText.nonEmpty){ +
    @Html(issue.highlightText)
    + } +
    + Opened by @issue.openedUserName + @helper.html.datetimeago(issue.registeredDate) + @if(issue.commentCount > 0){ +   @issue.commentCount @plural(issue.commentCount, "comment") + } +
    +
    + } + @helper.html.paginator(page, issues.size, IssueLimit, 10, + s"${url(repository)}/search?q=${urlEncode(query)}&type=issue") + } +} \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/search/menu.scala.html b/src/main/twirl/gitbucket/core/search/menu.scala.html new file mode 100644 index 0000000..93c7e38 --- /dev/null +++ b/src/main/twirl/gitbucket/core/search/menu.scala.html @@ -0,0 +1,38 @@ +@(active: String, fileCount: Int, issueCount: Int, query: String, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(body: Html)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.menu("", repository){ + +} \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/settings/collaborators.scala.html b/src/main/twirl/gitbucket/core/settings/collaborators.scala.html new file mode 100644 index 0000000..9eb2915 --- /dev/null +++ b/src/main/twirl/gitbucket/core/settings/collaborators.scala.html @@ -0,0 +1,35 @@ +@(collaborators: List[String], + isGroupRepository: Boolean, + repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main("Settings", Some(repository)){ + @html.menu("settings", repository){ + @menu("collaborators", repository){ +

    Manage Collaborators

    +
      + @collaborators.map { collaboratorName => +
    • + @collaboratorName + @if(!isGroupRepository){ + (remove) + } else { + @if(repository.managers.contains(collaboratorName)){ + (Manager) + } + } +
    • + } +
    + @if(!isGroupRepository){ +
    +
    + +
    + @helper.html.account("userName", 300) + +
    + } + } + } +} diff --git a/src/main/twirl/gitbucket/core/settings/danger.scala.html b/src/main/twirl/gitbucket/core/settings/danger.scala.html new file mode 100644 index 0000000..91ef578 --- /dev/null +++ b/src/main/twirl/gitbucket/core/settings/danger.scala.html @@ -0,0 +1,45 @@ +@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main("Danger Zone", Some(repository)){ + @html.menu("settings", repository){ + @menu("danger", repository){ +
    +
    Danger Zone
    +
    +
    +
    + +
    + Transfer this repo to another user or to group. +
    + @helper.html.account("newOwner", 150) + +
    + +
    +
    +
    +
    +
    +
    +
    + +
    + Once you delete a repository, there is no going back. + +
    +
    +
    +
    +
    + } + } +} + \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/settings/hooks.scala.html b/src/main/twirl/gitbucket/core/settings/hooks.scala.html new file mode 100644 index 0000000..bb6d681 --- /dev/null +++ b/src/main/twirl/gitbucket/core/settings/hooks.scala.html @@ -0,0 +1,27 @@ +@(webHooks: List[gitbucket.core.model.WebHook], + enteredUrl: Option[Any], + repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + info: Option[Any])(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main("Settings", Some(repository)){ + @html.menu("settings", repository){ + @menu("hooks", repository){ + @helper.html.information(info) +

    WebHook URLs

    +
      + @webHooks.map { webHook => +
    • @webHook.url (remove)
    • + } +
    +
    +
    + +
    + + + +
    + } + } +} diff --git a/src/main/twirl/gitbucket/core/settings/menu.scala.html b/src/main/twirl/gitbucket/core/settings/menu.scala.html new file mode 100644 index 0000000..94f7f81 --- /dev/null +++ b/src/main/twirl/gitbucket/core/settings/menu.scala.html @@ -0,0 +1,26 @@ +@(active: String, repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(body: Html)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +
    +
    +
    + +
    +
    +
    + @body +
    +
    diff --git a/src/main/twirl/gitbucket/core/settings/options.scala.html b/src/main/twirl/gitbucket/core/settings/options.scala.html new file mode 100644 index 0000000..e64287e --- /dev/null +++ b/src/main/twirl/gitbucket/core/settings/options.scala.html @@ -0,0 +1,100 @@ +@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo, info: Option[Any])(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main("Settings", Some(repository)){ + @html.menu("settings", repository){ + @menu("options", repository){ + @helper.html.information(info) +
    +
    +
    Settings
    +
    +
    + + + +
    +
    + + +
    +
    + + + @if(repository.branchList.isEmpty){ + + } + +
    +
    + +
    +
    + +
    +
    +
    + @* +
    +
    Features:
    +
    +
    +
    + +
    +
    + Adds lightweight Wiki system to this repository. + This is the simplest way to provide documentation or examples. + Only collaborators can edit Wiki pages. +
    +
    +
    +
    +
    + +
    +
    + Adds lightweight issue tracking integrated with this repository. + All users who have signed in and can access this repository can register an issue. +
    +
    +
    +
    + *@ +
    + +
    +
    + } + } +} diff --git a/src/main/twirl/gitbucket/core/signin.scala.html b/src/main/twirl/gitbucket/core/signin.scala.html new file mode 100644 index 0000000..ca80383 --- /dev/null +++ b/src/main/twirl/gitbucket/core/signin.scala.html @@ -0,0 +1,13 @@ +@()(implicit context: gitbucket.core.controller.Context) +@import context._ +@main("Sign in"){ + +} diff --git a/src/main/twirl/gitbucket/core/signinform.scala.html b/src/main/twirl/gitbucket/core/signinform.scala.html new file mode 100644 index 0000000..b3312f9 --- /dev/null +++ b/src/main/twirl/gitbucket/core/signinform.scala.html @@ -0,0 +1,29 @@ +@(systemSettings: gitbucket.core.service.SystemSettingsService.SystemSettings)(implicit context: gitbucket.core.controller.Context) +@import context._ + + + + + + + +
    + @if(systemSettings.allowAccountRegistration){ + + } + Sign in +
    +
    + + + + + + +
    + +
    +
    +
    \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/wiki/compare.scala.html b/src/main/twirl/gitbucket/core/wiki/compare.scala.html new file mode 100644 index 0000000..d793178 --- /dev/null +++ b/src/main/twirl/gitbucket/core/wiki/compare.scala.html @@ -0,0 +1,41 @@ +@(pageName: Option[String], + from: String, + to: String, + diffs: Seq[gitbucket.core.util.JGitUtil.DiffInfo], + repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + hasWritePermission: Boolean, + info: Option[Any])(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(s"Compare Revisions - ${repository.owner}/${repository.name}", Some(repository)){ + @helper.html.information(info) + @html.menu("wiki", repository){ + +
    + @helper.html.diff(diffs, repository, None, None, false, None, false, false) +
    + @if(hasWritePermission){ +
    + @if(pageName.isDefined){ + Revert Changes + } else { + Revert Changes + } +
    + } + } +} diff --git a/src/main/twirl/gitbucket/core/wiki/edit.scala.html b/src/main/twirl/gitbucket/core/wiki/edit.scala.html new file mode 100644 index 0000000..3b3e1a2 --- /dev/null +++ b/src/main/twirl/gitbucket/core/wiki/edit.scala.html @@ -0,0 +1,39 @@ +@(pageName: String, + page: Option[gitbucket.core.service.WikiService.WikiPageInfo], + repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(s"${if(pageName.isEmpty) "New Page" else pageName} - ${repository.owner}/${repository.name}", Some(repository)){ + @html.menu("wiki", repository){ + +
    + + + @helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, false, false, "width: 850px; height: 400px;", "") + + + + +
    + } +} + diff --git a/src/main/twirl/gitbucket/core/wiki/history.scala.html b/src/main/twirl/gitbucket/core/wiki/history.scala.html new file mode 100644 index 0000000..8c4d65c --- /dev/null +++ b/src/main/twirl/gitbucket/core/wiki/history.scala.html @@ -0,0 +1,77 @@ +@(pageName: Option[String], + commits: List[gitbucket.core.util.JGitUtil.CommitInfo], + repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(s"History - ${repository.owner}/${repository.name}", Some(repository)){ + @html.menu("wiki", repository){ + + + + + + @commits.map { commit => + + + + + + } +
    +
    Revisions
    +
    + +
    +
    @avatar(commit, 20) @user(commit.authorName, commit.authorEmailAddress) + @helper.html.datetimeago(commit.authorTime): @commit.shortMessage +
    + + } +} \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/wiki/page.scala.html b/src/main/twirl/gitbucket/core/wiki/page.scala.html new file mode 100644 index 0000000..02e5120 --- /dev/null +++ b/src/main/twirl/gitbucket/core/wiki/page.scala.html @@ -0,0 +1,74 @@ +@(pageName: String, + page: gitbucket.core.service.WikiService.WikiPageInfo, + pages: List[String], + repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@import gitbucket.core.service.WikiService._ +@html.main(s"${pageName} - ${repository.owner}/${repository.name}", Some(repository)){ + @html.menu("wiki", repository){ + +
    + + + + + + + +
    Pages @pages.length
    +
      + @pages.map { page => +
    • @page
    • + } +
    +
    +
    + Clone this wiki locally +
    + @helper.html.copy("repository-url-copy", httpUrl(repository)){ + + } + @if(settings.ssh && loginAccount.isDefined){ +
    + You can clone HTTP or SSH. +
    + } +
    +
    +
    + @markdown(page.content, repository, true, false, false, false, pages) +
    +
    + } +} +@if(settings.ssh && loginAccount.isDefined){ + +} \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/wiki/pages.scala.html b/src/main/twirl/gitbucket/core/wiki/pages.scala.html new file mode 100644 index 0000000..a8b5206 --- /dev/null +++ b/src/main/twirl/gitbucket/core/wiki/pages.scala.html @@ -0,0 +1,26 @@ +@(pages: List[String], + repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@html.main(s"Pages - ${repository.owner}/${repository.name}", Some(repository)){ + @html.menu("wiki", repository){ + +
      + @pages.map { page => +
    • @page
    • + } +
    + } +} \ No newline at end of file diff --git a/src/main/twirl/helper/account.scala.html b/src/main/twirl/helper/account.scala.html deleted file mode 100644 index e871d5d..0000000 --- a/src/main/twirl/helper/account.scala.html +++ /dev/null @@ -1,15 +0,0 @@ -@(id: String, width: Int)(implicit context: app.Context) -@import context._ - - diff --git a/src/main/twirl/helper/activities.scala.html b/src/main/twirl/helper/activities.scala.html deleted file mode 100644 index 34378af..0000000 --- a/src/main/twirl/helper/activities.scala.html +++ /dev/null @@ -1,99 +0,0 @@ -@(activities: List[model.Activity])(implicit context: app.Context) -@import context._ -@import view.helpers._ - -@if(activities.isEmpty){ - No activity -} else { - @activities.map { activity => -
    - @(activity.activityType match { - case "open_issue" => detailActivity(activity, "activity-issue.png") - case "comment_issue" => detailActivity(activity, "activity-comment.png") - case "comment_commit" => detailActivity(activity, "activity-comment.png") - case "close_issue" => detailActivity(activity, "activity-issue-close.png") - case "reopen_issue" => detailActivity(activity, "activity-issue-reopen.png") - case "open_pullreq" => detailActivity(activity, "activity-merge.png") - case "merge_pullreq" => detailActivity(activity, "activity-merge.png") - case "create_repository" => simpleActivity(activity, "activity-create-repository.png") - case "create_branch" => simpleActivity(activity, "activity-branch.png") - case "delete_branch" => simpleActivity(activity, "activity-delete.png") - case "create_tag" => simpleActivity(activity, "activity-tag.png") - case "delete_tag" => simpleActivity(activity, "activity-delete.png") - case "fork" => simpleActivity(activity, "activity-fork.png") - case "push" => customActivity(activity, "activity-commit.png"){ -
    - {activity.additionalInfo.get.split("\n").reverse.take(4).zipWithIndex.map{ case (commit, i) => - if(i == 3){ -
    ...
    - } else { - if(commit.nonEmpty){ -
    - {commit.substring(0, 7)} - {commit.substring(41)} -
    - } - } - }} -
    - } - case "create_wiki" => customActivity(activity, "activity-wiki.png"){ - - } - case "edit_wiki" => customActivity(activity, "activity-wiki.png"){ - activity.additionalInfo.get.split(":") match { - case Array(pageName, commitId) => - - case Array(pageName) => -
    - Edited {pageName}. -
    - } - } - }) -
    - } -} - -@detailActivity(activity: model.Activity, image: String) = { -
    -
    -
    @helper.html.datetimeago(activity.activityDate)
    -
    - @avatar(activity.activityUserName, 16) - @activityMessage(activity.message) -
    - @activity.additionalInfo.map { additionalInfo => -
    @additionalInfo
    - } -
    -} - -@customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = { -
    -
    -
    @helper.html.datetimeago(activity.activityDate)
    -
    - @avatar(activity.activityUserName, 16) - @activityMessage(activity.message) -
    - @additionalInfo -
    -} - -@simpleActivity(activity: model.Activity, image: String) = { -
    -
    -
    - @avatar(activity.activityUserName, 16) - @activityMessage(activity.message) - @helper.html.datetimeago(activity.activityDate) -
    -
    -} - diff --git a/src/main/twirl/helper/attached.scala.html b/src/main/twirl/helper/attached.scala.html deleted file mode 100644 index e0fbc5e..0000000 --- a/src/main/twirl/helper/attached.scala.html +++ /dev/null @@ -1,33 +0,0 @@ -@(owner: String, repository: String)(textarea: Html)(implicit context: app.Context) -@import context._ -
    - @textarea -
    Attach images by dragging & dropping, or selecting them.
    -
    -@defining("(id=\")([\\w\\-]*)(\")".r.findFirstMatchIn(textarea.body).map(_.group(2))){ textareaId => - -} diff --git a/src/main/twirl/helper/branchcontrol.scala.html b/src/main/twirl/helper/branchcontrol.scala.html deleted file mode 100644 index 5381688..0000000 --- a/src/main/twirl/helper/branchcontrol.scala.html +++ /dev/null @@ -1,62 +0,0 @@ -@(branch: String = "", - repository: service.RepositoryService.RepositoryInfo, - hasWritePermission: Boolean)(body: Html)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@helper.html.dropdown( - value = if(branch.length == 40) branch.substring(0, 10) else branch, - prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree", - mini = true -) { -
  • Switch branches
  • -
  • - @body - @if(hasWritePermission) { - - } -} - diff --git a/src/main/twirl/helper/checkicon.scala.html b/src/main/twirl/helper/checkicon.scala.html deleted file mode 100644 index b9726a7..0000000 --- a/src/main/twirl/helper/checkicon.scala.html +++ /dev/null @@ -1,6 +0,0 @@ -@(condition: => Boolean) -@if(condition){ - -} else { - -} \ No newline at end of file diff --git a/src/main/twirl/helper/commitcomment.scala.html b/src/main/twirl/helper/commitcomment.scala.html deleted file mode 100644 index 537b21c..0000000 --- a/src/main/twirl/helper/commitcomment.scala.html +++ /dev/null @@ -1,35 +0,0 @@ -@(comment: model.CommitComment, - hasWritePermission: Boolean, - repository: service.RepositoryService.RepositoryInfo, - latestCommitId: Option[String] = None)(implicit context: app.Context) -@import context._ -@import view.helpers._ -
    -
    @avatar(comment.commentedUserName, 48)
    -
    -
    - @user(comment.commentedUserName, styleClass="username strong") - - commented - @if(comment.pullRequest){ - on this Pull Request - }else{ - @if(comment.fileName.isDefined){ - on @comment.fileName.get - } - in @comment.commitId.substring(0, 7) - } - @helper.html.datetimeago(comment.registeredDate) - - - @if(hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false)){ -   - - } - -
    -
    - @markdown(comment.content, repository, false, true, true, hasWritePermission) -
    -
    -
    diff --git a/src/main/twirl/helper/copy.scala.html b/src/main/twirl/helper/copy.scala.html deleted file mode 100644 index 7a09a52..0000000 --- a/src/main/twirl/helper/copy.scala.html +++ /dev/null @@ -1,55 +0,0 @@ -@(id: String, value: String)(html: Html) -
    - @html - -
    - diff --git a/src/main/twirl/helper/datepicker.scala.html b/src/main/twirl/helper/datepicker.scala.html deleted file mode 100644 index 0247315..0000000 --- a/src/main/twirl/helper/datepicker.scala.html +++ /dev/null @@ -1,11 +0,0 @@ -@(name: String, value: Option[java.util.Date]) -@import view.helpers -
    - - -
    - diff --git a/src/main/twirl/helper/datetimeago.scala.html b/src/main/twirl/helper/datetimeago.scala.html deleted file mode 100644 index 3b68f34..0000000 --- a/src/main/twirl/helper/datetimeago.scala.html +++ /dev/null @@ -1,10 +0,0 @@ -@(latestUpdatedDate: java.util.Date, - recentOnly: Boolean = true) -@import view.helpers._ - - @if(recentOnly){ - @datetimeAgoRecentOnly(latestUpdatedDate) - }else{ - @datetimeAgo(latestUpdatedDate) - } - diff --git a/src/main/twirl/helper/diff.scala.html b/src/main/twirl/helper/diff.scala.html deleted file mode 100644 index 6c95cc5..0000000 --- a/src/main/twirl/helper/diff.scala.html +++ /dev/null @@ -1,251 +0,0 @@ -@(diffs: Seq[util.JGitUtil.DiffInfo], - repository: service.RepositoryService.RepositoryInfo, - newCommitId: Option[String], - oldCommitId: Option[String], - showIndex: Boolean, - issueId: Option[Int], - hasWritePermission: Boolean, - showLineNotes: Boolean)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@import org.eclipse.jgit.diff.DiffEntry.ChangeType -@if(showIndex){ - - -} -@diffs.zipWithIndex.map { case (diff, i) => - - - - - - - - -
    - @if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){ - @diff.oldPath -> @diff.newPath - @if(newCommitId.isDefined){ - - } - } - @if(diff.changeType == ChangeType.ADD || diff.changeType == ChangeType.MODIFY){ - @if(diff.changeType == ChangeType.ADD){ - - }else{ - - } @diff.newPath - @if(newCommitId.isDefined){ - - } - } - @if(diff.changeType == ChangeType.DELETE){ - @diff.oldPath - @if(oldCommitId.isDefined){ - - } - } -
    - @if(diff.newContent != None || diff.oldContent != None){ -
    - - - } else { - Not supported - } -
    -} - - - - diff --git a/src/main/twirl/helper/dropdown.scala.html b/src/main/twirl/helper/dropdown.scala.html deleted file mode 100644 index 20e2f4a..0000000 --- a/src/main/twirl/helper/dropdown.scala.html +++ /dev/null @@ -1,24 +0,0 @@ -@(value : String = "", - prefix: String = "", - mini : Boolean = true, - style : String = "", - right : Boolean = false, - flat : Boolean = false)(body: Html) -
    - - -
    diff --git a/src/main/twirl/helper/error.scala.html b/src/main/twirl/helper/error.scala.html deleted file mode 100644 index 00f43e2..0000000 --- a/src/main/twirl/helper/error.scala.html +++ /dev/null @@ -1,7 +0,0 @@ -@(error: Option[Any]) -@if(error.isDefined){ -
    - - @error -
    -} \ No newline at end of file diff --git a/src/main/twirl/helper/feed.scala.xml b/src/main/twirl/helper/feed.scala.xml deleted file mode 100644 index 7b1dd66..0000000 --- a/src/main/twirl/helper/feed.scala.xml +++ /dev/null @@ -1,27 +0,0 @@ -@(activities: List[model.Activity])(implicit context: app.Context) -@import context._ -@import view.helpers._ - - tag:@context.host,2013:gitbucket - Gitbucket's activities - - - Gitbucket - @context.baseUrl - - @datetimeRFC3339(if(activities.isEmpty) new java.util.Date else activities.map(_.activityDate).max) - @activities.map { activity => - - tag:@context.host,@date(activity.activityDate):activity:@activity.activityId - @datetimeRFC3339(activity.activityDate) - @datetimeRFC3339(activity.activityDate) - - @activity.activityType - - @activity.activityUserName - @url(activity.activityUserName) - - @activityMessage(activity.message) - - } - diff --git a/src/main/twirl/helper/forkrepository.scala.html b/src/main/twirl/helper/forkrepository.scala.html deleted file mode 100644 index e73ce34..0000000 --- a/src/main/twirl/helper/forkrepository.scala.html +++ /dev/null @@ -1,18 +0,0 @@ -@(repository: service.RepositoryService.RepositoryInfo, - groupAndPerm: Map[String, Boolean])(implicit context: app.Context) -@import context._ -@import view.helpers._ -

    Where should we fork this repository?

    -
    -
    -
    @avatar(loginAccount.get.userName, 100)@@@loginAccount.get.userName
    - @for((groupName, isManager) <- groupAndPerm) { - @if(isManager) { -
    @avatar(groupName, 100)@@@groupName
    - } else { -
    @avatar(groupName, 100)@@@groupName
    - } - } -
    - -
    \ No newline at end of file diff --git a/src/main/twirl/helper/information.scala.html b/src/main/twirl/helper/information.scala.html deleted file mode 100644 index ff382a2..0000000 --- a/src/main/twirl/helper/information.scala.html +++ /dev/null @@ -1,7 +0,0 @@ -@(info: Option[Any]) -@if(info.isDefined){ -
    - - @info -
    -} diff --git a/src/main/twirl/helper/paginator.scala.html b/src/main/twirl/helper/paginator.scala.html deleted file mode 100644 index 0925004..0000000 --- a/src/main/twirl/helper/paginator.scala.html +++ /dev/null @@ -1,34 +0,0 @@ -@(page: Int, count: Int, limit: Int, width: Int, baseURL: String) -@defining(view.Pagination(page, count, limit, width)){ p => - @if(p.count > p.limit){ - - } -} diff --git a/src/main/twirl/helper/preview.scala.html b/src/main/twirl/helper/preview.scala.html deleted file mode 100644 index 9a0f0bb..0000000 --- a/src/main/twirl/helper/preview.scala.html +++ /dev/null @@ -1,49 +0,0 @@ -@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean, hasWritePermission: Boolean, - style: String = "", placeholder: String = "Leave a comment", elastic: Boolean = false, uid: Long = new java.util.Date().getTime())(implicit context: app.Context) -@import context._ -@import view.helpers._ -
    - -
    -
    - - @textarea = { - - } - @if(enableWikiLink){ - @textarea - } else { - @helper.html.attached(repository.owner, repository.name)(textarea) - } -
    -
    -
    -
    -
    -
    -
    - - - diff --git a/src/main/twirl/helper/repositoryicon.scala.html b/src/main/twirl/helper/repositoryicon.scala.html deleted file mode 100644 index 95daa2d..0000000 --- a/src/main/twirl/helper/repositoryicon.scala.html +++ /dev/null @@ -1,12 +0,0 @@ -@(repository: service.RepositoryService.RepositoryInfo, large: Boolean)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@if(repository.repository.isPrivate){ - -} else { - @if(repository.repository.originUserName.isDefined){ - - } else { - - } -} diff --git a/src/main/twirl/helper/uploadavatar.scala.html b/src/main/twirl/helper/uploadavatar.scala.html deleted file mode 100644 index 06bc06e..0000000 --- a/src/main/twirl/helper/uploadavatar.scala.html +++ /dev/null @@ -1,51 +0,0 @@ -@(account: Option[model.Account])(implicit context: app.Context) -@import context._ -
    - @if(account.nonEmpty && account.get.image.nonEmpty){ - - } else { -
    Upload Image
    - } -
    -@if(account.nonEmpty && account.get.image.nonEmpty){ - -} - -@if(account.isEmpty || account.get.image.isEmpty){ - -} - \ No newline at end of file diff --git a/src/main/twirl/index.scala.html b/src/main/twirl/index.scala.html deleted file mode 100644 index 816569f..0000000 --- a/src/main/twirl/index.scala.html +++ /dev/null @@ -1,80 +0,0 @@ -@(activities: List[model.Activity], - recentRepositories: List[service.RepositoryService.RepositoryInfo], - userRepositories: List[service.RepositoryService.RepositoryInfo])(implicit context: app.Context) -@import context._ -@import view.helpers._ -@main("GitBucket"){ - @dashboard.html.tab() -
    -
    -
    -
    - activities -
    - @helper.html.activities(activities) -
    - -
    - @settings.information.map { information => -
    - - @Html(information) -
    - } - @if(loginAccount.isEmpty){ - @signinform(settings) - } else { - - - - - @if(userRepositories.isEmpty){ - - - - } else { - @userRepositories.map { repository => - - - - } - } -
    - - Your repositories (@userRepositories.size) -
    No repositories
    - @helper.html.repositoryicon(repository, false) - @if(repository.owner == loginAccount.get.userName){ - @repository.name - } else { - @repository.owner/@repository.name - } -
    - } - - - - - @if(recentRepositories.isEmpty){ - - - - } else { - @recentRepositories.map { repository => - - - - } - } -
    - Recent updated repositories -
    No repositories
    - @helper.html.repositoryicon(repository, false) - @repository.owner/@repository.name -
    -
    -
    -
    -} diff --git a/src/main/twirl/issues/commentform.scala.html b/src/main/twirl/issues/commentform.scala.html deleted file mode 100644 index 04169c7..0000000 --- a/src/main/twirl/issues/commentform.scala.html +++ /dev/null @@ -1,31 +0,0 @@ -@(issue: model.Issue, - reopenable: Boolean, - hasWritePermission: Boolean, - repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@if(loginAccount.isDefined){ -

    -
    -
    @avatar(loginAccount.get.userName, 48)
    -
    -
    - @helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 635px; height: 100px; max-height: 150px;", elastic = true) -
    -
    -
    - - - @if((reopenable || !issue.closed) && (hasWritePermission || issue.openedUserName == loginAccount.get.userName)){ - - } -
    -
    -} - diff --git a/src/main/twirl/issues/commentlist.scala.html b/src/main/twirl/issues/commentlist.scala.html deleted file mode 100644 index d28c17c..0000000 --- a/src/main/twirl/issues/commentlist.scala.html +++ /dev/null @@ -1,263 +0,0 @@ -@(issue: Option[model.Issue], - comments: List[model.Comment], - hasWritePermission: Boolean, - repository: service.RepositoryService.RepositoryInfo, - pullreq: Option[model.PullRequest] = None)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@if(issue.isDefined){ -
    @avatar(issue.get.openedUserName, 48)
    -
    -
    - @user(issue.get.openedUserName, styleClass="username strong") commented @helper.html.datetimeago(issue.get.registeredDate) - - @if(hasWritePermission || loginAccount.map(_.userName == issue.get.openedUserName).getOrElse(false)){ - - } - -
    -
    - @markdown(issue.get.content getOrElse "No description provided.", repository, false, true, true, hasWritePermission) -
    -
    -} - -@comments.map { - case comment: model.IssueComment => { - @if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){ -
    @avatar(comment.commentedUserName, 48)
    -
    -
    - @user(comment.commentedUserName, styleClass="username strong") - - @if(comment.action == "comment"){ - commented - } else { - @if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request } - } - @helper.html.datetimeago(comment.registeredDate) - - - @if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" - && (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){ -   - - } - -
    -
    - @if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){ - @defining(comment.content.substring(comment.content.length - 40)){ id => - - @markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true, true, hasWritePermission) - } - } else { - @if(comment.action == "refer"){ - @defining(comment.content.split(":")){ case Array(issueId, rest @ _*) => - Issue #@issueId: @rest.mkString(":") - } - } else { - @markdown(comment.content, repository, false, true, true, hasWritePermission) - } - } -
    -
    - } - @if(comment.action == "merge"){ -
    - Merged - @avatar(comment.commentedUserName, 20) - @user(comment.commentedUserName, styleClass="username strong") merged commit @pullreq.map(_.commitIdTo.substring(0, 7)) into - @if(pullreq.get.requestUserName == repository.owner){ - @pullreq.map(_.branch) from @pullreq.map(_.requestBranch) - } else { - @pullreq.map(_.userName):@pullreq.map(_.branch) from @pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch) - } - @helper.html.datetimeago(comment.registeredDate) -
    - } - @if(comment.action == "close" || comment.action == "close_comment"){ -
    - Closed - @avatar(comment.commentedUserName, 20) - @if(issue.isDefined && issue.get.isPullRequest){ - @user(comment.commentedUserName, styleClass="username strong") closed the pull request @helper.html.datetimeago(comment.registeredDate) - } else { - @user(comment.commentedUserName, styleClass="username strong") closed the issue @helper.html.datetimeago(comment.registeredDate) - } -
    - } - @if(comment.action == "reopen" || comment.action == "reopen_comment"){ -
    - Reopened - @avatar(comment.commentedUserName, 20) - @user(comment.commentedUserName, styleClass="username strong") reopened the issue @helper.html.datetimeago(comment.registeredDate) -
    - } - @if(comment.action == "delete_branch"){ -
    - Deleted - @avatar(comment.commentedUserName, 20) - @user(comment.commentedUserName, styleClass="username strong") deleted the @pullreq.map(_.requestBranch) branch @helper.html.datetimeago(comment.registeredDate) -
    - } - } - case comment: model.CommitComment => { - @helper.html.commitcomment(comment, hasWritePermission, repository, pullreq.map(_.commitIdTo)) - } -} - diff --git a/src/main/twirl/issues/create.scala.html b/src/main/twirl/issues/create.scala.html deleted file mode 100644 index 4062651..0000000 --- a/src/main/twirl/issues/create.scala.html +++ /dev/null @@ -1,148 +0,0 @@ -@(collaborators: List[String], - milestones: List[model.Milestone], - labels: List[model.Label], - hasWritePermission: Boolean, - repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){ - @html.menu("issues", repository){ - @navigation("issues", false, repository) -


    -
    -
    -
    -
    @avatar(loginAccount.get.userName, 48)
    -
    -
    - - -
    - No one is assigned - @if(hasWritePermission){ - - @helper.html.dropdown() { -
  • Clear assignee
  • - @collaborators.map { collaborator => -
  • @avatar(collaborator, 20) @collaborator
  • - } - } - } -
    - No milestone - @if(hasWritePermission){ - - @helper.html.dropdown() { -
  • Clear this milestone
  • - @milestones.filter(_.closedDate.isEmpty).map { milestone => -
  • - - @milestone.title -
    - @milestone.dueDate.map { dueDate => - @if(isPast(dueDate)){ - Due by @date(dueDate) - } else { - Due by @date(dueDate) - } - }.getOrElse { - No due date - } -
    -
    -
  • - } - } - } -
    -
    -
    - @helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 565px; height: 200px; max-height: 250px;", elastic = true) -
    -
    -
    - -
    -
    -
    - @if(hasWritePermission){ - Labels -
    -
    - - -
    -
    - } -
    -
    -
    - } -} - diff --git a/src/main/twirl/issues/editcomment.scala.html b/src/main/twirl/issues/editcomment.scala.html deleted file mode 100644 index 2353de4..0000000 --- a/src/main/twirl/issues/editcomment.scala.html +++ /dev/null @@ -1,42 +0,0 @@ -@(content: String, commentId: Int, owner: String, repository: String)(implicit context: app.Context) -@import context._ - -@helper.html.attached(owner, repository){ - -} -
    - - -
    - diff --git a/src/main/twirl/issues/editissue.scala.html b/src/main/twirl/issues/editissue.scala.html deleted file mode 100644 index 8ce1354..0000000 --- a/src/main/twirl/issues/editissue.scala.html +++ /dev/null @@ -1,38 +0,0 @@ -@(content: Option[String], issueId: Int, owner: String, repository: String)(implicit context: app.Context) -@import context._ -@helper.html.attached(owner, repository){ - -} -
    - - -
    - diff --git a/src/main/twirl/issues/issue.scala.html b/src/main/twirl/issues/issue.scala.html deleted file mode 100644 index ba17a12..0000000 --- a/src/main/twirl/issues/issue.scala.html +++ /dev/null @@ -1,92 +0,0 @@ -@(issue: model.Issue, - comments: List[model.IssueComment], - issueLabels: List[model.Label], - collaborators: List[String], - milestones: List[(model.Milestone, Int, Int)], - labels: List[model.Label], - hasWritePermission: Boolean, - repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){ - @html.menu("issues", repository){ -
    -
    - @if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){ - Edit - } - New issue -
    - -

    - - @issue.title - #@issue.issueId - - -

    -
    - @if(issue.closed) { - Closed - } else { - Open - } - - @user(issue.openedUserName, styleClass="username strong") opened this issue @helper.html.datetimeago(issue.registeredDate) - @defining( - comments.count( _.action.contains("comment") ) - ){ count => - @count @plural(count, "comment") - } - -

    -
    -
    -
    - @commentlist(Some(issue), comments, hasWritePermission, repository) - @commentform(issue, true, hasWritePermission, repository) -
    -
    - @issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository) -
    -
    - } -} - diff --git a/src/main/twirl/issues/issueinfo.scala.html b/src/main/twirl/issues/issueinfo.scala.html deleted file mode 100644 index 0b1c5df..0000000 --- a/src/main/twirl/issues/issueinfo.scala.html +++ /dev/null @@ -1,173 +0,0 @@ -@(issue: model.Issue, - comments: List[model.Comment], - issueLabels: List[model.Label], - collaborators: List[String], - milestones: List[(model.Milestone, Int, Int)], - labels: List[model.Label], - hasWritePermission: Boolean, - repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import view.helpers._ -
    - Labels - @if(hasWritePermission){ -
    - @helper.html.dropdown(right = true) { - @labels.map { label => -
  • - - @helper.html.checkicon(issueLabels.exists(_.labelId == label.labelId)) -   - @label.labelName - -
  • - } - } -
    - } -
    - -
    - -
    - @issue.milestoneId.map { milestoneId => - @milestones.collect { case (milestone, openCount, closeCount) if(milestone.milestoneId == milestoneId) => - @issues.milestones.html.progress(openCount + closeCount, closeCount) - } - } -
    - - @issue.milestoneId.map { milestoneId => - @milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) => - @milestone.title - } - }.getOrElse { - No milestone - } - -
    -
    - Assignee - @if(hasWritePermission){ -
    - @helper.html.dropdown(right = true) { -
  • Clear assignee
  • - @collaborators.map { collaborator => -
  • - - @helper.html.checkicon(Some(collaborator) == issue.assignedUserName)@avatar(collaborator, 20) @collaborator - -
  • - } - } -
    - } -
    - - @issue.assignedUserName.map { userName => - @avatar(userName, 20) @user(userName, styleClass="username strong small") - }.getOrElse{ - No one - } - -
    -
    - @defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants => -
    @participants.size @plural(participants.size, "participant")
    - @participants.map { participant => @avatarLink(participant, 20, tooltip = true) } - } -
    - diff --git a/src/main/twirl/issues/labellist.scala.html b/src/main/twirl/issues/labellist.scala.html deleted file mode 100644 index ded3112..0000000 --- a/src/main/twirl/issues/labellist.scala.html +++ /dev/null @@ -1,7 +0,0 @@ -@(issueLabels: List[model.Label]) -@if(issueLabels.isEmpty){ -
  • None yet
  • -} -@issueLabels.map { label => -
  • @label.labelName
  • -} diff --git a/src/main/twirl/issues/labels/edit.scala.html b/src/main/twirl/issues/labels/edit.scala.html deleted file mode 100644 index dd558d2..0000000 --- a/src/main/twirl/issues/labels/edit.scala.html +++ /dev/null @@ -1,61 +0,0 @@ -@(label: Option[model.Label], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@defining(label.map(_.labelId).getOrElse("new")){ labelId => -
    -
    - -
    - - -
    - - - - - - -
    -
    - -} diff --git a/src/main/twirl/issues/labels/label.scala.html b/src/main/twirl/issues/labels/label.scala.html deleted file mode 100644 index 3b0e60b..0000000 --- a/src/main/twirl/issues/labels/label.scala.html +++ /dev/null @@ -1,36 +0,0 @@ -@(label: model.Label, - counts: Map[String, Int], - repository: service.RepositoryService.RepositoryInfo, - hasWritePermission: Boolean)(implicit context: app.Context) -@import context._ -@import view.helpers._ - - -
    - -
    -
    - @counts.get(label.labelName).getOrElse(0) open issues -
    -
    - @if(hasWritePermission){ -
    -
    - Edit -    - Delete -
    -
    - } -
    - - diff --git a/src/main/twirl/issues/labels/list.scala.html b/src/main/twirl/issues/labels/list.scala.html deleted file mode 100644 index 471dcf5..0000000 --- a/src/main/twirl/issues/labels/list.scala.html +++ /dev/null @@ -1,67 +0,0 @@ -@(labels: List[model.Label], - counts: Map[String, Int], - repository: service.RepositoryService.RepositoryInfo, - hasWritePermission: Boolean)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(s"Labels - ${repository.owner}/${repository.name}"){ - @html.menu("issues", repository){ - @issues.html.navigation("labels", hasWritePermission, repository) -
    - - - - - - - - @labels.map { label => - @_root_.issues.labels.html.label(label, counts, repository, hasWritePermission) - } - @if(labels.isEmpty){ - - - - } -
    - @labels.size labels -
    - No labels to show. - @if(hasWritePermission){ - Create a new label. - } -
    - } -} - diff --git a/src/main/twirl/issues/list.scala.html b/src/main/twirl/issues/list.scala.html deleted file mode 100644 index b1e36b3..0000000 --- a/src/main/twirl/issues/list.scala.html +++ /dev/null @@ -1,83 +0,0 @@ -@(target: String, - issues: List[service.IssuesService.IssueInfo], - page: Int, - collaborators: List[String], - milestones: List[model.Milestone], - labels: List[model.Label], - openCount: Int, - closedCount: Int, - condition: service.IssuesService.IssueSearchCondition, - repository: service.RepositoryService.RepositoryInfo, - hasWritePermission: Boolean)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(repository)){ - @html.menu(target, repository){ - @navigation(target, true, repository, Some(condition)) - @listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission) - @if(hasWritePermission){ -
    - - - -
    - } - } -} -@if(hasWritePermission){ - -} diff --git a/src/main/twirl/issues/listparts.scala.html b/src/main/twirl/issues/listparts.scala.html deleted file mode 100644 index 6c93789..0000000 --- a/src/main/twirl/issues/listparts.scala.html +++ /dev/null @@ -1,218 +0,0 @@ -@(target: String, - issues: List[service.IssuesService.IssueInfo], - page: Int, - openCount: Int, - closedCount: Int, - condition: service.IssuesService.IssueSearchCondition, - collaborators: List[String] = Nil, - milestones: List[model.Milestone] = Nil, - labels: List[model.Label] = Nil, - repository: Option[service.RepositoryService.RepositoryInfo] = None, - hasWritePermission: Boolean = false)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@import service.IssuesService.IssueInfo -
    -@if(condition.nonEmpty){ - -} - - - - - @if(issues.isEmpty){ - - - - } - @issues.map { case IssueInfo(issue, labels, milestone, commentCount) => - - - - } -
    - - - - - @openCount Open -    - - - @closedCount Closed - - -
    - @helper.html.dropdown("Author", flat = true) { - @collaborators.map { collaborator => -
  • - - @helper.html.checkicon(condition.author == Some(collaborator)) - @avatar(collaborator, 20) @collaborator - -
  • - } - } - @helper.html.dropdown("Label", flat = true) { - @labels.map { label => -
  • - - @helper.html.checkicon(condition.labels.contains(label.labelName)) -    - @label.labelName - -
  • - } - } - @helper.html.dropdown("Milestone", flat = true) { -
  • - - @helper.html.checkicon(condition.milestone == Some(None)) Issues with no milestone - -
  • - @milestones.filter(_.closedDate.isEmpty).map { milestone => -
  • - - @helper.html.checkicon(condition.milestone == Some(Some(milestone.title))) @milestone.title - -
  • - } - } - @helper.html.dropdown("Assignee", flat = true) { - @collaborators.map { collaborator => -
  • - - @helper.html.checkicon(condition.assigned == Some(collaborator)) - @avatar(collaborator, 20) @collaborator - -
  • - } - } - @helper.html.dropdown("Sort", flat = true){ -
  • - - @helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest - -
  • -
  • - - @helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest - -
  • -
  • - - @helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented - -
  • -
  • - - @helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented - -
  • -
  • - - @helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated - -
  • -
  • - - @helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated - -
  • - } -
    - @if(hasWritePermission){ -
    - @helper.html.dropdown("Mark as", flat = true) { -
  • Open
  • -
  • Close
  • - } - @helper.html.dropdown("Label", flat = true) { - @labels.map { label => -
  • - - -   - @label.labelName - -
  • - } - } - @helper.html.dropdown("Milestone", flat = true) { -
  • No milestone
  • - @milestones.filter(_.closedDate.isEmpty).map { milestone => -
  • @milestone.title
  • - } - } - @helper.html.dropdown("Assignee", flat = true) { -
  • Clear assignee
  • - @collaborators.map { collaborator => -
  • @avatar(collaborator, 20) @collaborator
  • - } - } -
    - } -
    - @if(target == "issues"){ - No issues to show. - } else { - No pull requests to show. - } - @if(condition.labels.nonEmpty || condition.milestone.isDefined){ - Clear active filters. - } else { - @if(repository.isDefined){ - @if(target == "issues"){ - Create a new issue. - } else { - Create a new pull request. - } - } - } -
    - @if(hasWritePermission){ - - } - - @if(repository.isEmpty){ - @issue.repositoryName ・ - } - @if(target == "issues"){ - @issue.title - } else { - @issue.title - } - @labels.map { label => - @label.labelName - } - - @issue.assignedUserName.map { userName => - @avatar(userName, 20, tooltip = true) - } - @if(commentCount > 0){ - - @commentCount - - } else { - - @commentCount - - } - -
    - #@issue.issueId opened @helper.html.datetimeago(issue.registeredDate) by @user(issue.openedUserName, styleClass="username") - @milestone.map { milestone => - @milestone - } -
    -
    -
    - @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL) -
    - diff --git a/src/main/twirl/issues/milestones/edit.scala.html b/src/main/twirl/issues/milestones/edit.scala.html deleted file mode 100644 index f876cf4..0000000 --- a/src/main/twirl/issues/milestones/edit.scala.html +++ /dev/null @@ -1,46 +0,0 @@ -@(milestone: Option[model.Milestone], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(s"Milestones - ${repository.owner}/${repository.name}"){ - @html.menu("issues", repository){ - @if(milestone.isEmpty){ -

    New milestone

    -
    Create a new milestone to help organize your issues and pull requests.
    - } else { - @issues.html.navigation("milestones", false, repository) -

    - } -
    -
    -
    - - -
    -
    - - - -
    -
    - - @helper.html.datepicker("dueDate", milestone.flatMap(_.dueDate)) - -
    -
    -
    - @if(milestone.isEmpty){ - - } else { - @if(milestone.get.closedDate.isDefined){ - - } else { - - } - - } -
    -
    - } -} diff --git a/src/main/twirl/issues/milestones/list.scala.html b/src/main/twirl/issues/milestones/list.scala.html deleted file mode 100644 index 17476d1..0000000 --- a/src/main/twirl/issues/milestones/list.scala.html +++ /dev/null @@ -1,105 +0,0 @@ -@(state: String, - milestones: List[(model.Milestone, Int, Int)], - repository: service.RepositoryService.RepositoryInfo, - hasWritePermission: Boolean)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(s"Milestones - ${repository.owner}/${repository.name}"){ - @html.menu("issues", repository){ - @issues.html.navigation("milestones", hasWritePermission, repository) -
    - - - - - @defining(milestones.filter { case (milestone, _, _) => - milestone.closedDate.map(_ => state == "closed").getOrElse(state == "open") - }){ milestones => - @milestones.map { case (milestone, openCount, closedCount) => - - - - } - @if(milestones.isEmpty){ - - - - } - } -
    - - - - @milestones.count(_._1.closedDate.isEmpty) Open -    - - - @milestones.count(_._1.closedDate.isDefined) Closed - - -
    -
    -
    - @milestone.title -
    - @if(milestone.closedDate.isDefined){ - Closed @helper.html.datetimeago(milestone.closedDate.get) - } else { - @milestone.dueDate.map { dueDate => - @if(isPast(dueDate)){ - Due by @date(dueDate) - } else { - Due by @date(dueDate) - } - }.getOrElse { - No due date - } - } -
    -
    -
    - @progress(openCount + closedCount, closedCount) -
    -
    - @if(closedCount == 0){ - 0% - } else { - @((closedCount.toDouble / (openCount + closedCount).toDouble * 100).toInt)% - } complete    - @openCount open    - @closedCount closed -
    -
    - @if(hasWritePermission){ - Edit    - @if(milestone.closedDate.isDefined){ - Open    - } else { - Close    - } - Delete - } -
    -
    -
    -
    - @if(milestone.description.isDefined){ -
    - @markdown(milestone.description.get, repository, false, false) -
    - } -
    - No milestones to show. - @if(hasWritePermission){ - Create a new milestone. - } -
    - } -} - diff --git a/src/main/twirl/issues/milestones/progress.scala.html b/src/main/twirl/issues/milestones/progress.scala.html deleted file mode 100644 index d099190..0000000 --- a/src/main/twirl/issues/milestones/progress.scala.html +++ /dev/null @@ -1,6 +0,0 @@ -@(total: Int, progress: Int) -
    - @if(progress > 0){ - - } -
    diff --git a/src/main/twirl/issues/navigation.scala.html b/src/main/twirl/issues/navigation.scala.html deleted file mode 100644 index 9c1ee8c..0000000 --- a/src/main/twirl/issues/navigation.scala.html +++ /dev/null @@ -1,58 +0,0 @@ -@(active: String, - newButton: Boolean, - repository: service.RepositoryService.RepositoryInfo, - condition: Option[service.IssuesService.IssueSearchCondition] = None)(implicit context: app.Context) -@import context._ -@import view.helpers._ - diff --git a/src/main/twirl/main.scala.html b/src/main/twirl/main.scala.html deleted file mode 100644 index 6d35ff1..0000000 --- a/src/main/twirl/main.scala.html +++ /dev/null @@ -1,91 +0,0 @@ -@(title: String, repository: Option[service.RepositoryService.RepositoryInfo] = None)(body: Html)(implicit context: app.Context) -@import context._ -@import view.helpers._ - - - - - @title - - - - - - - - - - - - - - - - - - - - - - - - - - - @body - - @plugin.PluginRegistry().getJavaScript(request.getRequestURI).map { script => - - } - - diff --git a/src/main/twirl/menu.scala.html b/src/main/twirl/menu.scala.html deleted file mode 100644 index 8eb8800..0000000 --- a/src/main/twirl/menu.scala.html +++ /dev/null @@ -1,226 +0,0 @@ -@(active: String, - repository: service.RepositoryService.RepositoryInfo, - id: Option[String] = None, - expand: Boolean = false, - isNoGroup: Boolean = true, - info: Option[Any] = None, - error: Option[Any] = None)(body: Html)(implicit context: app.Context) -@import context._ -@import view.helpers._ - -@sidemenu(path: String, name: String, label: String, count: Int = 0) = { -
  • -
    - - @if(active == name){ - - } else { - - - } - @if(expand){ @label} - @if(expand && count > 0){ -
    @count
    - } -
    -
  • -} - -@sidemenuPlugin(path: String, name: String, label: String, icon: String) = { -
  • -
    - @if(expand){ @label} -
  • -} - -
    - @helper.html.information(info) - @helper.html.error(error) - @if(repository.commitCount > 0){ -
    -
    - @if(loginAccount.isEmpty){ - Fork - } else { - @if(isNoGroup) { - Fork - } else { - Fork - } - } - @repository.forkedCount -
    -
    - } -
    - @helper.html.repositoryicon(repository, true) - @repository.owner / @repository.name - - @defining(repository.repository){ x => - @if(repository.repository.originRepositoryName.isDefined){ - - } - } -
    -
    -
    -
    -
    -
      -
    • - @sidemenu("" , "code" , "Code") - @sidemenu("/issues", "issues", "Issues", repository.issueCount) - @sidemenu("/pulls" , "pulls" , "Pull Requests", repository.pullCount) - @sidemenu("/wiki" , "wiki" , "Wiki") - @if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){ - @sidemenu("/settings", "settings", "Settings") - } -
    • -
    - @if(expand){ -
    - HTTP clone URL -
    - @helper.html.copy("repository-url-copy", repository.httpUrl){ - - } - @if(settings.ssh && loginAccount.isDefined){ -
    - You can clone HTTP or SSH. -
    - } - @id.map { id => - - - } - } -
    -
    - @if(expand){ - @repository.repository.description.map { description => -

    @description

    - } - - } - @body -
    -
    - diff --git a/src/main/twirl/pulls/commits.scala.html b/src/main/twirl/pulls/commits.scala.html deleted file mode 100644 index a81588f..0000000 --- a/src/main/twirl/pulls/commits.scala.html +++ /dev/null @@ -1,34 +0,0 @@ -@(commits: Seq[Seq[util.JGitUtil.CommitInfo]], - comments: Option[List[model.Comment]] = None, - repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -
    - - @commits.map { day => - - - - @day.map { commit => - - - - - - - } - } -
    @date(day.head.commitTime)
    - @avatar(commit, 20) - @user(commit.authorName, commit.authorEmailAddress, "username") - @commit.shortMessage - @if(comments.isDefined){ - @comments.get.flatMap @{ - case comment: model.CommitComment => Some(comment) - case other => None - }.count(t => t.commitId == commit.id && !t.pullRequest) - } - - @commit.id.substring(0, 7) -
    -
    diff --git a/src/main/twirl/pulls/compare.scala.html b/src/main/twirl/pulls/compare.scala.html deleted file mode 100644 index b606715..0000000 --- a/src/main/twirl/pulls/compare.scala.html +++ /dev/null @@ -1,155 +0,0 @@ -@(commits: Seq[Seq[util.JGitUtil.CommitInfo]], - diffs: Seq[util.JGitUtil.DiffInfo], - members: List[(String, String)], - comments: List[model.Comment], - originId: String, - forkedId: String, - sourceId: String, - commitId: String, - repository: service.RepositoryService.RepositoryInfo, - originRepository: service.RepositoryService.RepositoryInfo, - forkedRepository: service.RepositoryService.RepositoryInfo, - hasWritePermission: Boolean)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(s"Pull Requests - ${repository.owner}/${repository.name}", Some(repository)){ - @html.menu("pulls", repository){ -
    -
    - Edit - @originRepository.owner:@originId ... @forkedRepository.owner:@forkedId -
    - -
    - @if(commits.nonEmpty && hasWritePermission){ - - - } - @if(commits.isEmpty){ - - - - -
    -

    There isn't anything to compare.

    - @originRepository.owner:@originId and @forkedRepository.owner:@forkedId are identical. -
    - } else { - @pulls.html.commits(commits, Some(comments), repository) - @helper.html.diff(diffs, repository, Some(commitId), Some(sourceId), true, None, hasWritePermission, false) -

    Showing you all comments on commits in this comparison.

    - @issues.html.commentlist(None, comments, hasWritePermission, repository, None) - } - } -} - diff --git a/src/main/twirl/pulls/conversation.scala.html b/src/main/twirl/pulls/conversation.scala.html deleted file mode 100644 index 33c923c..0000000 --- a/src/main/twirl/pulls/conversation.scala.html +++ /dev/null @@ -1,82 +0,0 @@ -@(issue: model.Issue, - pullreq: model.PullRequest, - comments: List[model.Comment], - issueLabels: List[model.Label], - collaborators: List[String], - milestones: List[(model.Milestone, Int, Int)], - labels: List[model.Label], - hasWritePermission: Boolean, - repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import model.IssueComment -@import view.helpers._ -
    -
    -
    - @issues.html.commentlist(Some(issue), comments, hasWritePermission, repository, Some(pullreq)) -
    - @defining(comments.flatMap { - case comment: IssueComment => Some(comment) - case other => None - }.exists(_.action == "merge")){ merged => - @if(hasWritePermission && !issue.closed){ -
    -
    -
    - -
    - -
    -
    - } - @if(hasWritePermission && issue.closed && pullreq.userName == pullreq.requestUserName && merged && - pullreq.repositoryName == pullreq.requestRepositoryName && repository.branchList.contains(pullreq.requestBranch)){ -
    -
    - Delete branch -
    - Pull request successfully merged and closed -
    - You're all set-the @pullreq.requestBranch branch can be safely deleted. -
    -
    - } - @issues.html.commentform(issue, !merged, hasWritePermission, repository) - } -
    -
    - @issues.html.issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository) -
    -
    - \ No newline at end of file diff --git a/src/main/twirl/pulls/mergecheck.scala.html b/src/main/twirl/pulls/mergecheck.scala.html deleted file mode 100644 index dcc5af8..0000000 --- a/src/main/twirl/pulls/mergecheck.scala.html +++ /dev/null @@ -1,9 +0,0 @@ -@(hasConflict: Boolean) -@if(hasConflict){ -

    We can’t automatically merge these branches

    -

    Don't worry, you can still submit the pull request.

    -} else { -

    Able to merge

    -

    These branches can be automatically merged.

    -} - diff --git a/src/main/twirl/pulls/mergeguide.scala.html b/src/main/twirl/pulls/mergeguide.scala.html deleted file mode 100644 index 7ea24de..0000000 --- a/src/main/twirl/pulls/mergeguide.scala.html +++ /dev/null @@ -1,84 +0,0 @@ -@(hasConflict: Boolean, - pullreq: model.PullRequest, - requestRepositoryUrl: String)(implicit context: app.Context) -@import context._ -@import view.helpers._ -
    - -
    -
    - @if(hasConflict){ - We can’t automatically merge this pull request. - } else { - This pull request can be automatically merged. - } -
    -
    - @if(hasConflict){ - Use the command line to resolve conflicts before continuing. - } else { - You can also merge branches on the command line. - } -
    - - \ No newline at end of file diff --git a/src/main/twirl/pulls/pullreq.scala.html b/src/main/twirl/pulls/pullreq.scala.html deleted file mode 100644 index f6c3abf..0000000 --- a/src/main/twirl/pulls/pullreq.scala.html +++ /dev/null @@ -1,124 +0,0 @@ -@(issue: model.Issue, - pullreq: model.PullRequest, - comments: List[model.Comment], - issueLabels: List[model.Label], - collaborators: List[String], - milestones: List[(model.Milestone, Int, Int)], - labels: List[model.Label], - dayByDayCommits: Seq[Seq[util.JGitUtil.CommitInfo]], - diffs: Seq[util.JGitUtil.DiffInfo], - hasWritePermission: Boolean, - repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(s"${issue.title} - Pull Request #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){ - @html.menu("pulls", repository){ - @defining(dayByDayCommits.flatten){ commits => -
    -
    - @if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){ - Edit - } - New issue -
    - -

    - - @issue.title - #@issue.issueId - - -

    -
    - @if(issue.closed) { - @comments.flatMap @{ - case comment: model.IssueComment => Some(comment) - case _ => None - }.find(_.action == "merge").map{ comment => - Merged - - @user(comment.commentedUserName, styleClass="username strong") merged @commits.size @plural(commits.size, "commit") - into @pullreq.userName:@pullreq.branch from @pullreq.requestUserName:@pullreq.requestBranch - @helper.html.datetimeago(comment.registeredDate) - - }.getOrElse { - Closed - - @user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit") - into @pullreq.userName:@pullreq.branch from @pullreq.requestUserName:@pullreq.requestBranch - - } - } else { - Open - - @user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit") - into @pullreq.userName:@pullreq.branch from @pullreq.requestUserName:@pullreq.requestBranch - - } -

    - -
    -
    - @pulls.html.conversation(issue, pullreq, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository) -
    -
    - @pulls.html.commits(dayByDayCommits, Some(comments), repository) -
    -
    - @helper.html.diff(diffs, repository, Some(commits.head.id), Some(commits.last.id), true, Some(pullreq.issueId), hasWritePermission, true) -
    -
    - } - } -} - \ No newline at end of file diff --git a/src/main/twirl/repo/blob.scala.html b/src/main/twirl/repo/blob.scala.html deleted file mode 100644 index 549d2e8..0000000 --- a/src/main/twirl/repo/blob.scala.html +++ /dev/null @@ -1,134 +0,0 @@ -@(branch: String, - repository: service.RepositoryService.RepositoryInfo, - pathList: List[String], - content: util.JGitUtil.ContentInfo, - latestCommit: util.JGitUtil.CommitInfo, - hasWritePermission: Boolean)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { - @html.menu("code", repository){ -
    - @helper.html.branchcontrol( - branch, - repository, - hasWritePermission - ){ - @repository.branchList.map { x => -
  • @helper.html.checkicon(x == branch) @x
  • - } - } - @repository.name / - @pathList.zipWithIndex.map { case (section, i) => - @if(i == pathList.length - 1){ - @section - } else { - @section / - } - } -
    - - - - - - - - -
    -
    - @avatar(latestCommit, 20) - @user(latestCommit.authorName, latestCommit.authorEmailAddress, "username strong") - @helper.html.datetimeago(latestCommit.commitTime) - @link(latestCommit.summary, repository) -
    -
    - @if(hasWritePermission && content.viewType == "text" && repository.branchList.contains(branch)){ - Edit - } - Raw - History - @if(hasWritePermission){ - Delete - } -
    -
    - @if(content.viewType == "text"){ - @defining(pathList.reverse.head) { file => - @if(renderableSuffixes.find(suffix => file.toLowerCase.endsWith(suffix))) { -
    - @renderMarkup(pathList, content.content.get, branch, repository, false, false) -
    - } else { -
    @content.content.get
    - } - } - } - @if(content.viewType == "image"){ - - } - @if(content.viewType == "large" || content.viewType == "binary"){ -
    - View Raw
    -
    - (Sorry about that, but we can't show files that are this big right now) -
    - } -
    - } -} - - \ No newline at end of file diff --git a/src/main/twirl/repo/branches.scala.html b/src/main/twirl/repo/branches.scala.html deleted file mode 100644 index 7f152f4..0000000 --- a/src/main/twirl/repo/branches.scala.html +++ /dev/null @@ -1,83 +0,0 @@ -@(branchInfo: Seq[(util.JGitUtil.BranchInfo, Option[(model.PullRequest, model.Issue)])], - hasWritePermission: Boolean, - repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { - @html.menu("code", repository){ -

    Branches

    - - - - - - - - @branchInfo.map { case (branch, prs) => - - } -
    All branches
    -
    - @branch.mergeInfo.map{ info => - @prs.map{ case (pull, issue) => - #@issue.issueId - @if(issue.closed) { - @if(info.isMerged){ - Merged - }else{ - Closed - } - } else { - Open - } - }.getOrElse{ - @if(context.loginAccount.isDefined){ - New Pull Request - }else{ - Compare - } - } - @if(hasWritePermission){ - @if(prs.map(!_._2.closed).getOrElse(false)){ - - }else{ - - } - } - } -
    -
    - @branch.name - - Updated @helper.html.datetimeago(branch.commitTime, false) - by @user(branch.committerName, branch.committerEmailAddress, "muted-link") - - -
    -
    - @if(branch.mergeInfo.isEmpty){ - Default - }else{ - @branch.mergeInfo.map{ info => -
    -
    -
    @info.ahead
    -
    @info.behind
    -
    -
    -
    - } - } - -
    - } -} - diff --git a/src/main/twirl/repo/commentform.scala.html b/src/main/twirl/repo/commentform.scala.html deleted file mode 100644 index 36ad52c..0000000 --- a/src/main/twirl/repo/commentform.scala.html +++ /dev/null @@ -1,71 +0,0 @@ -@(commitId: String, - fileName: Option[String] = None, - oldLineNumber: Option[Int] = None, - newLineNumber: Option[Int] = None, - issueId: Option[Int] = None, - hasWritePermission: Boolean, - repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@if(loginAccount.isDefined){ - @if(!fileName.isDefined){

    } -
    - @if(!fileName.isDefined){ -
    @avatar(loginAccount.get.userName, 48)
    - } -
    -
    - @helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 635px; height: 100px; max-height: 150px;", elastic = true) -
    - @if(fileName.isDefined){ -
    - - -
    - } -
    - @if(!fileName.isDefined){ -
    - -
    - } - @issueId.map { issueId => } - @fileName.map { fileName => } - @oldLineNumber.map { oldLineNumber => } - @newLineNumber.map { newLineNumber => } -
    - -} diff --git a/src/main/twirl/repo/commit.scala.html b/src/main/twirl/repo/commit.scala.html deleted file mode 100644 index 49338c1..0000000 --- a/src/main/twirl/repo/commit.scala.html +++ /dev/null @@ -1,154 +0,0 @@ -@(commitId: String, - commit: util.JGitUtil.CommitInfo, - branches: List[String], - tags: List[String], - comments: List[model.Comment], - repository: service.RepositoryService.RepositoryInfo, - diffs: Seq[util.JGitUtil.DiffInfo], - oldCommitId: Option[String], - hasWritePermission: Boolean)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@import util.Implicits._ -@html.main(commit.shortMessage, Some(repository)){ - @html.menu("code", repository){ - - - - - - - -
    - -
    @link(commit.summary, repository)
    - @if(commit.description.isDefined){ -
    @link(commit.description.get, repository)
    - } -
    - @if(branches.nonEmpty){ - - - @branches.zipWithIndex.map { case (branch, i) => - @branch - } - - } - @if(tags.nonEmpty){ - - - @tags.zipWithIndex.map { case (tag, i) => - @tag - } - - } -
    -
    -
    -
    - @if(commit.parents.size == 0){ - 0 parent - } - @if(commit.parents.size == 1){ - 1 parent - @commit.parents(0).substring(0, 7) - } - commit @commit.id -
    - @if(commit.parents.size > 1){ -
    - @commit.parents.size parents - @commit.parents.map { parent => - @parent.substring(0, 7) - }.mkHtml(" + ") - -
    - } -
    - -
    -
    - @avatar(commit, 20) - @user(commit.authorName, commit.authorEmailAddress, "username strong") - authored @helper.html.datetimeago(commit.authorTime) -
    - @if(commit.isDifferentFromAuthor) { -
    - - @user(commit.committerName, commit.committerEmailAddress, "username strong") - committed @helper.html.datetimeago(commit.commitTime) -
    - } -
    -
    - @helper.html.diff(diffs, repository, Some(commit.id), oldCommitId, true, None, hasWritePermission, true) - -
    - @issues.html.commentlist(None, comments, hasWritePermission, repository, None) -
    - @commentform(commitId = commitId, hasWritePermission = hasWritePermission, repository = repository) - } -} - - diff --git a/src/main/twirl/repo/commits.scala.html b/src/main/twirl/repo/commits.scala.html deleted file mode 100644 index 443ab07..0000000 --- a/src/main/twirl/repo/commits.scala.html +++ /dev/null @@ -1,81 +0,0 @@ -@(pathList: List[String], - branch: String, - repository: service.RepositoryService.RepositoryInfo, - commits: Seq[Seq[util.JGitUtil.CommitInfo]], - page: Int, - hasNext: Boolean, - hasWritePermission: Boolean)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { - @html.menu("code", repository){ -
    - @helper.html.branchcontrol( - branch, - repository, - hasWritePermission - ){ - @repository.branchList.map { x => -
  • @helper.html.checkicon(x == branch) @x
  • - } - } - @if(pathList.isEmpty){ - @repository.name / Commit History - } - @if(pathList.nonEmpty){ - History for - @repository.name / - @pathList.zipWithIndex.map { case (section, i) => - @if(i == pathList.length - 1){ - @section - } else { - @section / - } - } - } -
    - @commits.map { day => - - - - - @day.map { commit => - - - - } -
    @date(day.head.commitTime)
    - -
    -
    @avatar(commit, 40)
    -
    - @link(commit.summary, repository) - @if(commit.description.isDefined){ - ... - } -
    - @if(commit.description.isDefined){ - - } -
    - @user(commit.authorName, commit.authorEmailAddress, "username") - authored @helper.html.datetimeago(commit.authorTime) - @if(commit.isDifferentFromAuthor) { - - @user(commit.committerName, commit.committerEmailAddress, "username") - committed @helper.html.datetimeago(commit.authorTime) - } -
    -
    -
    -
    - } -
    - - -
    - } -} diff --git a/src/main/twirl/repo/delete.scala.html b/src/main/twirl/repo/delete.scala.html deleted file mode 100644 index 7d589f3..0000000 --- a/src/main/twirl/repo/delete.scala.html +++ /dev/null @@ -1,61 +0,0 @@ -@(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.menu("code", repository){ -
    -
    - @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/editcomment.scala.html b/src/main/twirl/repo/editcomment.scala.html deleted file mode 100644 index d62e490..0000000 --- a/src/main/twirl/repo/editcomment.scala.html +++ /dev/null @@ -1,45 +0,0 @@ -@(content: String, commentId: Int, owner: String, repository: String)(implicit context: app.Context) -@import context._ - -@helper.html.attached(owner, repository){ - -} -
    - - -
    - diff --git a/src/main/twirl/repo/editor.scala.html b/src/main/twirl/repo/editor.scala.html deleted file mode 100644 index 606bc93..0000000 --- a/src/main/twirl/repo/editor.scala.html +++ /dev/null @@ -1,147 +0,0 @@ -@(branch: String, - repository: service.RepositoryService.RepositoryInfo, - pathList: List[String], - fileName: Option[String], - content: util.JGitUtil.ContentInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(if(fileName.isEmpty) "New File" else s"Editing ${fileName.get} at ${branch} - ${repository.owner}/${repository.name}", Some(repository)) { - @html.menu("code", repository){ -
    - -
    - @repository.name / - @pathList.zipWithIndex.map { case (section, i) => - @section / - } - - - - -
    - - - - - - - -
    -
    - -
    -
    - - -
    -
    -
    - -
    -
    @avatar(loginAccount.get.userName, 48)
    -
    -
    -
    - Commit changes -
    -
    - -
    -
    - @if(fileName.isEmpty){ - Cancel - } else { - Cancel - } - - - - - -
    -
    -
    -
    - } -} - - - - - diff --git a/src/main/twirl/repo/files.scala.html b/src/main/twirl/repo/files.scala.html deleted file mode 100644 index f8ff085..0000000 --- a/src/main/twirl/repo/files.scala.html +++ /dev/null @@ -1,123 +0,0 @@ -@(branch: String, - repository: service.RepositoryService.RepositoryInfo, - pathList: List[String], - groupNames: List[String], - latestCommit: util.JGitUtil.CommitInfo, - files: List[util.JGitUtil.FileInfo], - readme: Option[(List[String], String)], - hasWritePermission: Boolean, - info: Option[Any] = None, - error: Option[Any] = None)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { - @html.menu("code", repository, Some(branch), pathList.isEmpty, groupNames.isEmpty, info, error){ -
    - @helper.html.branchcontrol( - branch, - repository, - hasWritePermission - ){ - @repository.branchList.map { x => -
  • @helper.html.checkicon(x == branch) @x
  • - } - } - @repository.name / - @pathList.zipWithIndex.map { case (section, i) => - @section / - } - @if(hasWritePermission){ - - } -
    - - - - - - - - @if(pathList.size > 0){ - - - - - - - } - @files.map { file => - - - - - - - } -
    - @link(latestCommit.summary, repository) - @if(latestCommit.description.isDefined){ - ... - - } -
    -
    - -
    -
    - @avatar(latestCommit, 20) - @user(latestCommit.authorName, latestCommit.authorEmailAddress, "username strong") - authored @helper.html.datetimeago(latestCommit.authorTime) -
    - @if(latestCommit.isDifferentFromAuthor) { -
    - - @user(latestCommit.committerName, latestCommit.committerEmailAddress, "username strong") - committed @helper.html.datetimeago(latestCommit.commitTime) -
    - } -
    -
    -
    ..
    - @if(file.isDirectory){ - @if(file.linkUrl.isDefined){ - - } else { - - } - } else { - - } - - @if(file.isDirectory){ - @if(file.linkUrl.isDefined){ - - @file.name.split("/").toList.init match { - case Nil => {} - case list => {@list.mkString("", "/", "/")} - }@file.name.split("/").toList.last - - } else { - - @file.name.split("/").toList.init match { - case Nil => {} - case list => {@list.mkString("", "/", "/")} - }@file.name.split("/").toList.last - - } - } else { - @file.name - } - - @link(file.message, repository) - [@user(file.author, file.mailAddress)] - @helper.html.datetimeago(file.time, false)
    - @readme.map { case(filePath, content) => -
    -
    @filePath.reverse.head
    -
    @renderMarkup(filePath, content, branch, repository, false, false)
    -
    - } - } -} diff --git a/src/main/twirl/repo/forked.scala.html b/src/main/twirl/repo/forked.scala.html deleted file mode 100644 index 0e5b4fe..0000000 --- a/src/main/twirl/repo/forked.scala.html +++ /dev/null @@ -1,35 +0,0 @@ -@(originRepository: Option[service.RepositoryService.RepositoryInfo], - members: List[(String, String)], - repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { - @html.menu("network", repository){ - -

    Members of the @repository.name Network

    -
    - @if(originRepository.isDefined){ - @avatar(originRepository.get.owner, 20) - - @originRepository.get.owner / @originRepository.get.name - - } else { - @avatar(repository.repository.originUserName.get, 20) - - @repository.repository.originUserName / @repository.repository.originRepositoryName - - } - (origin) -
    - @members.map { case (owner, name) => -
    - @avatar(owner, 20) - - @owner / @name - -
    - } - } -} diff --git a/src/main/twirl/repo/guide.scala.html b/src/main/twirl/repo/guide.scala.html deleted file mode 100644 index 4040097..0000000 --- a/src/main/twirl/repo/guide.scala.html +++ /dev/null @@ -1,42 +0,0 @@ -@(repository: service.RepositoryService.RepositoryInfo, - hasWritePermission: Boolean)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { - @html.menu("code", repository){ - @if(!hasWritePermission){ -

    This is an empty repository

    - } else { -

    Quick setup — if you've done this kind of thing before

    -
    - via HTTP - @if(settings.ssh && loginAccount.isDefined){ - or - SSH - } -
    -

    Create a new repository on the command line

    - @pre { - touch README.md - git init - git add README.md - git commit -m "first commit" - git remote add origin @repository.httpUrl - git push -u origin master - } -

    Push an existing repository from the command line

    - @pre { - git remote add origin @repository.httpUrl - git push -u origin master - } - - } - } -} \ No newline at end of file diff --git a/src/main/twirl/repo/tags.scala.html b/src/main/twirl/repo/tags.scala.html deleted file mode 100644 index cd611fb..0000000 --- a/src/main/twirl/repo/tags.scala.html +++ /dev/null @@ -1,27 +0,0 @@ -@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { - @html.menu("code", repository){ -

    Tags

    - - - - - - - - @repository.tags.reverse.map { tag => - - - - - - - } -
    TagDateCommitDownload
    @tag.name@helper.html.datetimeago(tag.time, false)@tag.id.substring(0, 10) - ZIP - TAR.GZ -
    - } -} \ No newline at end of file diff --git a/src/main/twirl/search/code.scala.html b/src/main/twirl/search/code.scala.html deleted file mode 100644 index cc45665..0000000 --- a/src/main/twirl/search/code.scala.html +++ /dev/null @@ -1,26 +0,0 @@ -@(files: List[service.RepositorySearchService.FileSearchResult], - issueCount: Int, - query: String, - page: Int, - repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@import service.RepositorySearchService._ -@html.main("Search Results", Some(repository)){ - @menu("code", files.size, issueCount, query, repository){ - @if(files.isEmpty){ -

    We couldn't find any code matching '@query'

    - } else { -

    We've found @files.size code @plural(files.size, "result")

    - } - @files.drop((page - 1) * CodeLimit).take(CodeLimit).map { file => -
    -
    @file.path
    -
    Last committed @helper.html.datetimeago(file.lastModified)
    -
    @Html(file.highlightText)
    -
    - } - @helper.html.paginator(page, files.size, CodeLimit, 10, - s"${url(repository)}/search?q=${urlEncode(query)}&type=code") - } -} \ No newline at end of file diff --git a/src/main/twirl/search/issues.scala.html b/src/main/twirl/search/issues.scala.html deleted file mode 100644 index dcbc2a2..0000000 --- a/src/main/twirl/search/issues.scala.html +++ /dev/null @@ -1,35 +0,0 @@ -@(issues: List[service.RepositorySearchService.IssueSearchResult], - fileCount: Int, - query: String, - page: Int, - repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@import service.RepositorySearchService._ -@html.main("Search Results", Some(repository)){ - @menu("issue", fileCount, issues.size, query, repository){ - @if(issues.isEmpty){ -

    We couldn't find any code matching '@query'

    - } else { -

    We've found @issues.size code @plural(issues.size, "result")

    - } - @issues.drop((page - 1) * IssueLimit).take(IssueLimit).map { issue => -
    -
    #@issue.issueId
    -

    @issue.title

    - @if(issue.highlightText.nonEmpty){ -
    @Html(issue.highlightText)
    - } -
    - Opened by @issue.openedUserName - @helper.html.datetimeago(issue.registeredDate) - @if(issue.commentCount > 0){ -   @issue.commentCount @plural(issue.commentCount, "comment") - } -
    -
    - } - @helper.html.paginator(page, issues.size, IssueLimit, 10, - s"${url(repository)}/search?q=${urlEncode(query)}&type=issue") - } -} \ No newline at end of file diff --git a/src/main/twirl/search/menu.scala.html b/src/main/twirl/search/menu.scala.html deleted file mode 100644 index c66b6ab..0000000 --- a/src/main/twirl/search/menu.scala.html +++ /dev/null @@ -1,38 +0,0 @@ -@(active: String, fileCount: Int, issueCount: Int, query: String, - repository: service.RepositoryService.RepositoryInfo)(body: Html)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.menu("", repository){ - -} \ No newline at end of file diff --git a/src/main/twirl/settings/collaborators.scala.html b/src/main/twirl/settings/collaborators.scala.html deleted file mode 100644 index e225153..0000000 --- a/src/main/twirl/settings/collaborators.scala.html +++ /dev/null @@ -1,35 +0,0 @@ -@(collaborators: List[String], - isGroupRepository: Boolean, - repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main("Settings", Some(repository)){ - @html.menu("settings", repository){ - @menu("collaborators", repository){ -

    Manage Collaborators

    -
      - @collaborators.map { collaboratorName => -
    • - @collaboratorName - @if(!isGroupRepository){ - (remove) - } else { - @if(repository.managers.contains(collaboratorName)){ - (Manager) - } - } -
    • - } -
    - @if(!isGroupRepository){ -
    -
    - -
    - @helper.html.account("userName", 300) - -
    - } - } - } -} diff --git a/src/main/twirl/settings/danger.scala.html b/src/main/twirl/settings/danger.scala.html deleted file mode 100644 index ca74460..0000000 --- a/src/main/twirl/settings/danger.scala.html +++ /dev/null @@ -1,45 +0,0 @@ -@(repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main("Danger Zone", Some(repository)){ - @html.menu("settings", repository){ - @menu("danger", repository){ -
    -
    Danger Zone
    -
    -
    -
    - -
    - Transfer this repo to another user or to group. -
    - @helper.html.account("newOwner", 150) - -
    - -
    -
    -
    -
    -
    -
    -
    - -
    - Once you delete a repository, there is no going back. - -
    -
    -
    -
    -
    - } - } -} - \ No newline at end of file diff --git a/src/main/twirl/settings/hooks.scala.html b/src/main/twirl/settings/hooks.scala.html deleted file mode 100644 index 64e5f2b..0000000 --- a/src/main/twirl/settings/hooks.scala.html +++ /dev/null @@ -1,27 +0,0 @@ -@(webHooks: List[model.WebHook], - enteredUrl: Option[Any], - repository: service.RepositoryService.RepositoryInfo, - info: Option[Any])(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main("Settings", Some(repository)){ - @html.menu("settings", repository){ - @menu("hooks", repository){ - @helper.html.information(info) -

    WebHook URLs

    -
      - @webHooks.map { webHook => -
    • @webHook.url (remove)
    • - } -
    -
    -
    - -
    - - - -
    - } - } -} diff --git a/src/main/twirl/settings/menu.scala.html b/src/main/twirl/settings/menu.scala.html deleted file mode 100644 index 5ce349f..0000000 --- a/src/main/twirl/settings/menu.scala.html +++ /dev/null @@ -1,26 +0,0 @@ -@(active: String, repository: service.RepositoryService.RepositoryInfo)(body: Html)(implicit context: app.Context) -@import context._ -@import view.helpers._ -
    -
    -
    - -
    -
    -
    - @body -
    -
    diff --git a/src/main/twirl/settings/options.scala.html b/src/main/twirl/settings/options.scala.html deleted file mode 100644 index 1483b43..0000000 --- a/src/main/twirl/settings/options.scala.html +++ /dev/null @@ -1,100 +0,0 @@ -@(repository: service.RepositoryService.RepositoryInfo, info: Option[Any])(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main("Settings", Some(repository)){ - @html.menu("settings", repository){ - @menu("options", repository){ - @helper.html.information(info) -
    -
    -
    Settings
    -
    -
    - - - -
    -
    - - -
    -
    - - - @if(repository.branchList.isEmpty){ - - } - -
    -
    - -
    -
    - -
    -
    -
    - @* -
    -
    Features:
    -
    -
    -
    - -
    -
    - Adds lightweight Wiki system to this repository. - This is the simplest way to provide documentation or examples. - Only collaborators can edit Wiki pages. -
    -
    -
    -
    -
    - -
    -
    - Adds lightweight issue tracking integrated with this repository. - All users who have signed in and can access this repository can register an issue. -
    -
    -
    -
    - *@ -
    - -
    -
    - } - } -} diff --git a/src/main/twirl/signin.scala.html b/src/main/twirl/signin.scala.html deleted file mode 100644 index 5416371..0000000 --- a/src/main/twirl/signin.scala.html +++ /dev/null @@ -1,13 +0,0 @@ -@()(implicit context: app.Context) -@import context._ -@main("Sign in"){ - -} diff --git a/src/main/twirl/signinform.scala.html b/src/main/twirl/signinform.scala.html deleted file mode 100644 index 8be7f17..0000000 --- a/src/main/twirl/signinform.scala.html +++ /dev/null @@ -1,29 +0,0 @@ -@(systemSettings: service.SystemSettingsService.SystemSettings)(implicit context: app.Context) -@import context._ - - - - - - - -
    - @if(systemSettings.allowAccountRegistration){ - - } - Sign in -
    -
    - - - - - - -
    - -
    -
    -
    \ No newline at end of file diff --git a/src/main/twirl/wiki/compare.scala.html b/src/main/twirl/wiki/compare.scala.html deleted file mode 100644 index e5462fc..0000000 --- a/src/main/twirl/wiki/compare.scala.html +++ /dev/null @@ -1,42 +0,0 @@ -@(pageName: Option[String], - from: String, - to: String, - diffs: Seq[util.JGitUtil.DiffInfo], - repository: service.RepositoryService.RepositoryInfo, - hasWritePermission: Boolean, - info: Option[Any])(implicit context: app.Context) -@import context._ -@import view.helpers._ -@import org.eclipse.jgit.diff.DiffEntry.ChangeType -@html.main(s"Compare Revisions - ${repository.owner}/${repository.name}", Some(repository)){ - @helper.html.information(info) - @html.menu("wiki", repository){ - -
    - @helper.html.diff(diffs, repository, None, None, false, None, false, false) -
    - @if(hasWritePermission){ -
    - @if(pageName.isDefined){ - Revert Changes - } else { - Revert Changes - } -
    - } - } -} diff --git a/src/main/twirl/wiki/edit.scala.html b/src/main/twirl/wiki/edit.scala.html deleted file mode 100644 index b918cc0..0000000 --- a/src/main/twirl/wiki/edit.scala.html +++ /dev/null @@ -1,39 +0,0 @@ -@(pageName: String, - page: Option[service.WikiService.WikiPageInfo], - repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(s"${if(pageName.isEmpty) "New Page" else pageName} - ${repository.owner}/${repository.name}", Some(repository)){ - @html.menu("wiki", repository){ - -
    - - - @helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, false, false, "width: 850px; height: 400px;", "") - - - - -
    - } -} - diff --git a/src/main/twirl/wiki/history.scala.html b/src/main/twirl/wiki/history.scala.html deleted file mode 100644 index a773160..0000000 --- a/src/main/twirl/wiki/history.scala.html +++ /dev/null @@ -1,77 +0,0 @@ -@(pageName: Option[String], - commits: List[util.JGitUtil.CommitInfo], - repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(s"History - ${repository.owner}/${repository.name}", Some(repository)){ - @html.menu("wiki", repository){ - - - - - - @commits.map { commit => - - - - - - } -
    -
    Revisions
    -
    - -
    -
    @avatar(commit, 20) @user(commit.authorName, commit.authorEmailAddress) - @helper.html.datetimeago(commit.authorTime): @commit.shortMessage -
    - - } -} \ No newline at end of file diff --git a/src/main/twirl/wiki/page.scala.html b/src/main/twirl/wiki/page.scala.html deleted file mode 100644 index bc4880a..0000000 --- a/src/main/twirl/wiki/page.scala.html +++ /dev/null @@ -1,74 +0,0 @@ -@(pageName: String, - page: service.WikiService.WikiPageInfo, - pages: List[String], - repository: service.RepositoryService.RepositoryInfo, - hasWritePermission: Boolean)(implicit context: app.Context) -@import context._ -@import service.WikiService._ -@import view.helpers._ -@html.main(s"${pageName} - ${repository.owner}/${repository.name}", Some(repository)){ - @html.menu("wiki", repository){ - -
    - - - - - - - -
    Pages @pages.length
    -
      - @pages.map { page => -
    • @page
    • - } -
    -
    -
    - Clone this wiki locally -
    - @helper.html.copy("repository-url-copy", httpUrl(repository)){ - - } - @if(settings.ssh && loginAccount.isDefined){ -
    - You can clone HTTP or SSH. -
    - } -
    -
    -
    - @markdown(page.content, repository, true, false, false, false, pages) -
    -
    - } -} -@if(settings.ssh && loginAccount.isDefined){ - -} \ No newline at end of file diff --git a/src/main/twirl/wiki/pages.scala.html b/src/main/twirl/wiki/pages.scala.html deleted file mode 100644 index ca1760d..0000000 --- a/src/main/twirl/wiki/pages.scala.html +++ /dev/null @@ -1,26 +0,0 @@ -@(pages: List[String], - repository: service.RepositoryService.RepositoryInfo, - hasWritePermission: Boolean)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(s"Pages - ${repository.owner}/${repository.name}", Some(repository)){ - @html.menu("wiki", repository){ - -
      - @pages.map { page => -
    • @page
    • - } -
    - } -} \ No newline at end of file diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index c015d24..37bca8f 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -8,14 +8,14 @@ - servlet.SessionCleanupListener + gitbucket.core.servlet.SessionCleanupListener - servlet.InitializeListener + gitbucket.core.servlet.InitializeListener @@ -30,7 +30,7 @@ - ssh.SshServerListener + gitbucket.core.ssh.SshServerListener @@ -38,7 +38,7 @@ GitRepositoryServlet - servlet.GitRepositoryServlet + gitbucket.core.servlet.GitRepositoryServlet diff --git a/src/test/scala/gitbucket/core/service/AccountServiceSpec.scala b/src/test/scala/gitbucket/core/service/AccountServiceSpec.scala new file mode 100644 index 0000000..1c4793f --- /dev/null +++ b/src/test/scala/gitbucket/core/service/AccountServiceSpec.scala @@ -0,0 +1,79 @@ +package gitbucket.core.service + +import gitbucket.core.model.GroupMember +import org.specs2.mutable.Specification +import java.util.Date + +class AccountServiceSpec extends Specification with ServiceSpecBase { + + "AccountService" should { + val RootMailAddress = "root@localhost" + + "getAllUsers" in { withTestDB { implicit session => + AccountService.getAllUsers() must be like{ + case List(model.Account("root", "root", RootMailAddress, _, true, _, _, _, None, None, false, false)) => ok + } + }} + + "getAccountByUserName" in { withTestDB { implicit session => + AccountService.getAccountByUserName("root") must beSome.like { + case user => user.userName must_== "root" + } + + AccountService.getAccountByUserName("invalid user name") must beNone + }} + + "getAccountByMailAddress" in { withTestDB { implicit session => + AccountService.getAccountByMailAddress(RootMailAddress) must beSome + }} + + "updateLastLoginDate" in { withTestDB { implicit session => + val root = "root" + def user() = + AccountService.getAccountByUserName(root).getOrElse(sys.error(s"user $root does not exists")) + + user().lastLoginDate must beNone + val date1 = new Date + AccountService.updateLastLoginDate(root) + user().lastLoginDate must beSome.like{ case date => + date must be_>(date1) + } + val date2 = new Date + Thread.sleep(1000) + AccountService.updateLastLoginDate(root) + user().lastLoginDate must beSome.like{ case date => + date must be_>(date2) + } + }} + + "updateAccount" in { withTestDB { implicit session => + val root = "root" + def user() = + AccountService.getAccountByUserName(root).getOrElse(sys.error(s"user $root does not exists")) + + val newAddress = "new mail address" + AccountService.updateAccount(user().copy(mailAddress = newAddress)) + user().mailAddress must_== newAddress + }} + + "group" in { withTestDB { implicit session => + val group1 = "group1" + val user1 = "root" + AccountService.createGroup(group1, None) + + AccountService.getGroupMembers(group1) must_== Nil + AccountService.getGroupsByUserName(user1) must_== Nil + + AccountService.updateGroupMembers(group1, List((user1, true))) + + AccountService.getGroupMembers(group1) must_== List(GroupMember(group1, user1, true)) + AccountService.getGroupsByUserName(user1) must_== List(group1) + + AccountService.updateGroupMembers(group1, Nil) + + AccountService.getGroupMembers(group1) must_== Nil + AccountService.getGroupsByUserName(user1) must_== Nil + }} + } +} + diff --git a/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala b/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala new file mode 100644 index 0000000..850a6fa --- /dev/null +++ b/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala @@ -0,0 +1,28 @@ +package gitbucket.core.service + +import gitbucket.core.model.Profile +import gitbucket.core.servlet.AutoUpdate +import gitbucket.core.util.{ControlUtil, DatabaseConfig, FileUtil} +import profile.simple._ +import ControlUtil._ +import java.sql.DriverManager +import org.apache.commons.io.FileUtils +import scala.util.Random +import java.io.File + +trait ServiceSpecBase { + + def withTestDB[A](action: (Session) => A): A = { + FileUtil.withTmpDir(new File(FileUtils.getTempDirectory(), Random.alphanumeric.take(10).mkString)){ dir => + val (url, user, pass) = (DatabaseConfig.url(Some(dir.toString)), DatabaseConfig.user, DatabaseConfig.password) + org.h2.Driver.load() + using(DriverManager.getConnection(url, user, pass)){ conn => + AutoUpdate.versions.reverse.foreach(_.update(conn, Thread.currentThread.getContextClassLoader)) + } + Database.forURL(url, user, pass).withSession { session => + action(session) + } + } + } + +} diff --git a/src/test/scala/gitbucket/core/ssh/GitCommandSpec.scala b/src/test/scala/gitbucket/core/ssh/GitCommandSpec.scala new file mode 100644 index 0000000..d3eb8a7 --- /dev/null +++ b/src/test/scala/gitbucket/core/ssh/GitCommandSpec.scala @@ -0,0 +1,40 @@ +package gitbucket.core.ssh + +import org.specs2.mutable._ +import org.specs2.mock.Mockito +import org.apache.sshd.server.command.UnknownCommand +import javax.servlet.ServletContext + +class GitCommandFactorySpec extends Specification with Mockito { + + val factory = new GitCommandFactory("http://localhost:8080") + + "createCommand" should { + "returns GitReceivePack when command is git-receive-pack" in { + factory.createCommand("git-receive-pack '/owner/repo.git'").isInstanceOf[GitReceivePack] must beTrue + factory.createCommand("git-receive-pack '/owner/repo.wiki.git'").isInstanceOf[GitReceivePack] must beTrue + + } + "returns GitUploadPack when command is git-upload-pack" in { + factory.createCommand("git-upload-pack '/owner/repo.git'").isInstanceOf[GitUploadPack] must beTrue + factory.createCommand("git-upload-pack '/owner/repo.wiki.git'").isInstanceOf[GitUploadPack] must beTrue + + } + "returns UnknownCommand when command is not git-(upload|receive)-pack" in { + factory.createCommand("git- '/owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue + factory.createCommand("git-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue + factory.createCommand("git-a-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue + factory.createCommand("git-up-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue + factory.createCommand("\ngit-upload-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue + } + "returns UnknownCommand when git command has no valid arguments" in { + // must be: git-upload-pack '/owner/repository_name.git' + factory.createCommand("git-upload-pack").isInstanceOf[UnknownCommand] must beTrue + factory.createCommand("git-upload-pack /owner/repo.git").isInstanceOf[UnknownCommand] must beTrue + factory.createCommand("git-upload-pack 'owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue + factory.createCommand("git-upload-pack '/ownerrepo.git'").isInstanceOf[UnknownCommand] must beTrue + factory.createCommand("git-upload-pack '/owner/repo.wiki'").isInstanceOf[UnknownCommand] must beTrue + } + } + +} diff --git a/src/test/scala/gitbucket/core/util/StringUtilSpec.scala b/src/test/scala/gitbucket/core/util/StringUtilSpec.scala new file mode 100644 index 0000000..a0632eb --- /dev/null +++ b/src/test/scala/gitbucket/core/util/StringUtilSpec.scala @@ -0,0 +1,56 @@ +package gitbucket.core.util + +import org.specs2.mutable._ + +class StringUtilSpec extends Specification { + + "urlDecode" should { + "decode encoded string to original string" in { + val encoded = StringUtil.urlEncode("あいうえお") + StringUtil.urlDecode(encoded) mustEqual "あいうえお" + } + } + + "splitWords" should { + "split string by whitespaces" in { + val split = StringUtil.splitWords("aa bb\tcc dd \t ee") + split mustEqual Array("aa", "bb", "cc", "dd", "ee") + } + } + + "escapeHtml" should { + "escape &, <, > and \"" in { + StringUtil.escapeHtml("a & b") mustEqual "<a href="/test">a & b</a>" + } + } + + "md5" should { + "generate MD5 hash" in { + StringUtil.md5("abc") mustEqual "900150983cd24fb0d6963f7d28e17f72" + } + } + + "sha1" should { + "generate SHA1 hash" in { + StringUtil.sha1("abc") mustEqual "a9993e364706816aba3e25717850c26c9cd0d89d" + } + } + + "extractIssueId" should { + "extract '#xxx' and return extracted id" in { + StringUtil.extractIssueId("(refs #123)").toSeq mustEqual Seq("123") + } + "returns Nil from message which does not contain #xxx" in { + StringUtil.extractIssueId("this is test!").toSeq mustEqual Nil + } + } + + "extractCloseId" should { + "extract 'close #xxx' and return extracted id" in { + StringUtil.extractCloseId("(close #123)").toSeq mustEqual Seq("123") + } + "returns Nil from message which does not contain close command" in { + StringUtil.extractCloseId("(refs #123)").toSeq mustEqual Nil + } + } +} diff --git a/src/test/scala/gitbucket/core/util/ValidationsSpec.scala b/src/test/scala/gitbucket/core/util/ValidationsSpec.scala new file mode 100644 index 0000000..65106b8 --- /dev/null +++ b/src/test/scala/gitbucket/core/util/ValidationsSpec.scala @@ -0,0 +1,36 @@ +package gitbucket.core.util + +import org.specs2.mutable._ +import org.scalatra.i18n.Messages + +class ValidationsSpec extends Specification with Validations { + + "identifier" should { + "validate id string " in { + identifier.validate("id", "aa_ZZ-00.01", null) mustEqual None + identifier.validate("id", "_aaaa", null) mustEqual Some("id starts with invalid character.") + identifier.validate("id", "-aaaa", null) mustEqual Some("id starts with invalid character.") + identifier.validate("id", "aa_ZZ#01", null) mustEqual Some("id contains invalid character.") + } + } + + "color" should { + "validate color string " in { + val messages = Messages() + color.validate("color", "#88aaff", messages) mustEqual None + color.validate("color", "#gghhii", messages) mustEqual Some("color must be '#[0-9a-fA-F]{6}'.") + } + } + + "date" should { +// "validate date string " in { +// date().validate("date", "2013-10-05", Map[String, String]()) mustEqual Nil +// date().validate("date", "2013-10-5" , Map[String, String]()) mustEqual List(("date", "date must be '\\d{4}-\\d{2}-\\d{2}'.")) +// } + "convert date string " in { + val result = date().convert("2013-10-05", null) + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(result) mustEqual "2013-10-05 00:00:00" + } + } + +} diff --git a/src/test/scala/service/AccountServiceSpec.scala b/src/test/scala/service/AccountServiceSpec.scala deleted file mode 100644 index a9fa493..0000000 --- a/src/test/scala/service/AccountServiceSpec.scala +++ /dev/null @@ -1,79 +0,0 @@ -package service - -import org.specs2.mutable.Specification -import java.util.Date -import model.GroupMember - -class AccountServiceSpec extends Specification with ServiceSpecBase { - - "AccountService" should { - val RootMailAddress = "root@localhost" - - "getAllUsers" in { withTestDB { implicit session => - AccountService.getAllUsers() must be like{ - case List(model.Account("root", "root", RootMailAddress, _, true, _, _, _, None, None, false, false)) => ok - } - }} - - "getAccountByUserName" in { withTestDB { implicit session => - AccountService.getAccountByUserName("root") must beSome.like { - case user => user.userName must_== "root" - } - - AccountService.getAccountByUserName("invalid user name") must beNone - }} - - "getAccountByMailAddress" in { withTestDB { implicit session => - AccountService.getAccountByMailAddress(RootMailAddress) must beSome - }} - - "updateLastLoginDate" in { withTestDB { implicit session => - val root = "root" - def user() = - AccountService.getAccountByUserName(root).getOrElse(sys.error(s"user $root does not exists")) - - user().lastLoginDate must beNone - val date1 = new Date - AccountService.updateLastLoginDate(root) - user().lastLoginDate must beSome.like{ case date => - date must be_>(date1) - } - val date2 = new Date - Thread.sleep(1000) - AccountService.updateLastLoginDate(root) - user().lastLoginDate must beSome.like{ case date => - date must be_>(date2) - } - }} - - "updateAccount" in { withTestDB { implicit session => - val root = "root" - def user() = - AccountService.getAccountByUserName(root).getOrElse(sys.error(s"user $root does not exists")) - - val newAddress = "new mail address" - AccountService.updateAccount(user().copy(mailAddress = newAddress)) - user().mailAddress must_== newAddress - }} - - "group" in { withTestDB { implicit session => - val group1 = "group1" - val user1 = "root" - AccountService.createGroup(group1, None) - - AccountService.getGroupMembers(group1) must_== Nil - AccountService.getGroupsByUserName(user1) must_== Nil - - AccountService.updateGroupMembers(group1, List((user1, true))) - - AccountService.getGroupMembers(group1) must_== List(GroupMember(group1, user1, true)) - AccountService.getGroupsByUserName(user1) must_== List(group1) - - AccountService.updateGroupMembers(group1, Nil) - - AccountService.getGroupMembers(group1) must_== Nil - AccountService.getGroupsByUserName(user1) must_== Nil - }} - } -} - diff --git a/src/test/scala/service/ServiceSpecBase.scala b/src/test/scala/service/ServiceSpecBase.scala deleted file mode 100644 index a87bb58..0000000 --- a/src/test/scala/service/ServiceSpecBase.scala +++ /dev/null @@ -1,27 +0,0 @@ -package service - -import model.Profile._ -import profile.simple._ -import util.ControlUtil._ -import util.DatabaseConfig -import java.sql.DriverManager -import org.apache.commons.io.FileUtils -import scala.util.Random -import java.io.File - -trait ServiceSpecBase { - - def withTestDB[A](action: (Session) => A): A = { - util.FileUtil.withTmpDir(new File(FileUtils.getTempDirectory(), Random.alphanumeric.take(10).mkString)){ dir => - val (url, user, pass) = (DatabaseConfig.url(Some(dir.toString)), DatabaseConfig.user, DatabaseConfig.password) - org.h2.Driver.load() - using(DriverManager.getConnection(url, user, pass)){ conn => - servlet.AutoUpdate.versions.reverse.foreach(_.update(conn, Thread.currentThread.getContextClassLoader)) - } - Database.forURL(url, user, pass).withSession { session => - action(session) - } - } - } - -} diff --git a/src/test/scala/ssh/GitCommandSpec.scala b/src/test/scala/ssh/GitCommandSpec.scala deleted file mode 100644 index 6c571ae..0000000 --- a/src/test/scala/ssh/GitCommandSpec.scala +++ /dev/null @@ -1,40 +0,0 @@ -package ssh - -import org.specs2.mutable._ -import org.specs2.mock.Mockito -import org.apache.sshd.server.command.UnknownCommand -import javax.servlet.ServletContext - -class GitCommandFactorySpec extends Specification with Mockito { - - val factory = new GitCommandFactory("http://localhost:8080") - - "createCommand" should { - "returns GitReceivePack when command is git-receive-pack" in { - factory.createCommand("git-receive-pack '/owner/repo.git'").isInstanceOf[GitReceivePack] must beTrue - factory.createCommand("git-receive-pack '/owner/repo.wiki.git'").isInstanceOf[GitReceivePack] must beTrue - - } - "returns GitUploadPack when command is git-upload-pack" in { - factory.createCommand("git-upload-pack '/owner/repo.git'").isInstanceOf[GitUploadPack] must beTrue - factory.createCommand("git-upload-pack '/owner/repo.wiki.git'").isInstanceOf[GitUploadPack] must beTrue - - } - "returns UnknownCommand when command is not git-(upload|receive)-pack" in { - factory.createCommand("git- '/owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue - factory.createCommand("git-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue - factory.createCommand("git-a-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue - factory.createCommand("git-up-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue - factory.createCommand("\ngit-upload-pack '/owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue - } - "returns UnknownCommand when git command has no valid arguments" in { - // must be: git-upload-pack '/owner/repository_name.git' - factory.createCommand("git-upload-pack").isInstanceOf[UnknownCommand] must beTrue - factory.createCommand("git-upload-pack /owner/repo.git").isInstanceOf[UnknownCommand] must beTrue - factory.createCommand("git-upload-pack 'owner/repo.git'").isInstanceOf[UnknownCommand] must beTrue - factory.createCommand("git-upload-pack '/ownerrepo.git'").isInstanceOf[UnknownCommand] must beTrue - factory.createCommand("git-upload-pack '/owner/repo.wiki'").isInstanceOf[UnknownCommand] must beTrue - } - } - -} diff --git a/src/test/scala/util/StringUtilSpec.scala b/src/test/scala/util/StringUtilSpec.scala deleted file mode 100644 index caaa2dc..0000000 --- a/src/test/scala/util/StringUtilSpec.scala +++ /dev/null @@ -1,56 +0,0 @@ -package util - -import org.specs2.mutable._ - -class StringUtilSpec extends Specification { - - "urlDecode" should { - "decode encoded string to original string" in { - val encoded = StringUtil.urlEncode("あいうえお") - StringUtil.urlDecode(encoded) mustEqual "あいうえお" - } - } - - "splitWords" should { - "split string by whitespaces" in { - val split = StringUtil.splitWords("aa bb\tcc dd \t ee") - split mustEqual Array("aa", "bb", "cc", "dd", "ee") - } - } - - "escapeHtml" should { - "escape &, <, > and \"" in { - StringUtil.escapeHtml("a & b") mustEqual "<a href="/test">a & b</a>" - } - } - - "md5" should { - "generate MD5 hash" in { - StringUtil.md5("abc") mustEqual "900150983cd24fb0d6963f7d28e17f72" - } - } - - "sha1" should { - "generate SHA1 hash" in { - StringUtil.sha1("abc") mustEqual "a9993e364706816aba3e25717850c26c9cd0d89d" - } - } - - "extractIssueId" should { - "extract '#xxx' and return extracted id" in { - StringUtil.extractIssueId("(refs #123)").toSeq mustEqual Seq("123") - } - "returns Nil from message which does not contain #xxx" in { - StringUtil.extractIssueId("this is test!").toSeq mustEqual Nil - } - } - - "extractCloseId" should { - "extract 'close #xxx' and return extracted id" in { - StringUtil.extractCloseId("(close #123)").toSeq mustEqual Seq("123") - } - "returns Nil from message which does not contain close command" in { - StringUtil.extractCloseId("(refs #123)").toSeq mustEqual Nil - } - } -} diff --git a/src/test/scala/util/ValidationsSpec.scala b/src/test/scala/util/ValidationsSpec.scala deleted file mode 100644 index 92b58a2..0000000 --- a/src/test/scala/util/ValidationsSpec.scala +++ /dev/null @@ -1,36 +0,0 @@ -package util - -import org.specs2.mutable._ -import org.scalatra.i18n.Messages - -class ValidationsSpec extends Specification with Validations { - - "identifier" should { - "validate id string " in { - identifier.validate("id", "aa_ZZ-00.01", null) mustEqual None - identifier.validate("id", "_aaaa", null) mustEqual Some("id starts with invalid character.") - identifier.validate("id", "-aaaa", null) mustEqual Some("id starts with invalid character.") - identifier.validate("id", "aa_ZZ#01", null) mustEqual Some("id contains invalid character.") - } - } - - "color" should { - "validate color string " in { - val messages = Messages() - color.validate("color", "#88aaff", messages) mustEqual None - color.validate("color", "#gghhii", messages) mustEqual Some("color must be '#[0-9a-fA-F]{6}'.") - } - } - - "date" should { -// "validate date string " in { -// date().validate("date", "2013-10-05", Map[String, String]()) mustEqual Nil -// date().validate("date", "2013-10-5" , Map[String, String]()) mustEqual List(("date", "date must be '\\d{4}-\\d{2}-\\d{2}'.")) -// } - "convert date string " in { - val result = date().convert("2013-10-05", null) - new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(result) mustEqual "2013-10-05 00:00:00" - } - } - -} diff --git a/src/test/scala/view/AvatarImageProviderSpec.scala b/src/test/scala/view/AvatarImageProviderSpec.scala index 78c32ab..ca7bda4 100644 --- a/src/test/scala/view/AvatarImageProviderSpec.scala +++ b/src/test/scala/view/AvatarImageProviderSpec.scala @@ -2,11 +2,12 @@ import java.util.Date +import gitbucket.core.model.Account +import gitbucket.core.service.{SystemSettingsService, RequestCache} +import gitbucket.core.view.AvatarImageProvider import org.specs2.mutable._ import org.specs2.mock.Mockito -import service.RequestCache -import model.Account -import service.SystemSettingsService.SystemSettings +import SystemSettingsService.SystemSettings import play.twirl.api.Html import javax.servlet.http.HttpServletRequest diff --git a/src/test/scala/view/GitBucketHtmlSerializerSpec.scala b/src/test/scala/view/GitBucketHtmlSerializerSpec.scala index 687db70..71a0615 100644 --- a/src/test/scala/view/GitBucketHtmlSerializerSpec.scala +++ b/src/test/scala/view/GitBucketHtmlSerializerSpec.scala @@ -1,5 +1,6 @@ package view +import gitbucket.core.view.GitBucketHtmlSerializer import org.specs2.mutable._ class GitBucketHtmlSerializerSpec extends Specification { diff --git a/src/test/scala/view/PaginationSpec.scala b/src/test/scala/view/PaginationSpec.scala index 20c3488..f320e38 100644 --- a/src/test/scala/view/PaginationSpec.scala +++ b/src/test/scala/view/PaginationSpec.scala @@ -1,7 +1,9 @@ package view +import gitbucket.core.util.ControlUtil +import gitbucket.core.view.Pagination import org.specs2.mutable._ -import util.ControlUtil._ +import ControlUtil._ class PaginationSpec extends Specification {