Merge branch 'master' into slick-3.1-blocking

Conflicts:
	src/main/scala/gitbucket/core/service/IssuesService.scala
	src/main/scala/gitbucket/core/service/RepositoryService.scala
	src/main/scala/gitbucket/core/util/DatabaseConfig.scala
	src/test/scala/gitbucket/core/service/ServiceSpecBase.scala
This commit is contained in:
Naoki Takezoe
2016-11-11 16:29:34 +09:00
88 changed files with 1350 additions and 870 deletions

View File

@@ -65,6 +65,21 @@ Support
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

View File

@@ -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 @@ libraryDependencies ++= Seq(
"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 @@ libraryDependencies ++= Seq(
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 @@ executableKey := {
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 @@ publishTo <<= version { (v: String) =>
}
publishMavenStyle := true
pomIncludeRepository := { _ => false }
artifact in Keys.`package` := Artifact(moduleName.value)
pomExtra := (
<url>https://github.com/gitbucket/gitbucket</url>
<licenses>

View File

@@ -46,9 +46,10 @@ $ sbt executable
### 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/.

View File

@@ -1 +1 @@
sbt.version=0.13.11
sbt.version=0.13.12

View File

@@ -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/

View File

@@ -1,3 +0,0 @@
#!/bin/sh
export GITBUCKET_VERSION=`cat ../build.sbt | grep 'val GitBucketVersion' | cut -d \" -f 2`
echo "GITBUCKET_VERSION: $GITBUCKET_VERSION"

View File

@@ -1,17 +0,0 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>jp.sf.amateras</groupId>
<artifactId>gitbucket-assembly</artifactId>
<version>0.0.1</version>
<build>
<extensions>
<extension>
<groupId>org.apache.maven.wagon</groupId>
<artifactId>wagon-ssh</artifactId>
<version>2.10</version>
</extension>
</extensions>
</build>
</project>

View File

@@ -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" %*

2
sbt.sh
View File

@@ -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 "$@"

View File

@@ -3,12 +3,14 @@ import org.eclipse.jetty.webapp.WebAppContext;
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 @@ public class JettyLauncher {
}
}
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) {

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<addColumn tableName="REPOSITORY">
<column name="ALLOW_FORK" type="boolean" nullable="false" defaultValueBoolean="true"/>
</addColumn>
</changeSet>

View File

@@ -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)

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<addColumn tableName="COLLABORATOR">
<column name="PERMISSION" type="varchar(10)" nullable="false" defaultValue="ADMIN"/>
</addColumn>
<addColumn tableName="REPOSITORY">
<column name="WIKI_OPTION" type="varchar(10)" nullable="false" defaultValue="DISABLE"/>
<column name="ISSUES_OPTION" type="varchar(10)" nullable="false" defaultValue="DISABLE"/>
</addColumn>
<update tableName="REPOSITORY">
<column name="WIKI_OPTION" value="DISABLE"/>
<where>ENABLE_WIKI = FALSE</where>
</update>
<update tableName="REPOSITORY">
<column name="WIKI_OPTION" value="PRIVATE"/>
<where>ENABLE_WIKI = TRUE AND ALLOW_WIKI_EDITING = FALSE</where>
</update>
<update tableName="REPOSITORY">
<column name="WIKI_OPTION" value="PUBLIC"/>
<where>ENABLE_WIKI = TRUE AND ALLOW_WIKI_EDITING = TRUE</where>
</update>
<update tableName="REPOSITORY">
<column name="ISSUES_OPTION" value="DISABLE"/>
<where>ENABLE_ISSUES = FALSE</where>
</update>
<update tableName="REPOSITORY">
<column name="ISSUES_OPTION" value="PUBLIC"/>
<where>ENABLE_ISSUES = TRUE</where>
</update>
<dropColumn tableName="REPOSITORY" columnName="ENABLE_WIKI"/>
<dropColumn tableName="REPOSITORY" columnName="ALLOW_WIKI_EDITING"/>
<dropColumn tableName="REPOSITORY" columnName="ENABLE_ISSUES"/>
</changeSet>

View File

@@ -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 @@ class ScalatraBootstrap extends LifeCycle {
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, "/*")

View File

@@ -14,5 +14,13 @@ object GitBucketCoreModule extends Module("gitbucket-core",
),
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")
)
)

View File

@@ -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)
}
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))
}
}
}

View File

@@ -30,7 +30,7 @@ object ApiUser{
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
)

View File

@@ -14,6 +14,7 @@ import gitbucket.core.util._
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 @@ trait AccountControllerBase extends AccountManagementControllerBase {
// 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 @@ trait AccountControllerBase extends AccountManagementControllerBase {
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
}
}
} getOrElse NotFound
} getOrElse NotFound()
}
get("/:userName.atom") {
@@ -156,7 +157,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
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 @@ trait AccountControllerBase extends AccountManagementControllerBase {
flash += "info" -> "Account information has been updated."
redirect(s"/${userName}/_edit")
} getOrElse NotFound
} getOrElse NotFound()
})
get("/:userName/_delete")(oneselfOnly {
@@ -196,14 +197,14 @@ trait AccountControllerBase extends AccountManagementControllerBase {
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 @@ trait AccountControllerBase extends AccountManagementControllerBase {
case _ => None
}
html.application(x, tokens, generatedToken)
} getOrElse NotFound
} getOrElse NotFound()
})
post("/:userName/_personalToken", personalTokenForm)(oneselfOnly { form =>
@@ -260,7 +261,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
} else {
html.register()
}
} else NotFound
} else NotFound()
}
post("/register", newForm){ form =>
@@ -268,7 +269,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
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 @@ trait AccountControllerBase extends AccountManagementControllerBase {
// 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 @@ trait AccountControllerBase extends AccountManagementControllerBase {
})
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(){

View File

@@ -7,9 +7,10 @@ import gitbucket.core.service.PullRequestService._
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 @@ class ApiController extends ApiControllerBase
with GroupManagerAuthenticator
with ReferrerAuthenticator
with ReadableUsersAuthenticator
with CollaboratorsAuthenticator
with WritableUsersAuthenticator
trait ApiControllerBase extends ControllerBase {
self: RepositoryService
@@ -51,7 +52,7 @@ trait ApiControllerBase extends ControllerBase {
with GroupManagerAuthenticator
with ReferrerAuthenticator
with ReadableUsersAuthenticator
with CollaboratorsAuthenticator =>
with WritableUsersAuthenticator =>
/**
* https://developer.github.com/v3/#root-endpoint
@@ -66,7 +67,7 @@ trait ApiControllerBase extends ControllerBase {
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 @@ trait ApiControllerBase extends ControllerBase {
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 @@ trait ApiControllerBase extends ControllerBase {
* 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(
"<div data-path=\"", path, "\" id=\"file\">", "<article>",
renderMarkup(path.split("/").toList, new String(c), refStr, repository, false, false, true).body,
"</article>", "</div>"
).mkString
)
case "application/vnd.github.v3.html" =>
content.map(c =>
List(
"<div data-path=\"", path, "\" id=\"file\">", "<div class=\"plain\">", "<pre>",
play.twirl.api.HtmlFormat.escape(new String(c)).body,
"</pre>", "</div>", "</div>"
).mkString
)
case _ =>
Some(JsonFormat(ApiContents(f, content)))
}
}).getOrElse(NotFound())
} else { // directory
JsonFormat(fileList.map{f => ApiContents(f, None)})
}
}
})
@@ -136,7 +177,8 @@ trait ApiControllerBase extends ControllerBase {
* 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 @@ trait ApiControllerBase extends ControllerBase {
get("/api/v3/user") {
context.loginAccount.map { account =>
JsonFormat(ApiUser(account))
} getOrElse Unauthorized
} getOrElse Unauthorized()
}
/**
@@ -179,7 +221,7 @@ trait ApiControllerBase extends ControllerBase {
)
}
}
}) getOrElse NotFound
}) getOrElse NotFound()
})
/**
@@ -203,7 +245,7 @@ trait ApiControllerBase extends ControllerBase {
)
}
}
}) getOrElse NotFound
}) getOrElse NotFound()
})
/**
@@ -221,7 +263,7 @@ trait ApiControllerBase extends ControllerBase {
disableBranchProtection(repository.owner, repository.name, branch)
}
JsonFormat(ApiBranch(branch, protection)(RepositoryName(repository)))
}) getOrElse NotFound
}) getOrElse NotFound()
})
/**
@@ -243,7 +285,7 @@ trait ApiControllerBase extends ControllerBase {
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 @@ trait ApiControllerBase extends ControllerBase {
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 @@ trait ApiControllerBase extends ControllerBase {
* 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 @@ trait ApiControllerBase extends ControllerBase {
* 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 @@ trait ApiControllerBase extends ControllerBase {
* 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 @@ trait ApiControllerBase extends ControllerBase {
ApiRepository(headRepo, ApiUser(headOwner)),
ApiRepository(repository, ApiUser(baseOwner)),
ApiUser(issueUser)))
}).getOrElse(NotFound)
}) getOrElse NotFound()
})
/**
@@ -412,7 +454,7 @@ trait ApiControllerBase extends ControllerBase {
JsonFormat(commits)
}
}
} getOrElse NotFound
} getOrElse NotFound()
})
/**
@@ -425,7 +467,7 @@ trait ApiControllerBase extends ControllerBase {
/**
* 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 @@ trait ApiControllerBase extends ControllerBase {
status <- getCommitStatus(repository.owner, repository.name, statusId)
} yield {
JsonFormat(ApiCommitStatus(status, ApiUser(creator)))
}) getOrElse NotFound
}) getOrElse NotFound()
})
/**
@@ -453,7 +495,7 @@ trait ApiControllerBase extends ControllerBase {
JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map{ case(status, creator) =>
ApiCommitStatus(status, ApiUser(creator))
})
}) getOrElse NotFound
}) getOrElse NotFound()
})
/**
@@ -478,7 +520,7 @@ trait ApiControllerBase extends ControllerBase {
} 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 =

View File

@@ -248,7 +248,7 @@ trait AccountManagementControllerBase extends ControllerBase {
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
}
}

View File

@@ -48,7 +48,7 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
// 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 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
}
}, FileUtil.isUploadableType)
}
} getOrElse BadRequest
} getOrElse BadRequest()
}
post("/import") {
@@ -93,7 +93,7 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
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 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
case Some(file) if(mimeTypeChcker(file.name)) =>
defining(FileUtil.generateFileId){ fileId =>
f(file, fileId)
Ok(fileId)
}
case _ => BadRequest
case _ => BadRequest()
}
}

View File

@@ -108,18 +108,29 @@ trait IndexControllerBase extends ControllerBase {
*/
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?

View File

@@ -2,24 +2,24 @@ package gitbucket.core.controller
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 @@ trait IssuesControllerBase extends ControllerBase {
_,
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 @@ trait IssuesControllerBase extends ControllerBase {
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 @@ trait IssuesControllerBase extends ControllerBase {
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 @@ trait IssuesControllerBase extends ControllerBase {
}
})
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 @@ trait IssuesControllerBase extends ControllerBase {
}
})
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 @@ trait IssuesControllerBase extends ControllerBase {
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 @@ trait IssuesControllerBase extends ControllerBase {
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 @@ trait IssuesControllerBase extends ControllerBase {
"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
}
}

View File

@@ -2,7 +2,7 @@ package gitbucket.core.controller
import gitbucket.core.issues.labels.html
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService}
import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
import gitbucket.core.util.Implicits._
import io.github.gitbucket.scalatra.forms._
import org.scalatra.i18n.Messages
@@ -10,11 +10,11 @@ import org.scalatra.Ok
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 @@ trait LabelsControllerBase extends ControllerBase {
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 @@ trait LabelsControllerBase extends ControllerBase {
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 @@ trait LabelsControllerBase extends ControllerBase {
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()
})

View File

@@ -2,17 +2,17 @@ package gitbucket.core.controller
import gitbucket.core.issues.milestones.html
import gitbucket.core.service.{RepositoryService, MilestonesService, AccountService}
import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
import gitbucket.core.util.{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 @@ trait MilestonesControllerBase extends ControllerBase {
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()
})
}

View File

@@ -6,6 +6,7 @@ import gitbucket.core.service.CommitStatusService
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.JGitUtil._
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 @@ trait PullRequestsControllerBase extends ControllerBase {
(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 @@ trait PullRequestsControllerBase extends ControllerBase {
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 @@ trait PullRequestsControllerBase extends ControllerBase {
}
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 @@ trait PullRequestsControllerBase extends ControllerBase {
}
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 @@ trait PullRequestsControllerBase extends ControllerBase {
}
}
}
} getOrElse NotFound
} getOrElse NotFound()
})
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
@@ -290,7 +290,7 @@ trait PullRequestsControllerBase extends ControllerBase {
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 @@ trait PullRequestsControllerBase extends ControllerBase {
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 @@ trait PullRequestsControllerBase extends ControllerBase {
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 @@ trait PullRequestsControllerBase extends ControllerBase {
}
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 @@ trait PullRequestsControllerBase extends ControllerBase {
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 @@ trait PullRequestsControllerBase extends ControllerBase {
"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
}
}
}

View File

@@ -31,22 +31,22 @@ trait RepositorySettingsControllerBase extends ControllerBase {
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 @@ trait RepositorySettingsControllerBase extends ControllerBase {
"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 @@ trait RepositorySettingsControllerBase extends ControllerBase {
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 @@ trait RepositorySettingsControllerBase extends ControllerBase {
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 @@ trait RepositorySettingsControllerBase extends ControllerBase {
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 @@ trait RepositorySettingsControllerBase extends ControllerBase {
}
}
/**
* 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.
@@ -421,6 +411,15 @@ trait RepositorySettingsControllerBase extends ControllerBase {
}
}
/**
*
*/
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.
*/

View File

@@ -31,7 +31,7 @@ import org.scalatra._
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 @@ class RepositoryViewerController extends RepositoryViewerControllerBase
*/
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 @@ trait RepositoryViewerControllerBase extends ControllerBase {
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 @@ trait RepositoryViewerControllerBase extends ControllerBase {
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 @@ trait RepositoryViewerControllerBase extends ControllerBase {
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 @@ trait RepositoryViewerControllerBase extends ControllerBase {
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 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}")
})
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 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}")
})
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 @@ trait RepositoryViewerControllerBase extends ControllerBase {
loader.copyTo(response.outputStream)
()
}
} getOrElse NotFound
} getOrElse NotFound()
}
})
@@ -270,7 +270,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
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 @@ trait RepositoryViewerControllerBase extends ControllerBase {
hasWritePermission(repository.owner, repository.name, context.loginAccount),
request.paths(2) == "blame")
}
} getOrElse NotFound
} getOrElse NotFound()
}
})
@@ -334,7 +334,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
}
} catch {
case e:MissingObjectException => NotFound
case e:MissingObjectException => NotFound()
}
})
@@ -397,8 +397,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
)
))
}
} else Unauthorized
} getOrElse NotFound
} else Unauthorized()
} getOrElse NotFound()
})
ajaxPost("/:owner/:repository/commit_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
@@ -407,8 +407,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
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 @@ trait RepositoryViewerControllerBase extends ControllerBase {
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 @@ trait RepositoryViewerControllerBase extends ControllerBase {
/**
* 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 @@ trait RepositoryViewerControllerBase extends ControllerBase {
/**
* 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 @@ trait RepositoryViewerControllerBase extends ControllerBase {
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 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val ref = multiParams("splat").head
JGitUtil.getTreeId(git, ref).map{ treeId =>
html.find(ref, treeId, repository)
} getOrElse NotFound
} getOrElse NotFound()
}
})
@@ -571,7 +573,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
getPullRequestFromBranch(repository.owner, repository.name, revstr, repository.repository.defaultBranch),
flash.get("info"), flash.get("error"))
}
} getOrElse NotFound
} getOrElse NotFound()
}
}
}
@@ -591,14 +593,18 @@ trait RepositoryViewerControllerBase extends ControllerBase {
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 @@ trait RepositoryViewerControllerBase extends ControllerBase {
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)

View File

@@ -233,7 +233,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
updateImage(userName, form.fileId, form.clearImage)
redirect("/admin/users")
}
} getOrElse NotFound
} getOrElse NotFound()
})
get("/admin/users/_newgroup")(adminOnly {
@@ -279,19 +279,19 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
} 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()
}
})

View File

@@ -14,10 +14,10 @@ import org.scalatra.i18n.Messages
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 @@ trait WikiControllerBase extends ControllerBase {
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 @@ trait WikiControllerBase extends ControllerBase {
}
})
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 @@ trait WikiControllerBase extends ControllerBase {
} 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 @@ trait WikiControllerBase extends ControllerBase {
} 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 @@ trait WikiControllerBase extends ControllerBase {
} 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 @@ trait WikiControllerBase extends ControllerBase {
} 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 @@ trait WikiControllerBase extends ControllerBase {
}
} else Unauthorized()
})
get("/:owner/:repository/wiki/_pages")(referrersOnly { repository =>
html.pages(getWikiPageList(repository.owner, repository.name), repository, isEditable(repository))
})
@@ -190,7 +190,7 @@ trait WikiControllerBase extends ControllerBase {
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 @@ trait WikiControllerBase extends ControllerBase {
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 @@ trait WikiControllerBase extends ControllerBase {
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
}
}
}

View File

@@ -7,7 +7,8 @@ trait CollaboratorComponent extends TemplateComponent { self: Profile =>
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 @@ trait CollaboratorComponent extends TemplateComponent { self: Profile =>
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)
}

View File

@@ -7,24 +7,61 @@ trait RepositoryComponent extends TemplateComponent { self: Profile =>
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 @@ case class Repository(
originRepositoryName: Option[String],
parentUserName: Option[String],
parentRepositoryName: Option[String],
enableIssues: Boolean,
externalIssuesUrl: Option[String],
enableWiki: Boolean,
allowWikiEditing: Boolean,
externalWikiUrl: Option[String]
options: RepositoryOptions
)
case class RepositoryOptions(
issuesOption: String,
externalIssuesUrl: Option[String],
wikiOption: String,
externalWikiUrl: Option[String],
allowFork: Boolean
)

View File

@@ -181,7 +181,6 @@ trait AccountService {
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] = {

View File

@@ -13,7 +13,7 @@ trait HandleCommentService {
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 @@ trait HandleCommentService {
// 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

View File

@@ -3,7 +3,7 @@ package gitbucket.core.service
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 @@ import gitbucket.core.model.Profile.dateColumnType
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 @@ trait IssuesService {
(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 @@ trait IssuesService {
* @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 @@ trait IssuesService {
}
}
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 {

View File

@@ -21,12 +21,12 @@ trait RepositoryCreationService {
// 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)

View File

@@ -2,7 +2,7 @@ package gitbucket.core.service
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 @@ trait RepositoryService { self: AccountService =>
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 @@ trait RepositoryService { self: AccountService =>
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 @@ trait RepositoryService { self: AccountService =>
*/
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 @@ trait RepositoryService { self: AccountService =>
.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
}
}

View File

@@ -11,7 +11,7 @@ import Implicits.request2Session
* 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)

View File

@@ -73,7 +73,7 @@ trait SystemSettingsService {
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),

View File

@@ -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.
* <code>/:user/:repo.git/</code>
*/
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() = {}
}

View File

@@ -53,6 +53,13 @@ object Database {
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)
}

View File

@@ -37,7 +37,7 @@ abstract class GitCommand extends Command with SessionAware {
override def run(): Unit = {
authUser match {
case Some(authUser) =>
Database() withSession { implicit session =>
Database() withTransaction { implicit session =>
try {
runTask(authUser)
callback.onExit(0)

View File

@@ -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 @@ trait OwnerAuthenticator { self: ControllerBase with RepositoryService with Acco
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 @@ trait AdminAuthenticator { self: ControllerBase =>
}
/**
* 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 @@ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
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 @@ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
}
/**
* 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 @@ trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService =
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()

View File

@@ -19,6 +19,11 @@ object DatabaseConfig {
| 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 @@ object DatabaseConfig {
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
}
}

View File

@@ -830,14 +830,16 @@ object JGitUtil {
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
}
}
}

View File

@@ -23,8 +23,10 @@ trait Notifier extends RepositoryService with AccountService with IssuesService
(
// 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)

View File

@@ -86,8 +86,9 @@ object StringUtil {
*@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 @@ object StringUtil {
* @param message the message which may contains close command
* @return the iterator of issue id
*/
def extractCloseId(message: String): Iterator[String] =
"(?i)(?<!\\w)(?:fix(?:e[sd])?|resolve[sd]?|close[sd]?)\\s+#(\\d+)(?!\\w)".r.findAllIn(message).matchData.map(_.group(1))
def extractCloseId(message: String): Seq[String] =
"(?i)(?<!\\w)(?:fix(?:e[sd])?|resolve[sd]?|close[sd]?)\\s+#(\\d+)(?!\\w)".r
.findAllIn(message).matchData.map(_.group(1)).toSeq.distinct
/**

View File

@@ -37,7 +37,7 @@ trait LinkConverter { self: RequestCache =>
// convert username/project@SHA to link
.replaceBy("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)/([a-zA-Z0-9\\-_\\.]+)@([a-f0-9]{40})(?=(\\W|$))".r){ m =>
getAccountByUserName(m.group(2)).map { _ =>
s"""<a href="${context.path}/${m.group(2)}/${m.group(3)}/commit/${m.group(4)}">${m.group(2)}/${m.group(3)}@${m.group(4).substring(0, 7)}</a>"""
s"""<code><a href="${context.path}/${m.group(2)}/${m.group(3)}/commit/${m.group(4)}">${m.group(2)}/${m.group(3)}@${m.group(4).substring(0, 7)}</a></code>"""
}
}
@@ -56,7 +56,7 @@ trait LinkConverter { self: RequestCache =>
// convert username@SHA to link
.replaceBy( ("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)@([a-f0-9]{40})(?=(\\W|$))").r ) { m =>
getAccountByUserName(m.group(2)).map { _ =>
s"""<a href="${context.path}/${m.group(2)}/${repository.name}/commit/${m.group(3)}">${m.group(2)}@${m.group(3).substring(0, 7)}</a>"""
s"""<code><a href="${context.path}/${m.group(2)}/${repository.name}/commit/${m.group(3)}">${m.group(2)}@${m.group(3).substring(0, 7)}</a></code>"""
}
}
@@ -93,6 +93,8 @@ trait LinkConverter { self: RequestCache =>
}
// convert commit id to link
.replaceAll("(?<=(^|[^\\w/@]))([a-f0-9]{40})(?=(\\W|$))", s"""<a href="${context.path}/${repository.owner}/${repository.name}/commit/$$2">$$2</a>""")
.replaceBy("(?<=(^|[^\\w/@]))([a-f0-9]{40})(?=(\\W|$))".r){ m =>
Some(s"""<code><a href="${context.path}/${repository.owner}/${repository.name}/commit/${m.group(2)}">${m.group(2).substring(0, 7)}</a></code>""")
}
}
}

View File

@@ -147,21 +147,23 @@ object Markdown {
}
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

View File

@@ -31,7 +31,7 @@
<label class="strong">Members</label>
<ul id="member-list" class="collaborator">
</ul>
@gitbucket.core.helper.html.account("memberName", 200)
@gitbucket.core.helper.html.account("memberName", 200, true, false)
<input type="button" class="btn btn-default" value="Add" id="addMember"/>
<input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/>
<div>
@@ -80,15 +80,14 @@ $(function(){
}
// 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(){

View File

@@ -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 =>
<div class="block">
<div class="block-header">
@helpers.avatar(userName, 20) <a href="@helpers.url(userName)">@userName</a>
@helpers.avatar(member.userName, 20) <a href="@helpers.url(member.userName)">@member.userName</a>
@if(member.isManager){ (Manager) }
</div>
</div>
}

View File

@@ -34,7 +34,7 @@
<label class="strong">Members</label>
<ul id="member-list" class="collaborator">
</ul>
@gitbucket.core.helper.html.account("memberName", 200)
@gitbucket.core.helper.html.account("memberName", 200, true, false)
<input type="button" class="btn btn-default" value="Add" id="addMember"/>
<input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/>
<div>
@@ -75,16 +75,14 @@ $(function(){
}
// 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(){

View File

@@ -11,7 +11,7 @@
@gitbucket.core.dashboard.html.sidebar(recentRepositories, userRepositories){
@gitbucket.core.dashboard.html.tab("issues")
<div class="container">
@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)
</div>
}

View File

@@ -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 @@
<li class="@(if(condition.state == "closed"){"active"})">
<a href="@condition.copy(state = "closed").toURL">Closed <span class="badge">@closedCount</span></a>
</li>
@*
<li class="@if(filter == "created_by"){active}">
<a href="@path/dashboard/@active/created_by@condition.copy(author = None, assigned = None).toURL">Created</a>
</li>
<li class="@if(filter == "assigned"){active}">
<a href="@path/dashboard/@active/assigned@condition.copy(author = None, assigned = None).toURL">Assigned</a>
</li>
<li class="@if(filter == "mentioned"){active}">
<a href="@path/dashboard/@active/mentioned@condition.copy(author = None, assigned = None).toURL">Mentioned</a>
</li>
*@
</ul>
<div class="btn-group pull-right" data-toggle="buttons">
<a class="switch btn btn-default @if(filter == "created_by"){active}" href="@context.path/dashboard/@active/created_by@condition.copy(author = None, assigned = None).toURL">Created</a>
<a class="switch btn btn-default @if(filter == "assigned" ){active}" href="@context.path/dashboard/@active/assigned@condition.copy(author = None, assigned = None).toURL">Assigned</a>
<a class="switch btn btn-default @if(filter == "mentioned" ){active}" href="@context.path/dashboard/@active/mentioned@condition.copy(author = None, assigned = None).toURL">Mentioned</a>
</div>
<script>
$(function(){
$('a.switch').click(function(){
location.href = $(this).attr('href');
});
})
</script>

View File

@@ -11,7 +11,7 @@
@gitbucket.core.dashboard.html.sidebar(recentRepositories, userRepositories){
@gitbucket.core.dashboard.html.tab("pulls")
<div class="container">
@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)
</div>
}

View File

@@ -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)
<span style="margin-right: 0px;">
<input type="text" name="@id" id="@id" class="form-control" autocomplete="off" style="width: @{width}px; margin-bottom: 0px; display: inline; vertical-align: middle;"/>
</span>
<script>
$(function(){
$('#@id').typeahead({
// highlighter: function(item) {
// var x = item.split(':');
// return $('<div><strong>' + x[0] + '</strong>' + (x[1] == 'true' ? ' (group)' : '') + '</div>');
// },
// updater: function (item) {
// return item.split(':')[0];
// },
source: function (query, process) {
return $.get('@context.path/_user/proposals', { query: query },
return $.get('@context.path/_user/proposals', { query: query, user: @user, group: @group },
function (data) {
return process(data.options);
});

View File

@@ -7,7 +7,7 @@
prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree"
) {
<li><div id="branch-control-title">Switch branches<button id="branch-control-close" class="pull-right">&times</button></div></li>
<li><input id="branch-control-input" type="text" class="form-control input-sm" placeholder="Find or create branch ..."/></li>
<li><input id="branch-control-input" type="text" class="form-control input-sm dropdown-filter-input" placeholder="Find or create branch ..."/></li>
@body
@if(hasWritePermission) {
<li id="create-branch" style="display: none;">

View File

@@ -8,9 +8,9 @@
@if(comment.fileName.isDefined){filename="@comment.fileName.get"}
@if(comment.newLine.isDefined){newline="@comment.newLine.get"}
@if(comment.oldLine.isDefined){oldline="@comment.oldLine.get"}>
<div class="issue-avatar-image">@helpers.avatarLink(comment.commentedUserName, 48)</div>
<div class="panel panel-default commit-comment-box commit-comment-@comment.commentId">
<div class="panel-heading">
@helpers.avatar(comment.commentedUserName, 20)
@helpers.user(comment.commentedUserName, styleClass="username strong")
<span class="muted">
commented

View File

@@ -1,7 +1,8 @@
@(value : String = "",
prefix: String = "",
style : String = "",
right : Boolean = false)(body: Html)
right : Boolean = false,
filter: String = "")(body: Html)
<div class="btn-group" @if(style.nonEmpty){style="@style"}>
<button
class="dropdown-toggle btn btn-default btn-sm" data-toggle="dropdown">
@@ -16,6 +17,28 @@
<span class="caret"></span>
</button>
<ul class="dropdown-menu@if(right){ pull-right}">
@if(filter.nonEmpty) {
<li><input id="@filter-input" type="text" class="form-control input-sm dropdown-filter-input" placeholder="Filter"/></li>
}
@body
</ul>
</div>
@if(filter.nonEmpty) {
<script>
$(function(){
$('#@{filter}-input').parent().click(function(e) {
e.stopPropagation();
});
$('#@{filter}-input').keyup(function() {
var inputVal = $('#@{filter}-input').val();
$.each($('#@{filter}-input').parent().parent().find('a'), function(index, elem) {
if (!inputVal || !elem.text.trim() || elem.text.trim().lastIndexOf(inputVal, 0) >= 0) {
$(elem).parent().show();
} else {
$(elem).parent().hide();
}
});
});
});
</script>
}

View File

@@ -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){
<hr/><br/>
<form method="POST" validate="true">
<div class="issue-avatar-image">@helpers.avatarLink(context.loginAccount.get.userName, 48)</div>
<div class="panel panel-default issue-comment-box">
<div class="panel-body">
@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 @@
)
<div class="text-right">
<input type="hidden" name="issueId" value="@issue.issueId"/>
@if((reopenable || !issue.closed) && (hasWritePermission || issue.openedUserName == context.loginAccount.get.userName)){
@if((reopenable || !issue.closed) && (isManageable || issue.openedUserName == context.loginAccount.get.userName)){
<input type="submit" class="btn btn-default" tabindex="3" formaction="@helpers.url(repository)/issue_comments/state" value="@{if(issue.closed) "Reopen" else "Close"}" id="action"/>
}
<input type="submit" class="btn btn-success" tabindex="2" formaction="@helpers.url(repository)/issue_comments/new" value="Comment"/>

View File

@@ -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){
<div class="issue-avatar-image">@helpers.avatarLink(issue.get.openedUserName, 48)</div>
<div class="panel panel-default issue-comment-box">
<div class="panel-heading">
@helpers.user(issue.get.openedUserName, styleClass="username strong") <span class="muted">commented @gitbucket.core.helper.html.datetimeago(issue.get.registeredDate)</span>
@helpers.avatar(issue.get.openedUserName, 20)
@helpers.user(issue.get.openedUserName, styleClass="username strong")
<span class="muted">commented @gitbucket.core.helper.html.datetimeago(issue.get.registeredDate)</span>
<span class="pull-right">
@if(hasWritePermission || context.loginAccount.map(_.userName == issue.get.openedUserName).getOrElse(false)){
@if(isManageable || context.loginAccount.map(_.userName == issue.get.openedUserName).getOrElse(false)){
<a href="#" data-issue-id="@issue.get.issueId"><i class="octicon octicon-pencil" aria-label="Edit"></i></a>
}
</span>
@@ -24,7 +25,7 @@
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = hasWritePermission
hasWritePermission = isManageable
)
</div>
</div>
@@ -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"){
<div class="issue-avatar-image">@helpers.avatarLink(comment.commentedUserName, 48)</div>
<div class="panel panel-default issue-comment-box" id="comment-@comment.commentId">
<div class="panel-heading">
@helpers.avatar(comment.commentedUserName, 20)
@helpers.user(comment.commentedUserName, styleClass="username strong")
<span class="muted">
@if(comment.action == "comment"){
@@ -48,7 +49,7 @@
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</span>
@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))){
<span class="pull-right">
<a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-pencil" aria-label="Edit"></i></a>&nbsp;
<a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-x" aria-label="Remove"></i></a>
@@ -63,7 +64,7 @@
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = hasWritePermission
hasWritePermission = isManageable
)
</div>
</div>
@@ -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))
}
}
<script>

View File

@@ -1,7 +1,7 @@
@(collaborators: List[String],
milestones: List[gitbucket.core.model.Milestone],
labels: List[gitbucket.core.model.Label],
hasWritePermission: Boolean,
isManageable: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
@@ -18,7 +18,7 @@
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = hasWritePermission,
hasWritePermission = isManageable,
completionContext = "issues",
style = "height: 200px; max-height: 250px;",
elastic = true
@@ -28,7 +28,7 @@
</div>
</div>
<div class="col-md-3">
@gitbucket.core.issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map(x => (x, 0, 0)), labels, hasWritePermission, repository)
@gitbucket.core.issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map(x => (x, 0, 0)), labels, isManageable, repository)
</div>
</div>
</form>

View File

@@ -4,17 +4,20 @@
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.html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
@gitbucket.core.html.menu("issues", repository){
<div>
<div class="show-title pull-right">
@if(hasWritePermission || context.loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
@if(isManageable || context.loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a class="btn btn-default" href="#" id="edit">Edit</a>
}
<a class="btn btn-success" href="@helpers.url(repository)/issues/new">New issue</a>
@if(isEditable){
<a class="btn btn-success" href="@helpers.url(repository)/issues/new">New issue</a>
}
</div>
<div class="edit-title pull-right" style="display: none;">
<a class="btn btn-success" href="#" id="update">Save</a> <a class="btn btn-default" href="#" id="cancel">Cancel</a>
@@ -47,11 +50,11 @@
<hr>
<div style="margin-top: 15px;">
<div class="col-md-9">
@gitbucket.core.issues.html.commentlist(Some(issue), comments, hasWritePermission, repository)
@gitbucket.core.issues.html.commentform(issue, true, hasWritePermission, repository)
@gitbucket.core.issues.html.commentlist(Some(issue), comments, isManageable, repository)
@gitbucket.core.issues.html.commentform(issue, true, isEditable, isManageable, repository)
</div>
<div class="col-md-3">
@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)
</div>
</div>
}

View File

@@ -4,14 +4,14 @@
collaborators: List[String],
milestones: List[(gitbucket.core.model.Milestone, Int, Int)],
labels: List[gitbucket.core.model.Label],
hasWritePermission: Boolean,
isManageable: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
<div style="margin-bottom: 14px;">
<span class="muted small strong">Labels</span>
@if(hasWritePermission){
@if(isManageable){
<div class="pull-right">
@gitbucket.core.helper.html.dropdown("Edit", right = true) {
@gitbucket.core.helper.html.dropdown("Edit", right = true, filter = "labels") {
@labels.map { label =>
<li>
<a href="#" class="toggle-label" data-label-id="@label.labelId">
@@ -34,9 +34,9 @@
<hr/>
<div style="margin-bottom: 14px;">
<span class="muted small strong">Milestone</span>
@if(hasWritePermission){
@if(isManageable){
<div class="pull-right">
@gitbucket.core.helper.html.dropdown("Edit", right = true) {
@gitbucket.core.helper.html.dropdown("Edit", right = true, filter = "milestone") {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="octicon octicon-x"></i> Clear this milestone</a></li>
@milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
<li>
@@ -86,9 +86,9 @@
<hr/>
<div style="margin-bottom: 14px;">
<span class="muted small strong">Assignee</span>
@if(hasWritePermission){
@if(isManageable){
<div class="pull-right">
@gitbucket.core.helper.html.dropdown("Edit", right = true) {
@gitbucket.core.helper.html.dropdown("Edit", right = true, filter = "assignee") {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="octicon octicon-x"></i> Clear assignee</a></li>
@collaborators.map { collaborator =>
<li>

View File

@@ -8,7 +8,8 @@
closedCount: Int,
condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context)
isEditable: Boolean,
isManageable: Boolean)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(repository)){
@gitbucket.core.html.menu(target, repository){
@@ -21,7 +22,7 @@
</li>
</ul>
<form method="GET" id="search-filter-form" class="form-inline pull-right">
@if(context.loginAccount.isDefined){
@if(isEditable){
@if(target == "issues"){
<a class="btn btn-success" href="@helpers.url(repository)/issues/new">New issue</a>
}
@@ -30,8 +31,8 @@
}
}
</form>
@gitbucket.core.issues.html.listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
@if(hasWritePermission){
@gitbucket.core.issues.html.listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), isManageable)
@if(isManageable){
<form id="batcheditForm" method="POST">
<input type="hidden" name="value"/>
<input type="hidden" name="checked"/>
@@ -40,7 +41,7 @@
}
}
}
@if(hasWritePermission){
@if(isManageable){
<script>
$(function(){
$('a.header-link').mouseover(function(e){

View File

@@ -8,7 +8,7 @@
milestones: List[gitbucket.core.model.Milestone] = Nil,
labels: List[gitbucket.core.model.Label] = Nil,
repository: Option[gitbucket.core.service.RepositoryService.RepositoryInfo] = None,
hasWritePermission: Boolean = false)(implicit context: gitbucket.core.controller.Context)
isManageable: Boolean = false)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@import gitbucket.core.service.IssuesService.IssueInfo
@*
@@ -110,7 +110,7 @@
</li>
}
</span>
@if(hasWritePermission){
@if(isManageable){
<span id="table-issues-batchedit">
@gitbucket.core.helper.html.dropdown("Mark as") {
<li><a href="javascript:void(0);" class="toggle-state" data-id="open">Open</a></li>
@@ -174,7 +174,7 @@
@issues.map { case IssueInfo(issue, labels, milestone, commentCount, commitStatus) =>
<tr>
<td style="padding-top: 12px; padding-bottom: 12px;">
@if(hasWritePermission){
@if(isManageable){
<input type="checkbox" value="@issue.issueId"/>
}
@*

View File

@@ -39,7 +39,7 @@
}
<script src="@helpers.assets/vendors/AdminLTE-2.3.6/js/app.js" type="text/javascript"></script>
</head>
<body class="skin-blue">
<body class="skin-blue page-load">
<div class="wrapper">
<header class="main-header">
<a href="@context.path/" class="logo">

View File

@@ -9,11 +9,11 @@
<li @if(active == name){class="active"}>
@if(path.startsWith("http")){
<a href="@path" target="_blank">
<i class="menu-icon octicon octicon-@icon"></i> @label @if(count > 0) { <span class="label label-primary pull-right">@count</span> }
<i class="menu-icon octicon octicon-@icon"></i> @label @if(count > 0) { <span class="label label-primary pull-right-container">@count</span> }
</a>
} else {
<a href="@helpers.url(repository)@path">
<i class="menu-icon octicon octicon-@icon"></i> @label @if(count > 0) { <span class="label label-primary pull-right">@count</span> }
<i class="menu-icon octicon octicon-@icon"></i> @label @if(count > 0) { <span class="label label-primary pull-right-container">@count</span> }
</a>
}
</li>
@@ -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")
}

View File

@@ -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
<div class="col-md-9">
<div id="comment-list">
@gitbucket.core.issues.html.commentlist(Some(issue), comments, hasWritePermission, repository, Some(pullreq))
@gitbucket.core.issues.html.commentlist(Some(issue), comments, isManageable, repository, Some(pullreq))
</div>
@defining(comments.flatMap {
case comment: gitbucket.core.model.IssueComment => Some(comment)
@@ -25,7 +26,7 @@
</div>
</div>
}
@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)){
<div class="issue-comment-box" style="background-color: #d0eeff;">
<div class="box-content"class="issue-content" style="border: 1px solid #87a8c9; padding: 10px;">
@@ -37,11 +38,11 @@
</div>
</div>
}
@gitbucket.core.issues.html.commentform(issue, !merged, hasWritePermission, repository)
@gitbucket.core.issues.html.commentform(issue, !merged, isEditable, isManageable, repository)
}
</div>
<div class="col-md-3">
@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)
</div>
<script>
$(function(){
@@ -55,7 +56,7 @@ $(function(){
$.get('@helpers.url(repository)/pull/@issue.issueId/mergeguide', function(data){ $('.check-conflict').html(data); });
}
@if(hasWritePermission){
@if(isManageable){
$('.delete-branch').click(function(e){
var branchName = $(e.target).data('name');
return confirm('Are you sure you want to remove the ' + branchName + ' branch?');

View File

@@ -7,7 +7,8 @@
labels: List[gitbucket.core.model.Label],
dayByDayCommits: Seq[Seq[gitbucket.core.util.JGitUtil.CommitInfo]],
diffs: Seq[gitbucket.core.util.JGitUtil.DiffInfo],
hasWritePermission: Boolean,
isEditable: Boolean,
isManageable: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
flash: Map[String, String])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@@ -18,7 +19,7 @@
@defining(dayByDayCommits.flatten){ commits =>
<div>
<div class="show-title pull-right">
@if(hasWritePermission || context.loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
@if(isManageable || context.loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a class="btn" href="#" id="edit">Edit</a>
}
@if(context.loginAccount.isDefined){
@@ -82,13 +83,13 @@
@flash.get("info").map{ info =>
<div class="alert alert-info">@info</div>
}
@gitbucket.core.pulls.html.conversation(issue, pullreq, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
@gitbucket.core.pulls.html.conversation(issue, pullreq, comments, issueLabels, collaborators, milestones, labels, isEditable, isManageable, repository)
</div>
<div class="tab-pane" id="commits">
@gitbucket.core.pulls.html.commits(dayByDayCommits, Some(comments), repository)
</div>
<div class="tab-pane" id="files">
@gitbucket.core.helper.html.diff(diffs, repository, Some(commits.head.id), Some(commits.last.id), true, Some(pullreq.issueId), hasWritePermission, true)
@gitbucket.core.helper.html.diff(diffs, repository, Some(commits.head.id), Some(commits.last.id), true, Some(pullreq.issueId), isManageable, true)
</div>
</div>
}

View File

@@ -73,7 +73,7 @@
</div>
} else {
<div class="box-content-bottom">
<pre class="prettyprint linenums blob @if(!isRenderable){ no-renderable } ">@content.content.get</pre>
<pre class="prettyprint linenums blob @if(!isRenderable){ no-renderable } ">@content.content.map(_.replaceAll("^(\r?\n)", "$1$1"))</pre>
</div>
}
}

View File

@@ -22,21 +22,21 @@
}, Some(repository)) {
@gitbucket.core.html.menu("files", repository, Some(branch), info, error){
<div class="head">
<div class="pull-right pc">
<div class="pull-right">
<div class="btn-group">
<a href="@helpers.url(repository)/find/@helpers.encodeRefName(branch)" class="btn btn-sm btn-default pc" data-hotkey="t"><i class="octicon octicon-search"></i></a>
<a href="@helpers.url(repository)/commits/@helpers.encodeRefName(branch)/@pathList.mkString("/")" class="btn btn-sm btn-default"><i class="octicon octicon-history"></i></a>
<a href="@helpers.url(repository)/find/@helpers.encodeRefName(branch)" class="btn btn-sm btn-default" data-hotkey="t"><i class="octicon octicon-search"></i></a>
<a href="@helpers.url(repository)/commits/@helpers.encodeRefName(branch)/@pathList.mkString("/")" class="btn btn-sm btn-default"><i class="octicon octicon-history"></i> @if(repository.commitCount > 10000){10000+} else {@repository.commitCount} @helpers.plural(repository.commitCount, "commit")</a>
</div>
</div>
@if(pathList.isEmpty){
@if(context.platform != "linux" && context.platform != null){
<div class="pull-right pc" style="margin-right: 5px;">
<div class="btn-group">
<div class="pull-right pc" style="margin-right: 5px;">
<div class="btn-group">
@if(context.platform != "linux" && context.platform != null){
<a href="@RepositoryService.openRepoUrl(repository.httpUrl)" id="repository-clone-url" class="btn btn-sm btn-default"><i class="octicon octicon-desktop-download"></i></a>
<a href="@{helpers.url(repository)}/archive/@{helpers.encodeRefName(branch)}.zip" class="btn btn-sm btn-default"><i class="octicon octicon-cloud-download"></i> Download ZIP</a>
</div>
}
<a href="@{helpers.url(repository)}/archive/@{helpers.encodeRefName(branch)}.zip" class="btn btn-sm btn-default"><i class="octicon octicon-cloud-download"></i> Download ZIP</a>
</div>
}
</div>
<div class="pull-right pc">
<div style="width: 240px; margin-right: 5px; margin-left: 5px;">
@gitbucket.core.helper.html.copy("repository-url-copy", repository.httpUrl){

View File

@@ -1,34 +1,139 @@
@(collaborators: List[String],
@(collaborators: List[(gitbucket.core.model.Collaborator, Boolean)],
isGroupRepository: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@import gitbucket.core.model.Permission
@gitbucket.core.html.main("Settings", Some(repository)){
@gitbucket.core.html.menu("settings", repository){
@gitbucket.core.settings.html.menu("collaborators", repository){
<h3>Manage Collaborators</h3>
<ul class="collaborator">
@collaborators.map { collaboratorName =>
<li>
<a href="@helpers.url(collaboratorName)">@collaboratorName</a>
@if(!isGroupRepository){
<a href="@helpers.url(repository)/settings/collaborators/remove?name=@collaboratorName" class="remove">(remove)</a>
} else {
@if(repository.managers.contains(collaboratorName)){
(Manager)
}
}
</li>
}
</ul>
@if(!isGroupRepository){
<form method="POST" action="@helpers.url(repository)/settings/collaborators/add" validate="true" autocomplete="off">
<div>
<span class="error" id="error-userName"></span>
<form id="form" method="post" action="@helpers.url(repository)/settings/collaborators">
<div class="panel panel-default">
<div class="panel-heading strong">Collaborators</div>
<div class="panel-body">
<ul id="collaborator-list" class="collaborator">
</ul>
@gitbucket.core.helper.html.account("userName-collaborator", 200, true, false)
<input type="button" class="btn btn-default add" value="Add" id="addCollaborator"/>
<div>
<span class="error" id="error-collaborator"></span>
</div>
</div>
@gitbucket.core.helper.html.account("userName", 300)
<input type="submit" class="btn btn-default" value="Add"/>
</form>
}
</div>
<div class="panel panel-default">
<div class="panel-heading strong">Groups</div>
<div class="panel-body">
<ul id="group-list" class="collaborator">
</ul>
@gitbucket.core.helper.html.account("userName-group", 200, false, true)
<input type="button" class="btn btn-default add" value="Add" id="addGroup"/>
<div>
<span class="error" id="error-group"></span>
</div>
</div>
</div>
<div class="align-right" style="margin-top: 20px;">
<input type="hidden" id="collaborators" name="collaborators" />
<input type="submit" class="btn btn-success" value="Apply changes"/>
</div>
</form>
}
}
}
<script>
$(function(){
$('input[type=submit]').click(function(){
updateValues();
});
$('.add').click(function(){
var id = $(this).attr('id') == 'addCollaborator' ? 'collaborator' : 'group';
$('#error-' + id).text('');
var userName = $('#userName-' + id).val();
// check empty
if($.trim(userName) == ''){
return false;
}
// check owner
var owner = '@repository.owner' == userName
if(owner){
$('#error-' + id).text('User is owner of this repository.');
return false;
}
// check duplication
var exists = $('#' + id + '-list li').filter(function(){
return $(this).data('name') == userName;
}).length > 0;
if(exists){
$('#error-' + id).text('User has been already added.');
return false;
}
// check existence
$.post('@context.path/_user/existence', { 'userName': userName },
function(data, status){
if(data != ''){
addListHTML(userName, '@Permission.ADMIN.name', '#' + id + '-list');
} else {
$('#error-' + id).text('User does not exist.');
}
});
});
$(document).on('click', '.remove', function(){
$(this).parent().remove();
});
// Don't submit form by ENTER key
$('#userName-collaborator, #userName-group').keypress(function(e){
return !(e.keyCode == 13);
});
@collaborators.map { case (collaborator, isGroup) =>
addListHTML('@collaborator.collaboratorName', '@collaborator.permission', @if(isGroup){'#group-list'}else{'#collaborator-list'});
}
function addListHTML(userName, permission, id){
var adminButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="@Permission.ADMIN.name" name="' + userName + '">Admin</label>');
if(permission == '@Permission.ADMIN.name'){
adminButton.addClass('active');
}
var writeButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="@Permission.WRITE.name" name="' + userName + '">Write</label>');
if(permission == '@Permission.WRITE.name'){
writeButton.addClass('active');
}
var readButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="@Permission.READ.name" name="' + userName + '">Read</label>');
if(permission == '@Permission.READ.name'){
readButton.addClass('active');
}
$(id).append($('<li>')
.data('name', userName)
.append($('<div class="btn-group permission" data-toggle="buttons">')
.append(adminButton)
.append(writeButton)
.append(readButton))
.append(' ')
.append($('<a target="_blank">').attr('href', '@context.path/' + userName).text(userName))
.append($('<a href="#" class="remove pull-right">(remove)</a>')));
}
function updateValues(){
var collaborators = $('#collaborator-list li').map(function(i, e){
var userName = $(e).data('name');
return userName + ':' + $(e).find('label.active input[type=radio]').attr('value');
}).get().join(',');
var groups = $('#group-list li').map(function(i, e){
var userName = $(e).data('name');
return userName + ':' + $(e).find('label.active input[type=radio]').attr('value');
}).get().join(',');
$('#collaborators').val(collaborators + ',' + groups);
}
});
</script>

View File

@@ -13,7 +13,7 @@
<div>
Transfer this repo to another user or to group.
<div class="pull-right">
@gitbucket.core.helper.html.account("newOwner", 200)
@gitbucket.core.helper.html.account("newOwner", 200, true, true)
<input type="submit" class="btn btn-danger" value="Transfer"/>
<div>
<span id="error-newOwner" class="error"></span>

View File

@@ -39,40 +39,66 @@
</div>
</label>
</fieldset>
<fieldset class="form-group">
<label class="checkbox" for="allowFork">
<input type="checkbox" id="allowFork" name="allowFork"@if(repository.repository.options.allowFork){ checked}/>
Forks<br>
<div class="normal muted">
Allow repository forking to users who can access this repository.
</div>
</label>
</fieldset>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading strong">Features</div>
<div class="panel-heading strong">Issues</div>
<div class="panel-body">
<fieldset class="form-group">
<label class="checkbox" for="enableIssues">
<input type="checkbox" id="enableIssues" name="enableIssues"@if(repository.repository.enableIssues){ checked}/>
Issues<br>
<div class="normal muted">
Provides Lightweight issue tracking integrated with this repository. Add issues to milestones, label issues, and close & reference issues from commit messages.
</div>
</label>
<div class="radio">
<label>
<input type="radio" name="issuesOption" value="DISABLE" @if(repository.repository.options.issuesOption == "DISABLE"){ checked}> Disables issues tracking system
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="issuesOption" value="PRIVATE" @if(repository.repository.options.issuesOption == "PRIVATE"){ checked}> Writable users can view, create and comment on issues
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="issuesOption" value="PUBLIC" @if(repository.repository.options.issuesOption == "PUBLIC"){ checked}> Readable users can view, create and comment on isues
</label>
</div>
<label for="externalIssuesUrl" class="strong">External URL:
<span class="normal muted">(Put if you have the external issue tracking system for this project)</span>
</label>
<input type="text" class="form-control" id="externalIssuesUrl" name="externalIssuesUrl" value="@repository.repository.externalIssuesUrl"/>
<input type="text" class="form-control" id="externalIssuesUrl" name="externalIssuesUrl" value="@repository.repository.options.externalIssuesUrl"/>
</fieldset>
<fieldset class="form-group margin">
<label class="checkbox" for="enableWiki">
<input type="checkbox" id="enableWiki" name="enableWiki"@if(repository.repository.enableWiki){ checked}/>
Wiki<br>
<div class="normal muted">
Provides a simple solution to manage documents. All users who can look this repository can read and collaborators can edit pages.
</div>
</label>
<label class="checkbox" for="allowWikiEditing">
<input type="checkbox" id="allowWikiEditing" name="allowWikiEditing"@if(repository.repository.allowWikiEditing){ checked}/>
Allow read-only users to edit Wiki pages<br>
</label>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading strong">Wiki</div>
<div class="panel-body">
<fieldset class="form-group">
<div class="radio">
<label>
<input type="radio" name="wikiOption" value="DISABLE" @if(repository.repository.options.wikiOption == "DISABLE"){ checked}> Disables wiki
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="wikiOption" value="PRIVATE" @if(repository.repository.options.wikiOption == "PRIVATE"){ checked}> Writable users can view, create and edit wiki pages
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="wikiOption" value="PUBLIC" @if(repository.repository.options.wikiOption == "PUBLIC"){ checked}> Readable users can view, create and edit wiki pages
</label>
</div>
<label for="externalWikiUrl" class="strong">External URL:
<span class="normal muted">(Put if you have the external Wiki for this project)</span>
</label>
<input type="text" class="form-control" id="externalWikiUrl" name="externalWikiUrl" value="@repository.repository.externalWikiUrl"/>
<input type="text" class="form-control" id="externalWikiUrl" name="externalWikiUrl" value="@repository.repository.options.externalWikiUrl"/>
</fieldset>
</div>
</div>
@@ -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'));
}
</script>

View File

@@ -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 @@
<div class="pull-left">
@gitbucket.core.helper.html.diff(diffs, repository, None, None, false, None, false, false)
</div>
@if(hasWritePermission){
@if(isEditable){
<div>
@if(pageName.isDefined){
<a href="@helpers.url(repository)/wiki/@helpers.urlEncode(pageName)/_revert/@from...@to" class="btn">Revert Changes</a>

View File

@@ -44,7 +44,7 @@
</form>
}
}
<script>
<script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script><script>
$(function(){
try {
$('.clickable').dropzone({

View File

@@ -1,21 +1,20 @@
@(pageName: Option[String],
commits: List[gitbucket.core.util.JGitUtil.CommitInfo],
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
isEditable: Boolean)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main(s"History - ${repository.owner}/${repository.name}", Some(repository)){
@gitbucket.core.html.menu("wiki", repository){
<div class="pull-right">
@if(pageName.isEmpty){
@if(context.loginAccount.isDefined){
<a class="btn btn-small" href="@helpers.url(repository)/wiki/_new">New Page</a>
}
} else {
@if(context.loginAccount.isDefined){
<a class="btn btn-small btn-default" href="@helpers.url(repository)/wiki/@helpers.urlEncode(pageName)/_edit">Edit Page</a>
<a class="btn btn-small btn-success" href="@helpers.url(repository)/wiki/_new">New Page</a>
}
@if(isEditable) {
<div class="pull-right">
@if(pageName.isEmpty) {
<a class="btn btn-small" href="@helpers.url(repository)/wiki/_new">New Page</a>
} else {
<a class="btn btn-small btn-default" href="@helpers.url(repository)/wiki/@helpers.urlEncode(pageName)/_edit">Edit Page</a>
<a class="btn btn-small btn-success" href="@helpers.url(repository)/wiki/_new">New Page</a>
}
</div>
}
</div>
<h1 class="wiki-title">
@if(pageName.isEmpty){
<span class="muted">History</span>

View File

@@ -2,7 +2,7 @@
page: gitbucket.core.service.WikiService.WikiPageInfo,
pages: List[String],
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean,
isEditable: Boolean,
sidebar: Option[gitbucket.core.service.WikiService.WikiPageInfo],
footer: Option[gitbucket.core.service.WikiService.WikiPageInfo])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@@ -10,12 +10,13 @@
@gitbucket.core.html.main(s"${pageName} - ${repository.owner}/${repository.name}", Some(repository)){
@gitbucket.core.html.menu("wiki", repository){
<div>
@if(hasWritePermission){
<div class="pull-right">
<div class="pull-right">
<a class="btn btn-small btn-default" href="@helpers.url(repository)/wiki/@helpers.urlEncode(pageName)/_history">Page History</a>
@if(isEditable){
<a class="btn btn-default" href="@helpers.url(repository)/wiki/@helpers.urlEncode(pageName)/_edit">Edit Page</a>
<a class="btn btn-success" href="@helpers.url(repository)/wiki/_new">New Page</a>
</div>
}
}
</div>
<h1 class="body-title">@pageName</h1>
<div>
<span class="muted"><strong>@page.committer</strong> edited this page @gitbucket.core.helper.html.datetimeago(page.time)</span>
@@ -48,13 +49,13 @@
}
@sidebar.map { sidebarPage =>
<div class="wiki-sidebar">
@if(hasWritePermission){
@if(isEditable){
<a href="@helpers.url(repository)/wiki/_Sidebar/_edit" style="text-decoration: none;"><span class="octicon octicon-pencil pull-right"></span></a>
}
@helpers.markdown(sidebarPage.content, repository, true, false, false, false, pages)
</div>
}.getOrElse{
@if(hasWritePermission){
@if(isEditable){
<a class="button-link" href="@helpers.url(repository)/wiki/_Sidebar/_edit" style="text-decoration: none;">
<div class="wiki-sidebar-dotted text-center"><i class="octicon octicon-plus"></i> Add a custom sidebar</div>
</a>
@@ -87,13 +88,13 @@
</div>
@footer.map { footerPage =>
<div class="wiki-sidebar wiki-footer">
@if(hasWritePermission){
@if(isEditable){
<a href="@helpers.url(repository)/wiki/_Footer/_edit" style="text-decoration: none;"><span class="octicon octicon-pencil pull-right"></span></a>
}
@helpers.markdown(footerPage.content, repository, true, false, false, false, pages)
</div>
}.getOrElse{
@if(hasWritePermission){
@if(isEditable){
<a class="button-link" href="@helpers.url(repository)/wiki/_Footer/_edit" style="text-decoration: none;">
<div class="wiki-sidebar-dotted text-center"><i class="octicon octicon-plus"></i> Add a custom footer</div>
</a>

View File

@@ -1,6 +1,6 @@
@(pages: List[String],
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context)
isEditable: Boolean)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main(s"Pages - ${repository.owner}/${repository.name}", Some(repository)){
@gitbucket.core.html.menu("wiki", repository){
@@ -10,7 +10,7 @@
</li>
<li class="pull-right">
<div class="btn-group">
@if(hasWritePermission){
@if(isEditable){
<a class="btn btn-small btn-default" href="@helpers.url(repository)/wiki/_new">New Page</a>
}
</div>

View File

@@ -88,6 +88,9 @@
<!-- ===================================================================== -->
<session-config>
<session-timeout>1440</session-timeout>
<cookie-config>
<http-only>true</http-only>
</cookie-config>
</session-config>
<!-- ===================================================================== -->

View File

@@ -326,6 +326,12 @@ div.account-image {
margin-bottom: 8px;
}
.dropdown-filter-input {
border: solid 1px #ccc;
margin: 4px;
width: 96%;
}
ul.dropdown-menu {
padding: 2px 0;
}
@@ -338,7 +344,7 @@ ul.dropdown-menu li a {
padding: 2px 10px;
}
ul.dropdown-menu :last-child {
ul.dropdown-menu li:last-child {
border-bottom: none;
}
@@ -632,12 +638,6 @@ span.simplified-path {
line-height: 15px;
}
#branch-control-input {
border: solid 1px #ccc;
margin: 4px;
width: 96%;
}
.new-branch-name {
font-weight: bold;
font-size: 1.2em;
@@ -779,10 +779,6 @@ h4#issueTitle {
padding: 0px;
}
div.issue-avatar-image {
float: left;
}
div.issue-participants {
margin-bottom: 15px;
margin-left: 50px;
@@ -790,7 +786,6 @@ div.issue-participants {
div.issue-comment-box, div.commit-comment-box {
margin-bottom: 15px;
margin-left: 70px;
}
div.issue-comment-box > div.panel-body,
@@ -879,7 +874,7 @@ li.task-list-item input.task-list-item-checkbox {
.discussion-item {
position: relative;
margin: 15px 0 15px 79px;
margin: 15px 0 15px 20px;
padding-left: 25px;
}
.discussion-item-header {
@@ -1588,7 +1583,7 @@ a.markdown-anchor-link span.octicon {
vertical-align: baseline;
font-size: 100%;
height: inherit;
width: 780px;
width: 300px;
}
.find-input{
font-size: 18px;
@@ -1762,3 +1757,14 @@ div.container.blame-container{
width: 20px;
height: 20px;
}
/****************************************************************************/
/* Suppress transition animation on load */
/****************************************************************************/
body.page-load * {
-webkit-transition: none !important;
-moz-transition: none !important;
-ms-transition: none !important;
-o-transition: none !important;
transition: none !important;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -28,6 +28,9 @@ $(function(){
// syntax highlighting by google-code-prettify
prettyPrint();
// Suppress transition animation on load
$("body").removeClass("page-load");
});
function displayErrors(data, elem){

View File

@@ -5,7 +5,6 @@ import org.eclipse.jgit.transport.{ReceivePack, ReceiveCommand}
import org.eclipse.jgit.lib.ObjectId
import gitbucket.core.model.CommitState
import gitbucket.core.service.ProtectedBranchService.{ProtectedBranchReceiveHook, ProtectedBranchInfo}
import scalaz._, Scalaz._
import org.scalatest.FunSpec
class ProtectedBranchServiceSpec extends FunSpec with ServiceSpecBase with ProtectedBranchService with CommitStatusService {
@@ -53,7 +52,8 @@ class ProtectedBranchServiceSpec extends FunSpec with ServiceSpecBase with Prote
it("getBranchProtectedReason on force push from admin") {
withTestDB { implicit session =>
withTestRepository { git =>
val rp = new ReceivePack(git.getRepository) <| { _.setAllowNonFastForwards(true) }
val rp = new ReceivePack(git.getRepository)
rp.setAllowNonFastForwards(true)
val rc = new ReceiveCommand(ObjectId.fromString(sha), ObjectId.fromString(sha2), "refs/heads/branch", ReceiveCommand.Type.UPDATE_NONFASTFORWARD)
generateNewUserWithDBRepository("user1", "repo1")
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1") == None)
@@ -65,7 +65,8 @@ class ProtectedBranchServiceSpec extends FunSpec with ServiceSpecBase with Prote
it("getBranchProtectedReason on force push from other") {
withTestDB { implicit session =>
withTestRepository { git =>
val rp = new ReceivePack(git.getRepository) <| { _.setAllowNonFastForwards(true) }
val rp = new ReceivePack(git.getRepository)
rp.setAllowNonFastForwards(true)
val rc = new ReceiveCommand(ObjectId.fromString(sha), ObjectId.fromString(sha2), "refs/heads/branch", ReceiveCommand.Type.UPDATE_NONFASTFORWARD)
generateNewUserWithDBRepository("user1", "repo1")
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2") == None)
@@ -77,7 +78,8 @@ class ProtectedBranchServiceSpec extends FunSpec with ServiceSpecBase with Prote
it("getBranchProtectedReason check status on push from other") {
withTestDB { implicit session =>
withTestRepository { git =>
val rp = new ReceivePack(git.getRepository) <| { _.setAllowNonFastForwards(false) }
val rp = new ReceivePack(git.getRepository)
rp.setAllowNonFastForwards(false)
val rc = new ReceiveCommand(ObjectId.fromString(sha), ObjectId.fromString(sha2), "refs/heads/branch", ReceiveCommand.Type.UPDATE)
val user1 = generateNewUserWithDBRepository("user1", "repo1")
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2") == None)
@@ -97,7 +99,8 @@ class ProtectedBranchServiceSpec extends FunSpec with ServiceSpecBase with Prote
it("getBranchProtectedReason check status on push from admin") {
withTestDB { implicit session =>
withTestRepository { git =>
val rp = new ReceivePack(git.getRepository) <| { _.setAllowNonFastForwards(false) }
val rp = new ReceivePack(git.getRepository)
rp.setAllowNonFastForwards(false)
val rc = new ReceiveCommand(ObjectId.fromString(sha), ObjectId.fromString(sha2), "refs/heads/branch", ReceiveCommand.Type.UPDATE)
val user1 = generateNewUserWithDBRepository("user1", "repo1")
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1") == None)
@@ -183,4 +186,4 @@ class ProtectedBranchServiceSpec extends FunSpec with ServiceSpecBase with Prote
}
}
}
}
}

View File

@@ -3,7 +3,8 @@ package gitbucket.core.service
import gitbucket.core.model._
import org.scalatest.FunSpec
class PullRequestServiceSpec extends FunSpec with ServiceSpecBase with PullRequestService with IssuesService with AccountService {
class PullRequestServiceSpec extends FunSpec with ServiceSpecBase
with PullRequestService with IssuesService with AccountService with RepositoryService {
def swap(r: (Issue, PullRequest)) = (r._2 -> r._1)

View File

@@ -10,8 +10,6 @@ import liquibase.database.core.H2Database
import liquibase.database.jvm.JdbcConnection
import profile._
import profile.blockingApi._
import scalaz._
import Scalaz._
import org.apache.commons.io.FileUtils
@@ -29,7 +27,8 @@ trait ServiceSpecBase {
org.h2.Driver.load()
using(DriverManager.getConnection(url, user, pass)){ conn =>
val solidbase = new Solidbase()
val db = new H2Database() <| { _.setConnection(new JdbcConnection(conn)) } // TODO Remove setConnection in the future
val db = new H2Database()
db.setConnection(new JdbcConnection(conn)) // TODO Remove setConnection in the future
solidbase.migrate(conn, Thread.currentThread.getContextClassLoader, db, GitBucketCoreModule)
}
Database.forURL(url, user, pass).withSession { session =>