diff --git a/README.md b/README.md index a2b6586..eb3b260 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,21 @@ Release Notes ------------- +### 4.6 - 29 Oct 2016 +- Add disable option for forking +- Add History button to wiki page +- Git repository URL redirection for GitHub compatibility +- Get-Content API improvement +- Indicate who is group master in Members tab in group view + +### 4.5 - 29 Sep 2016 +- Attach files by dropping into textarea +- Issues / Pull requests switcher in dashboard +- HikariCP could be configured in `GITBUCKET_HOME/database.conf` +- Improve Cookie security +- Display commit count on the history button +- Improve mobile view + ### 4.4 - 28 Aug 2016 - Import a SQL dump file to the database - `go get` support in private repositories diff --git a/build.sbt b/build.sbt index 090ac28..16c00bd 100644 --- a/build.sbt +++ b/build.sbt @@ -1,6 +1,6 @@ val Organization = "io.github.gitbucket" val Name = "gitbucket" -val GitBucketVersion = "4.4.0" +val GitBucketVersion = "4.6.0" val ScalatraVersion = "2.4.1" val JettyVersion = "9.3.9.v20160517" @@ -51,7 +51,6 @@ "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided", "junit" % "junit" % "4.12" % "test", "org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test", - "org.scalaz" %% "scalaz-core" % "7.2.4" % "test", "com.wix" % "wix-embedded-mysql" % "1.0.3" % "test", "ru.yandex.qatools.embed" % "postgresql-embedded" % "1.14" % "test" ) @@ -107,7 +106,6 @@ val executableKey = TaskKey[File]("executable") executableKey := { - import org.apache.ivy.util.ChecksumHelper import java.util.jar.{ Manifest => JarManifest } import java.util.jar.Attributes.{ Name => AttrName } @@ -165,12 +163,6 @@ log info s"built executable webapp ${outputFile}" outputFile } -/* -Keys.artifact in (Compile, executableKey) ~= { - _ copy (`type` = "war", extension = "war")) -} -addArtifact(Keys.artifact in (Compile, executableKey), executableKey) -*/ publishTo <<= version { (v: String) => val nexus = "https://oss.sonatype.org/" if (v.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "content/repositories/snapshots") @@ -178,7 +170,6 @@ } publishMavenStyle := true pomIncludeRepository := { _ => false } -artifact in Keys.`package` := Artifact(moduleName.value) pomExtra := ( https://github.com/gitbucket/gitbucket diff --git a/doc/release.md b/doc/release.md index 504778c..682dc88 100644 --- a/doc/release.md +++ b/doc/release.md @@ -46,9 +46,10 @@ ### Deploy assembly jar file -For plug-in development, we have to publish the assembly jar file to the public Maven repository by `release/deploy-assembly-jar.sh`. +For plug-in development, we have to publish the GitBucket jar file to the Maven central repository as well. At first, hit following command to publish artifacts to the sonatype OSS repository: ```bash -$ cd release/ -$ ./deploy-assembly-jar.sh +$ sbt publish-signed ``` + +Then operate release sequence at https://oss.sonatype.org/. diff --git a/project/build.properties b/project/build.properties index 43b8278..35c88ba 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.11 +sbt.version=0.13.12 diff --git a/release/deploy-assembly-jar.sh b/release/deploy-assembly-jar.sh deleted file mode 100755 index 43778d7..0000000 --- a/release/deploy-assembly-jar.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh -. ./env.sh - -cd ../ -./sbt.sh clean assembly - -cd release - -if [[ "$GITBUCKET_VERSION" =~ -SNAPSHOT$ ]]; then - MVN_DEPLOY_PATH=mvn-snapshot -else - MVN_DEPLOY_PATH=mvn -fi - -echo $MVN_DEPLOY_PATH - -mvn deploy:deploy-file \ - -DgroupId=gitbucket\ - -DartifactId=gitbucket-assembly\ - -Dversion=$GITBUCKET_VERSION\ - -Dpackaging=jar\ - -Dfile=../target/scala-2.11/gitbucket-assembly-$GITBUCKET_VERSION.jar\ - -DrepositoryId=sourceforge.jp\ - -Durl=scp://shell.sourceforge.jp/home/groups/a/am/amateras/htdocs/$MVN_DEPLOY_PATH/ diff --git a/release/env.sh b/release/env.sh deleted file mode 100644 index 5892171..0000000 --- a/release/env.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -export GITBUCKET_VERSION=`cat ../build.sbt | grep 'val GitBucketVersion' | cut -d \" -f 2` -echo "GITBUCKET_VERSION: $GITBUCKET_VERSION" diff --git a/release/pom.xml b/release/pom.xml deleted file mode 100644 index 0d84d21..0000000 --- a/release/pom.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - 4.0.0 - jp.sf.amateras - gitbucket-assembly - 0.0.1 - - - - org.apache.maven.wagon - wagon-ssh - 2.10 - - - - \ No newline at end of file diff --git a/sbt-launch-0.13.12.jar b/sbt-launch-0.13.12.jar new file mode 100644 index 0000000..871dedd --- /dev/null +++ b/sbt-launch-0.13.12.jar Binary files differ diff --git a/sbt-launch-0.13.9.jar b/sbt-launch-0.13.9.jar deleted file mode 100644 index c065b47..0000000 --- a/sbt-launch-0.13.9.jar +++ /dev/null Binary files differ diff --git a/sbt.bat b/sbt.bat index 726d347..bfc44ca 100644 --- a/sbt.bat +++ b/sbt.bat @@ -1,2 +1,2 @@ set SCRIPT_DIR=%~dp0 -java %JAVA_OPTS% -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.9.jar" %* +java %JAVA_OPTS% -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.12.jar" %* diff --git a/sbt.sh b/sbt.sh index e0f14b2..55d0be1 100755 --- a/sbt.sh +++ b/sbt.sh @@ -1,2 +1,2 @@ #!/bin/sh -java $JAVA_OPTS -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.9.jar "$@" +java $JAVA_OPTS -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.12.jar "$@" diff --git a/src/main/java/JettyLauncher.java b/src/main/java/JettyLauncher.java index 443954c..1ac4187 100644 --- a/src/main/java/JettyLauncher.java +++ b/src/main/java/JettyLauncher.java @@ -3,12 +3,14 @@ import java.io.File; import java.net.URL; +import java.net.InetSocketAddress; import java.security.ProtectionDomain; public class JettyLauncher { public static void main(String[] args) throws Exception { String host = null; int port = 8080; + InetSocketAddress address = null; String contextPath = "/"; boolean forceHttps = false; @@ -29,7 +31,13 @@ } } - Server server = new Server(port); + if(host != null) { + address = new InetSocketAddress(host, port); + } else { + address = new InetSocketAddress(port); + } + + Server server = new Server(address); // SelectChannelConnector connector = new SelectChannelConnector(); // if(host != null) { diff --git a/src/main/resources/update/gitbucket-core_4.6.xml b/src/main/resources/update/gitbucket-core_4.6.xml new file mode 100644 index 0000000..6bf4acf --- /dev/null +++ b/src/main/resources/update/gitbucket-core_4.6.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/resources/update/gitbucket-core_4.7.sql b/src/main/resources/update/gitbucket-core_4.7.sql new file mode 100644 index 0000000..ef13c70 --- /dev/null +++ b/src/main/resources/update/gitbucket-core_4.7.sql @@ -0,0 +1,2 @@ +-- DELETE COLLABORATORS IN GROUP REPOSITORIES +DELETE FROM COLLABORATOR WHERE USER_NAME IN (SELECT USER_NAME FROM ACCOUNT WHERE GROUP_ACCOUNT = TRUE) diff --git a/src/main/resources/update/gitbucket-core_4.7.xml b/src/main/resources/update/gitbucket-core_4.7.xml new file mode 100644 index 0000000..a128800 --- /dev/null +++ b/src/main/resources/update/gitbucket-core_4.7.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + ENABLE_WIKI = FALSE + + + + ENABLE_WIKI = TRUE AND ALLOW_WIKI_EDITING = FALSE + + + + ENABLE_WIKI = TRUE AND ALLOW_WIKI_EDITING = TRUE + + + + ENABLE_ISSUES = FALSE + + + + ENABLE_ISSUES = TRUE + + + + + diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala index fbb488a..949bf80 100644 --- a/src/main/scala/ScalatraBootstrap.scala +++ b/src/main/scala/ScalatraBootstrap.scala @@ -1,17 +1,23 @@ -import gitbucket.core.controller._ -import gitbucket.core.plugin.PluginRegistry -import gitbucket.core.servlet.{ApiAuthenticationFilter, GitAuthenticationFilter, Database, TransactionFilter} -import gitbucket.core.util.Directory - import java.util.EnumSet import javax.servlet._ +import gitbucket.core.controller._ +import gitbucket.core.plugin.PluginRegistry +import gitbucket.core.service.SystemSettingsService +import gitbucket.core.servlet._ +import gitbucket.core.util.Directory import org.scalatra._ -class ScalatraBootstrap extends LifeCycle { +class ScalatraBootstrap extends LifeCycle with SystemSettingsService { override def init(context: ServletContext) { + + val settings = loadSystemSettings() + if(settings.baseUrl.exists(_.startsWith("https://"))) { + context.getSessionCookieConfig.setSecure(true) + } + // Register TransactionFilter and BasicAuthenticationFilter at first context.addFilter("transactionFilter", new TransactionFilter) context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*") @@ -19,6 +25,9 @@ context.getFilterRegistration("gitAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*") context.addFilter("apiAuthenticationFilter", new ApiAuthenticationFilter) context.getFilterRegistration("apiAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/api/v3/*") + context.addFilter("ghCompatRepositoryAccessFilter", new GHCompatRepositoryAccessFilter) + context.getFilterRegistration("ghCompatRepositoryAccessFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*") + // Register controllers context.mount(new AnonymousAccessController, "/*") diff --git a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala index d1685ba..c04a687 100644 --- a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala +++ b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala @@ -14,5 +14,13 @@ ), new Version("4.2.1"), new Version("4.3.0"), - new Version("4.4.0") + new Version("4.4.0"), + new Version("4.5.0"), + new Version("4.6.0", + new LiquibaseMigration("update/gitbucket-core_4.6.xml") + ), + new Version("4.7.0", + new LiquibaseMigration("update/gitbucket-core_4.7.xml"), + new SqlMigration("update/gitbucket-core_4.7.sql") + ) ) diff --git a/src/main/scala/gitbucket/core/api/ApiContents.scala b/src/main/scala/gitbucket/core/api/ApiContents.scala index db70320..1582370 100644 --- a/src/main/scala/gitbucket/core/api/ApiContents.scala +++ b/src/main/scala/gitbucket/core/api/ApiContents.scala @@ -1,11 +1,18 @@ package gitbucket.core.api import gitbucket.core.util.JGitUtil.FileInfo +import org.apache.commons.codec.binary.Base64 -case class ApiContents(`type`: String, name: String) +case class ApiContents(`type`: String, name: String, content: Option[String], encoding: Option[String]) object ApiContents{ - def apply(fileInfo: FileInfo): ApiContents = - if(fileInfo.isDirectory) ApiContents("dir", fileInfo.name) - else ApiContents("file", fileInfo.name) -} \ No newline at end of file + def apply(fileInfo: FileInfo, content: Option[Array[Byte]]): ApiContents = { + if(fileInfo.isDirectory) { + ApiContents("dir", fileInfo.name, None, None) + } else { + content.map(arr => + ApiContents("file", fileInfo.name, Some(Base64.encodeBase64String(arr)), Some("base64")) + ).getOrElse(ApiContents("file", fileInfo.name, None, None)) + } + } +} diff --git a/src/main/scala/gitbucket/core/api/ApiUser.scala b/src/main/scala/gitbucket/core/api/ApiUser.scala index 7259c12..9b3dc9d 100644 --- a/src/main/scala/gitbucket/core/api/ApiUser.scala +++ b/src/main/scala/gitbucket/core/api/ApiUser.scala @@ -30,7 +30,7 @@ def apply(user: Account): ApiUser = ApiUser( login = user.userName, email = user.mailAddress, - `type` = if(user.isGroupAccount){ "Organization" }else{ "User" }, + `type` = if(user.isGroupAccount){ "Organization" } else { "User" }, site_admin = user.isAdmin, created_at = user.registeredDate ) diff --git a/src/main/scala/gitbucket/core/controller/AccountController.scala b/src/main/scala/gitbucket/core/controller/AccountController.scala index c0f0212..bfb9ccd 100644 --- a/src/main/scala/gitbucket/core/controller/AccountController.scala +++ b/src/main/scala/gitbucket/core/controller/AccountController.scala @@ -14,6 +14,7 @@ import io.github.gitbucket.scalatra.forms._ import org.apache.commons.io.FileUtils import org.scalatra.i18n.Messages +import org.scalatra.BadRequest class AccountController extends AccountControllerBase @@ -120,7 +121,7 @@ // Members case "members" if(account.isGroupAccount) => { val members = getGroupMembers(account.userName) - gitbucket.core.account.html.members(account, members.map(_.userName), + gitbucket.core.account.html.members(account, members, context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })) } @@ -133,7 +134,7 @@ context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })) } } - } getOrElse NotFound + } getOrElse NotFound() } get("/:userName.atom") { @@ -156,7 +157,7 @@ val userName = params("userName") getAccountByUserName(userName).map { x => html.edit(x, flash.get("info"), flash.get("error")) - } getOrElse NotFound + } getOrElse NotFound() }) post("/:userName/_edit", editForm)(oneselfOnly { form => @@ -172,7 +173,7 @@ flash += "info" -> "Account information has been updated." redirect(s"/${userName}/_edit") - } getOrElse NotFound + } getOrElse NotFound() }) get("/:userName/_delete")(oneselfOnly { @@ -196,14 +197,14 @@ session.invalidate redirect("/") } - } getOrElse NotFound + } getOrElse NotFound() }) get("/:userName/_ssh")(oneselfOnly { val userName = params("userName") getAccountByUserName(userName).map { x => html.ssh(x, getPublicKeys(x.userName)) - } getOrElse NotFound + } getOrElse NotFound() }) post("/:userName/_ssh", sshKeyForm)(oneselfOnly { form => @@ -234,7 +235,7 @@ case _ => None } html.application(x, tokens, generatedToken) - } getOrElse NotFound + } getOrElse NotFound() }) post("/:userName/_personalToken", personalTokenForm)(oneselfOnly { form => @@ -260,7 +261,7 @@ } else { html.register() } - } else NotFound + } else NotFound() } post("/register", newForm){ form => @@ -268,7 +269,7 @@ createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url) updateImage(form.userName, form.fileId, false) redirect("/signin") - } else NotFound + } else NotFound() } get("/groups/new")(usersOnly { @@ -318,18 +319,18 @@ // 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) - } - } +// // 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 + } getOrElse NotFound() } }) @@ -355,76 +356,80 @@ }) 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}") - } + if(repository.repository.options.allowFork){ + 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}") + } + } else BadRequest() }) post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) => - val loginAccount = context.loginAccount.get - val loginUserName = loginAccount.userName - val accountName = form.accountName + if(repository.repository.options.allowFork){ + val loginAccount = context.loginAccount.get + val loginUserName = loginAccount.userName + val accountName = form.accountName - LockUtil.lock(s"${accountName}/${repository.name}"){ - if(getRepository(accountName, repository.name).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) + LockUtil.lock(s"${accountName}/${repository.name}"){ + if(getRepository(accountName, repository.name).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) - insertRepository( - 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) - ) + insertRepository( + 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) + ) - // Add collaborators for group repository - val ownerAccount = getAccountByUserName(accountName).get - if(ownerAccount.isGroupAccount){ - getGroupMembers(accountName).foreach { member => - addCollaborator(accountName, repository.name, member.userName) - } +// // Add collaborators for group repository +// val ownerAccount = getAccountByUserName(accountName).get +// if(ownerAccount.isGroupAccount){ +// getGroupMembers(accountName).foreach { member => +// addCollaborator(accountName, repository.name, member.userName) +// } +// } + + // 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}") } - - // 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}") } - } + } else BadRequest() }) private def existsAccount: Constraint = new Constraint(){ diff --git a/src/main/scala/gitbucket/core/controller/ApiController.scala b/src/main/scala/gitbucket/core/controller/ApiController.scala index 9a2f2a4..eddd941 100644 --- a/src/main/scala/gitbucket/core/controller/ApiController.scala +++ b/src/main/scala/gitbucket/core/controller/ApiController.scala @@ -7,9 +7,10 @@ import gitbucket.core.service._ import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.Directory._ -import gitbucket.core.util.JGitUtil.{CommitInfo, getFileList, getBranches, getDefaultBranch} +import gitbucket.core.util.JGitUtil._ import gitbucket.core.util._ import gitbucket.core.util.Implicits._ +import gitbucket.core.view.helpers.{renderMarkup, isRenderable} import org.eclipse.jgit.api.Git import org.scalatra.{NoContent, UnprocessableEntity, Created} import scala.collection.JavaConverters._ @@ -34,7 +35,7 @@ with GroupManagerAuthenticator with ReferrerAuthenticator with ReadableUsersAuthenticator - with CollaboratorsAuthenticator + with WritableUsersAuthenticator trait ApiControllerBase extends ControllerBase { self: RepositoryService @@ -51,7 +52,7 @@ with GroupManagerAuthenticator with ReferrerAuthenticator with ReadableUsersAuthenticator - with CollaboratorsAuthenticator => + with WritableUsersAuthenticator => /** * https://developer.github.com/v3/#root-endpoint @@ -66,7 +67,7 @@ get("/api/v3/orgs/:groupName") { getAccountByUserName(params("groupName")).filter(account => account.isGroupAccount).map { account => JsonFormat(ApiUser(account)) - } getOrElse NotFound + } getOrElse NotFound() } /** @@ -75,7 +76,7 @@ get("/api/v3/users/:userName") { getAccountByUserName(params("userName")).filterNot(account => account.isGroupAccount).map { account => JsonFormat(ApiUser(account)) - } getOrElse NotFound + } getOrElse NotFound() } /** @@ -109,13 +110,53 @@ * https://developer.github.com/v3/repos/contents/#get-contents */ get("/api/v3/repos/:owner/:repo/contents/*")(referrersOnly { repository => + def getFileInfo(git: Git, revision: String, pathStr: String): Option[FileInfo] = { + val path = new java.io.File(pathStr) + val dirName = path.getParent match { + case null => "." + case s => s + } + getFileList(git, revision, dirName).find(f => f.name.equals(path.getName)) + } + val path = multiParams("splat").head match { case s if s.isEmpty => "." case s => s } val refStr = params.getOrElse("ref", repository.repository.defaultBranch) + using(Git.open(getRepositoryDir(params("owner"), params("repo")))){ git => - JsonFormat(getFileList(git, refStr, path).map{f => ApiContents(f)}) + val fileList = getFileList(git, refStr, path) + if (fileList.isEmpty) { // file or NotFound + getFileInfo(git, refStr, path).flatMap(f => { + val largeFile = params.get("large_file").exists(s => s.equals("true")) + val content = getContentFromId(git, f.id, largeFile) + request.getHeader("Accept") match { + case "application/vnd.github.v3.raw" => + content + case "application/vnd.github.v3.html" if isRenderable(f.name) => + content.map(c => + List( + "
", "
", + renderMarkup(path.split("/").toList, new String(c), refStr, repository, false, false, true).body, + "
", "
" + ).mkString + ) + case "application/vnd.github.v3.html" => + content.map(c => + List( + "
", "
", "
",
+                  play.twirl.api.HtmlFormat.escape(new String(c)).body,
+                  "
", "
", "
" + ).mkString + ) + case _ => + Some(JsonFormat(ApiContents(f, content))) + } + }).getOrElse(NotFound()) + } else { // directory + JsonFormat(fileList.map{f => ApiContents(f, None)}) + } } }) @@ -136,7 +177,8 @@ * https://developer.github.com/v3/repos/collaborators/#list-collaborators */ get("/api/v3/repos/:owner/:repo/collaborators") (referrersOnly { repository => - JsonFormat(getCollaborators(params("owner"), params("repo")).map(u => ApiUser(getAccountByUserName(u).get))) + // TODO Should ApiUser take permission? getCollaboratorUserNames does not return owner group members. + JsonFormat(getCollaboratorUserNames(params("owner"), params("repo")).map(u => ApiUser(getAccountByUserName(u).get))) }) /** @@ -145,7 +187,7 @@ get("/api/v3/user") { context.loginAccount.map { account => JsonFormat(ApiUser(account)) - } getOrElse Unauthorized + } getOrElse Unauthorized() } /** @@ -179,7 +221,7 @@ ) } } - }) getOrElse NotFound + }) getOrElse NotFound() }) /** @@ -203,7 +245,7 @@ ) } } - }) getOrElse NotFound + }) getOrElse NotFound() }) /** @@ -221,7 +263,7 @@ disableBranchProtection(repository.owner, repository.name, branch) } JsonFormat(ApiBranch(branch, protection)(RepositoryName(repository))) - }) getOrElse NotFound + }) getOrElse NotFound() }) /** @@ -243,7 +285,7 @@ comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt) } yield { JsonFormat(comments.map{ case (issueComment, user, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) }) - }).getOrElse(NotFound) + }) getOrElse NotFound() }) /** @@ -259,7 +301,7 @@ issueComment <- getComment(repository.owner, repository.name, id.toString()) } yield { JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get), issue.isPullRequest)) - }) getOrElse NotFound + }) getOrElse NotFound() }) /** @@ -286,7 +328,7 @@ * Create a label * https://developer.github.com/v3/issues/labels/#create-a-label */ - post("/api/v3/repos/:owner/:repository/labels")(collaboratorsOnly { repository => + post("/api/v3/repos/:owner/:repository/labels")(writableUsersOnly { repository => (for{ data <- extractFromJsonBody[CreateALabel] if data.isValid } yield { @@ -311,7 +353,7 @@ * Update a label * https://developer.github.com/v3/issues/labels/#update-a-label */ - patch("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository => + patch("/api/v3/repos/:owner/:repository/labels/:labelName")(writableUsersOnly { repository => (for{ data <- extractFromJsonBody[CreateALabel] if data.isValid } yield { @@ -337,7 +379,7 @@ * Delete a label * https://developer.github.com/v3/issues/labels/#delete-a-label */ - delete("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository => + delete("/api/v3/repos/:owner/:repository/labels/:labelName")(writableUsersOnly { repository => LockUtil.lock(RepositoryName(repository).fullName) { getLabel(repository.owner, repository.name, params("labelName")).map { label => deleteLabel(repository.owner, repository.name, label.labelId) @@ -393,7 +435,7 @@ ApiRepository(headRepo, ApiUser(headOwner)), ApiRepository(repository, ApiUser(baseOwner)), ApiUser(issueUser))) - }).getOrElse(NotFound) + }) getOrElse NotFound() }) /** @@ -412,7 +454,7 @@ JsonFormat(commits) } } - } getOrElse NotFound + } getOrElse NotFound() }) /** @@ -425,7 +467,7 @@ /** * https://developer.github.com/v3/repos/statuses/#create-a-status */ - post("/api/v3/repos/:owner/:repo/statuses/:sha")(collaboratorsOnly { repository => + post("/api/v3/repos/:owner/:repo/statuses/:sha")(writableUsersOnly { repository => (for{ ref <- params.get("sha") sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref) @@ -437,7 +479,7 @@ status <- getCommitStatus(repository.owner, repository.name, statusId) } yield { JsonFormat(ApiCommitStatus(status, ApiUser(creator))) - }) getOrElse NotFound + }) getOrElse NotFound() }) /** @@ -453,7 +495,7 @@ JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map{ case(status, creator) => ApiCommitStatus(status, ApiUser(creator)) }) - }) getOrElse NotFound + }) getOrElse NotFound() }) /** @@ -478,7 +520,7 @@ } yield { val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha) JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner))) - }) getOrElse NotFound + }) getOrElse NotFound() }) private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean = diff --git a/src/main/scala/gitbucket/core/controller/ControllerBase.scala b/src/main/scala/gitbucket/core/controller/ControllerBase.scala index db795dd..ef1a634 100644 --- a/src/main/scala/gitbucket/core/controller/ControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/ControllerBase.scala @@ -248,7 +248,7 @@ protected def reservedNames(): Constraint = new Constraint(){ override def validate(name: String, value: String, messages: Messages): Option[String] = if(allReservedNames.contains(value)){ Some(s"${value} is reserved") - }else{ + } else { None } } diff --git a/src/main/scala/gitbucket/core/controller/FileUploadController.scala b/src/main/scala/gitbucket/core/controller/FileUploadController.scala index ac90bbc..d264341 100644 --- a/src/main/scala/gitbucket/core/controller/FileUploadController.scala +++ b/src/main/scala/gitbucket/core/controller/FileUploadController.scala @@ -48,7 +48,7 @@ // Check whether logged-in user is collaborator collaboratorsOnly(owner, repository, loginAccount){ execute({ (file, fileId) => - val fileName = file.getName + val fileName = file.getName LockUtil.lock(s"${owner}/${repository}/wiki") { using(Git.open(Directory.getWikiRepositoryDir(owner, repository))) { git => val builder = DirCache.newInCore.builder() @@ -75,7 +75,7 @@ } }, FileUtil.isUploadableType) } - } getOrElse BadRequest + } getOrElse BadRequest() } post("/import") { @@ -93,7 +93,7 @@ loginAccount match { case x if(x.isAdmin) => action case x if(getCollaborators(owner, repository).contains(x.userName)) => action - case _ => BadRequest + case _ => BadRequest() } } @@ -101,10 +101,9 @@ case Some(file) if(mimeTypeChcker(file.name)) => defining(FileUtil.generateFileId){ fileId => f(file, fileId) - Ok(fileId) } - case _ => BadRequest + case _ => BadRequest() } } diff --git a/src/main/scala/gitbucket/core/controller/IndexController.scala b/src/main/scala/gitbucket/core/controller/IndexController.scala index 6bc2751..0d69ebe 100644 --- a/src/main/scala/gitbucket/core/controller/IndexController.scala +++ b/src/main/scala/gitbucket/core/controller/IndexController.scala @@ -108,18 +108,29 @@ */ get("/_user/proposals")(usersOnly { contentType = formats("json") + val user = params("user").toBoolean + val group = params("group").toBoolean org.json4s.jackson.Serialization.write( - Map("options" -> getAllUsers(false).filter(!_.isGroupAccount).map(_.userName).toArray) + Map("options" -> ( + getAllUsers(false) + .withFilter { t => (user, group) match { + case (true, true) => true + case (true, false) => !t.isGroupAccount + case (false, true) => t.isGroupAccount + case (false, false) => false + }}.map { t => t.userName } + )) ) }) /** - * JSON API for checking user existence. + * JSON API for checking user or group existence. + * Returns a single string which is any of "group", "user" or "". */ post("/_user/existence")(usersOnly { getAccountByUserName(params("userName")).map { account => - if(params.get("userOnly").isDefined) !account.isGroupAccount else true - } getOrElse false + if(account.isGroupAccount) "group" else "user" + } getOrElse "" }) // TODO Move to RepositoryViwerController? diff --git a/src/main/scala/gitbucket/core/controller/IssuesController.scala b/src/main/scala/gitbucket/core/controller/IssuesController.scala index f425b60..90aca13 100644 --- a/src/main/scala/gitbucket/core/controller/IssuesController.scala +++ b/src/main/scala/gitbucket/core/controller/IssuesController.scala @@ -2,24 +2,24 @@ import gitbucket.core.issues.html import gitbucket.core.service.IssuesService._ +import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.service._ import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.Implicits._ import gitbucket.core.util._ import gitbucket.core.view import gitbucket.core.view.Markdown - import io.github.gitbucket.scalatra.forms._ import org.scalatra.Ok class IssuesController extends IssuesControllerBase with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with HandleCommentService - with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService + with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator with PullRequestService with WebHookIssueCommentService trait IssuesControllerBase extends ControllerBase { self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with HandleCommentService - with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService => + with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator with PullRequestService with WebHookIssueCommentService => case class IssueCreateForm(title: String, content: Option[String], assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String]) @@ -67,142 +67,147 @@ _, getComments(owner, name, issueId.toInt), getIssueLabels(owner, name, issueId.toInt), - (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, + getAssignableUserNames(owner, name), getMilestonesWithIssueCount(owner, name), getLabels(owner, name), - hasWritePermission(owner, name, context.loginAccount), + isEditable(repository), + isManageable(repository), repository) - } getOrElse NotFound + } 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, + if(isEditable(repository)){ // TODO Should this check is provided by authenticator? + defining(repository.owner, repository.name){ case (owner, name) => + html.create( + getAssignableUserNames(owner, name), getMilestones(owner, name), getLabels(owner, name), hasWritePermission(owner, name, context.loginAccount), repository) - } + } + } else Unauthorized() }) 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 + if(isEditable(repository)){ // TODO Should this check is provided by authenticator? + defining(repository.owner, repository.name){ case (owner, name) => + val manageable = isManageable(repository) + 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 issue + val issueId = createIssue(owner, name, userName, form.title, form.content, + if (manageable) form.assignedUserName else None, + if (manageable) 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) + // insert labels + if (manageable) { + 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) + // record activity + recordCreateIssueActivity(owner, name, userName, issueId, form.title) - getIssue(owner, name, issueId.toString).foreach { issue => - // extract references and create refer comment - createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get) + getIssue(owner, name, issueId.toString).foreach { issue => + // extract references and create refer comment + createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get) - // call web hooks - callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get) + // call web hooks + callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get) - // notifications - Notifier().toNotify(repository, issue, form.content.getOrElse("")){ - Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") + // notifications + Notifier().toNotify(repository, issue, form.content.getOrElse("")) { + Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") + } } - } - redirect(s"/${owner}/${name}/issues/${issueId}") - } + redirect(s"/${owner}/${name}/issues/${issueId}") + } + } else Unauthorized() }) 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)){ + if(isEditableContent(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, context.loginAccount.get) redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") - } else Unauthorized - } getOrElse NotFound + } 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)){ + if(isEditableContent(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(""), context.loginAccount.get) redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") - } else Unauthorized - } getOrElse NotFound + } else Unauthorized() + } getOrElse NotFound() } }) post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue => - val actionOpt = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName)) + val actionOpt = params.get("action").filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName)) handleComment(issue, Some(form.content), repository, actionOpt) map { case (issue, id) => redirect(s"/${repository.owner}/${repository.name}/${ if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") } - } getOrElse NotFound + } getOrElse NotFound() }) post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) => getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue => - val actionOpt = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName)) + val actionOpt = params.get("action").filter(_ => isEditableContent(issue.userName, issue.repositoryName, issue.openedUserName)) handleComment(issue, form.content, repository, actionOpt) map { case (issue, id) => redirect(s"/${repository.owner}/${repository.name}/${ if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") } - } getOrElse NotFound + } 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)){ + if(isEditableContent(owner, name, comment.commentedUserName)){ updateComment(comment.commentId, form.content) redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}") - } else Unauthorized - } getOrElse NotFound + } 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)){ + if(isEditableContent(owner, name, comment.commentedUserName)){ Ok(deleteComment(comment.commentId)) - } else Unauthorized - } getOrElse NotFound + } 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)){ + if(isEditableContent(x.userName, x.repositoryName, x.openedUserName)){ params.get("dataType") collect { case t if t == "html" => html.editissue(x.content, x.issueId, repository) } getOrElse { @@ -218,18 +223,18 @@ enableAnchor = true, enableLineBreaks = true, enableTaskList = true, - hasWritePermission = isEditable(x.userName, x.repositoryName, x.openedUserName) + hasWritePermission = true ) ) ) } - } else Unauthorized - } getOrElse NotFound + } 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)){ + if(isEditableContent(x.userName, x.repositoryName, x.commentedUserName)){ params.get("dataType") collect { case t if t == "html" => html.editcomment(x.content, x.commentId, repository) } getOrElse { @@ -244,51 +249,51 @@ enableAnchor = true, enableLineBreaks = true, enableTaskList = true, - hasWritePermission = isEditable(x.userName, x.repositoryName, x.commentedUserName) + hasWritePermission = true ) ) ) } - } else Unauthorized - } getOrElse NotFound + } else Unauthorized() + } getOrElse NotFound() }) - ajaxPost("/:owner/:repository/issues/new/label")(collaboratorsOnly { repository => + ajaxPost("/:owner/:repository/issues/new/label")(writableUsersOnly { repository => val labelNames = params("labelNames").split(",") val labels = getLabels(repository.owner, repository.name).filter(x => labelNames.contains(x.labelName)) html.labellist(labels) }) - ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository => + ajaxPost("/:owner/:repository/issues/:id/label/new")(writableUsersOnly { 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 => + ajaxPost("/:owner/:repository/issues/:id/label/delete")(writableUsersOnly { 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 => + ajaxPost("/:owner/:repository/issues/:id/assign")(writableUsersOnly { repository => updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName")) Ok("updated") }) - ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository => + ajaxPost("/:owner/:repository/issues/:id/milestone")(writableUsersOnly { 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 NotFound() } getOrElse Ok() }) - post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => + post("/:owner/:repository/issues/batchedit/state")(writableUsersOnly { repository => defining(params.get("value")){ action => action match { case Some("open") => executeBatch(repository) { issueId => @@ -306,17 +311,17 @@ } }) - post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository => + post("/:owner/:repository/issues/batchedit/label")(writableUsersOnly { 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 + } getOrElse NotFound() }) - post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository => + post("/:owner/:repository/issues/batchedit/assign")(writableUsersOnly { repository => defining(assignedUserName("value")){ value => executeBatch(repository) { updateAssignedUserName(repository.owner, repository.name, _, value) @@ -324,7 +329,7 @@ } }) - post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository => + post("/:owner/:repository/issues/batchedit/milestone")(writableUsersOnly { repository => defining(milestoneId("value")){ value => executeBatch(repository) { updateMilestoneId(repository.owner, repository.name, _, value) @@ -340,15 +345,12 @@ RawData(FileUtil.getMimeType(file.getName), file) } case _ => None - }) getOrElse NotFound + }) 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 { @@ -359,8 +361,7 @@ 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) + val page = IssueSearchCondition.page(request) // retrieve search condition val condition = IssueSearchCondition(request) @@ -369,18 +370,41 @@ "issues", searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), page, - if(!getAccountByUserName(owner).exists(_.isGroupAccount)){ - (getCollaborators(owner, repoName) :+ owner).sorted - } else { - getCollaborators(owner, repoName) - }, + getAssignableUserNames(owner, repoName), 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)) + isEditable(repository), + isManageable(repository)) } } + + /** + * Tests whether an logged-in user can manage issues. + */ + private def isManageable(repository: RepositoryInfo)(implicit context: Context): Boolean = { + hasWritePermission(repository.owner, repository.name, context.loginAccount) + } + + /** + * Tests whether an logged-in user can post issues. + */ + private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = { + repository.repository.options.issuesOption match { + case "PUBLIC" => hasReadPermission(repository.owner, repository.name, context.loginAccount) + case "PRIVATE" => hasWritePermission(repository.owner, repository.name, context.loginAccount) + case "DISABLE" => false + } + } + + /** + * Tests whether an issue or a comment is editable by a logged-in user. + */ + private def isEditableContent(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/LabelsController.scala b/src/main/scala/gitbucket/core/controller/LabelsController.scala index 9b17841..ab658eb 100644 --- a/src/main/scala/gitbucket/core/controller/LabelsController.scala +++ b/src/main/scala/gitbucket/core/controller/LabelsController.scala @@ -2,7 +2,7 @@ import gitbucket.core.issues.labels.html import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService} -import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator} +import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator} import gitbucket.core.util.Implicits._ import io.github.gitbucket.scalatra.forms._ import org.scalatra.i18n.Messages @@ -10,11 +10,11 @@ class LabelsController extends LabelsControllerBase with LabelsService with IssuesService with RepositoryService with AccountService -with ReferrerAuthenticator with CollaboratorsAuthenticator +with ReferrerAuthenticator with WritableUsersAuthenticator trait LabelsControllerBase extends ControllerBase { self: LabelsService with IssuesService with RepositoryService - with ReferrerAuthenticator with CollaboratorsAuthenticator => + with ReferrerAuthenticator with WritableUsersAuthenticator => case class LabelForm(labelName: String, color: String) @@ -32,11 +32,11 @@ hasWritePermission(repository.owner, repository.name, context.loginAccount)) }) - ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository => + ajaxGet("/:owner/:repository/issues/labels/new")(writableUsersOnly { repository => html.edit(None, repository) }) - ajaxPost("/:owner/:repository/issues/labels/new", labelForm)(collaboratorsOnly { (form, repository) => + ajaxPost("/:owner/:repository/issues/labels/new", labelForm)(writableUsersOnly { (form, repository) => val labelId = createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1)) html.label( getLabel(repository.owner, repository.name, labelId).get, @@ -46,13 +46,13 @@ hasWritePermission(repository.owner, repository.name, context.loginAccount)) }) - ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository => + ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(writableUsersOnly { 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) => + ajaxPost("/:owner/:repository/issues/labels/:labelId/edit", labelForm)(writableUsersOnly { (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, @@ -62,7 +62,7 @@ hasWritePermission(repository.owner, repository.name, context.loginAccount)) }) - ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository => + ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(writableUsersOnly { repository => deleteLabel(repository.owner, repository.name, params("labelId").toInt) Ok() }) diff --git a/src/main/scala/gitbucket/core/controller/MilestonesController.scala b/src/main/scala/gitbucket/core/controller/MilestonesController.scala index 50cd068..f75ea60 100644 --- a/src/main/scala/gitbucket/core/controller/MilestonesController.scala +++ b/src/main/scala/gitbucket/core/controller/MilestonesController.scala @@ -2,17 +2,17 @@ import gitbucket.core.issues.milestones.html import gitbucket.core.service.{RepositoryService, MilestonesService, AccountService} -import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator} +import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator} import gitbucket.core.util.Implicits._ import io.github.gitbucket.scalatra.forms._ class MilestonesController extends MilestonesControllerBase with MilestonesService with RepositoryService with AccountService - with ReferrerAuthenticator with CollaboratorsAuthenticator + with ReferrerAuthenticator with WritableUsersAuthenticator trait MilestonesControllerBase extends ControllerBase { self: MilestonesService with RepositoryService - with ReferrerAuthenticator with CollaboratorsAuthenticator => + with ReferrerAuthenticator with WritableUsersAuthenticator => case class MilestoneForm(title: String, description: Option[String], dueDate: Option[java.util.Date]) @@ -30,55 +30,55 @@ hasWritePermission(repository.owner, repository.name, context.loginAccount)) }) - get("/:owner/:repository/issues/milestones/new")(collaboratorsOnly { + get("/:owner/:repository/issues/milestones/new")(writableUsersOnly { html.edit(None, _) }) - post("/:owner/:repository/issues/milestones/new", milestoneForm)(collaboratorsOnly { (form, repository) => + post("/:owner/:repository/issues/milestones/new", milestoneForm)(writableUsersOnly { (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 => + get("/:owner/:repository/issues/milestones/:milestoneId/edit")(writableUsersOnly { repository => params("milestoneId").toIntOpt.map{ milestoneId => html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository) - } getOrElse NotFound + } getOrElse NotFound() }) - post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(collaboratorsOnly { (form, repository) => + post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(writableUsersOnly { (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 + } getOrElse NotFound() }) - get("/:owner/:repository/issues/milestones/:milestoneId/close")(collaboratorsOnly { repository => + get("/:owner/:repository/issues/milestones/:milestoneId/close")(writableUsersOnly { 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 + } getOrElse NotFound() }) - get("/:owner/:repository/issues/milestones/:milestoneId/open")(collaboratorsOnly { repository => + get("/:owner/:repository/issues/milestones/:milestoneId/open")(writableUsersOnly { 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 + } getOrElse NotFound() }) - get("/:owner/:repository/issues/milestones/:milestoneId/delete")(collaboratorsOnly { repository => + get("/:owner/:repository/issues/milestones/:milestoneId/delete")(writableUsersOnly { 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 + } getOrElse NotFound() }) } diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala index 803ab51..548da42 100644 --- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala +++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala @@ -6,6 +6,7 @@ import gitbucket.core.service.MergeService import gitbucket.core.service.IssuesService._ import gitbucket.core.service.PullRequestService._ +import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.service._ import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.Directory._ @@ -14,28 +15,26 @@ import gitbucket.core.util._ import gitbucket.core.view import gitbucket.core.view.helpers - import io.github.gitbucket.scalatra.forms._ import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib.PersonIdent -import org.slf4j.LoggerFactory import scala.collection.JavaConverters._ class PullRequestsController extends PullRequestsControllerBase with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService - with CommitsService with ActivityService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator + with CommitsService with ActivityService with WebHookPullRequestService + with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator with CommitStatusService with MergeService with ProtectedBranchService trait PullRequestsControllerBase extends ControllerBase { self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService - with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator + with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService + with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator with CommitStatusService with MergeService with ProtectedBranchService => - private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase]) - val pullRequestForm = mapping( "title" -> trim(label("Title" , text(required, maxlength(100)))), "content" -> trim(label("Content", optional(text()))), @@ -94,17 +93,18 @@ (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, + getAssignableUserNames(owner, name), getMilestonesWithIssueCount(owner, name), getLabels(owner, name), commits, diffs, - hasWritePermission(owner, name, context.loginAccount), + isEditable(repository), + isManageable(repository), repository, flash.toMap.map(f => f._1 -> f._2.toString)) } } - } getOrElse NotFound + } getOrElse NotFound() }) ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository => @@ -138,10 +138,10 @@ repository, getRepository(pullreq.requestUserName, pullreq.requestRepositoryName).get) } - } getOrElse NotFound + } getOrElse NotFound() }) - get("/:owner/:repository/pull/:id/delete/*")(collaboratorsOnly { repository => + get("/:owner/:repository/pull/:id/delete/*")(writableUsersOnly { repository => params("id").toIntOpt.map { issueId => val branchName = multiParams("splat").head val userName = context.loginAccount.get.userName @@ -153,7 +153,7 @@ } createComment(repository.owner, repository.name, userName, issueId, branchName, "delete_branch") redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") - } getOrElse NotFound + } getOrElse NotFound() }) post("/:owner/:repository/pull/:id/update_branch")(referrersOnly { baseRepository => @@ -222,10 +222,10 @@ } redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") } - }) getOrElse NotFound + }) getOrElse NotFound() }) - post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) => + post("/:owner/:repository/pull/:id/merge", mergeForm)(writableUsersOnly { (form, repository) => params("id").toIntOpt.flatMap { issueId => val owner = repository.owner val name = repository.name @@ -273,7 +273,7 @@ } } } - } getOrElse NotFound + } getOrElse NotFound() }) get("/:owner/:repository/compare")(referrersOnly { forkedRepository => @@ -290,7 +290,7 @@ redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}") } - } getOrElse NotFound + } getOrElse NotFound() } case _ => { using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git => @@ -375,7 +375,7 @@ originRepository, forkedRepository, hasWritePermission(originRepository.owner, originRepository.name, context.loginAccount), - (getCollaborators(originRepository.owner, originRepository.name) ::: (if(getAccountByUserName(originRepository.owner).get.isGroupAccount) Nil else List(originRepository.owner))).sorted, + getAssignableUserNames(originRepository.owner, originRepository.name), getMilestones(originRepository.owner, originRepository.name), getLabels(originRepository.owner, originRepository.name) ) @@ -386,10 +386,10 @@ s"${forkedOwner}:${newId.map(_ => forkedId).getOrElse(forkedRepository.repository.defaultBranch)}") } } - }) getOrElse NotFound + }) getOrElse NotFound() }) - ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { forkedRepository => + ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(writableUsersOnly { forkedRepository => val Seq(origin, forked) = multiParams("splat") val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner) val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner) @@ -416,67 +416,71 @@ } html.mergecheck(conflict) } - }) getOrElse NotFound + }) getOrElse NotFound() }) - post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) => + post("/:owner/:repository/pulls/new", pullRequestForm)(readableUsersOnly { (form, repository) => defining(repository.owner, repository.name){ case (owner, name) => - val writable = hasWritePermission(owner, name, context.loginAccount) - val loginUserName = context.loginAccount.get.userName + val manageable = isManageable(repository) + val editable = isEditable(repository) - val issueId = createIssue( - owner = repository.owner, - repository = repository.name, - loginUser = loginUserName, - title = form.title, - content = form.content, - assignedUserName = if(writable) form.assignedUserName else None, - milestoneId = if(writable) form.milestoneId else None, - isPullRequest = true) + if(editable) { + val loginUserName = context.loginAccount.get.userName - 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) + val issueId = createIssue( + owner = repository.owner, + repository = repository.name, + loginUser = loginUserName, + title = form.title, + content = form.content, + assignedUserName = if (manageable) form.assignedUserName else None, + milestoneId = if (manageable) form.milestoneId else None, + isPullRequest = true) - // insert labels - if(writable){ - form.labelNames.map { value => - val labels = getLabels(owner, name) - value.split(",").foreach { labelName => - labels.find(_.labelName == labelName).map { label => - registerIssueLabel(repository.owner, repository.name, issueId, label.labelId) + 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) + + // insert labels + if (manageable) { + form.labelNames.map { value => + val labels = getLabels(owner, name) + value.split(",").foreach { labelName => + labels.find(_.labelName == labelName).map { label => + registerIssueLabel(repository.owner, repository.name, issueId, label.labelId) + } } } } - } - // fetch requested branch - fetchAsPullRequest(owner, name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId) + // fetch requested branch + fetchAsPullRequest(owner, name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId) - // record activity - recordPullRequestActivity(owner, name, loginUserName, issueId, form.title) + // record activity + recordPullRequestActivity(owner, name, loginUserName, issueId, form.title) - // call web hook - callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get) + // call web hook + callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get) - getIssue(owner, name, issueId.toString) foreach { issue => - // extract references and create refer comment - createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get) + getIssue(owner, name, issueId.toString) foreach { issue => + // extract references and create refer comment + createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get) - // notifications - Notifier().toNotify(repository, issue, form.content.getOrElse("")){ - Notifier.msgPullRequest(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}") + // notifications + Notifier().toNotify(repository, issue, form.content.getOrElse("")) { + Notifier.msgPullRequest(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}") + } } - } - redirect(s"/${owner}/${name}/pull/${issueId}") + redirect(s"/${owner}/${name}/pull/${issueId}") + } else Unauthorized() } }) @@ -516,8 +520,7 @@ 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) + val page = IssueSearchCondition.page(request) // retrieve search condition val condition = IssueSearchCondition(request) @@ -526,18 +529,33 @@ "pulls", searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), page, - if(!getAccountByUserName(owner).exists(_.isGroupAccount)){ - (getCollaborators(owner, repoName) :+ owner).sorted - } else { - getCollaborators(owner, repoName) - }, + getAssignableUserNames(owner, repoName), 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)) + isEditable(repository), + isManageable(repository)) } + /** + * Tests whether an logged-in user can manage pull requests. + */ + private def isManageable(repository: RepositoryInfo)(implicit context: Context): Boolean = { + hasWritePermission(repository.owner, repository.name, context.loginAccount) + } + + /** + * Tests whether an logged-in user can post pull requests. + */ + private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = { + repository.repository.options.issuesOption match { + case "PUBLIC" => hasReadPermission(repository.owner, repository.name, context.loginAccount) + case "PRIVATE" => hasWritePermission(repository.owner, repository.name, context.loginAccount) + case "DISABLE" => false + } + } + } diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index b60ae5d..5a2ca91 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -31,22 +31,22 @@ repositoryName: String, description: Option[String], isPrivate: Boolean, - enableIssues: Boolean, + issuesOption: String, externalIssuesUrl: Option[String], - enableWiki: Boolean, - allowWikiEditing: Boolean, - externalWikiUrl: Option[String] + wikiOption: String, + externalWikiUrl: Option[String], + allowFork: Boolean ) val optionsForm = mapping( "repositoryName" -> trim(label("Repository Name" , text(required, maxlength(40), identifier, renameRepositoryName))), "description" -> trim(label("Description" , optional(text()))), "isPrivate" -> trim(label("Repository Type" , boolean())), - "enableIssues" -> trim(label("Enable Issues" , boolean())), + "issuesOption" -> trim(label("Issues Option" , text(required, featureOption))), "externalIssuesUrl" -> trim(label("External Issues URL", optional(text(maxlength(200))))), - "enableWiki" -> trim(label("Enable Wiki" , boolean())), - "allowWikiEditing" -> trim(label("Allow Wiki Editing" , boolean())), - "externalWikiUrl" -> trim(label("External Wiki URL" , optional(text(maxlength(200))))) + "wikiOption" -> trim(label("Wiki Option" , text(required, featureOption))), + "externalWikiUrl" -> trim(label("External Wiki URL" , optional(text(maxlength(200))))), + "allowFork" -> trim(label("Allow Forking" , boolean())) )(OptionsForm.apply) // for default branch @@ -56,12 +56,12 @@ "defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))) )(DefaultBranchForm.apply) - // for collaborator addition - case class CollaboratorForm(userName: String) - - val collaboratorForm = mapping( - "userName" -> trim(label("Username", text(required, collaborator))) - )(CollaboratorForm.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, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String]) @@ -107,11 +107,11 @@ repository.repository.parentUserName.map { _ => repository.repository.isPrivate } getOrElse form.isPrivate, - form.enableIssues, + form.issuesOption, form.externalIssuesUrl, - form.enableWiki, - form.allowWikiEditing, - form.externalWikiUrl + form.wikiOption, + form.externalWikiUrl, + form.allowFork ) // Change repository name if(repository.name != form.repositoryName){ @@ -175,22 +175,12 @@ 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")) + post("/:owner/:repository/settings/collaborators")(ownerOnly { repository => + val collaborators = params("collaborators") + removeCollaborators(repository.owner, repository.name) + collaborators.split(",").withFilter(_.nonEmpty).map { collaborator => + val userName :: permission :: Nil = collaborator.split(":").toList + addCollaborator(repository.owner, repository.name, userName, permission) } redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") }) @@ -297,7 +287,7 @@ get("/:owner/:repository/settings/hooks/edit")(ownerOnly { repository => getWebHook(repository.owner, repository.name, params("url")).map{ case (webhook, events) => html.edithooks(webhook, events, repository, flash.get("info"), false) - } getOrElse NotFound + } getOrElse NotFound() }) /** @@ -394,20 +384,20 @@ } } - /** - * 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 - } - } +// /** +// * 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(value + " is repository owner.") // TODO also group members? +// case _ => None +// } +// } /** * Duplicate check for the rename repository name. @@ -422,6 +412,15 @@ } /** + * + */ + private def featureOption: Constraint = new Constraint(){ + override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = + if(Seq("DISABLE", "PRIVATE", "PUBLIC").contains(value)) None else Some("Option is invalid.") + } + + + /** * Provides Constraint to validate the repository transfer user. */ private def transferUser: Constraint = new Constraint(){ diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala index cee8f4f..ad8b091 100644 --- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala @@ -31,7 +31,7 @@ class RepositoryViewerController extends RepositoryViewerControllerBase with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService - with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService + with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator with PullRequestService with CommitStatusService with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBranchService /** @@ -39,7 +39,7 @@ */ trait RepositoryViewerControllerBase extends ControllerBase { self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService - with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService + with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator with PullRequestService with CommitStatusService with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBranchService => ArchiveCommand.registerFormat("zip", new ZipFormat) @@ -152,12 +152,12 @@ 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 + case Left(_) => NotFound() } } }) - get("/:owner/:repository/new/*")(collaboratorsOnly { repository => + get("/:owner/:repository/new/*")(writableUsersOnly { repository => val (branch, path) = repository.splitPath(multiParams("splat").head) val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName) html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList, @@ -165,7 +165,7 @@ protectedBranch) }) - get("/:owner/:repository/edit/*")(collaboratorsOnly { repository => + get("/:owner/:repository/edit/*")(writableUsersOnly { repository => val (branch, path) = repository.splitPath(multiParams("splat").head) val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName) @@ -177,11 +177,11 @@ html.editor(branch, repository, paths.take(paths.size - 1).toList, Some(paths.last), JGitUtil.getContentInfo(git, path, objectId), protectedBranch) - } getOrElse NotFound + } getOrElse NotFound() } }) - get("/:owner/:repository/remove/*")(collaboratorsOnly { repository => + get("/:owner/:repository/remove/*")(writableUsersOnly { repository => val (branch, path) = repository.splitPath(multiParams("splat").head) using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch)) @@ -190,11 +190,11 @@ val paths = path.split("/") html.delete(branch, repository, paths.take(paths.size - 1).toList, paths.last, JGitUtil.getContentInfo(git, path, objectId)) - } getOrElse NotFound + } getOrElse NotFound() } }) - post("/:owner/:repository/create", editorForm)(collaboratorsOnly { (form, repository) => + post("/:owner/:repository/create", editorForm)(writableUsersOnly { (form, repository) => commitFile( repository = repository, branch = form.branch, @@ -211,7 +211,7 @@ }") }) - post("/:owner/:repository/update", editorForm)(collaboratorsOnly { (form, repository) => + post("/:owner/:repository/update", editorForm)(writableUsersOnly { (form, repository) => commitFile( repository = repository, branch = form.branch, @@ -232,7 +232,7 @@ }") }) - post("/:owner/:repository/remove", deleteForm)(collaboratorsOnly { (form, repository) => + post("/:owner/:repository/remove", deleteForm)(writableUsersOnly { (form, repository) => commitFile(repository, form.branch, form.path, None, Some(form.fileName), "", "", form.message.getOrElse(s"Delete ${form.fileName}")) @@ -250,7 +250,7 @@ loader.copyTo(response.outputStream) () } - } getOrElse NotFound + } getOrElse NotFound() } }) @@ -270,7 +270,7 @@ response.setContentLength(loader.getSize.toInt) loader.copyTo(response.outputStream) () - } getOrElse NotFound + } getOrElse NotFound() } else { html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId), @@ -278,7 +278,7 @@ hasWritePermission(repository.owner, repository.name, context.loginAccount), request.paths(2) == "blame") } - } getOrElse NotFound + } getOrElse NotFound() } }) @@ -334,7 +334,7 @@ } } } catch { - case e:MissingObjectException => NotFound + case e:MissingObjectException => NotFound() } }) @@ -397,8 +397,8 @@ ) )) } - } else Unauthorized - } getOrElse NotFound + } else Unauthorized() + } getOrElse NotFound() }) ajaxPost("/:owner/:repository/commit_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => @@ -407,8 +407,8 @@ if(isEditable(owner, name, comment.commentedUserName)){ updateCommitComment(comment.commentId, form.content) redirect(s"/${owner}/${name}/commit_comments/_data/${comment.commentId}") - } else Unauthorized - } getOrElse NotFound + } else Unauthorized() + } getOrElse NotFound() } }) @@ -417,8 +417,8 @@ getCommitComment(owner, name, params("id")).map { comment => if(isEditable(owner, name, comment.commentedUserName)){ Ok(deleteCommitComment(comment.commentId)) - } else Unauthorized - } getOrElse NotFound + } else Unauthorized() + } getOrElse NotFound() } }) @@ -443,7 +443,7 @@ /** * Creates a branch. */ - post("/:owner/:repository/branches")(collaboratorsOnly { repository => + post("/:owner/:repository/branches")(writableUsersOnly { repository => val newBranchName = params.getOrElse("new", halt(400)) val fromBranchName = params.getOrElse("from", halt(400)) using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => @@ -461,7 +461,7 @@ /** * Deletes branch. */ - get("/:owner/:repository/delete/*")(collaboratorsOnly { repository => + get("/:owner/:repository/delete/*")(writableUsersOnly { repository => val branchName = multiParams("splat").head val userName = context.loginAccount.get.userName if(repository.repository.defaultBranch != branchName){ @@ -489,23 +489,25 @@ archiveRepository(name, ".zip", repository) case name if name.endsWith(".tar.gz") => archiveRepository(name, ".tar.gz", repository) - case _ => BadRequest + case _ => BadRequest() } }) get("/:owner/:repository/network/members")(referrersOnly { repository => - html.forked( - getRepository( - repository.repository.originUserName.getOrElse(repository.owner), - repository.repository.originRepositoryName.getOrElse(repository.name)), - getForkedRepositories( - repository.repository.originUserName.getOrElse(repository.owner), - repository.repository.originRepositoryName.getOrElse(repository.name)), - context.loginAccount match { - case None => List() - case account: Option[Account] => getGroupsByUserName(account.get.userName) - }, // groups of current user - repository) + if(repository.repository.options.allowFork) { + html.forked( + getRepository( + repository.repository.originUserName.getOrElse(repository.owner), + repository.repository.originRepositoryName.getOrElse(repository.name)), + getForkedRepositories( + repository.repository.originUserName.getOrElse(repository.owner), + repository.repository.originRepositoryName.getOrElse(repository.name)), + context.loginAccount match { + case None => List() + case account: Option[Account] => getGroupsByUserName(account.get.userName) + }, // groups of current user + repository) + } else BadRequest() }) /** @@ -516,7 +518,7 @@ val ref = multiParams("splat").head JGitUtil.getTreeId(git, ref).map{ treeId => html.find(ref, treeId, repository) - } getOrElse NotFound + } getOrElse NotFound() } }) @@ -571,7 +573,7 @@ getPullRequestFromBranch(repository.owner, repository.name, revstr, repository.repository.defaultBranch), flash.get("info"), flash.get("error")) } - } getOrElse NotFound + } getOrElse NotFound() } } } @@ -591,14 +593,18 @@ val headName = s"refs/heads/${branch}" val headTip = git.getRepository.resolve(headName) - JGitUtil.processTree(git, headTip){ (path, tree) => + val permission = JGitUtil.processTree(git, headTip){ (path, tree) => + // Add all entries except the editing file if(!newPath.exists(_ == path) && !oldPath.exists(_ == path)){ builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) } - } + // Retrieve permission if file exists to keep it + oldPath.collect { case x if x == path => tree.getEntryFileMode.getBits } + }.flatten.headOption newPath.foreach { newPath => - builder.add(JGitUtil.createDirCacheEntry(newPath, FileMode.REGULAR_FILE, + builder.add(JGitUtil.createDirCacheEntry(newPath, + permission.map { bits => FileMode.fromBits(bits) } getOrElse FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes(charset)))) } builder.finish() @@ -621,8 +627,11 @@ updatePullRequests(repository.owner, repository.name, branch) // record activity - recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch, - List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId)))) + val commitInfo = new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId)) + recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch, List(commitInfo)) + + // create issue comment by commit message + createIssueComment(repository.owner, repository.name, commitInfo) // close issue by commit message closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name) diff --git a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala index deaf4d0..bfca858 100644 --- a/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/SystemSettingsController.scala @@ -233,7 +233,7 @@ updateImage(userName, form.fileId, form.clearImage) redirect("/admin/users") } - } getOrElse NotFound + } getOrElse NotFound() }) get("/admin/users/_newgroup")(adminOnly { @@ -279,19 +279,19 @@ } 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) - } - } +// // 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 + } getOrElse NotFound() } }) diff --git a/src/main/scala/gitbucket/core/controller/WikiController.scala b/src/main/scala/gitbucket/core/controller/WikiController.scala index ad98cee..64d0b27 100644 --- a/src/main/scala/gitbucket/core/controller/WikiController.scala +++ b/src/main/scala/gitbucket/core/controller/WikiController.scala @@ -14,10 +14,10 @@ class WikiController extends WikiControllerBase with WikiService with RepositoryService with AccountService with ActivityService - with CollaboratorsAuthenticator with ReferrerAuthenticator + with ReadableUsersAuthenticator with ReferrerAuthenticator trait WikiControllerBase extends ControllerBase { - self: WikiService with RepositoryService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator => + self: WikiService with RepositoryService with ActivityService with ReadableUsersAuthenticator with ReferrerAuthenticator => case class WikiPageEditForm(pageName: String, content: String, message: Option[String], currentPageName: String, id: String) @@ -62,7 +62,7 @@ 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 Right((logs, hasNext)) => html.history(Some(pageName), logs, repository, isEditable(repository)) case Left(_) => NotFound() } } @@ -87,7 +87,7 @@ } }) - get("/:owner/:repository/wiki/:page/_revert/:commitId")(referrersOnly { repository => + get("/:owner/:repository/wiki/:page/_revert/:commitId")(readableUsersOnly { repository => if(isEditable(repository)){ val pageName = StringUtil.urlDecode(params("page")) val Array(from, to) = params("commitId").split("\\.\\.\\.") @@ -101,7 +101,7 @@ } else Unauthorized() }) - get("/:owner/:repository/wiki/_revert/:commitId")(referrersOnly { repository => + get("/:owner/:repository/wiki/_revert/:commitId")(readableUsersOnly { repository => if(isEditable(repository)){ val Array(from, to) = params("commitId").split("\\.\\.\\.") @@ -114,14 +114,14 @@ } else Unauthorized() }) - get("/:owner/:repository/wiki/:page/_edit")(referrersOnly { repository => + get("/:owner/:repository/wiki/:page/_edit")(readableUsersOnly { repository => if(isEditable(repository)){ val pageName = StringUtil.urlDecode(params("page")) html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository) } else Unauthorized() }) - post("/:owner/:repository/wiki/_edit", editForm)(referrersOnly { (form, repository) => + post("/:owner/:repository/wiki/_edit", editForm)(readableUsersOnly { (form, repository) => if(isEditable(repository)){ defining(context.loginAccount.get){ loginAccount => saveWikiPage( @@ -146,13 +146,13 @@ } else Unauthorized() }) - get("/:owner/:repository/wiki/_new")(referrersOnly { repository => + get("/:owner/:repository/wiki/_new")(readableUsersOnly { repository => if(isEditable(repository)){ html.edit("", None, repository) } else Unauthorized() }) - post("/:owner/:repository/wiki/_new", newForm)(referrersOnly { (form, repository) => + post("/:owner/:repository/wiki/_new", newForm)(readableUsersOnly { (form, repository) => if(isEditable(repository)){ defining(context.loginAccount.get){ loginAccount => saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName, @@ -170,7 +170,7 @@ } else Unauthorized() }) - get("/:owner/:repository/wiki/:page/_delete")(referrersOnly { repository => + get("/:owner/:repository/wiki/:page/_delete")(readableUsersOnly { repository => if(isEditable(repository)){ val pageName = StringUtil.urlDecode(params("page")) @@ -182,7 +182,7 @@ } } else Unauthorized() }) - + get("/:owner/:repository/wiki/_pages")(referrersOnly { repository => html.pages(getWikiPageList(repository.owner, repository.name), repository, isEditable(repository)) }) @@ -190,7 +190,7 @@ 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 Right((logs, hasNext)) => html.history(None, logs, repository, isEditable(repository)) case Left(_) => NotFound() } } @@ -201,7 +201,7 @@ getFileContent(repository.owner, repository.name, path).map { bytes => RawData(FileUtil.getContentType(path, bytes), bytes) - } getOrElse NotFound + } getOrElse NotFound() }) private def unique: Constraint = new Constraint(){ @@ -240,9 +240,13 @@ private def targetWikiPage = getWikiPage(params("owner"), params("repository"), params("pageName")) - private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = - repository.repository.allowWikiEditing || ( - hasWritePermission(repository.owner, repository.name, context.loginAccount) - ) + private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = { + repository.repository.options.wikiOption match { +// case "ALL" => repository.repository.isPrivate == false || hasReadPermission(repository.owner, repository.name, context.loginAccount) + case "PUBLIC" => hasReadPermission(repository.owner, repository.name, context.loginAccount) + case "PRIVATE" => hasWritePermission(repository.owner, repository.name, context.loginAccount) + case "DISABLE" => false + } + } } diff --git a/src/main/scala/gitbucket/core/model/Collaborator.scala b/src/main/scala/gitbucket/core/model/Collaborator.scala index d3af76a..0838165 100644 --- a/src/main/scala/gitbucket/core/model/Collaborator.scala +++ b/src/main/scala/gitbucket/core/model/Collaborator.scala @@ -7,7 +7,8 @@ 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) + val permission = column[String]("PERMISSION") + def * = (userName, repositoryName, collaboratorName, permission) <> (Collaborator.tupled, Collaborator.unapply) def byPrimaryKey(owner: String, repository: String, collaborator: String) = byRepository(owner, repository) && (collaboratorName === collaborator.bind) @@ -17,5 +18,23 @@ case class Collaborator( userName: String, repositoryName: String, - collaboratorName: String + collaboratorName: String, + permission: String ) + +sealed abstract class Permission(val name: String) + +object Permission { + object ADMIN extends Permission("ADMIN") + object WRITE extends Permission("WRITE") + object READ extends Permission("READ") + +// val values: Vector[Permission] = Vector(ADMIN, WRITE, READ) +// +// private val map: Map[String, Permission] = values.map(enum => enum.name -> enum).toMap +// +// def apply(name: String): Permission = map(name) +// +// def valueOf(name: String): Option[Permission] = map.get(name) + +} diff --git a/src/main/scala/gitbucket/core/model/Repository.scala b/src/main/scala/gitbucket/core/model/Repository.scala index fbc2ff3..66cb641 100644 --- a/src/main/scala/gitbucket/core/model/Repository.scala +++ b/src/main/scala/gitbucket/core/model/Repository.scala @@ -7,24 +7,61 @@ 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 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 parentUserName = column[String]("PARENT_USER_NAME") val parentRepositoryName = column[String]("PARENT_REPOSITORY_NAME") - val enableIssues = column[Boolean]("ENABLE_ISSUES") - val externalIssuesUrl = column[String]("EXTERNAL_ISSUES_URL") - val enableWiki = column[Boolean]("ENABLE_WIKI") - val allowWikiEditing = column[Boolean]("ALLOW_WIKI_EDITING") - val externalWikiUrl = column[String]("EXTERNAL_WIKI_URL") - def * = (userName, repositoryName, isPrivate, description.?, defaultBranch, - registeredDate, updatedDate, lastActivityDate, originUserName.?, originRepositoryName.?, parentUserName.?, parentRepositoryName.?, - enableIssues, externalIssuesUrl.?, enableWiki, allowWikiEditing, externalWikiUrl.?) <> (Repository.tupled, Repository.unapply) + val issuesOption = column[String]("ISSUES_OPTION") + val externalIssuesUrl = column[String]("EXTERNAL_ISSUES_URL") + val wikiOption = column[String]("WIKI_OPTION") + val externalWikiUrl = column[String]("EXTERNAL_WIKI_URL") + val allowFork = column[Boolean]("ALLOW_FORK") + + def * = ( + (userName, repositoryName, isPrivate, description.?, defaultBranch, + registeredDate, updatedDate, lastActivityDate, originUserName.?, originRepositoryName.?, parentUserName.?, parentRepositoryName.?), + (issuesOption, externalIssuesUrl.?, wikiOption, externalWikiUrl.?, allowFork) + ).shaped <> ( + { case (repository, options) => + Repository( + repository._1, + repository._2, + repository._3, + repository._4, + repository._5, + repository._6, + repository._7, + repository._8, + repository._9, + repository._10, + repository._11, + repository._12, + RepositoryOptions.tupled.apply(options) + ) + }, { (r: Repository) => + Some((( + r.userName, + r.repositoryName, + r.isPrivate, + r.description, + r.defaultBranch, + r.registeredDate, + r.updatedDate, + r.lastActivityDate, + r.originUserName, + r.originRepositoryName, + r.parentUserName, + r.parentRepositoryName + ),( + RepositoryOptions.unapply(r.options).get + ))) + }) def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) } @@ -43,9 +80,13 @@ originRepositoryName: Option[String], parentUserName: Option[String], parentRepositoryName: Option[String], - enableIssues: Boolean, + options: RepositoryOptions +) + +case class RepositoryOptions( + issuesOption: String, externalIssuesUrl: Option[String], - enableWiki: Boolean, - allowWikiEditing: Boolean, - externalWikiUrl: Option[String] + wikiOption: String, + externalWikiUrl: Option[String], + allowFork: Boolean ) diff --git a/src/main/scala/gitbucket/core/service/AccountService.scala b/src/main/scala/gitbucket/core/service/AccountService.scala index d6ab131..5a6c748 100644 --- a/src/main/scala/gitbucket/core/service/AccountService.scala +++ b/src/main/scala/gitbucket/core/service/AccountService.scala @@ -181,7 +181,6 @@ 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] = { diff --git a/src/main/scala/gitbucket/core/service/HandleCommentService.scala b/src/main/scala/gitbucket/core/service/HandleCommentService.scala index 0d73d42..225766b 100644 --- a/src/main/scala/gitbucket/core/service/HandleCommentService.scala +++ b/src/main/scala/gitbucket/core/service/HandleCommentService.scala @@ -13,7 +13,7 @@ with WebHookService with WebHookIssueCommentService with WebHookPullRequestService => /** - * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]] + * @see [[https://github.com/gitbucket/gitbucket/wiki/CommentAction]] */ def handleComment(issue: Issue, content: Option[String], repository: RepositoryService.RepositoryInfo, actionOpt: Option[String]) (implicit context: Context, s: Session) = { @@ -54,18 +54,20 @@ // call web hooks action match { - case None => commentId.map{ commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, context.loginAccount.get) } - case Some(act) => val webHookAction = act match { - case "open" => "opened" - case "reopen" => "reopened" - case "close" => "closed" - case _ => act - } - if(issue.isPullRequest){ + case None => commentId.map { commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, context.loginAccount.get) } + case Some(act) => { + val webHookAction = act match { + case "open" => "opened" + case "reopen" => "reopened" + case "close" => "closed" + case _ => act + } + if (issue.isPullRequest) { callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, context.loginAccount.get) } else { callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, context.loginAccount.get) } + } } // notifications diff --git a/src/main/scala/gitbucket/core/service/IssuesService.scala b/src/main/scala/gitbucket/core/service/IssuesService.scala index c38b291..e43f399 100644 --- a/src/main/scala/gitbucket/core/service/IssuesService.scala +++ b/src/main/scala/gitbucket/core/service/IssuesService.scala @@ -3,7 +3,7 @@ import gitbucket.core.util.JGitUtil.CommitInfo import gitbucket.core.util.StringUtil._ import gitbucket.core.util.Implicits._ -import gitbucket.core.model.{Account, CommitState, Issue, IssueComment, IssueLabel, Label, PullRequest, Repository} +import gitbucket.core.model._ import gitbucket.core.model.Profile._ import profile._ import profile.blockingApi._ @@ -11,7 +11,7 @@ trait IssuesService { - self: AccountService => + self: AccountService with RepositoryService => import IssuesService._ def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) = @@ -124,25 +124,19 @@ (implicit s: Session): List[IssueInfo] = { // get issues and comment count and labels val result = searchIssueQueryBase(condition, pullRequest, offset, limit, repos) - .joinLeft (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } - .joinLeft (Labels) .on { case (((t1, t2), t3), t4) => t3.map(_.byLabel(t4.userName, t4.repositoryName, t4.labelId)) } - .joinLeft (Milestones) .on { case ((((t1, t2), t3), t4), t5) => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) } - .map { case ((((t1, t2), t3), t4), t5) => + .joinLeft (IssueLabels) .on { case (((t1, t2), i), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } + .joinLeft (Labels) .on { case ((((t1, t2), i), t3), t4) => t3.map(_.byLabel(t4.userName, t4.repositoryName, t4.labelId)) } + .joinLeft (Milestones) .on { case (((((t1, t2), i), t3), t4), t5) => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) } + .map { case (((((t1, t2), i), t3), t4), t5) => (t1, t2.commentCount, t4.map(_.labelId), t4.map(_.labelName), t4.map(_.color), t5.map(_.title)) } .list - .splitWith { (c1, c2) => - c1._1.userName == c2._1.userName && - c1._1.repositoryName == c2._1.repositoryName && - c1._1.issueId == c2._1.issueId - } + .splitWith { (c1, c2) => c1._1.userName == c2._1.userName && c1._1.repositoryName == c2._1.repositoryName && c1._1.issueId == c2._1.issueId } result.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, + issues.flatMap { t => t._3.map (Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get))} toList, milestone, commentCount, getCommitStatues(issue.userName, issue.repositoryName, issue.issueId)) @@ -154,40 +148,40 @@ * @return (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) */ def searchPullRequestByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*) - (implicit s: Session): List[(Issue, Account, Int, PullRequest, Repository, Account)] = { + (implicit s: Session): List[(Issue, Account, Int, PullRequest, Repository, Account)] = { // get issues and comment count and labels searchIssueQueryBase(condition, true, offset, limit, repos) - .join(PullRequests).on { case ((t1, t2), t3) => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) } - .join(Repositories).on { case (((t1, t2), t3), t4) => t4.byRepository(t1.userName, t1.repositoryName) } - .join(Accounts).on { case ((((t1, t2), t3), t4), t5) => t5.userName === t1.openedUserName } - .join(Accounts).on { case (((((t1, t2), t3), t4), t5), t6) => t6.userName === t4.userName } - .map { case (((((t1, t2), t3), t4), t5), t6) => - (t1, t5, t2.commentCount, t3, t4, t6) - } + .join(PullRequests).on { case (((t1, t2), i), t3) => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) } + .join(Repositories).on { case ((((t1, t2), i), t3), t4) => t4.byRepository(t1.userName, t1.repositoryName) } + .join(Accounts).on { case (((((t1, t2), i), t3), t4), t5) => t5.userName === t1.openedUserName } + .join(Accounts).on { case ((((((t1, t2), i), t3), t4), t5), t6) => t6.userName === t4.userName } + .sortBy { case ((((((t1, t2), i), t3), t4), t5), t6) => i asc } + .map { case ((((((t1, t2), i), t3), t4), t5), t6) => (t1, t5, t2.commentCount, t3, t4, t6) } .list } private def searchIssueQueryBase(condition: IssueSearchCondition, pullRequest: Boolean, offset: Int, limit: Int, repos: Seq[(String, String)]) - (implicit s: Session) = + (implicit s: Session) = searchIssueQuery(repos, condition, pullRequest) - .join(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } - .sortBy { case (t1, t2) => - condition.sort match { - case "created" => condition.direction match { - case "asc" => t1.registeredDate asc - case "desc" => t1.registeredDate desc - } - case "comments" => condition.direction match { - case "asc" => t2.commentCount asc - case "desc" => t2.commentCount desc - } - case "updated" => condition.direction match { - case "asc" => t1.updatedDate asc - case "desc" => t1.updatedDate desc - } + .join(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } + .sortBy { case (t1, t2) => t1.issueId desc } + .sortBy { case (t1, t2) => + condition.sort match { + case "created" => condition.direction match { + case "asc" => t1.registeredDate asc + case "desc" => t1.registeredDate desc + } + case "comments" => condition.direction match { + case "asc" => t2.commentCount asc + case "desc" => t2.commentCount desc + } + case "updated" => condition.direction match { + case "asc" => t1.updatedDate asc + case "desc" => t1.updatedDate desc } } - .drop(offset).take(limit) + } + .drop(offset).take(limit).zipWithIndex /** @@ -393,6 +387,11 @@ } } + def getAssignableUserNames(owner: String, repository: String)(implicit s: Session): List[String] = { + (getCollaboratorUserNames(owner, repository, Seq(Permission.ADMIN, Permission.WRITE)) ::: + (if (getAccountByUserName(owner).get.isGroupAccount) getGroupMembers(owner).map(_.userName) else List(owner))).sorted + } + } object IssuesService { diff --git a/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala b/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala index d27d268..85962f3 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala @@ -21,12 +21,12 @@ // Insert to the database at first insertRepository(name, owner, description, isPrivate) - // Add collaborators for group repository - if(ownerAccount.isGroupAccount){ - getGroupMembers(owner).foreach { member => - addCollaborator(owner, name, member.userName) - } - } +// // Add collaborators for group repository +// if(ownerAccount.isGroupAccount){ +// getGroupMembers(owner).foreach { member => +// addCollaborator(owner, name, member.userName) +// } +// } // Insert default labels insertDefaultLabels(owner, name) diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala index 46f2fd4..6ecc93a 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala @@ -2,7 +2,7 @@ import gitbucket.core.controller.Context import gitbucket.core.util.JGitUtil -import gitbucket.core.model.{Collaborator, Repository, Account} +import gitbucket.core.model.{Collaborator, Repository, RepositoryOptions, Account, Permission} import gitbucket.core.model.Profile._ import profile._ import profile.blockingApi._ @@ -39,11 +39,13 @@ originRepositoryName = originRepositoryName, parentUserName = parentUserName, parentRepositoryName = parentRepositoryName, - enableIssues = true, - externalIssuesUrl = None, - enableWiki = true, - allowWikiEditing = true, - externalWikiUrl = None + options = RepositoryOptions( + issuesOption = "PUBLIC", // TODO DISABLE for the forked repository? + externalIssuesUrl = None, + wikiOption = "PUBLIC", // TODO DISABLE for the forked repository? + externalWikiUrl = None, + allowFork = true + ) ) IssueId insert (userName, repositoryName, 0) @@ -128,11 +130,8 @@ 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)) :_*) - } + // TODO Drop transfered owner from collaborators? + Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) // Update activity messages Activities.filter { t => @@ -327,12 +326,12 @@ */ def saveRepositoryOptions(userName: String, repositoryName: String, description: Option[String], isPrivate: Boolean, - enableIssues: Boolean, externalIssuesUrl: Option[String], - enableWiki: Boolean, allowWikiEditing: Boolean, externalWikiUrl: Option[String])(implicit s: Session): Unit = { + issuesOption: String, externalIssuesUrl: Option[String], + wikiOption: String, externalWikiUrl: Option[String], + allowFork: Boolean)(implicit s: Session): Unit = Repositories.filter(_.byRepository(userName, repositoryName)) - .map { r => (r.description.?, r.isPrivate, r.enableIssues, r.externalIssuesUrl.?, r.enableWiki, r.allowWikiEditing, r.externalWikiUrl.?, r.updatedDate) } - .update (description, isPrivate, enableIssues, externalIssuesUrl, enableWiki, allowWikiEditing, externalWikiUrl, currentDate) - } + .map { r => (r.description.?, r.isPrivate, r.issuesOption, r.externalIssuesUrl.?, r.wikiOption, r.externalWikiUrl.?, r.allowFork, r.updatedDate) } + .update (description, isPrivate, issuesOption, externalIssuesUrl, wikiOption, externalWikiUrl, allowFork, currentDate) def saveRepositoryDefaultBranch(userName: String, repositoryName: String, defaultBranch: String)(implicit s: Session): Unit = @@ -341,49 +340,64 @@ .update (defaultBranch) /** - * Add collaborator to the repository. - * - * @param userName the user name of the repository owner - * @param repositoryName the repository name - * @param collaboratorName the collaborator name + * Add collaborator (user or group) to the repository. */ - 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 + def addCollaborator(userName: String, repositoryName: String, collaboratorName: String, permission: String)(implicit s: Session): Unit = + Collaborators insert Collaborator(userName, repositoryName, collaboratorName, permission) /** * 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 + * Returns the list of collaborators name (user name or group name) which is sorted with ascending order. */ - def getCollaborators(userName: String, repositoryName: String)(implicit s: Session): List[String] = - Collaborators.filter(_.byRepository(userName, repositoryName)).sortBy(_.collaboratorName).map(_.collaboratorName).list + def getCollaborators(userName: String, repositoryName: String)(implicit s: Session): List[(Collaborator, Boolean)] = + Collaborators + .join(Accounts).on(_.collaboratorName === _.userName) + .filter { case (t1, t2) => t1.byRepository(userName, repositoryName) } + .map { case (t1, t2) => (t1, t2.groupAccount) } + .sortBy { case (t1, t2) => t1.collaboratorName } + .list + + /** + * Returns the list of all collaborator name and permission which is sorted with ascending order. + * If a group is added as a collaborator, this method returns users who are belong to that group. + */ + def getCollaboratorUserNames(userName: String, repositoryName: String, filter: Seq[Permission] = Nil)(implicit s: Session): List[String] = { + val q1 = Collaborators + .join(Accounts).on { case (t1, t2) => (t1.collaboratorName === t2.userName) && (t2.groupAccount === false.bind) } + .filter { case (t1, t2) => t1.byRepository(userName, repositoryName) } + .map { case (t1, t2) => (t1.collaboratorName, t1.permission) } + + val q2 = Collaborators + .join(Accounts).on { case (t1, t2) => (t1.collaboratorName === t2.userName) && (t2.groupAccount === true.bind) } + .join(GroupMembers).on { case ((t1, t2), t3) => t2.userName === t3.groupName } + .filter { case ((t1, t2), t3) => t1.byRepository(userName, repositoryName) } + .map { case ((t1, t2), t3) => (t3.userName, t1.permission) } + + q1.union(q2).list.filter { x => filter.isEmpty || filter.exists(_.name == x._2) }.map(_._1) + } + 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 Some(a) if(getGroupMembers(owner).exists(_.userName == a.userName)) => true + case Some(a) if(getCollaboratorUserNames(owner, repository, Seq(Permission.ADMIN, Permission.WRITE)).contains(a.userName)) => true + case _ => false + } + } + + def hasReadPermission(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(getGroupMembers(owner).exists(_.userName == a.userName)) => true + case Some(a) if(getCollaboratorUserNames(owner, repository, Seq(Permission.ADMIN, Permission.WRITE, Permission.READ)).contains(a.userName)) => true case _ => false } } diff --git a/src/main/scala/gitbucket/core/service/RequestCache.scala b/src/main/scala/gitbucket/core/service/RequestCache.scala index 768a3b4..bd03cd8 100644 --- a/src/main/scala/gitbucket/core/service/RequestCache.scala +++ b/src/main/scala/gitbucket/core/service/RequestCache.scala @@ -11,7 +11,7 @@ * 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 { +trait RequestCache extends SystemSettingsService with AccountService with IssuesService with RepositoryService { private implicit def context2Session(implicit context: Context): Session = request2Session(context.request) diff --git a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala index 668eb9a..c2aa28d 100644 --- a/src/main/scala/gitbucket/core/service/SystemSettingsService.scala +++ b/src/main/scala/gitbucket/core/service/SystemSettingsService.scala @@ -73,7 +73,7 @@ getValue(props, AllowAccountRegistration, false), getValue(props, AllowAnonymousAccess, true), getValue(props, IsCreateRepoOptionPublic, true), - getValue(props, Gravatar, true), + getValue(props, Gravatar, false), getValue(props, Notification, false), getOptionValue[Int](props, ActivityLogLimit, None), getValue(props, Ssh, false), diff --git a/src/main/scala/gitbucket/core/servlet/GHCompatRepositoryAccessFilter.scala b/src/main/scala/gitbucket/core/servlet/GHCompatRepositoryAccessFilter.scala new file mode 100644 index 0000000..0f1322d --- /dev/null +++ b/src/main/scala/gitbucket/core/servlet/GHCompatRepositoryAccessFilter.scala @@ -0,0 +1,36 @@ +package gitbucket.core.servlet + +import javax.servlet._ +import javax.servlet.http.{HttpServletRequest, HttpServletResponse} + +import gitbucket.core.service.SystemSettingsService + +/** + * A controller to provide GitHub compatible URL for Git clients. + */ +class GHCompatRepositoryAccessFilter extends Filter with SystemSettingsService { + + /** + * Pattern of GitHub compatible repository URL. + * /:user/:repo.git/ + */ + private val githubRepositoryPattern = """^/[^/]+/[^/]+\.git/.*""".r + + override def init(filterConfig: FilterConfig) = {} + + override def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain) = { + implicit val request = req.asInstanceOf[HttpServletRequest] + val response = res.asInstanceOf[HttpServletResponse] + val requestPath = request.getRequestURI.substring(request.getContextPath.length) + requestPath match { + case githubRepositoryPattern() => + response.sendRedirect(baseUrl + "/git" + requestPath) + + case _ => + chain.doFilter(req, res) + } + } + + override def destroy() = {} + +} diff --git a/src/main/scala/gitbucket/core/servlet/TransactionFilter.scala b/src/main/scala/gitbucket/core/servlet/TransactionFilter.scala index e3fff33..891f1f0 100644 --- a/src/main/scala/gitbucket/core/servlet/TransactionFilter.scala +++ b/src/main/scala/gitbucket/core/servlet/TransactionFilter.scala @@ -53,6 +53,13 @@ config.setJdbcUrl(DatabaseConfig.url) config.setUsername(DatabaseConfig.user) config.setPassword(DatabaseConfig.password) + config.setAutoCommit(false) + DatabaseConfig.connectionTimeout.foreach(config.setConnectionTimeout) + DatabaseConfig.idleTimeout.foreach(config.setIdleTimeout) + DatabaseConfig.maxLifetime.foreach(config.setMaxLifetime) + DatabaseConfig.minimumIdle.foreach(config.setMinimumIdle) + DatabaseConfig.maximumPoolSize.foreach(config.setMaximumPoolSize) + logger.debug("load database connection pool") new HikariDataSource(config) } diff --git a/src/main/scala/gitbucket/core/ssh/GitCommand.scala b/src/main/scala/gitbucket/core/ssh/GitCommand.scala index 3abfb22..c7b4277 100644 --- a/src/main/scala/gitbucket/core/ssh/GitCommand.scala +++ b/src/main/scala/gitbucket/core/ssh/GitCommand.scala @@ -37,7 +37,7 @@ override def run(): Unit = { authUser match { case Some(authUser) => - Database() withSession { implicit session => + Database() withTransaction { implicit session => try { runTask(authUser) callback.onExit(0) diff --git a/src/main/scala/gitbucket/core/util/Authenticator.scala b/src/main/scala/gitbucket/core/util/Authenticator.scala index 0035494..b4cfef5 100644 --- a/src/main/scala/gitbucket/core/util/Authenticator.scala +++ b/src/main/scala/gitbucket/core/util/Authenticator.scala @@ -1,11 +1,14 @@ package gitbucket.core.util import gitbucket.core.controller.ControllerBase -import gitbucket.core.service.{RepositoryService, AccountService} +import gitbucket.core.service.{AccountService, RepositoryService} +import gitbucket.core.model.Permission import RepositoryService.RepositoryInfo import Implicits._ import ControlUtil._ +import scala.collection.Searching.search + /** * Allows only oneself and administrators. */ @@ -40,9 +43,9 @@ 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) + // TODO Repository management is allowed for only group managers? + case Some(x) if(getGroupMembers(repository.owner).exists { m => m.userName == x.userName && m.isManager == true }) => action(repository) + case Some(x) if(getCollaboratorUserNames(paths(0), paths(1), Seq(Permission.ADMIN)).contains(x.userName)) => action(repository) case _ => Unauthorized() } } getOrElse NotFound() @@ -86,32 +89,9 @@ } /** - * Allows only collaborators and administrators. + * Allows only guests and signed in users who can access the repository. */ -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)).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 => +trait ReferrerAuthenticator { self: ControllerBase with RepositoryService with AccountService => protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } protected def referrersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } @@ -125,7 +105,8 @@ 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 Some(x) if(getGroupMembers(repository.owner).exists(_.userName == x.userName)) => action(repository) + case Some(x) if(getCollaboratorUserNames(paths(0), paths(1)).contains(x.userName)) => action(repository) case _ => Unauthorized() } } @@ -136,9 +117,9 @@ } /** - * Allows only signed in users which can access the repository. + * Allows only signed in users who have read permission for the repository. */ -trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService => +trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService with AccountService => protected def readableUsersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } protected def readableUsersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } @@ -150,7 +131,32 @@ 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 Some(x) if(getGroupMembers(repository.owner).exists(_.userName == x.userName)) => action(repository) + case Some(x) if(getCollaboratorUserNames(paths(0), paths(1)).contains(x.userName)) => action(repository) + case _ => Unauthorized() + } + } getOrElse NotFound() + } + } + } +} + +/** + * Allows only signed in users who have write permission for the repository. + */ +trait WritableUsersAuthenticator { self: ControllerBase with RepositoryService with AccountService => + protected def writableUsersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } + protected def writableUsersOnly[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)).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(getGroupMembers(repository.owner).exists(_.userName == x.userName)) => action(repository) + case Some(x) if(getCollaboratorUserNames(paths(0), paths(1), Seq(Permission.ADMIN, Permission.WRITE)).contains(x.userName)) => action(repository) case _ => Unauthorized() } } getOrElse NotFound() diff --git a/src/main/scala/gitbucket/core/util/DatabaseConfig.scala b/src/main/scala/gitbucket/core/util/DatabaseConfig.scala index 9047991..681cebc 100644 --- a/src/main/scala/gitbucket/core/util/DatabaseConfig.scala +++ b/src/main/scala/gitbucket/core/util/DatabaseConfig.scala @@ -19,6 +19,11 @@ | url = "jdbc:h2:${DatabaseHome};MVCC=true" | user = "sa" | password = "sa" + |# connectionTimeout = 30000 + |# idleTimeout = 600000 + |# maxLifetime = 1800000 + |# minimumIdle = 10 + |# maximumPoolSize = 10 |} |""".stripMargin, "UTF-8") } @@ -30,12 +35,21 @@ def url(directory: Option[String]): String = dbUrl.replace("${DatabaseHome}", directory.getOrElse(DatabaseHome)) - lazy val url: String = url(None) - lazy val user: String = config.getString("db.user") - lazy val password: String = config.getString("db.password") - lazy val jdbcDriver: String = DatabaseType(url).jdbcDriver - lazy val slickDriver: BlockingJdbcProfile = DatabaseType(url).slickDriver - lazy val liquiDriver: AbstractJdbcDatabase = DatabaseType(url).liquiDriver + lazy val url : String = url(None) + lazy val user : String = config.getString("db.user") + lazy val password : String = config.getString("db.password") + lazy val jdbcDriver : String = DatabaseType(url).jdbcDriver + lazy val slickDriver : BlockingJdbcProfile = DatabaseType(url).slickDriver + lazy val liquiDriver : AbstractJdbcDatabase = DatabaseType(url).liquiDriver + lazy val connectionTimeout : Option[Long] = getOptionValue("db.connectionTimeout", config.getLong) + lazy val idleTimeout : Option[Long] = getOptionValue("db.idleTimeout" , config.getLong) + lazy val maxLifetime : Option[Long] = getOptionValue("db.maxLifetime" , config.getLong) + lazy val minimumIdle : Option[Int] = getOptionValue("db.minimumIdle" , config.getInt) + lazy val maximumPoolSize : Option[Int] = getOptionValue("db.maximumPoolSize" , config.getInt) + + private def getOptionValue[T](path: String, f: String => T): Option[T] = { + if(config.hasPath(path)) Some(f(path)) else None + } } diff --git a/src/main/scala/gitbucket/core/util/JGitUtil.scala b/src/main/scala/gitbucket/core/util/JGitUtil.scala index 230fa6f..7c5f687 100644 --- a/src/main/scala/gitbucket/core/util/JGitUtil.scala +++ b/src/main/scala/gitbucket/core/util/JGitUtil.scala @@ -830,14 +830,16 @@ existIds.toSeq } - def processTree(git: Git, id: ObjectId)(f: (String, CanonicalTreeParser) => Unit) = { + def processTree[T](git: Git, id: ObjectId)(f: (String, CanonicalTreeParser) => T): Seq[T] = { using(new RevWalk(git.getRepository)){ revWalk => using(new TreeWalk(git.getRepository)){ treeWalk => val index = treeWalk.addTree(revWalk.parseTree(id)) treeWalk.setRecursive(true) + val result = new collection.mutable.ListBuffer[T]() while(treeWalk.next){ - f(treeWalk.getPathString, treeWalk.getTree(index, classOf[CanonicalTreeParser])) + result += f(treeWalk.getPathString, treeWalk.getTree(index, classOf[CanonicalTreeParser])) } + result.toSeq } } } diff --git a/src/main/scala/gitbucket/core/util/Notifier.scala b/src/main/scala/gitbucket/core/util/Notifier.scala index 3270bb0..41d5b6b 100644 --- a/src/main/scala/gitbucket/core/util/Notifier.scala +++ b/src/main/scala/gitbucket/core/util/Notifier.scala @@ -23,8 +23,10 @@ ( // individual repository's owner issue.userName :: + // group members of group repository + getGroupMembers(issue.userName).map(_.userName) ::: // collaborators - getCollaborators(issue.userName, issue.repositoryName) ::: + getCollaboratorUserNames(issue.userName, issue.repositoryName) ::: // participants issue.openedUserName :: getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName) diff --git a/src/main/scala/gitbucket/core/util/StringUtil.scala b/src/main/scala/gitbucket/core/util/StringUtil.scala index aef8e62..9710b50 100644 --- a/src/main/scala/gitbucket/core/util/StringUtil.scala +++ b/src/main/scala/gitbucket/core/util/StringUtil.scala @@ -86,8 +86,9 @@ *@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)) + def extractIssueId(message: String): Seq[String] = + "(^|\\W)#(\\d+)(\\W|$)".r + .findAllIn(message).matchData.map(_.group(2)).toSeq.distinct /** * Extract close issue id like ```close #issueId ``` from the given message. @@ -95,8 +96,9 @@ * @param message the message which may contains close command * @return the iterator of issue id */ - def extractCloseId(message: String): Iterator[String] = - "(?i)(? getAccountByUserName(m.group(2)).map { _ => - s"""${m.group(2)}/${m.group(3)}@${m.group(4).substring(0, 7)}""" + s"""${m.group(2)}/${m.group(3)}@${m.group(4).substring(0, 7)}""" } } @@ -56,7 +56,7 @@ // convert username@SHA to link .replaceBy( ("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)@([a-f0-9]{40})(?=(\\W|$))").r ) { m => getAccountByUserName(m.group(2)).map { _ => - s"""${m.group(2)}@${m.group(3).substring(0, 7)}""" + s"""${m.group(2)}@${m.group(3).substring(0, 7)}""" } } @@ -93,6 +93,8 @@ } // convert commit id to link - .replaceAll("(?<=(^|[^\\w/@]))([a-f0-9]{40})(?=(\\W|$))", s"""$$2""") + .replaceBy("(?<=(^|[^\\w/@]))([a-f0-9]{40})(?=(\\W|$))".r){ m => + Some(s"""${m.group(2).substring(0, 7)}""") + } } } diff --git a/src/main/scala/gitbucket/core/view/Markdown.scala b/src/main/scala/gitbucket/core/view/Markdown.scala index 49ceeed..6eb3641 100644 --- a/src/main/scala/gitbucket/core/view/Markdown.scala +++ b/src/main/scala/gitbucket/core/view/Markdown.scala @@ -147,21 +147,23 @@ } private def fixUrl(url: String, isImage: Boolean = false): String = { + lazy val urlWithRawParam: String = url + (if(isImage && !url.endsWith("?raw=true")) "?raw=true" else "") + if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("/")){ url } else if(url.startsWith("#")){ ("#" + generateAnchorName(url.substring(1))) } else if(!enableWikiLink){ if(context.currentPath.contains("/blob/")){ - url + (if(isImage) "?raw=true" else "") + urlWithRawParam } 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 "") + repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + urlWithRawParam } 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 "") + repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + urlWithRawParam } } else { repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url diff --git a/src/main/twirl/gitbucket/core/account/group.scala.html b/src/main/twirl/gitbucket/core/account/group.scala.html index 1bda182..797c669 100644 --- a/src/main/twirl/gitbucket/core/account/group.scala.html +++ b/src/main/twirl/gitbucket/core/account/group.scala.html @@ -31,7 +31,7 @@ - @gitbucket.core.helper.html.account("memberName", 200) + @gitbucket.core.helper.html.account("memberName", 200, true, false)
@@ -80,15 +80,14 @@ } // check existence - $.post('@context.path/_user/existence', { - 'userName': userName - }, function(data, status){ - if(data == 'true'){ - addMemberHTML(userName, false); - } else { - $('#error-members').text('User does not exist.'); - } - }); + $.post('@context.path/_user/existence', { 'userName': userName }, + function(data, status){ + if(data == 'user'){ + addMemberHTML(userName, false); + } else { + $('#error-members').text('User does not exist.'); + } + }); }); $(document).on('click', '.remove', function(){ diff --git a/src/main/twirl/gitbucket/core/account/members.scala.html b/src/main/twirl/gitbucket/core/account/members.scala.html index 816f6fb..996dc1d 100644 --- a/src/main/twirl/gitbucket/core/account/members.scala.html +++ b/src/main/twirl/gitbucket/core/account/members.scala.html @@ -1,13 +1,14 @@ -@(account: gitbucket.core.model.Account, members: List[String], isGroupManager: Boolean)(implicit context: gitbucket.core.controller.Context) +@(account: gitbucket.core.model.Account, members: List[gitbucket.core.model.GroupMember], isGroupManager: Boolean)(implicit context: gitbucket.core.controller.Context) @import gitbucket.core.view.helpers @gitbucket.core.account.html.main(account, Nil, "members", isGroupManager){ @if(members.isEmpty){ No members } else { - @members.map { userName => + @members.map { member =>
- @helpers.avatar(userName, 20) @userName + @helpers.avatar(member.userName, 20) @member.userName + @if(member.isManager){ (Manager) }
} diff --git a/src/main/twirl/gitbucket/core/admin/usergroup.scala.html b/src/main/twirl/gitbucket/core/admin/usergroup.scala.html index ec99a73..b72ae1d 100644 --- a/src/main/twirl/gitbucket/core/admin/usergroup.scala.html +++ b/src/main/twirl/gitbucket/core/admin/usergroup.scala.html @@ -34,7 +34,7 @@ - @gitbucket.core.helper.html.account("memberName", 200) + @gitbucket.core.helper.html.account("memberName", 200, true, false)
@@ -75,16 +75,14 @@ } // check existence - $.post('@context.path/_user/existence', { - 'userName': userName, - 'userOnly': true - }, function(data, status){ - if(data == 'true'){ - addMemberHTML(userName, false); - } else { - $('#error-members').text('User does not exist.'); - } - }); + $.post('@context.path/_user/existence', { 'userName': userName }, + function(data, status){ + if(data == 'user'){ + addMemberHTML(userName, false); + } else { + $('#error-members').text('User does not exist.'); + } + }); }); $(document).on('click', '.remove', function(){ diff --git a/src/main/twirl/gitbucket/core/dashboard/issues.scala.html b/src/main/twirl/gitbucket/core/dashboard/issues.scala.html index d051d76..f23e576 100644 --- a/src/main/twirl/gitbucket/core/dashboard/issues.scala.html +++ b/src/main/twirl/gitbucket/core/dashboard/issues.scala.html @@ -11,7 +11,7 @@ @gitbucket.core.dashboard.html.sidebar(recentRepositories, userRepositories){ @gitbucket.core.dashboard.html.tab("issues")
- @gitbucket.core.dashboard.html.issuesnavi(filter, openCount, closedCount, condition) + @gitbucket.core.dashboard.html.issuesnavi("issues", filter, openCount, closedCount, condition) @gitbucket.core.dashboard.html.issueslist(issues, page, openCount, closedCount, condition, filter, groups)
} diff --git a/src/main/twirl/gitbucket/core/dashboard/issuesnavi.scala.html b/src/main/twirl/gitbucket/core/dashboard/issuesnavi.scala.html index a328c5c..7504d1a 100644 --- a/src/main/twirl/gitbucket/core/dashboard/issuesnavi.scala.html +++ b/src/main/twirl/gitbucket/core/dashboard/issuesnavi.scala.html @@ -1,4 +1,5 @@ -@(filter: String, +@(active: String, + filter: String, openCount: Int, closedCount: Int, condition: gitbucket.core.service.IssuesService.IssueSearchCondition)(implicit context: gitbucket.core.controller.Context) @@ -9,15 +10,18 @@
  • Closed @closedCount
  • - @* -
  • - Created -
  • -
  • - Assigned -
  • -
  • - Mentioned -
  • - *@ + + + + diff --git a/src/main/twirl/gitbucket/core/dashboard/pulls.scala.html b/src/main/twirl/gitbucket/core/dashboard/pulls.scala.html index 33bb057..6d16867 100644 --- a/src/main/twirl/gitbucket/core/dashboard/pulls.scala.html +++ b/src/main/twirl/gitbucket/core/dashboard/pulls.scala.html @@ -11,7 +11,7 @@ @gitbucket.core.dashboard.html.sidebar(recentRepositories, userRepositories){ @gitbucket.core.dashboard.html.tab("pulls")
    - @gitbucket.core.dashboard.html.issuesnavi(filter, openCount, closedCount, condition) + @gitbucket.core.dashboard.html.issuesnavi("pulls", filter, openCount, closedCount, condition) @gitbucket.core.dashboard.html.issueslist(issues, page, openCount, closedCount, condition, filter, groups)
    } diff --git a/src/main/twirl/gitbucket/core/helper/account.scala.html b/src/main/twirl/gitbucket/core/helper/account.scala.html index e3dc42c..0e14ced 100644 --- a/src/main/twirl/gitbucket/core/helper/account.scala.html +++ b/src/main/twirl/gitbucket/core/helper/account.scala.html @@ -1,12 +1,19 @@ -@(id: String, width: Int)(implicit context: gitbucket.core.controller.Context) +@(id: String, width: Int, user: Boolean, group: Boolean)(implicit context: gitbucket.core.controller.Context) +} \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/issues/commentform.scala.html b/src/main/twirl/gitbucket/core/issues/commentform.scala.html index 9f11990..a022ac1 100644 --- a/src/main/twirl/gitbucket/core/issues/commentform.scala.html +++ b/src/main/twirl/gitbucket/core/issues/commentform.scala.html @@ -1,12 +1,12 @@ @(issue: gitbucket.core.model.Issue, reopenable: Boolean, - hasWritePermission: Boolean, + isEditable: Boolean, + isManageable: Boolean, repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) @import gitbucket.core.view.helpers -@if(context.loginAccount.isDefined){ +@if(isEditable){

    -
    @helpers.avatarLink(context.loginAccount.get.userName, 48)
    @gitbucket.core.helper.html.preview( @@ -16,7 +16,7 @@ enableRefsLink = true, enableLineBreaks = true, enableTaskList = true, - hasWritePermission = hasWritePermission, + hasWritePermission = isEditable, completionContext = "issues", style = "", elastic = true, @@ -24,7 +24,7 @@ )
    - @if((reopenable || !issue.closed) && (hasWritePermission || issue.openedUserName == context.loginAccount.get.userName)){ + @if((reopenable || !issue.closed) && (isManageable || issue.openedUserName == context.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 index 268c7a5..a08d310 100644 --- a/src/main/twirl/gitbucket/core/issues/commentlist.scala.html +++ b/src/main/twirl/gitbucket/core/issues/commentlist.scala.html @@ -1,17 +1,18 @@ @(issue: Option[gitbucket.core.model.Issue], comments: List[gitbucket.core.model.Comment], - hasWritePermission: Boolean, + isManageable: Boolean, repository: gitbucket.core.service.RepositoryService.RepositoryInfo, pullreq: Option[gitbucket.core.model.PullRequest] = None)(implicit context: gitbucket.core.controller.Context) @import gitbucket.core.view.helpers @import gitbucket.core.model.CommitComment @if(issue.isDefined){ -
    @helpers.avatarLink(issue.get.openedUserName, 48)
    - @helpers.user(issue.get.openedUserName, styleClass="username strong") commented @gitbucket.core.helper.html.datetimeago(issue.get.registeredDate) + @helpers.avatar(issue.get.openedUserName, 20) + @helpers.user(issue.get.openedUserName, styleClass="username strong") + commented @gitbucket.core.helper.html.datetimeago(issue.get.registeredDate) - @if(hasWritePermission || context.loginAccount.map(_.userName == issue.get.openedUserName).getOrElse(false)){ + @if(isManageable || context.loginAccount.map(_.userName == issue.get.openedUserName).getOrElse(false)){ } @@ -24,7 +25,7 @@ enableRefsLink = true, enableLineBreaks = true, enableTaskList = true, - hasWritePermission = hasWritePermission + hasWritePermission = isManageable )
    @@ -35,9 +36,9 @@ case comment: gitbucket.core.model.IssueComment => { @if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch" && comment.action != "commit" && comment.action != "refer"){ -
    @helpers.avatarLink(comment.commentedUserName, 48)
    + @helpers.avatar(comment.commentedUserName, 20) @helpers.user(comment.commentedUserName, styleClass="username strong") @if(comment.action == "comment"){ @@ -48,7 +49,7 @@ @gitbucket.core.helper.html.datetimeago(comment.registeredDate) @if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" - && (hasWritePermission || context.loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){ + && (isManageable || context.loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){   @@ -63,7 +64,7 @@ enableRefsLink = true, enableLineBreaks = true, enableTaskList = true, - hasWritePermission = hasWritePermission + hasWritePermission = isManageable )
    @@ -166,7 +167,7 @@ } } case comment: CommitComment => { - @gitbucket.core.helper.html.commitcomment(comment, hasWritePermission, repository, pullreq.map(_.commitIdTo)) + @gitbucket.core.helper.html.commitcomment(comment, isManageable, repository, pullreq.map(_.commitIdTo)) } } - +
  • - @label @if(count > 0) { @count } + @label @if(count > 0) { @count } } else { - @label @if(count > 0) { @count } + @label @if(count > 0) { @count } }
  • @@ -27,24 +27,26 @@ @menuitem("/branches", "branches", "Branches", "git-branch", repository.branchList.length) @menuitem("/tags", "tags", "Tags", "tag", repository.tags.length) } - @if(repository.repository.enableIssues) { + @if(repository.repository.options.issuesOption != "DISABLE") { @menuitem("/issues", "issues", "Issues", "issue-opened", repository.issueCount) @menuitem("/pulls", "pulls", "Pull Requests", "git-pull-request", repository.pullCount) @menuitem("/issues/labels", "labels", "Labels", "tag") @menuitem("/issues/milestones", "milestones", "Milestones", "milestone") } else { - @repository.repository.externalIssuesUrl.map { externalIssuesUrl => + @repository.repository.options.externalIssuesUrl.map { externalIssuesUrl => @menuitem(externalIssuesUrl, "issues", "Issues", "issue-opened") } } - @if(repository.repository.enableWiki) { + @if(repository.repository.options.wikiOption != "DISABLE") { @menuitem("/wiki", "wiki", "Wiki", "book") } else { - @repository.repository.externalWikiUrl.map { externalWikiUrl => + @repository.repository.options.externalWikiUrl.map { externalWikiUrl => @menuitem(externalWikiUrl, "wiki", "Wiki", "book") } } - @menuitem("/network/members", "fork", "Forks", "repo-forked", repository.forkedCount) + @if(repository.repository.options.allowFork) { + @menuitem("/network/members", "fork", "Forks", "repo-forked", repository.forkedCount) + } @if(context.loginAccount.isDefined && (context.loginAccount.get.isAdmin || repository.managers.contains(context.loginAccount.get.userName))){ @menuitem("/settings", "settings", "Settings", "tools") } diff --git a/src/main/twirl/gitbucket/core/pulls/conversation.scala.html b/src/main/twirl/gitbucket/core/pulls/conversation.scala.html index 5536fe3..94dcbbb 100644 --- a/src/main/twirl/gitbucket/core/pulls/conversation.scala.html +++ b/src/main/twirl/gitbucket/core/pulls/conversation.scala.html @@ -5,12 +5,13 @@ collaborators: List[String], milestones: List[(gitbucket.core.model.Milestone, Int, Int)], labels: List[gitbucket.core.model.Label], - hasWritePermission: Boolean, + isEditable: Boolean, + isManageable: Boolean, repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) @import gitbucket.core.view.helpers
    - @gitbucket.core.issues.html.commentlist(Some(issue), comments, hasWritePermission, repository, Some(pullreq)) + @gitbucket.core.issues.html.commentlist(Some(issue), comments, isManageable, repository, Some(pullreq))
    @defining(comments.flatMap { case comment: gitbucket.core.model.IssueComment => Some(comment) @@ -25,7 +26,7 @@
    } - @if(hasWritePermission && issue.closed && pullreq.userName == pullreq.requestUserName && merged && + @if(isManageable && issue.closed && pullreq.userName == pullreq.requestUserName && merged && pullreq.repositoryName == pullreq.requestRepositoryName && repository.branchList.contains(pullreq.requestBranch)){
    @@ -37,11 +38,11 @@
    } - @gitbucket.core.issues.html.commentform(issue, !merged, hasWritePermission, repository) + @gitbucket.core.issues.html.commentform(issue, !merged, isEditable, isManageable, repository) }
    - @gitbucket.core.issues.html.issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository) + @gitbucket.core.issues.html.issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, labels, isManageable, repository)
    \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/settings/danger.scala.html b/src/main/twirl/gitbucket/core/settings/danger.scala.html index 60b4007..7df1170 100644 --- a/src/main/twirl/gitbucket/core/settings/danger.scala.html +++ b/src/main/twirl/gitbucket/core/settings/danger.scala.html @@ -13,7 +13,7 @@
    Transfer this repo to another user or to group.
    - @gitbucket.core.helper.html.account("newOwner", 200) + @gitbucket.core.helper.html.account("newOwner", 200, true, true)
    diff --git a/src/main/twirl/gitbucket/core/settings/options.scala.html b/src/main/twirl/gitbucket/core/settings/options.scala.html index 490da70..f978b58 100644 --- a/src/main/twirl/gitbucket/core/settings/options.scala.html +++ b/src/main/twirl/gitbucket/core/settings/options.scala.html @@ -39,40 +39,66 @@
    +
    + +
    -
    Features
    +
    Issues
    - +
    + +
    +
    + +
    +
    + +
    - +
    -
    - - +
    +
    +
    +
    Wiki
    +
    +
    +
    + +
    +
    + +
    +
    + +
    - +
    @@ -87,14 +113,13 @@ $(function(){ updateFeatures(); - $('#enableIssues, #enableWiki').click(function(){ + $('input[name=issuesOption], input[name=wikiOption]').click(function(){ updateFeatures(); }); }); function updateFeatures() { - $('#externalIssuesUrl').prop('disabled', $('#enableIssues').prop('checked')); - $('#allowWikiEditing').prop('disabled', !$('#enableWiki').prop('checked')); - $('#externalWikiUrl').prop('disabled', $('#enableWiki').prop('checked')); + $('#externalIssuesUrl').prop('disabled', !$('input[name=issuesOption]').select('[value=DISABLE]').prop('checked')); + $('#externalWikiUrl').prop('disabled', !$('input[name=wikiOption]').select('[value=DISABLE]').prop('checked')); } diff --git a/src/main/twirl/gitbucket/core/wiki/compare.scala.html b/src/main/twirl/gitbucket/core/wiki/compare.scala.html index 3745cb7..e2fe163 100644 --- a/src/main/twirl/gitbucket/core/wiki/compare.scala.html +++ b/src/main/twirl/gitbucket/core/wiki/compare.scala.html @@ -3,7 +3,7 @@ to: String, diffs: Seq[gitbucket.core.util.JGitUtil.DiffInfo], repository: gitbucket.core.service.RepositoryService.RepositoryInfo, - hasWritePermission: Boolean, + isEditable: Boolean, info: Option[Any])(implicit context: gitbucket.core.controller.Context) @import gitbucket.core.view.helpers @gitbucket.core.html.main(s"Compare Revisions - ${repository.owner}/${repository.name}", Some(repository)){ @@ -27,7 +27,7 @@
    @gitbucket.core.helper.html.diff(diffs, repository, None, None, false, None, false, false)
    - @if(hasWritePermission){ + @if(isEditable){
    @if(pageName.isDefined){ Revert Changes diff --git a/src/main/twirl/gitbucket/core/wiki/edit.scala.html b/src/main/twirl/gitbucket/core/wiki/edit.scala.html index 5a0dc7d..05b3264 100644 --- a/src/main/twirl/gitbucket/core/wiki/edit.scala.html +++ b/src/main/twirl/gitbucket/core/wiki/edit.scala.html @@ -44,7 +44,7 @@ } } -