Compare commits

...

61 Commits
4.5 ... 4.7

Author SHA1 Message Date
Naoki Takezoe
7bb5379b45 Update for 4.7 release 2016-11-26 13:31:12 +09:00
Naoki Takezoe
5692a8c83e Fix update branch authentication 2016-11-26 13:07:54 +09:00
Naoki Takezoe
6c6126148e Refactor new permission system 2016-11-26 13:00:24 +09:00
Naoki Takezoe
5b2e24daef Merge pull request #1353 from mashabow/link-milestone-label
Link milestone label to its issue list
2016-11-25 12:01:13 +09:00
Masaya Nakamura
29f390e48c Use helpers.urlEncode()/encodeURIComponent() 2016-11-25 11:40:05 +09:00
Masaya Nakamura
c49eff6e54 Link milestone label to its issue list 2016-11-24 17:35:49 +09:00
Naoki Takezoe
27930a5849 Fix testcase 2016-11-20 00:00:58 +09:00
Naoki Takezoe
6ffc139d2f (refs #1275)Keep sidebar status during same session 2016-11-19 23:02:41 +09:00
Naoki Takezoe
59ed027b60 Distinct assignable user names 2016-11-19 21:28:39 +09:00
Naoki Takezoe
5dc55822d7 Fix multi statement splitting 2016-11-17 00:16:02 +09:00
Naoki Takezoe
9bfe5115cc (refs #1350) Accept max 100 characters as repository name 2016-11-15 02:21:55 +09:00
Naoki Takezoe
aaf9c65f30 Fix scaladoc 2016-11-12 18:29:03 +09:00
Naoki Takezoe
d6197261fb (refs #1343) Keep file permission in online file editing 2016-11-09 10:34:53 +09:00
Naoki Takezoe
8fd7df2a9d (refs #1345) Remove large size avatar image to use width efficiently 2016-11-09 02:11:21 +09:00
Naoki Takezoe
4eb148f4a6 (refs #1340)Add dropdown filter
Because the large dropdown list makes  impossible to choose items below the screen.
2016-11-08 21:19:37 +09:00
Naoki Takezoe
8f1e460893 Merge pull request #1338 from gitbucket/new-permission-system
New permission system
2016-11-08 18:04:17 +09:00
Naoki Takezoe
8c80f8a506 (refs #1286) Rename CollaboratorsAuthenticator to WritableUsersAutheticator 2016-11-08 17:48:28 +09:00
Naoki Takezoe
9eb9fc666c (refs #1286) Bugfix 2016-11-08 02:32:11 +09:00
Naoki Takezoe
d70c6cece7 (refs #1286) Fix testcase 2016-11-06 16:37:44 +09:00
Naoki Takezoe
dbdee135a3 (refs #1286) Update collaborators setting form 2016-11-06 16:32:46 +09:00
Naoki Takezoe
132bb6bee4 (refs #1286) Update controllers 2016-11-04 13:57:39 +09:00
Naoki Takezoe
2dfa7a1190 (refs #1286) Update the repository settings form 2016-11-02 10:34:28 +09:00
Naoki Takezoe
06d559b47e (refs #1286) Add columns: ISSUES_OPTION and WIKI_OPTION 2016-11-02 02:15:21 +09:00
Naoki Takezoe
83baaa6ed9 (refs #1286) Show group marker when collaborator is added 2016-11-01 21:04:27 +09:00
Naoki Takezoe
85d38a47f1 (refs #1286) Refactoring 2016-11-01 18:01:57 +09:00
Naoki Takezoe
0c3c6ea15a (refs #1286) Show whether group account on the collaborators proposal 2016-11-01 16:03:02 +09:00
Naoki Takezoe
2ce436bddc Merge pull request #1336 from yanma/issue_1318
(refs #1318) make record***Activity via ssh works again
2016-11-01 14:16:26 +09:00
Hiroaki Yamazoe
a60c607fcb enable transaction for SSH access 2016-11-01 11:56:36 +09:00
Naoki Takezoe
0456739118 (refs #1286) Update collaborators setting form 2016-11-01 09:10:40 +09:00
Naoki Takezoe
368052bd8f (refs #1286) Fix services and beat compilation errors 2016-11-01 07:24:51 +09:00
Naoki Takezoe
ce916a7d4b (refs #1286) Fix models 2016-11-01 06:51:30 +09:00
Naoki Takezoe
60ff046823 (refs #1286) Prototyping of new permission system 2016-11-01 06:45:18 +09:00
Naoki Takezoe
7d3bda42e2 Update version 2016-10-29 15:00:20 +09:00
Naoki Takezoe
83a39f1e39 Update README.md 2016-10-29 15:00:01 +09:00
Naoki Takezoe
de726d8d96 (refs #1325) Prepend one more empty line when the first line is an empty line. 2016-10-26 12:21:21 +09:00
Naoki Takezoe
91bb241e8c (refs #1334) Indicate who is group manager 2016-10-26 10:44:20 +09:00
Naoki Takezoe
8da55d8aa8 Merge pull request #1311 from gitbucket/fix-issues-sorting
(refs #1308)Fix issues sorting
2016-10-19 01:29:00 +09:00
Naoki Takezoe
3355c46503 (refs #1308)Fix issues sorting again 2016-10-18 02:30:23 +09:00
Naoki Takezoe
0a3d457218 Merge pull request #1328 from kw-udon/custom-media-type-in-content-api
Support custom media types in get-content API
2016-10-17 09:31:24 +09:00
Naoki Takezoe
7fa5fdfbd0 Merge pull request #1326 from kounoike/pr/suppress-transition-on-load-in-ie
Suppress noisy transition animation on load in IE11
2016-10-17 01:14:43 +09:00
Naoki Takezoe
95f88891d0 Merge pull request #1327 from kounoike/pr/fix-logo
Cleanup white pixels in gitbucket logo.
2016-10-17 01:13:20 +09:00
Keiichi Watanabe
550f8f415c Support custom media types in get-content API
cf. https://developer.github.com/v3/repos/contents/#custom-media-types
2016-10-16 20:31:26 +09:00
KOUNOIKE Yuusuke
5ab947d8ec Cleanup white pixels in gitbucket logo. 2016-10-16 13:02:20 +09:00
KOUNOIKE Yuusuke
ec793535e7 Suppress noisy transition animation on load in IE11
http://stackoverflow.com/a/25674229
2016-10-16 12:44:43 +09:00
Naoki Takezoe
2f1d81cc4c Create issue comment by online file editing as well 2016-10-13 20:36:11 +09:00
Naoki Takezoe
0f189ca710 (refs #1319)Get rid of the duplication of issue id extracted from commit message 2016-10-13 20:24:01 +09:00
Naoki Takezoe
6afd51bb8d (refs #1312)Fix badge position on the side menu 2016-10-13 06:42:58 +09:00
Naoki Takezoe
e415f9d24e (refs #1316)Add "Page History" button to the wiki page view 2016-10-13 00:59:19 +09:00
Naoki Takezoe
ba5d587a1e Merge pull request #1321 from int128/gh-compatibility
Improve GitHub compatibility for Jenkins
2016-10-11 12:32:43 +09:00
Naoki Takezoe
92f778b6e9 Merge pull request #1320 from kw-udon/file-content-api
Add API to get a file content
2016-10-10 18:12:03 +09:00
Hidetake Iwata
b52981a845 Provide GitHub compatible URL for Git clients 2016-10-10 15:13:47 +09:00
Keiichi Watanabe
9c5d3edc72 Add API to get a file content 2016-10-10 02:04:43 +09:00
Naoki Takezoe
56d68c6145 Merge pull request #1313 from xuwei-k/remove-scalaz
remove unused scalaz
2016-10-05 17:45:01 +09:00
xuwei-k
4d13282915 remove unused scalaz 2016-10-05 12:14:43 +09:00
Naoki Takezoe
872320ccab (refs #1129)Not use Option.get for non-able value 2016-10-04 10:28:47 +09:00
Naoki Takezoe
28ee80b727 (refs #1129)Not delete from REPOSITORY table when user is disabled 2016-10-03 16:55:20 +09:00
Naoki Takezoe
2621de2cde Fix error responses 2016-10-03 16:01:57 +09:00
Naoki Takezoe
82b102845f (refs #1292)Add new option to disable repository forking 2016-10-03 15:26:23 +09:00
Naoki Takezoe
28c9f8b89a (refs #1308)Fix sorting in issue query 2016-10-03 01:42:56 +09:00
Naoki Takezoe
23fa937fd1 Remove unnecessary lines 2016-10-02 02:27:48 +09:00
Naoki Takezoe
02330a2050 (refs #1304)Remove package artifact overriding 2016-10-01 03:24:21 +09:00
73 changed files with 1339 additions and 821 deletions

View File

@@ -65,6 +65,19 @@ Support
Release Notes Release Notes
------------- -------------
### 4.7 - 26 Nov 2016
- New permission system
- Dropdown filter for issue labels, milestones and assignees
- Keep sidebar folding status
- Link from milestone label to the issue list
### 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 ### 4.5 - 29 Sep 2016
- Attach files by dropping into textarea - Attach files by dropping into textarea
- Issues / Pull requests switcher in dashboard - Issues / Pull requests switcher in dashboard

View File

@@ -1,6 +1,6 @@
val Organization = "io.github.gitbucket" val Organization = "io.github.gitbucket"
val Name = "gitbucket" val Name = "gitbucket"
val GitBucketVersion = "4.5.0" val GitBucketVersion = "4.7.0"
val ScalatraVersion = "2.4.1" val ScalatraVersion = "2.4.1"
val JettyVersion = "9.3.9.v20160517" val JettyVersion = "9.3.9.v20160517"
@@ -50,7 +50,6 @@ libraryDependencies ++= Seq(
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided", "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
"junit" % "junit" % "4.12" % "test", "junit" % "junit" % "4.12" % "test",
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test", "org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test",
"org.scalaz" %% "scalaz-core" % "7.2.4" % "test",
"com.wix" % "wix-embedded-mysql" % "1.0.3" % "test", "com.wix" % "wix-embedded-mysql" % "1.0.3" % "test",
"ru.yandex.qatools.embed" % "postgresql-embedded" % "1.14" % "test" "ru.yandex.qatools.embed" % "postgresql-embedded" % "1.14" % "test"
) )
@@ -106,7 +105,6 @@ libraryDependencies ++= Seq(
val executableKey = TaskKey[File]("executable") val executableKey = TaskKey[File]("executable")
executableKey := { executableKey := {
import org.apache.ivy.util.ChecksumHelper
import java.util.jar.{ Manifest => JarManifest } import java.util.jar.{ Manifest => JarManifest }
import java.util.jar.Attributes.{ Name => AttrName } import java.util.jar.Attributes.{ Name => AttrName }
@@ -164,12 +162,6 @@ executableKey := {
log info s"built executable webapp ${outputFile}" log info s"built executable webapp ${outputFile}"
outputFile outputFile
} }
/*
Keys.artifact in (Compile, executableKey) ~= {
_ copy (`type` = "war", extension = "war"))
}
addArtifact(Keys.artifact in (Compile, executableKey), executableKey)
*/
publishTo <<= version { (v: String) => publishTo <<= version { (v: String) =>
val nexus = "https://oss.sonatype.org/" val nexus = "https://oss.sonatype.org/"
if (v.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "content/repositories/snapshots") if (v.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "content/repositories/snapshots")
@@ -177,7 +169,6 @@ publishTo <<= version { (v: String) =>
} }
publishMavenStyle := true publishMavenStyle := true
pomIncludeRepository := { _ => false } pomIncludeRepository := { _ => false }
artifact in Keys.`package` := Artifact(moduleName.value)
pomExtra := ( pomExtra := (
<url>https://github.com/gitbucket/gitbucket</url> <url>https://github.com/gitbucket/gitbucket</url>
<licenses> <licenses>

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="ROLE" 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,12 +1,12 @@
import gitbucket.core.controller._
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.servlet.{ApiAuthenticationFilter, Database, GitAuthenticationFilter, TransactionFilter}
import gitbucket.core.util.Directory
import java.util.EnumSet import java.util.EnumSet
import javax.servlet._ import javax.servlet._
import gitbucket.core.controller._
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.SystemSettingsService import gitbucket.core.service.SystemSettingsService
import gitbucket.core.servlet._
import gitbucket.core.util.Directory
import org.scalatra._ import org.scalatra._
@@ -25,6 +25,9 @@ class ScalatraBootstrap extends LifeCycle with SystemSettingsService {
context.getFilterRegistration("gitAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*") context.getFilterRegistration("gitAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
context.addFilter("apiAuthenticationFilter", new ApiAuthenticationFilter) context.addFilter("apiAuthenticationFilter", new ApiAuthenticationFilter)
context.getFilterRegistration("apiAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/api/v3/*") 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 // Register controllers
context.mount(new AnonymousAccessController, "/*") context.mount(new AnonymousAccessController, "/*")

View File

@@ -15,5 +15,12 @@ object GitBucketCoreModule extends Module("gitbucket-core",
new Version("4.2.1"), new Version("4.2.1"),
new Version("4.3.0"), new Version("4.3.0"),
new Version("4.4.0"), new Version("4.4.0"),
new Version("4.5.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 package gitbucket.core.api
import gitbucket.core.util.JGitUtil.FileInfo 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{ object ApiContents{
def apply(fileInfo: FileInfo): ApiContents = def apply(fileInfo: FileInfo, content: Option[Array[Byte]]): ApiContents = {
if(fileInfo.isDirectory) ApiContents("dir", fileInfo.name) if(fileInfo.isDirectory) {
else ApiContents("file", fileInfo.name) 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( def apply(user: Account): ApiUser = ApiUser(
login = user.userName, login = user.userName,
email = user.mailAddress, email = user.mailAddress,
`type` = if(user.isGroupAccount){ "Organization" }else{ "User" }, `type` = if(user.isGroupAccount){ "Organization" } else { "User" },
site_admin = user.isAdmin, site_admin = user.isAdmin,
created_at = user.registeredDate created_at = user.registeredDate
) )

View File

@@ -11,7 +11,7 @@ case class CreateARepository(
auto_init: Boolean = false auto_init: Boolean = false
) { ) {
def isValid: Boolean = { def isValid: Boolean = {
name.length<=40 && name.length <= 100 &&
name.matches("[a-zA-Z0-9\\-\\+_.]+") && name.matches("[a-zA-Z0-9\\-\\+_.]+") &&
!name.startsWith("_") && !name.startsWith("_") &&
!name.startsWith("-") !name.startsWith("-")

View File

@@ -14,6 +14,7 @@ import gitbucket.core.util._
import io.github.gitbucket.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.scalatra.BadRequest
class AccountController extends AccountControllerBase class AccountController extends AccountControllerBase
@@ -120,7 +121,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
// Members // Members
case "members" if(account.isGroupAccount) => { case "members" if(account.isGroupAccount) => {
val members = getGroupMembers(account.userName) 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 })) 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 })) context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
} }
} }
} getOrElse NotFound } getOrElse NotFound()
} }
get("/:userName.atom") { get("/:userName.atom") {
@@ -156,7 +157,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName).map { x => getAccountByUserName(userName).map { x =>
html.edit(x, flash.get("info"), flash.get("error")) html.edit(x, flash.get("info"), flash.get("error"))
} getOrElse NotFound } getOrElse NotFound()
}) })
post("/:userName/_edit", editForm)(oneselfOnly { form => post("/:userName/_edit", editForm)(oneselfOnly { form =>
@@ -172,7 +173,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
flash += "info" -> "Account information has been updated." flash += "info" -> "Account information has been updated."
redirect(s"/${userName}/_edit") redirect(s"/${userName}/_edit")
} getOrElse NotFound } getOrElse NotFound()
}) })
get("/:userName/_delete")(oneselfOnly { get("/:userName/_delete")(oneselfOnly {
@@ -196,14 +197,14 @@ trait AccountControllerBase extends AccountManagementControllerBase {
session.invalidate session.invalidate
redirect("/") redirect("/")
} }
} getOrElse NotFound } getOrElse NotFound()
}) })
get("/:userName/_ssh")(oneselfOnly { get("/:userName/_ssh")(oneselfOnly {
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName).map { x => getAccountByUserName(userName).map { x =>
html.ssh(x, getPublicKeys(x.userName)) html.ssh(x, getPublicKeys(x.userName))
} getOrElse NotFound } getOrElse NotFound()
}) })
post("/:userName/_ssh", sshKeyForm)(oneselfOnly { form => post("/:userName/_ssh", sshKeyForm)(oneselfOnly { form =>
@@ -234,7 +235,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
case _ => None case _ => None
} }
html.application(x, tokens, generatedToken) html.application(x, tokens, generatedToken)
} getOrElse NotFound } getOrElse NotFound()
}) })
post("/:userName/_personalToken", personalTokenForm)(oneselfOnly { form => post("/:userName/_personalToken", personalTokenForm)(oneselfOnly { form =>
@@ -260,7 +261,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
} else { } else {
html.register() html.register()
} }
} else NotFound } else NotFound()
} }
post("/register", newForm){ form => 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) createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, false, form.url)
updateImage(form.userName, form.fileId, false) updateImage(form.userName, form.fileId, false)
redirect("/signin") redirect("/signin")
} else NotFound } else NotFound()
} }
get("/groups/new")(usersOnly { get("/groups/new")(usersOnly {
@@ -318,18 +319,18 @@ trait AccountControllerBase extends AccountManagementControllerBase {
// Update GROUP_MEMBER // Update GROUP_MEMBER
updateGroupMembers(form.groupName, members) updateGroupMembers(form.groupName, members)
// Update COLLABORATOR for group repositories // // Update COLLABORATOR for group repositories
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => // getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
removeCollaborators(form.groupName, repositoryName) // removeCollaborators(form.groupName, repositoryName)
members.foreach { case (userName, isManager) => // members.foreach { case (userName, isManager) =>
addCollaborator(form.groupName, repositoryName, userName) // addCollaborator(form.groupName, repositoryName, userName)
} // }
} // }
updateImage(form.groupName, form.fileId, form.clearImage) updateImage(form.groupName, form.fileId, form.clearImage)
redirect(s"/${form.groupName}") redirect(s"/${form.groupName}")
} getOrElse NotFound } getOrElse NotFound()
} }
}) })
@@ -355,6 +356,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
}) })
get("/:owner/:repository/fork")(readableUsersOnly { repository => get("/:owner/:repository/fork")(readableUsersOnly { repository =>
if(repository.repository.options.allowFork){
val loginAccount = context.loginAccount.get val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName val loginUserName = loginAccount.userName
val groups = getGroupsByUserName(loginUserName) val groups = getGroupsByUserName(loginUserName)
@@ -370,9 +372,11 @@ trait AccountControllerBase extends AccountManagementControllerBase {
) )
case _ => redirect(s"/${loginUserName}") case _ => redirect(s"/${loginUserName}")
} }
} else BadRequest()
}) })
post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) =>
if(repository.repository.options.allowFork){
val loginAccount = context.loginAccount.get val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName val loginUserName = loginAccount.userName
val accountName = form.accountName val accountName = form.accountName
@@ -398,13 +402,13 @@ trait AccountControllerBase extends AccountManagementControllerBase {
parentUserName = Some(repository.owner) parentUserName = Some(repository.owner)
) )
// Add collaborators for group repository // // Add collaborators for group repository
val ownerAccount = getAccountByUserName(accountName).get // val ownerAccount = getAccountByUserName(accountName).get
if(ownerAccount.isGroupAccount){ // if(ownerAccount.isGroupAccount){
getGroupMembers(accountName).foreach { member => // getGroupMembers(accountName).foreach { member =>
addCollaborator(accountName, repository.name, member.userName) // addCollaborator(accountName, repository.name, member.userName)
} // }
} // }
// Insert default labels // Insert default labels
insertDefaultLabels(accountName, repository.name) insertDefaultLabels(accountName, repository.name)
@@ -425,6 +429,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
redirect(s"/${accountName}/${repository.name}") redirect(s"/${accountName}/${repository.name}")
} }
} }
} else BadRequest()
}) })
private def existsAccount: Constraint = new Constraint(){ private def existsAccount: Constraint = new Constraint(){

View File

@@ -7,9 +7,10 @@ import gitbucket.core.service.PullRequestService._
import gitbucket.core.service._ import gitbucket.core.service._
import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._ 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._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.view.helpers.{renderMarkup, isRenderable}
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.scalatra.{NoContent, UnprocessableEntity, Created} import org.scalatra.{NoContent, UnprocessableEntity, Created}
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
@@ -34,7 +35,7 @@ class ApiController extends ApiControllerBase
with GroupManagerAuthenticator with GroupManagerAuthenticator
with ReferrerAuthenticator with ReferrerAuthenticator
with ReadableUsersAuthenticator with ReadableUsersAuthenticator
with CollaboratorsAuthenticator with WritableUsersAuthenticator
trait ApiControllerBase extends ControllerBase { trait ApiControllerBase extends ControllerBase {
self: RepositoryService self: RepositoryService
@@ -51,7 +52,7 @@ trait ApiControllerBase extends ControllerBase {
with GroupManagerAuthenticator with GroupManagerAuthenticator
with ReferrerAuthenticator with ReferrerAuthenticator
with ReadableUsersAuthenticator with ReadableUsersAuthenticator
with CollaboratorsAuthenticator => with WritableUsersAuthenticator =>
/** /**
* https://developer.github.com/v3/#root-endpoint * https://developer.github.com/v3/#root-endpoint
@@ -66,7 +67,7 @@ trait ApiControllerBase extends ControllerBase {
get("/api/v3/orgs/:groupName") { get("/api/v3/orgs/:groupName") {
getAccountByUserName(params("groupName")).filter(account => account.isGroupAccount).map { account => getAccountByUserName(params("groupName")).filter(account => account.isGroupAccount).map { account =>
JsonFormat(ApiUser(account)) JsonFormat(ApiUser(account))
} getOrElse NotFound } getOrElse NotFound()
} }
/** /**
@@ -75,7 +76,7 @@ trait ApiControllerBase extends ControllerBase {
get("/api/v3/users/:userName") { get("/api/v3/users/:userName") {
getAccountByUserName(params("userName")).filterNot(account => account.isGroupAccount).map { account => getAccountByUserName(params("userName")).filterNot(account => account.isGroupAccount).map { account =>
JsonFormat(ApiUser(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 * https://developer.github.com/v3/repos/contents/#get-contents
*/ */
get("/api/v3/repos/:owner/:repo/contents/*")(referrersOnly { repository => 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 { val path = multiParams("splat").head match {
case s if s.isEmpty => "." case s if s.isEmpty => "."
case s => s case s => s
} }
val refStr = params.getOrElse("ref", repository.repository.defaultBranch) val refStr = params.getOrElse("ref", repository.repository.defaultBranch)
using(Git.open(getRepositoryDir(params("owner"), params("repo")))){ git => 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 * https://developer.github.com/v3/repos/collaborators/#list-collaborators
*/ */
get("/api/v3/repos/:owner/:repo/collaborators") (referrersOnly { repository => 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") { get("/api/v3/user") {
context.loginAccount.map { account => context.loginAccount.map { account =>
JsonFormat(ApiUser(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) disableBranchProtection(repository.owner, repository.name, branch)
} }
JsonFormat(ApiBranch(branch, protection)(RepositoryName(repository))) 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) comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt)
} yield { } yield {
JsonFormat(comments.map{ case (issueComment, user, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) }) 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()) issueComment <- getComment(repository.owner, repository.name, id.toString())
} yield { } yield {
JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get), issue.isPullRequest)) 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 * Create a label
* https://developer.github.com/v3/issues/labels/#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{ (for{
data <- extractFromJsonBody[CreateALabel] if data.isValid data <- extractFromJsonBody[CreateALabel] if data.isValid
} yield { } yield {
@@ -311,7 +353,7 @@ trait ApiControllerBase extends ControllerBase {
* Update a label * Update a label
* https://developer.github.com/v3/issues/labels/#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{ (for{
data <- extractFromJsonBody[CreateALabel] if data.isValid data <- extractFromJsonBody[CreateALabel] if data.isValid
} yield { } yield {
@@ -337,7 +379,7 @@ trait ApiControllerBase extends ControllerBase {
* Delete a label * Delete a label
* https://developer.github.com/v3/issues/labels/#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) { LockUtil.lock(RepositoryName(repository).fullName) {
getLabel(repository.owner, repository.name, params("labelName")).map { label => getLabel(repository.owner, repository.name, params("labelName")).map { label =>
deleteLabel(repository.owner, repository.name, label.labelId) deleteLabel(repository.owner, repository.name, label.labelId)
@@ -393,7 +435,7 @@ trait ApiControllerBase extends ControllerBase {
ApiRepository(headRepo, ApiUser(headOwner)), ApiRepository(headRepo, ApiUser(headOwner)),
ApiRepository(repository, ApiUser(baseOwner)), ApiRepository(repository, ApiUser(baseOwner)),
ApiUser(issueUser))) ApiUser(issueUser)))
}).getOrElse(NotFound) }) getOrElse NotFound()
}) })
/** /**
@@ -412,7 +454,7 @@ trait ApiControllerBase extends ControllerBase {
JsonFormat(commits) JsonFormat(commits)
} }
} }
} getOrElse NotFound } getOrElse NotFound()
}) })
/** /**
@@ -425,7 +467,7 @@ trait ApiControllerBase extends ControllerBase {
/** /**
* https://developer.github.com/v3/repos/statuses/#create-a-status * 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{ (for{
ref <- params.get("sha") ref <- params.get("sha")
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref) sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
@@ -437,7 +479,7 @@ trait ApiControllerBase extends ControllerBase {
status <- getCommitStatus(repository.owner, repository.name, statusId) status <- getCommitStatus(repository.owner, repository.name, statusId)
} yield { } yield {
JsonFormat(ApiCommitStatus(status, ApiUser(creator))) 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) => JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map{ case(status, creator) =>
ApiCommitStatus(status, ApiUser(creator)) ApiCommitStatus(status, ApiUser(creator))
}) })
}) getOrElse NotFound }) getOrElse NotFound()
}) })
/** /**
@@ -478,11 +520,11 @@ trait ApiControllerBase extends ControllerBase {
} yield { } yield {
val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha) val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha)
JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner))) JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner)))
}) getOrElse NotFound }) getOrElse NotFound()
}) })
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean = private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
} }

View File

@@ -191,6 +191,7 @@ case class Context(settings: SystemSettingsService.SystemSettings, loginAccount:
case agent if agent.contains("Win") => "windows" case agent if agent.contains("Win") => "windows"
case _ => null case _ => null
} }
val sidebarCollapse = request.getSession.getAttribute("sidebar-collapse") != null
/** /**
* Get object from cache. * Get object from cache.
@@ -248,7 +249,7 @@ trait AccountManagementControllerBase extends ControllerBase {
protected def reservedNames(): Constraint = new Constraint(){ protected def reservedNames(): Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = if(allReservedNames.contains(value)){ override def validate(name: String, value: String, messages: Messages): Option[String] = if(allReservedNames.contains(value)){
Some(s"${value} is reserved") Some(s"${value} is reserved")
}else{ } else {
None None
} }
} }

View File

@@ -75,7 +75,7 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
} }
}, FileUtil.isUploadableType) }, FileUtil.isUploadableType)
} }
} getOrElse BadRequest } getOrElse BadRequest()
} }
post("/import") { post("/import") {
@@ -93,7 +93,7 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
loginAccount match { loginAccount match {
case x if(x.isAdmin) => action case x if(x.isAdmin) => action
case x if(getCollaborators(owner, repository).contains(x.userName)) => 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)) => case Some(file) if(mimeTypeChcker(file.name)) =>
defining(FileUtil.generateFileId){ fileId => defining(FileUtil.generateFileId){ fileId =>
f(file, fileId) f(file, fileId)
Ok(fileId) Ok(fileId)
} }
case _ => BadRequest case _ => BadRequest()
} }
} }

View File

@@ -5,9 +5,9 @@ import gitbucket.core.model.Account
import gitbucket.core.service._ import gitbucket.core.service._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator, ReferrerAuthenticator, StringUtil} import gitbucket.core.util.{Keys, LDAPUtil, ReferrerAuthenticator, StringUtil, UsersAuthenticator}
import io.github.gitbucket.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
import org.scalatra.Ok
class IndexController extends IndexControllerBase class IndexController extends IndexControllerBase
@@ -81,6 +81,15 @@ trait IndexControllerBase extends ControllerBase {
xml.feed(getRecentActivities()) xml.feed(getRecentActivities())
} }
get("/sidebar-collapse"){
if(params("collapse") == "true"){
session.setAttribute("sidebar-collapse", "true")
} else {
session.setAttribute("sidebar-collapse", null)
}
Ok()
}
/** /**
* Set account information into HttpSession and redirect. * Set account information into HttpSession and redirect.
*/ */
@@ -108,18 +117,29 @@ trait IndexControllerBase extends ControllerBase {
*/ */
get("/_user/proposals")(usersOnly { get("/_user/proposals")(usersOnly {
contentType = formats("json") contentType = formats("json")
val user = params("user").toBoolean
val group = params("group").toBoolean
org.json4s.jackson.Serialization.write( 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 { post("/_user/existence")(usersOnly {
getAccountByUserName(params("userName")).map { account => getAccountByUserName(params("userName")).map { account =>
if(params.get("userOnly").isDefined) !account.isGroupAccount else true if(account.isGroupAccount) "group" else "user"
} getOrElse false } getOrElse ""
}) })
// TODO Move to RepositoryViwerController? // TODO Move to RepositoryViwerController?

View File

@@ -2,24 +2,24 @@ package gitbucket.core.controller
import gitbucket.core.issues.html import gitbucket.core.issues.html
import gitbucket.core.service.IssuesService._ import gitbucket.core.service.IssuesService._
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service._ import gitbucket.core.service._
import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util._ import gitbucket.core.util._
import gitbucket.core.view import gitbucket.core.view
import gitbucket.core.view.Markdown import gitbucket.core.view.Markdown
import io.github.gitbucket.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
import org.scalatra.Ok import org.scalatra.Ok
class IssuesController extends IssuesControllerBase class IssuesController extends IssuesControllerBase
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with HandleCommentService 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 { trait IssuesControllerBase extends ControllerBase {
self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with HandleCommentService 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], case class IssueCreateForm(title: String, content: Option[String],
assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String]) assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
@@ -67,38 +67,42 @@ trait IssuesControllerBase extends ControllerBase {
_, _,
getComments(owner, name, issueId.toInt), getComments(owner, name, issueId.toInt),
getIssueLabels(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), getMilestonesWithIssueCount(owner, name),
getLabels(owner, name), getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount), isEditable(repository),
isManageable(repository),
repository) repository)
} getOrElse NotFound } getOrElse NotFound()
} }
}) })
get("/:owner/:repository/issues/new")(readableUsersOnly { repository => get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
if(isEditable(repository)){ // TODO Should this check is provided by authenticator?
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
html.create( html.create(
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, getAssignableUserNames(owner, name),
getMilestones(owner, name), getMilestones(owner, name),
getLabels(owner, name), getLabels(owner, name),
hasWritePermission(owner, name, context.loginAccount), isManageable(repository),
repository) repository)
} }
} else Unauthorized()
}) })
post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) =>
if(isEditable(repository)){ // TODO Should this check is provided by authenticator?
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
val writable = hasWritePermission(owner, name, context.loginAccount) val manageable = isManageable(repository)
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
// insert issue // insert issue
val issueId = createIssue(owner, name, userName, form.title, form.content, val issueId = createIssue(owner, name, userName, form.title, form.content,
if(writable) form.assignedUserName else None, if (manageable) form.assignedUserName else None,
if(writable) form.milestoneId else None) if (manageable) form.milestoneId else None)
// insert labels // insert labels
if(writable){ if (manageable) {
form.labelNames.map { value => form.labelNames.map { value =>
val labels = getLabels(owner, name) val labels = getLabels(owner, name)
value.split(",").foreach { labelName => value.split(",").foreach { labelName =>
@@ -120,89 +124,90 @@ trait IssuesControllerBase extends ControllerBase {
callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get) callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get)
// notifications // notifications
Notifier().toNotify(repository, issue, form.content.getOrElse("")){ Notifier().toNotify(repository, issue, form.content.getOrElse("")) {
Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") 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) => ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) =>
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue => getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){ if(isEditableContent(owner, name, issue.openedUserName)){
// update issue // update issue
updateIssue(owner, name, issue.issueId, title, issue.content) updateIssue(owner, name, issue.issueId, title, issue.content)
// extract references and create refer comment // extract references and create refer comment
createReferComment(owner, name, issue.copy(title = title), title, context.loginAccount.get) createReferComment(owner, name, issue.copy(title = title), title, context.loginAccount.get)
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized } else Unauthorized()
} getOrElse NotFound } getOrElse NotFound()
} }
}) })
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) => ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) =>
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue => getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){ if(isEditableContent(owner, name, issue.openedUserName)){
// update issue // update issue
updateIssue(owner, name, issue.issueId, issue.title, content) updateIssue(owner, name, issue.issueId, issue.title, content)
// extract references and create refer comment // extract references and create refer comment
createReferComment(owner, name, issue, content.getOrElse(""), context.loginAccount.get) createReferComment(owner, name, issue, content.getOrElse(""), context.loginAccount.get)
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized } else Unauthorized()
} getOrElse NotFound } getOrElse NotFound()
} }
}) })
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue => 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) => handleComment(issue, Some(form.content), repository, actionOpt) map { case (issue, id) =>
redirect(s"/${repository.owner}/${repository.name}/${ redirect(s"/${repository.owner}/${repository.name}/${
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
} }
} getOrElse NotFound } getOrElse NotFound()
}) })
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) => post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue => 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) => handleComment(issue, form.content, repository, actionOpt) map { case (issue, id) =>
redirect(s"/${repository.owner}/${repository.name}/${ redirect(s"/${repository.owner}/${repository.name}/${
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") 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) => ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
getComment(owner, name, params("id")).map { comment => getComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){ if(isEditableContent(owner, name, comment.commentedUserName)){
updateComment(comment.commentId, form.content) updateComment(comment.commentId, form.content)
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}") redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
} else Unauthorized } else Unauthorized()
} getOrElse NotFound } getOrElse NotFound()
} }
}) })
ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository => ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
getComment(owner, name, params("id")).map { comment => getComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){ if(isEditableContent(owner, name, comment.commentedUserName)){
Ok(deleteComment(comment.commentId)) Ok(deleteComment(comment.commentId))
} else Unauthorized } else Unauthorized()
} getOrElse NotFound } getOrElse NotFound()
} }
}) })
ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository => ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository =>
getIssue(repository.owner, repository.name, params("id")) map { x => 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 { params.get("dataType") collect {
case t if t == "html" => html.editissue(x.content, x.issueId, repository) case t if t == "html" => html.editissue(x.content, x.issueId, repository)
} getOrElse { } getOrElse {
@@ -218,18 +223,18 @@ trait IssuesControllerBase extends ControllerBase {
enableAnchor = true, enableAnchor = true,
enableLineBreaks = true, enableLineBreaks = true,
enableTaskList = true, enableTaskList = true,
hasWritePermission = isEditable(x.userName, x.repositoryName, x.openedUserName) hasWritePermission = true
) )
) )
) )
} }
} else Unauthorized } else Unauthorized()
} getOrElse NotFound } getOrElse NotFound()
}) })
ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository => ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository =>
getComment(repository.owner, repository.name, params("id")) map { x => 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 { params.get("dataType") collect {
case t if t == "html" => html.editcomment(x.content, x.commentId, repository) case t if t == "html" => html.editcomment(x.content, x.commentId, repository)
} getOrElse { } getOrElse {
@@ -244,51 +249,51 @@ trait IssuesControllerBase extends ControllerBase {
enableAnchor = true, enableAnchor = true,
enableLineBreaks = true, enableLineBreaks = true,
enableTaskList = true, enableTaskList = true,
hasWritePermission = isEditable(x.userName, x.repositoryName, x.commentedUserName) hasWritePermission = true
) )
) )
) )
} }
} else Unauthorized } else Unauthorized()
} getOrElse NotFound } getOrElse NotFound()
}) })
ajaxPost("/:owner/:repository/issues/new/label")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/new/label")(writableUsersOnly { repository =>
val labelNames = params("labelNames").split(",") val labelNames = params("labelNames").split(",")
val labels = getLabels(repository.owner, repository.name).filter(x => labelNames.contains(x.labelName)) val labels = getLabels(repository.owner, repository.name).filter(x => labelNames.contains(x.labelName))
html.labellist(labels) 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 => defining(params("id").toInt){ issueId =>
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) 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 => defining(params("id").toInt){ issueId =>
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) 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")) updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName"))
Ok("updated") 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")) updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId"))
milestoneId("milestoneId").map { milestoneId => milestoneId("milestoneId").map { milestoneId =>
getMilestonesWithIssueCount(repository.owner, repository.name) getMilestonesWithIssueCount(repository.owner, repository.name)
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) => .find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
gitbucket.core.issues.milestones.html.progress(openCount + closeCount, closeCount) gitbucket.core.issues.milestones.html.progress(openCount + closeCount, closeCount)
} getOrElse NotFound } getOrElse NotFound()
} getOrElse Ok() } getOrElse Ok()
}) })
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/state")(writableUsersOnly { repository =>
defining(params.get("value")){ action => defining(params.get("value")){ action =>
action match { action match {
case Some("open") => executeBatch(repository) { issueId => 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 => params("value").toIntOpt.map{ labelId =>
executeBatch(repository) { issueId => executeBatch(repository) { issueId =>
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse { getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
registerIssueLabel(repository.owner, repository.name, issueId, labelId) 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 => defining(assignedUserName("value")){ value =>
executeBatch(repository) { executeBatch(repository) {
updateAssignedUserName(repository.owner, repository.name, _, value) 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 => defining(milestoneId("value")){ value =>
executeBatch(repository) { executeBatch(repository) {
updateMilestoneId(repository.owner, repository.name, _, value) updateMilestoneId(repository.owner, repository.name, _, value)
@@ -340,15 +345,12 @@ trait IssuesControllerBase extends ControllerBase {
RawData(FileUtil.getMimeType(file.getName), file) RawData(FileUtil.getMimeType(file.getName), file)
} }
case _ => None case _ => None
}) getOrElse NotFound }) getOrElse NotFound()
}) })
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "") val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt) 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) = { private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
params("checked").split(',') map(_.toInt) foreach execute params("checked").split(',') map(_.toInt) foreach execute
params("from") match { params("from") match {
@@ -360,7 +362,6 @@ trait IssuesControllerBase extends ControllerBase {
private def searchIssues(repository: RepositoryService.RepositoryInfo) = { private def searchIssues(repository: RepositoryService.RepositoryInfo) = {
defining(repository.owner, repository.name){ case (owner, repoName) => defining(repository.owner, repository.name){ case (owner, repoName) =>
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
val sessionKey = Keys.Session.Issues(owner, repoName)
// retrieve search condition // retrieve search condition
val condition = IssueSearchCondition(request) val condition = IssueSearchCondition(request)
@@ -369,18 +370,42 @@ trait IssuesControllerBase extends ControllerBase {
"issues", "issues",
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page, page,
if(!getAccountByUserName(owner).exists(_.isGroupAccount)){ getAssignableUserNames(owner, repoName),
(getCollaborators(owner, repoName) :+ owner).sorted
} else {
getCollaborators(owner, repoName)
},
getMilestones(owner, repoName), getMilestones(owner, repoName),
getLabels(owner, repoName), getLabels(owner, repoName),
countIssue(condition.copy(state = "open" ), false, owner -> repoName), countIssue(condition.copy(state = "open" ), false, owner -> repoName),
countIssue(condition.copy(state = "closed"), false, owner -> repoName), countIssue(condition.copy(state = "closed"), false, owner -> repoName),
condition, condition,
repository, 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 = {
hasDeveloperRole(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 "ALL" => !repository.repository.isPrivate && context.loginAccount.isDefined
case "PUBLIC" => hasGuestRole(repository.owner, repository.name, context.loginAccount)
case "PRIVATE" => hasDeveloperRole(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 = {
hasDeveloperRole(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.issues.labels.html
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService} 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 gitbucket.core.util.Implicits._
import io.github.gitbucket.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
@@ -10,11 +10,11 @@ import org.scalatra.Ok
class LabelsController extends LabelsControllerBase class LabelsController extends LabelsControllerBase
with LabelsService with IssuesService with RepositoryService with AccountService with LabelsService with IssuesService with RepositoryService with AccountService
with ReferrerAuthenticator with CollaboratorsAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator
trait LabelsControllerBase extends ControllerBase { trait LabelsControllerBase extends ControllerBase {
self: LabelsService with IssuesService with RepositoryService self: LabelsService with IssuesService with RepositoryService
with ReferrerAuthenticator with CollaboratorsAuthenticator => with ReferrerAuthenticator with WritableUsersAuthenticator =>
case class LabelForm(labelName: String, color: String) case class LabelForm(labelName: String, color: String)
@@ -29,40 +29,40 @@ trait LabelsControllerBase extends ControllerBase {
getLabels(repository.owner, repository.name), getLabels(repository.owner, repository.name),
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository, repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount)) hasDeveloperRole(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) 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)) val labelId = createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
html.label( html.label(
getLabel(repository.owner, repository.name, labelId).get, getLabel(repository.owner, repository.name, labelId).get,
// TODO futility // TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository, repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount)) hasDeveloperRole(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 => getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
html.edit(Some(label), repository) html.edit(Some(label), repository)
} getOrElse NotFound() } 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)) updateLabel(repository.owner, repository.name, params("labelId").toInt, form.labelName, form.color.substring(1))
html.label( html.label(
getLabel(repository.owner, repository.name, params("labelId").toInt).get, getLabel(repository.owner, repository.name, params("labelId").toInt).get,
// TODO futility // TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository, repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount)) hasDeveloperRole(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) deleteLabel(repository.owner, repository.name, params("labelId").toInt)
Ok() Ok()
}) })

View File

@@ -2,17 +2,17 @@ package gitbucket.core.controller
import gitbucket.core.issues.milestones.html import gitbucket.core.issues.milestones.html
import gitbucket.core.service.{RepositoryService, MilestonesService, AccountService} 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 gitbucket.core.util.Implicits._
import io.github.gitbucket.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
class MilestonesController extends MilestonesControllerBase class MilestonesController extends MilestonesControllerBase
with MilestonesService with RepositoryService with AccountService with MilestonesService with RepositoryService with AccountService
with ReferrerAuthenticator with CollaboratorsAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator
trait MilestonesControllerBase extends ControllerBase { trait MilestonesControllerBase extends ControllerBase {
self: MilestonesService with RepositoryService 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]) case class MilestoneForm(title: String, description: Option[String], dueDate: Option[java.util.Date])
@@ -27,58 +27,58 @@ trait MilestonesControllerBase extends ControllerBase {
params.getOrElse("state", "open"), params.getOrElse("state", "open"),
getMilestonesWithIssueCount(repository.owner, repository.name), getMilestonesWithIssueCount(repository.owner, repository.name),
repository, repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount)) hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
}) })
get("/:owner/:repository/issues/milestones/new")(collaboratorsOnly { get("/:owner/:repository/issues/milestones/new")(writableUsersOnly {
html.edit(None, _) 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) createMilestone(repository.owner, repository.name, form.title, form.description, form.dueDate)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones") 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 => params("milestoneId").toIntOpt.map{ milestoneId =>
html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository) 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 => params("milestoneId").toIntOpt.flatMap{ milestoneId =>
getMilestone(repository.owner, repository.name, milestoneId).map { milestone => getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate)) updateMilestone(milestone.copy(title = form.title, description = form.description, dueDate = form.dueDate))
redirect(s"/${repository.owner}/${repository.name}/issues/milestones") 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 => params("milestoneId").toIntOpt.flatMap{ milestoneId =>
getMilestone(repository.owner, repository.name, milestoneId).map { milestone => getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
closeMilestone(milestone) closeMilestone(milestone)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones") 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 => params("milestoneId").toIntOpt.flatMap{ milestoneId =>
getMilestone(repository.owner, repository.name, milestoneId).map { milestone => getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
openMilestone(milestone) openMilestone(milestone)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones") 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 => params("milestoneId").toIntOpt.flatMap{ milestoneId =>
getMilestone(repository.owner, repository.name, milestoneId).map { milestone => getMilestone(repository.owner, repository.name, milestoneId).map { milestone =>
deleteMilestone(repository.owner, repository.name, milestone.milestoneId) deleteMilestone(repository.owner, repository.name, milestone.milestoneId)
redirect(s"/${repository.owner}/${repository.name}/issues/milestones") 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.MergeService
import gitbucket.core.service.IssuesService._ import gitbucket.core.service.IssuesService._
import gitbucket.core.service.PullRequestService._ import gitbucket.core.service.PullRequestService._
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service._ import gitbucket.core.service._
import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
@@ -14,28 +15,26 @@ import gitbucket.core.util.JGitUtil._
import gitbucket.core.util._ import gitbucket.core.util._
import gitbucket.core.view import gitbucket.core.view
import gitbucket.core.view.helpers import gitbucket.core.view.helpers
import io.github.gitbucket.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.PersonIdent import org.eclipse.jgit.lib.PersonIdent
import org.slf4j.LoggerFactory
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
class PullRequestsController extends PullRequestsControllerBase class PullRequestsController extends PullRequestsControllerBase
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService 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 with CommitStatusService with MergeService with ProtectedBranchService
trait PullRequestsControllerBase extends ControllerBase { trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService 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 => with CommitStatusService with MergeService with ProtectedBranchService =>
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
val pullRequestForm = mapping( val pullRequestForm = mapping(
"title" -> trim(label("Title" , text(required, maxlength(100)))), "title" -> trim(label("Title" , text(required, maxlength(100)))),
"content" -> trim(label("Content", optional(text()))), "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)) (commits.flatten.map(commit => getCommitComments(owner, name, commit.id, true)).flatten.toList ::: getComments(owner, name, issueId))
.sortWith((a, b) => a.registeredDate before b.registeredDate), .sortWith((a, b) => a.registeredDate before b.registeredDate),
getIssueLabels(owner, name, issueId), getIssueLabels(owner, name, issueId),
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, getAssignableUserNames(owner, name),
getMilestonesWithIssueCount(owner, name), getMilestonesWithIssueCount(owner, name),
getLabels(owner, name), getLabels(owner, name),
commits, commits,
diffs, diffs,
hasWritePermission(owner, name, context.loginAccount), isEditable(repository),
isManageable(repository),
repository, repository,
flash.toMap.map(f => f._1 -> f._2.toString)) flash.toMap.map(f => f._1 -> f._2.toString))
} }
} }
} getOrElse NotFound } getOrElse NotFound()
}) })
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository => ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository =>
@@ -115,7 +115,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val hasConflict = LockUtil.lock(s"${owner}/${name}"){ val hasConflict = LockUtil.lock(s"${owner}/${name}"){
checkConflict(owner, name, pullreq.branch, issueId) checkConflict(owner, name, pullreq.branch, issueId)
} }
val hasMergePermission = hasWritePermission(owner, name, context.loginAccount) val hasMergePermission = hasDeveloperRole(owner, name, context.loginAccount)
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.branch) val branchProtection = getProtectedBranchInfo(owner, name, pullreq.branch)
val mergeStatus = PullRequestService.MergeStatus( val mergeStatus = PullRequestService.MergeStatus(
hasConflict = hasConflict, hasConflict = hasConflict,
@@ -125,7 +125,7 @@ trait PullRequestsControllerBase extends ControllerBase {
needStatusCheck = context.loginAccount.map{ u => needStatusCheck = context.loginAccount.map{ u =>
branchProtection.needStatusCheck(u.userName) branchProtection.needStatusCheck(u.userName)
}.getOrElse(true), }.getOrElse(true),
hasUpdatePermission = hasWritePermission(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount) && hasUpdatePermission = hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount) &&
context.loginAccount.map{ u => context.loginAccount.map{ u =>
!getProtectedBranchInfo(pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch).needStatusCheck(u.userName) !getProtectedBranchInfo(pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch).needStatusCheck(u.userName)
}.getOrElse(false), }.getOrElse(false),
@@ -138,10 +138,10 @@ trait PullRequestsControllerBase extends ControllerBase {
repository, repository,
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName).get) 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 => params("id").toIntOpt.map { issueId =>
val branchName = multiParams("splat").head val branchName = multiParams("splat").head
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
@@ -153,27 +153,27 @@ trait PullRequestsControllerBase extends ControllerBase {
} }
createComment(repository.owner, repository.name, userName, issueId, branchName, "delete_branch") createComment(repository.owner, repository.name, userName, issueId, branchName, "delete_branch")
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} getOrElse NotFound } getOrElse NotFound()
}) })
post("/:owner/:repository/pull/:id/update_branch")(referrersOnly { baseRepository => post("/:owner/:repository/pull/:id/update_branch")(writableUsersOnly { baseRepository =>
(for { (for {
issueId <- params("id").toIntOpt issueId <- params("id").toIntOpt
loginAccount <- context.loginAccount loginAccount <- context.loginAccount
(issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId) (issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId)
owner = pullreq.requestUserName owner = pullreq.requestUserName
name = pullreq.requestRepositoryName name = pullreq.requestRepositoryName
if hasWritePermission(owner, name, context.loginAccount) if hasDeveloperRole(owner, name, context.loginAccount)
} yield { } yield {
val repository = getRepository(owner, name).get
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch) val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch)
if(branchProtection.needStatusCheck(loginAccount.userName)){ if(branchProtection.needStatusCheck(loginAccount.userName)){
flash += "error" -> s"branch ${pullreq.requestBranch} is protected need status check." flash += "error" -> s"branch ${pullreq.requestBranch} is protected need status check."
} else { } else {
val repository = getRepository(owner, name).get
LockUtil.lock(s"${owner}/${name}"){ LockUtil.lock(s"${owner}/${name}"){
val alias = if(pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName){ val alias = if(pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName){
pullreq.branch pullreq.branch
}else{ } else {
s"${pullreq.userName}:${pullreq.branch}" s"${pullreq.userName}:${pullreq.branch}"
} }
val existIds = using(Git.open(Directory.getRepositoryDir(owner, name))) { git => JGitUtil.getAllCommitIds(git) }.toSet val existIds = using(Git.open(Directory.getRepositoryDir(owner, name))) { git => JGitUtil.getAllCommitIds(git) }.toSet
@@ -187,11 +187,10 @@ trait PullRequestsControllerBase extends ControllerBase {
using(Git.open(Directory.getRepositoryDir(owner, name))) { git => using(Git.open(Directory.getRepositoryDir(owner, name))) { git =>
// after update branch // after update branch
val newCommitId = git.getRepository.resolve(s"refs/heads/${pullreq.requestBranch}") val newCommitId = git.getRepository.resolve(s"refs/heads/${pullreq.requestBranch}")
val commits = git.log.addRange(oldId, newCommitId).call.iterator.asScala.map(c => new JGitUtil.CommitInfo(c)).toList val commits = git.log.addRange(oldId, newCommitId).call.iterator.asScala.map(c => new JGitUtil.CommitInfo(c)).toList
commits.foreach{ commit => commits.foreach { commit =>
if(!existIds.contains(commit.id)){ if(!existIds.contains(commit.id)){
createIssueComment(owner, name, commit) createIssueComment(owner, name, commit)
} }
@@ -220,12 +219,13 @@ trait PullRequestsControllerBase extends ControllerBase {
flash += "info" -> s"Merge branch '${alias}' into ${pullreq.requestBranch}" flash += "info" -> s"Merge branch '${alias}' into ${pullreq.requestBranch}"
} }
} }
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} }
}) getOrElse NotFound redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
}) 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 => params("id").toIntOpt.flatMap { issueId =>
val owner = repository.owner val owner = repository.owner
val name = repository.name val name = repository.name
@@ -273,7 +273,7 @@ trait PullRequestsControllerBase extends ControllerBase {
} }
} }
} }
} getOrElse NotFound } getOrElse NotFound()
}) })
get("/:owner/:repository/compare")(referrersOnly { forkedRepository => get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
@@ -290,7 +290,7 @@ trait PullRequestsControllerBase extends ControllerBase {
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}") redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
} }
} getOrElse NotFound } getOrElse NotFound()
} }
case _ => { case _ => {
using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git => using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
@@ -374,8 +374,8 @@ trait PullRequestsControllerBase extends ControllerBase {
forkedRepository, forkedRepository,
originRepository, originRepository,
forkedRepository, forkedRepository,
hasWritePermission(originRepository.owner, originRepository.name, context.loginAccount), hasDeveloperRole(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), getMilestones(originRepository.owner, originRepository.name),
getLabels(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)}") 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")(readableUsersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat") val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner) val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner) val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
@@ -416,12 +416,15 @@ trait PullRequestsControllerBase extends ControllerBase {
} }
html.mergecheck(conflict) 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) => defining(repository.owner, repository.name){ case (owner, name) =>
val writable = hasWritePermission(owner, name, context.loginAccount) val manageable = isManageable(repository)
val editable = isEditable(repository)
if(editable) {
val loginUserName = context.loginAccount.get.userName val loginUserName = context.loginAccount.get.userName
val issueId = createIssue( val issueId = createIssue(
@@ -430,8 +433,8 @@ trait PullRequestsControllerBase extends ControllerBase {
loginUser = loginUserName, loginUser = loginUserName,
title = form.title, title = form.title,
content = form.content, content = form.content,
assignedUserName = if(writable) form.assignedUserName else None, assignedUserName = if (manageable) form.assignedUserName else None,
milestoneId = if(writable) form.milestoneId else None, milestoneId = if (manageable) form.milestoneId else None,
isPullRequest = true) isPullRequest = true)
createPullRequest( createPullRequest(
@@ -446,7 +449,7 @@ trait PullRequestsControllerBase extends ControllerBase {
commitIdTo = form.commitIdTo) commitIdTo = form.commitIdTo)
// insert labels // insert labels
if(writable){ if (manageable) {
form.labelNames.map { value => form.labelNames.map { value =>
val labels = getLabels(owner, name) val labels = getLabels(owner, name)
value.split(",").foreach { labelName => value.split(",").foreach { labelName =>
@@ -471,12 +474,13 @@ trait PullRequestsControllerBase extends ControllerBase {
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get) createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get)
// notifications // notifications
Notifier().toNotify(repository, issue, form.content.getOrElse("")){ Notifier().toNotify(repository, issue, form.content.getOrElse("")) {
Notifier.msgPullRequest(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}") Notifier.msgPullRequest(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
} }
} }
redirect(s"/${owner}/${name}/pull/${issueId}") redirect(s"/${owner}/${name}/pull/${issueId}")
} else Unauthorized()
} }
}) })
@@ -517,7 +521,6 @@ trait PullRequestsControllerBase extends ControllerBase {
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) = private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
defining(repository.owner, repository.name){ case (owner, repoName) => defining(repository.owner, repository.name){ case (owner, repoName) =>
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
val sessionKey = Keys.Session.Pulls(owner, repoName)
// retrieve search condition // retrieve search condition
val condition = IssueSearchCondition(request) val condition = IssueSearchCondition(request)
@@ -526,18 +529,34 @@ trait PullRequestsControllerBase extends ControllerBase {
"pulls", "pulls",
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
page, page,
if(!getAccountByUserName(owner).exists(_.isGroupAccount)){ getAssignableUserNames(owner, repoName),
(getCollaborators(owner, repoName) :+ owner).sorted
} else {
getCollaborators(owner, repoName)
},
getMilestones(owner, repoName), getMilestones(owner, repoName),
getLabels(owner, repoName), getLabels(owner, repoName),
countIssue(condition.copy(state = "open" ), true, owner -> repoName), countIssue(condition.copy(state = "open" ), true, owner -> repoName),
countIssue(condition.copy(state = "closed"), true, owner -> repoName), countIssue(condition.copy(state = "closed"), true, owner -> repoName),
condition, condition,
repository, 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 = {
hasDeveloperRole(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 "ALL" => !repository.repository.isPrivate && context.loginAccount.isDefined
case "PUBLIC" => hasGuestRole(repository.owner, repository.name, context.loginAccount)
case "PRIVATE" => hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
case "DISABLE" => false
}
} }
} }

View File

@@ -31,22 +31,22 @@ trait RepositorySettingsControllerBase extends ControllerBase {
repositoryName: String, repositoryName: String,
description: Option[String], description: Option[String],
isPrivate: Boolean, isPrivate: Boolean,
enableIssues: Boolean, issuesOption: String,
externalIssuesUrl: Option[String], externalIssuesUrl: Option[String],
enableWiki: Boolean, wikiOption: String,
allowWikiEditing: Boolean, externalWikiUrl: Option[String],
externalWikiUrl: Option[String] allowFork: Boolean
) )
val optionsForm = mapping( val optionsForm = mapping(
"repositoryName" -> trim(label("Repository Name" , text(required, maxlength(40), identifier, renameRepositoryName))), "repositoryName" -> trim(label("Repository Name" , text(required, maxlength(100), identifier, renameRepositoryName))),
"description" -> trim(label("Description" , optional(text()))), "description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type" , boolean())), "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))))), "externalIssuesUrl" -> trim(label("External Issues URL", optional(text(maxlength(200))))),
"enableWiki" -> trim(label("Enable Wiki" , boolean())), "wikiOption" -> trim(label("Wiki Option" , text(required, featureOption))),
"allowWikiEditing" -> trim(label("Allow Wiki Editing" , boolean())), "externalWikiUrl" -> trim(label("External Wiki URL" , optional(text(maxlength(200))))),
"externalWikiUrl" -> trim(label("External Wiki URL" , optional(text(maxlength(200))))) "allowFork" -> trim(label("Allow Forking" , boolean()))
)(OptionsForm.apply) )(OptionsForm.apply)
// for default branch // for default branch
@@ -56,12 +56,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))) "defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100))))
)(DefaultBranchForm.apply) )(DefaultBranchForm.apply)
// for collaborator addition // // for collaborator addition
case class CollaboratorForm(userName: String) // case class CollaboratorForm(userName: String)
//
val collaboratorForm = mapping( // val collaboratorForm = mapping(
"userName" -> trim(label("Username", text(required, collaborator))) // "userName" -> trim(label("Username", text(required, collaborator)))
)(CollaboratorForm.apply) // )(CollaboratorForm.apply)
// for web hook url addition // for web hook url addition
case class WebHookForm(url: String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String]) 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.parentUserName.map { _ =>
repository.repository.isPrivate repository.repository.isPrivate
} getOrElse form.isPrivate, } getOrElse form.isPrivate,
form.enableIssues, form.issuesOption,
form.externalIssuesUrl, form.externalIssuesUrl,
form.enableWiki, form.wikiOption,
form.allowWikiEditing, form.externalWikiUrl,
form.externalWikiUrl form.allowFork
) )
// Change repository name // Change repository name
if(repository.name != form.repositoryName){ if(repository.name != form.repositoryName){
@@ -175,22 +175,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
repository) repository)
}) })
/** post("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
* Add the collaborator. val collaborators = params("collaborators")
*/ removeCollaborators(repository.owner, repository.name)
post("/:owner/:repository/settings/collaborators/add", collaboratorForm)(ownerOnly { (form, repository) => collaborators.split(",").withFilter(_.nonEmpty).map { collaborator =>
if(!getAccountByUserName(repository.owner).get.isGroupAccount){ val userName :: role :: Nil = collaborator.split(":").toList
addCollaborator(repository.owner, repository.name, form.userName) addCollaborator(repository.owner, repository.name, userName, role)
}
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
})
/**
* Add the collaborator.
*/
get("/:owner/:repository/settings/collaborators/remove")(ownerOnly { repository =>
if(!getAccountByUserName(repository.owner).get.isGroupAccount){
removeCollaborator(repository.owner, repository.name, params("name"))
} }
redirect(s"/${repository.owner}/${repository.name}/settings/collaborators") redirect(s"/${repository.owner}/${repository.name}/settings/collaborators")
}) })
@@ -297,7 +287,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
get("/:owner/:repository/settings/hooks/edit")(ownerOnly { repository => get("/:owner/:repository/settings/hooks/edit")(ownerOnly { repository =>
getWebHook(repository.owner, repository.name, params("url")).map{ case (webhook, events) => getWebHook(repository.owner, repository.name, params("url")).map{ case (webhook, events) =>
html.edithooks(webhook, events, repository, flash.get("info"), false) 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. // * Provides Constraint to validate the collaborator name.
*/ // */
private def collaborator: Constraint = new Constraint(){ // private def collaborator: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = // override def validate(name: String, value: String, messages: Messages): Option[String] =
getAccountByUserName(value) match { // getAccountByUserName(value) match {
case None => Some("User does not exist.") // case None => Some("User does not exist.")
case Some(x) if(x.isGroupAccount) //// case Some(x) if(x.isGroupAccount)
=> Some("User does not exist.") //// => Some("User does not exist.")
case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName)) // case Some(x) if(x.userName == params("owner") || getCollaborators(params("owner"), params("repository")).contains(x.userName))
=> Some("User can access this repository already.") // => Some(value + " is repository owner.") // TODO also group members?
case _ => None // case _ => None
} // }
} // }
/** /**
* Duplicate check for the rename repository name. * 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", "ALL").contains(value)) None else Some("Option is invalid.")
}
/** /**
* Provides Constraint to validate the repository transfer user. * Provides Constraint to validate the repository transfer user.
*/ */

View File

@@ -31,7 +31,7 @@ import org.scalatra._
class RepositoryViewerController extends RepositoryViewerControllerBase class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService 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 with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBranchService
/** /**
@@ -39,7 +39,7 @@ class RepositoryViewerController extends RepositoryViewerControllerBase
*/ */
trait RepositoryViewerControllerBase extends ControllerBase { trait RepositoryViewerControllerBase extends ControllerBase {
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService 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 => with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBranchService =>
ArchiveCommand.registerFormat("zip", new ZipFormat) ArchiveCommand.registerFormat("zip", new ZipFormat)
@@ -110,7 +110,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
enableLineBreaks = params("enableLineBreaks").toBoolean, enableLineBreaks = params("enableLineBreaks").toBoolean,
enableTaskList = params("enableTaskList").toBoolean, enableTaskList = params("enableTaskList").toBoolean,
enableAnchor = false, enableAnchor = false,
hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount) hasWritePermission = hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
) )
}) })
@@ -151,13 +151,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository, html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) => logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime) view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}, page, hasNext, hasWritePermission(repository.owner, repository.name, context.loginAccount)) }, page, hasNext, hasDeveloperRole(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 (branch, path) = repository.splitPath(multiParams("splat").head)
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName) 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, html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
@@ -165,7 +165,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
protectedBranch) protectedBranch)
}) })
get("/:owner/:repository/edit/*")(collaboratorsOnly { repository => get("/:owner/:repository/edit/*")(writableUsersOnly { repository =>
val (branch, path) = repository.splitPath(multiParams("splat").head) val (branch, path) = repository.splitPath(multiParams("splat").head)
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName) 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), html.editor(branch, repository, paths.take(paths.size - 1).toList, Some(paths.last),
JGitUtil.getContentInfo(git, path, objectId), JGitUtil.getContentInfo(git, path, objectId),
protectedBranch) 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) val (branch, path) = repository.splitPath(multiParams("splat").head)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch)) val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
@@ -190,11 +190,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val paths = path.split("/") val paths = path.split("/")
html.delete(branch, repository, paths.take(paths.size - 1).toList, paths.last, html.delete(branch, repository, paths.take(paths.size - 1).toList, paths.last,
JGitUtil.getContentInfo(git, path, objectId)) 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( commitFile(
repository = repository, repository = repository,
branch = form.branch, 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( commitFile(
repository = repository, repository = repository,
branch = form.branch, 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), "", "", commitFile(repository, form.branch, form.path, None, Some(form.fileName), "", "",
form.message.getOrElse(s"Delete ${form.fileName}")) form.message.getOrElse(s"Delete ${form.fileName}"))
@@ -250,7 +250,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
loader.copyTo(response.outputStream) loader.copyTo(response.outputStream)
() ()
} }
} getOrElse NotFound } getOrElse NotFound()
} }
}) })
@@ -270,15 +270,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
response.setContentLength(loader.getSize.toInt) response.setContentLength(loader.getSize.toInt)
loader.copyTo(response.outputStream) loader.copyTo(response.outputStream)
() ()
} getOrElse NotFound } getOrElse NotFound()
} else { } else {
html.blob(id, repository, path.split("/").toList, html.blob(id, repository, path.split("/").toList,
JGitUtil.getContentInfo(git, path, objectId), JGitUtil.getContentInfo(git, path, objectId),
new JGitUtil.CommitInfo(JGitUtil.getLastModifiedCommit(git, revCommit, path)), new JGitUtil.CommitInfo(JGitUtil.getLastModifiedCommit(git, revCommit, path)),
hasWritePermission(repository.owner, repository.name, context.loginAccount), hasDeveloperRole(repository.owner, repository.name, context.loginAccount),
request.paths(2) == "blame") request.paths(2) == "blame")
} }
} getOrElse NotFound } getOrElse NotFound()
} }
}) })
@@ -329,12 +329,12 @@ trait RepositoryViewerControllerBase extends ControllerBase {
JGitUtil.getBranchesOfCommit(git, revCommit.getName), JGitUtil.getBranchesOfCommit(git, revCommit.getName),
JGitUtil.getTagsOfCommit(git, revCommit.getName), JGitUtil.getTagsOfCommit(git, revCommit.getName),
getCommitComments(repository.owner, repository.name, id, false), getCommitComments(repository.owner, repository.name, id, false),
repository, diffs, oldCommitId, hasWritePermission(repository.owner, repository.name, context.loginAccount)) repository, diffs, oldCommitId, hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
} }
} }
} }
} catch { } catch {
case e:MissingObjectException => NotFound case e:MissingObjectException => NotFound()
} }
}) })
@@ -358,7 +358,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
html.commentform( html.commentform(
commitId = id, commitId = id,
fileName, oldLineNumber, newLineNumber, issueId, fileName, oldLineNumber, newLineNumber, issueId,
hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount), hasWritePermission = hasDeveloperRole(repository.owner, repository.name, context.loginAccount),
repository = repository repository = repository
) )
}) })
@@ -374,7 +374,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
callPullRequestReviewCommentWebHook("create", comment, repository, issueId, context.baseUrl, context.loginAccount.get) callPullRequestReviewCommentWebHook("create", comment, repository, issueId, context.baseUrl, context.loginAccount.get)
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content) case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
} }
helper.html.commitcomment(comment, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository) helper.html.commitcomment(comment, hasDeveloperRole(repository.owner, repository.name, context.loginAccount), repository)
}) })
ajaxGet("/:owner/:repository/commit_comments/_data/:id")(readableUsersOnly { repository => ajaxGet("/:owner/:repository/commit_comments/_data/:id")(readableUsersOnly { repository =>
@@ -393,12 +393,12 @@ trait RepositoryViewerControllerBase extends ControllerBase {
enableRefsLink = true, enableRefsLink = true,
enableAnchor = true, enableAnchor = true,
enableLineBreaks = true, enableLineBreaks = true,
hasWritePermission = isEditable(x.userName, x.repositoryName, x.commentedUserName) hasWritePermission = true
) )
)) ))
} }
} else Unauthorized } else Unauthorized()
} getOrElse NotFound } getOrElse NotFound()
}) })
ajaxPost("/:owner/:repository/commit_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => 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)){ if(isEditable(owner, name, comment.commentedUserName)){
updateCommitComment(comment.commentId, form.content) updateCommitComment(comment.commentId, form.content)
redirect(s"/${owner}/${name}/commit_comments/_data/${comment.commentId}") redirect(s"/${owner}/${name}/commit_comments/_data/${comment.commentId}")
} else Unauthorized } else Unauthorized()
} getOrElse NotFound } getOrElse NotFound()
} }
}) })
@@ -417,8 +417,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
getCommitComment(owner, name, params("id")).map { comment => getCommitComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){ if(isEditable(owner, name, comment.commentedUserName)){
Ok(deleteCommitComment(comment.commentId)) Ok(deleteCommitComment(comment.commentId))
} else Unauthorized } else Unauthorized()
} getOrElse NotFound } getOrElse NotFound()
} }
}) })
@@ -437,13 +437,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
.map(br => (br, getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId), protectedBranches.contains(br.name))) .map(br => (br, getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId), protectedBranches.contains(br.name)))
.reverse .reverse
html.branches(branches, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository) html.branches(branches, hasDeveloperRole(repository.owner, repository.name, context.loginAccount), repository)
}) })
/** /**
* Creates a branch. * Creates a branch.
*/ */
post("/:owner/:repository/branches")(collaboratorsOnly { repository => post("/:owner/:repository/branches")(writableUsersOnly { repository =>
val newBranchName = params.getOrElse("new", halt(400)) val newBranchName = params.getOrElse("new", halt(400))
val fromBranchName = params.getOrElse("from", halt(400)) val fromBranchName = params.getOrElse("from", halt(400))
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
@@ -461,7 +461,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
/** /**
* Deletes branch. * Deletes branch.
*/ */
get("/:owner/:repository/delete/*")(collaboratorsOnly { repository => get("/:owner/:repository/delete/*")(writableUsersOnly { repository =>
val branchName = multiParams("splat").head val branchName = multiParams("splat").head
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){ if(repository.repository.defaultBranch != branchName){
@@ -489,11 +489,12 @@ trait RepositoryViewerControllerBase extends ControllerBase {
archiveRepository(name, ".zip", repository) archiveRepository(name, ".zip", repository)
case name if name.endsWith(".tar.gz") => case name if name.endsWith(".tar.gz") =>
archiveRepository(name, ".tar.gz", repository) archiveRepository(name, ".tar.gz", repository)
case _ => BadRequest case _ => BadRequest()
} }
}) })
get("/:owner/:repository/network/members")(referrersOnly { repository => get("/:owner/:repository/network/members")(referrersOnly { repository =>
if(repository.repository.options.allowFork) {
html.forked( html.forked(
getRepository( getRepository(
repository.repository.originUserName.getOrElse(repository.owner), repository.repository.originUserName.getOrElse(repository.owner),
@@ -506,6 +507,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
case account: Option[Account] => getGroupsByUserName(account.get.userName) case account: Option[Account] => getGroupsByUserName(account.get.userName)
}, // groups of current user }, // groups of current user
repository) repository)
} else BadRequest()
}) })
/** /**
@@ -516,7 +518,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val ref = multiParams("splat").head val ref = multiParams("splat").head
JGitUtil.getTreeId(git, ref).map{ treeId => JGitUtil.getTreeId(git, ref).map{ treeId =>
html.find(ref, treeId, repository) html.find(ref, treeId, repository)
} getOrElse NotFound } getOrElse NotFound()
} }
}) })
@@ -545,7 +547,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
*/ */
private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = { private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = {
if(repository.commitCount == 0){ if(repository.commitCount == 0){
html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount)) html.guide(repository, hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
} else { } else {
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
// get specified commit // get specified commit
@@ -567,11 +569,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
html.files(revision, repository, html.files(revision, repository,
if(path == ".") Nil else path.split("/").toList, // current path if(path == ".") Nil else path.split("/").toList, // current path
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount), files, readme, hasDeveloperRole(repository.owner, repository.name, context.loginAccount),
getPullRequestFromBranch(repository.owner, repository.name, revstr, repository.repository.defaultBranch), getPullRequestFromBranch(repository.owner, repository.name, revstr, repository.repository.defaultBranch),
flash.get("info"), flash.get("error")) 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 headName = s"refs/heads/${branch}"
val headTip = git.getRepository.resolve(headName) 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)){ if(!newPath.exists(_ == path) && !oldPath.exists(_ == path)){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId)) 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 => 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)))) inserter.insert(Constants.OBJ_BLOB, content.getBytes(charset))))
} }
builder.finish() builder.finish()
@@ -621,8 +627,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
updatePullRequests(repository.owner, repository.name, branch) updatePullRequests(repository.owner, repository.name, branch)
// record activity // record activity
recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch, val commitInfo = new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
List(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 // close issue by commit message
closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name) closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name)
@@ -682,7 +691,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean = private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
override protected def renderUncaughtException(e: Throwable)(implicit request: HttpServletRequest, response: HttpServletResponse): Unit = { override protected def renderUncaughtException(e: Throwable)(implicit request: HttpServletRequest, response: HttpServletResponse): Unit = {
e.printStackTrace() e.printStackTrace()

View File

@@ -233,7 +233,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
updateImage(userName, form.fileId, form.clearImage) updateImage(userName, form.fileId, form.clearImage)
redirect("/admin/users") redirect("/admin/users")
} }
} getOrElse NotFound } getOrElse NotFound()
}) })
get("/admin/users/_newgroup")(adminOnly { get("/admin/users/_newgroup")(adminOnly {
@@ -279,19 +279,19 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
} else { } else {
// Update GROUP_MEMBER // Update GROUP_MEMBER
updateGroupMembers(form.groupName, members) updateGroupMembers(form.groupName, members)
// Update COLLABORATOR for group repositories // // Update COLLABORATOR for group repositories
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName => // getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
removeCollaborators(form.groupName, repositoryName) // removeCollaborators(form.groupName, repositoryName)
members.foreach { case (userName, isManager) => // members.foreach { case (userName, isManager) =>
addCollaborator(form.groupName, repositoryName, userName) // addCollaborator(form.groupName, repositoryName, userName)
} // }
} // }
} }
updateImage(form.groupName, form.fileId, form.clearImage) updateImage(form.groupName, form.fileId, form.clearImage)
redirect("/admin/users") redirect("/admin/users")
} getOrElse NotFound } getOrElse NotFound()
} }
}) })

View File

@@ -14,10 +14,10 @@ import org.scalatra.i18n.Messages
class WikiController extends WikiControllerBase class WikiController extends WikiControllerBase
with WikiService with RepositoryService with AccountService with ActivityService with WikiService with RepositoryService with AccountService with ActivityService
with CollaboratorsAuthenticator with ReferrerAuthenticator with ReadableUsersAuthenticator with ReferrerAuthenticator
trait WikiControllerBase extends ControllerBase { 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) 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 => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match { 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() 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)){ if(isEditable(repository)){
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
val Array(from, to) = params("commitId").split("\\.\\.\\.") val Array(from, to) = params("commitId").split("\\.\\.\\.")
@@ -101,7 +101,7 @@ trait WikiControllerBase extends ControllerBase {
} else Unauthorized() } else Unauthorized()
}) })
get("/:owner/:repository/wiki/_revert/:commitId")(referrersOnly { repository => get("/:owner/:repository/wiki/_revert/:commitId")(readableUsersOnly { repository =>
if(isEditable(repository)){ if(isEditable(repository)){
val Array(from, to) = params("commitId").split("\\.\\.\\.") val Array(from, to) = params("commitId").split("\\.\\.\\.")
@@ -114,14 +114,14 @@ trait WikiControllerBase extends ControllerBase {
} else Unauthorized() } else Unauthorized()
}) })
get("/:owner/:repository/wiki/:page/_edit")(referrersOnly { repository => get("/:owner/:repository/wiki/:page/_edit")(readableUsersOnly { repository =>
if(isEditable(repository)){ if(isEditable(repository)){
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository) html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
} else Unauthorized() } else Unauthorized()
}) })
post("/:owner/:repository/wiki/_edit", editForm)(referrersOnly { (form, repository) => post("/:owner/:repository/wiki/_edit", editForm)(readableUsersOnly { (form, repository) =>
if(isEditable(repository)){ if(isEditable(repository)){
defining(context.loginAccount.get){ loginAccount => defining(context.loginAccount.get){ loginAccount =>
saveWikiPage( saveWikiPage(
@@ -146,13 +146,13 @@ trait WikiControllerBase extends ControllerBase {
} else Unauthorized() } else Unauthorized()
}) })
get("/:owner/:repository/wiki/_new")(referrersOnly { repository => get("/:owner/:repository/wiki/_new")(readableUsersOnly { repository =>
if(isEditable(repository)){ if(isEditable(repository)){
html.edit("", None, repository) html.edit("", None, repository)
} else Unauthorized() } else Unauthorized()
}) })
post("/:owner/:repository/wiki/_new", newForm)(referrersOnly { (form, repository) => post("/:owner/:repository/wiki/_new", newForm)(readableUsersOnly { (form, repository) =>
if(isEditable(repository)){ if(isEditable(repository)){
defining(context.loginAccount.get){ loginAccount => defining(context.loginAccount.get){ loginAccount =>
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName, saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
@@ -170,7 +170,7 @@ trait WikiControllerBase extends ControllerBase {
} else Unauthorized() } else Unauthorized()
}) })
get("/:owner/:repository/wiki/:page/_delete")(referrersOnly { repository => get("/:owner/:repository/wiki/:page/_delete")(readableUsersOnly { repository =>
if(isEditable(repository)){ if(isEditable(repository)){
val pageName = StringUtil.urlDecode(params("page")) val pageName = StringUtil.urlDecode(params("page"))
@@ -190,7 +190,7 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki/_history")(referrersOnly { repository => get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, "master") match { 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() case Left(_) => NotFound()
} }
} }
@@ -201,7 +201,7 @@ trait WikiControllerBase extends ControllerBase {
getFileContent(repository.owner, repository.name, path).map { bytes => getFileContent(repository.owner, repository.name, path).map { bytes =>
RawData(FileUtil.getContentType(path, bytes), bytes) RawData(FileUtil.getContentType(path, bytes), bytes)
} getOrElse NotFound } getOrElse NotFound()
}) })
private def unique: Constraint = new Constraint(){ 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 targetWikiPage = getWikiPage(params("owner"), params("repository"), params("pageName"))
private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean = {
repository.repository.allowWikiEditing || ( repository.repository.options.wikiOption match {
hasWritePermission(repository.owner, repository.name, context.loginAccount) case "ALL" => !repository.repository.isPrivate && context.loginAccount.isDefined
) case "PUBLIC" => hasGuestRole(repository.owner, repository.name, context.loginAccount)
case "PRIVATE" => hasDeveloperRole(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 { class Collaborators(tag: Tag) extends Table[Collaborator](tag, "COLLABORATOR") with BasicTemplate {
val collaboratorName = column[String]("COLLABORATOR_NAME") val collaboratorName = column[String]("COLLABORATOR_NAME")
def * = (userName, repositoryName, collaboratorName) <> (Collaborator.tupled, Collaborator.unapply) val role = column[String]("ROLE")
def * = (userName, repositoryName, collaboratorName, role) <> (Collaborator.tupled, Collaborator.unapply)
def byPrimaryKey(owner: String, repository: String, collaborator: String) = def byPrimaryKey(owner: String, repository: String, collaborator: String) =
byRepository(owner, repository) && (collaboratorName === collaborator.bind) byRepository(owner, repository) && (collaboratorName === collaborator.bind)
@@ -17,5 +18,23 @@ trait CollaboratorComponent extends TemplateComponent { self: Profile =>
case class Collaborator( case class Collaborator(
userName: String, userName: String,
repositoryName: String, repositoryName: String,
collaboratorName: String collaboratorName: String,
role: String
) )
sealed abstract class Role(val name: String)
object Role {
object ADMIN extends Role("ADMIN")
object DEVELOPER extends Role("DEVELOPER")
object GUEST extends Role("GUEST")
// 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

@@ -17,14 +17,51 @@ trait RepositoryComponent extends TemplateComponent { self: Profile =>
val originRepositoryName = column[String]("ORIGIN_REPOSITORY_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 parentRepositoryName = column[String]("PARENT_REPOSITORY_NAME")
val enableIssues = column[Boolean]("ENABLE_ISSUES") val issuesOption = column[String]("ISSUES_OPTION")
val externalIssuesUrl = column[String]("EXTERNAL_ISSUES_URL") val externalIssuesUrl = column[String]("EXTERNAL_ISSUES_URL")
val enableWiki = column[Boolean]("ENABLE_WIKI") val wikiOption = column[String]("WIKI_OPTION")
val allowWikiEditing = column[Boolean]("ALLOW_WIKI_EDITING")
val externalWikiUrl = column[String]("EXTERNAL_WIKI_URL") val externalWikiUrl = column[String]("EXTERNAL_WIKI_URL")
def * = (userName, repositoryName, isPrivate, description.?, defaultBranch, val allowFork = column[Boolean]("ALLOW_FORK")
registeredDate, updatedDate, lastActivityDate, originUserName.?, originRepositoryName.?, parentUserName.?, parentRepositoryName.?,
enableIssues, externalIssuesUrl.?, enableWiki, allowWikiEditing, externalWikiUrl.?) <> (Repository.tupled, Repository.unapply) 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) def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
} }
@@ -43,9 +80,13 @@ case class Repository(
originRepositoryName: Option[String], originRepositoryName: Option[String],
parentUserName: Option[String], parentUserName: Option[String],
parentRepositoryName: Option[String], parentRepositoryName: Option[String],
enableIssues: Boolean, options: RepositoryOptions
externalIssuesUrl: Option[String], )
enableWiki: Boolean,
allowWikiEditing: Boolean, case class RepositoryOptions(
externalWikiUrl: Option[String] 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 = { def removeUserRelatedData(userName: String)(implicit s: Session): Unit = {
GroupMembers.filter(_.userName === userName.bind).delete GroupMembers.filter(_.userName === userName.bind).delete
Collaborators.filter(_.collaboratorName === userName.bind).delete Collaborators.filter(_.collaboratorName === userName.bind).delete
Repositories.filter(_.userName === userName.bind).delete
} }
def getGroupNames(userName: String)(implicit s: Session): List[String] = { def getGroupNames(userName: String)(implicit s: Session): List[String] = {

View File

@@ -13,7 +13,7 @@ trait HandleCommentService {
with WebHookService with WebHookIssueCommentService with WebHookPullRequestService => 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]) def handleComment(issue: Issue, content: Option[String], repository: RepositoryService.RepositoryInfo, actionOpt: Option[String])
(implicit context: Context, s: Session) = { (implicit context: Context, s: Session) = {
@@ -54,19 +54,21 @@ trait HandleCommentService {
// call web hooks // call web hooks
action match { action match {
case None => commentId.map{ commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, context.loginAccount.get) } case None => commentId.map { commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, context.loginAccount.get) }
case Some(act) => val webHookAction = act match { case Some(act) => {
val webHookAction = act match {
case "open" => "opened" case "open" => "opened"
case "reopen" => "reopened" case "reopen" => "reopened"
case "close" => "closed" case "close" => "closed"
case _ => act case _ => act
} }
if(issue.isPullRequest){ if (issue.isPullRequest) {
callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, context.loginAccount.get) callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, context.loginAccount.get)
} else { } else {
callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, context.loginAccount.get) callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, context.loginAccount.get)
} }
} }
}
// notifications // notifications
Notifier() match { Notifier() match {

View File

@@ -14,7 +14,7 @@ import Q.interpolation
trait IssuesService { trait IssuesService {
self: AccountService => self: AccountService with RepositoryService =>
import IssuesService._ import IssuesService._
def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) = def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) =
@@ -163,18 +163,14 @@ trait IssuesService {
(implicit s: Session): List[IssueInfo] = { (implicit s: Session): List[IssueInfo] = {
// get issues and comment count and labels // get issues and comment count and labels
val result = searchIssueQueryBase(condition, pullRequest, offset, limit, repos) val result = searchIssueQueryBase(condition, pullRequest, offset, limit, repos)
.leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } .leftJoin (IssueLabels) .on { case (((t1, t2), i), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
.leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) } .leftJoin (Labels) .on { case ((((t1, t2), i), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
.leftJoin (Milestones) .on { case ((((t1, t2), t3), t4), t5) => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) } .leftJoin (Milestones) .on { case (((((t1, t2), i), t3), t4), t5) => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) }
.map { case ((((t1, t2), t3), t4), t5) => .sortBy { case (((((t1, t2), i), t3), t4), t5) => i asc }
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?, t5.title.?) .map { case (((((t1, t2), i), t3), t4), t5) => (t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?, t5.title.?) }
}
.list .list
.splitWith { (c1, c2) => .splitWith { (c1, c2) => c1._1.userName == c2._1.userName && c1._1.repositoryName == c2._1.repositoryName && c1._1.issueId == c2._1.issueId }
c1._1.userName == c2._1.userName &&
c1._1.repositoryName == c2._1.repositoryName &&
c1._1.issueId == c2._1.issueId
}
val status = getCommitStatues(result.map(_.head._1).map(is => (is.userName, is.repositoryName, is.issueId))) val status = getCommitStatues(result.map(_.head._1).map(is => (is.userName, is.repositoryName, is.issueId)))
result.map { issues => issues.head match { result.map { issues => issues.head match {
@@ -196,13 +192,12 @@ trait IssuesService {
(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 // get issues and comment count and labels
searchIssueQueryBase(condition, true, offset, limit, repos) searchIssueQueryBase(condition, true, offset, limit, repos)
.innerJoin(PullRequests).on { case ((t1, t2), t3) => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) } .innerJoin(PullRequests).on { case (((t1, t2), i), t3) => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) }
.innerJoin(Repositories).on { case (((t1, t2), t3), t4) => t4.byRepository(t1.userName, t1.repositoryName) } .innerJoin(Repositories).on { case ((((t1, t2), i), t3), t4) => t4.byRepository(t1.userName, t1.repositoryName) }
.innerJoin(Accounts).on { case ((((t1, t2), t3), t4), t5) => t5.userName === t1.openedUserName } .innerJoin(Accounts).on { case (((((t1, t2), i), t3), t4), t5) => t5.userName === t1.openedUserName }
.innerJoin(Accounts).on { case (((((t1, t2), t3), t4), t5), t6) => t6.userName === t4.userName } .innerJoin(Accounts).on { case ((((((t1, t2), i), t3), t4), t5), t6) => t6.userName === t4.userName }
.map { case (((((t1, t2), t3), t4), t5), t6) => .sortBy { case ((((((t1, t2), i), t3), t4), t5), t6) => i asc }
(t1, t5, t2.commentCount, t3, t4, t6) .map { case ((((((t1, t2), i), t3), t4), t5), t6) => (t1, t5, t2.commentCount, t3, t4, t6) }
}
.list .list
} }
@@ -210,6 +205,7 @@ trait IssuesService {
(implicit s: Session) = (implicit s: Session) =
searchIssueQuery(repos, condition, pullRequest) searchIssueQuery(repos, condition, pullRequest)
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
.sortBy { case (t1, t2) => t1.issueId desc }
.sortBy { case (t1, t2) => .sortBy { case (t1, t2) =>
(condition.sort match { (condition.sort match {
case "created" => t1.registeredDate case "created" => t1.registeredDate
@@ -222,7 +218,7 @@ trait IssuesService {
} }
} }
} }
.drop(offset).take(limit) .drop(offset).take(limit).zipWithIndex
/** /**
@@ -437,6 +433,11 @@ trait IssuesService {
} }
} }
def getAssignableUserNames(owner: String, repository: String)(implicit s: Session): List[String] = {
(getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER)) :::
(if (getAccountByUserName(owner).get.isGroupAccount) getGroupMembers(owner).map(_.userName) else List(owner))).distinct.sorted
}
} }
object IssuesService { object IssuesService {

View File

@@ -21,12 +21,12 @@ trait RepositoryCreationService {
// Insert to the database at first // Insert to the database at first
insertRepository(name, owner, description, isPrivate) insertRepository(name, owner, description, isPrivate)
// Add collaborators for group repository // // Add collaborators for group repository
if(ownerAccount.isGroupAccount){ // if(ownerAccount.isGroupAccount){
getGroupMembers(owner).foreach { member => // getGroupMembers(owner).foreach { member =>
addCollaborator(owner, name, member.userName) // addCollaborator(owner, name, member.userName)
} // }
} // }
// Insert default labels // Insert default labels
insertDefaultLabels(owner, name) insertDefaultLabels(owner, name)

View File

@@ -1,7 +1,7 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.controller.Context import gitbucket.core.controller.Context
import gitbucket.core.model.{Collaborator, Repository, Account} import gitbucket.core.model.{Collaborator, Repository, RepositoryOptions, Account, Role}
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile._
import gitbucket.core.util.JGitUtil import gitbucket.core.util.JGitUtil
import profile.simple._ import profile.simple._
@@ -37,11 +37,13 @@ trait RepositoryService { self: AccountService =>
originRepositoryName = originRepositoryName, originRepositoryName = originRepositoryName,
parentUserName = parentUserName, parentUserName = parentUserName,
parentRepositoryName = parentRepositoryName, parentRepositoryName = parentRepositoryName,
enableIssues = true, options = RepositoryOptions(
issuesOption = "PUBLIC", // TODO DISABLE for the forked repository?
externalIssuesUrl = None, externalIssuesUrl = None,
enableWiki = true, wikiOption = "PUBLIC", // TODO DISABLE for the forked repository?
allowWikiEditing = true, externalWikiUrl = None,
externalWikiUrl = None allowFork = true
)
) )
IssueId insert (userName, repositoryName, 0) IssueId insert (userName, repositoryName, 0)
@@ -121,11 +123,8 @@ trait RepositoryService { self: AccountService =>
repositoryName = newRepositoryName repositoryName = newRepositoryName
)) :_*) )) :_*)
if(account.isGroupAccount){ // TODO Drop transfered owner from collaborators?
Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*)
} else {
Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
}
// Update activity messages // Update activity messages
Activities.filter { t => Activities.filter { t =>
@@ -320,11 +319,12 @@ trait RepositoryService { self: AccountService =>
*/ */
def saveRepositoryOptions(userName: String, repositoryName: String, def saveRepositoryOptions(userName: String, repositoryName: String,
description: Option[String], isPrivate: Boolean, description: Option[String], isPrivate: Boolean,
enableIssues: Boolean, externalIssuesUrl: Option[String], issuesOption: String, externalIssuesUrl: Option[String],
enableWiki: Boolean, allowWikiEditing: Boolean, externalWikiUrl: Option[String])(implicit s: Session): Unit = wikiOption: String, externalWikiUrl: Option[String],
allowFork: Boolean)(implicit s: Session): Unit =
Repositories.filter(_.byRepository(userName, repositoryName)) Repositories.filter(_.byRepository(userName, repositoryName))
.map { r => (r.description.?, r.isPrivate, r.enableIssues, r.externalIssuesUrl.?, r.enableWiki, r.allowWikiEditing, r.externalWikiUrl.?, r.updatedDate) } .map { r => (r.description.?, r.isPrivate, r.issuesOption, r.externalIssuesUrl.?, r.wikiOption, r.externalWikiUrl.?, r.allowFork, r.updatedDate) }
.update (description, isPrivate, enableIssues, externalIssuesUrl, enableWiki, allowWikiEditing, externalWikiUrl, currentDate) .update (description, isPrivate, issuesOption, externalIssuesUrl, wikiOption, externalWikiUrl, allowFork, currentDate)
def saveRepositoryDefaultBranch(userName: String, repositoryName: String, def saveRepositoryDefaultBranch(userName: String, repositoryName: String,
defaultBranch: String)(implicit s: Session): Unit = defaultBranch: String)(implicit s: Session): Unit =
@@ -333,49 +333,64 @@ trait RepositoryService { self: AccountService =>
.update (defaultBranch) .update (defaultBranch)
/** /**
* Add collaborator to the repository. * Add collaborator (user or group) to the repository.
*
* @param userName the user name of the repository owner
* @param repositoryName the repository name
* @param collaboratorName the collaborator name
*/ */
def addCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit = def addCollaborator(userName: String, repositoryName: String, collaboratorName: String, role: String)(implicit s: Session): Unit =
Collaborators insert Collaborator(userName, repositoryName, collaboratorName) Collaborators insert Collaborator(userName, repositoryName, collaboratorName, role)
/**
* Remove collaborator from the repository.
*
* @param userName the user name of the repository owner
* @param repositoryName the repository name
* @param collaboratorName the collaborator name
*/
def removeCollaborator(userName: String, repositoryName: String, collaboratorName: String)(implicit s: Session): Unit =
Collaborators.filter(_.byPrimaryKey(userName, repositoryName, collaboratorName)).delete
/** /**
* Remove all collaborators from the repository. * 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 = def removeCollaborators(userName: String, repositoryName: String)(implicit s: Session): Unit =
Collaborators.filter(_.byRepository(userName, repositoryName)).delete Collaborators.filter(_.byRepository(userName, repositoryName)).delete
/** /**
* Returns the list of collaborators name which is sorted with ascending order. * Returns the list of collaborators name (user name or group name) which is sorted with ascending order.
*
* @param userName the user name of the repository owner
* @param repositoryName the repository name
* @return the list of collaborators name
*/ */
def getCollaborators(userName: String, repositoryName: String)(implicit s: Session): List[String] = def getCollaborators(userName: String, repositoryName: String)(implicit s: Session): List[(Collaborator, Boolean)] =
Collaborators.filter(_.byRepository(userName, repositoryName)).sortBy(_.collaboratorName).map(_.collaboratorName).list Collaborators
.innerJoin(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
def hasWritePermission(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = { /**
* 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[Role] = Nil)(implicit s: Session): List[String] = {
val q1 = Collaborators
.innerJoin(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.role) }
val q2 = Collaborators
.innerJoin(Accounts).on { case (t1, t2) => (t1.collaboratorName === t2.userName) && (t2.groupAccount === true.bind) }
.innerJoin(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.role) }
q1.union(q2).list.filter { x => filter.isEmpty || filter.exists(_.name == x._2) }.map(_._1)
}
def hasDeveloperRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match { loginAccount match {
case Some(a) if(a.isAdmin) => true case Some(a) if(a.isAdmin) => true
case Some(a) if(a.userName == owner) => 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(Role.ADMIN, Role.DEVELOPER)).contains(a.userName)) => true
case _ => false
}
}
def hasGuestRole(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(Role.ADMIN, Role.DEVELOPER, Role.GUEST)).contains(a.userName)) => true
case _ => false case _ => false
} }
} }

View File

@@ -11,7 +11,7 @@ import Implicits.request2Session
* It may be called many times in one request, so each method stores * It may be called many times in one request, so each method stores
* its result into the cache which available during a request. * 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 = private implicit def context2Session(implicit context: Context): Session =
request2Session(context.request) request2Session(context.request)

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

@@ -84,7 +84,7 @@ class GitAuthenticationFilter extends Filter with RepositoryService with Account
Array(username, password) = AuthUtil.decodeAuthHeader(auth).split(":", 2) Array(username, password) = AuthUtil.decodeAuthHeader(auth).split(":", 2)
account <- authenticate(settings, username, password) account <- authenticate(settings, username, password)
} yield if(isUpdating || repository.repository.isPrivate){ } yield if(isUpdating || repository.repository.isPrivate){
if(hasWritePermission(repository.owner, repository.name, Some(account))){ if(hasDeveloperRole(repository.owner, repository.name, Some(account))){
request.setAttribute(Keys.Request.UserName, account.userName) request.setAttribute(Keys.Request.UserName, account.userName)
true true
} else false } else false

View File

@@ -36,7 +36,7 @@ abstract class GitCommand extends Command with SessionAware {
override def run(): Unit = { override def run(): Unit = {
authUser match { authUser match {
case Some(authUser) => case Some(authUser) =>
Database() withSession { implicit session => Database() withTransaction { implicit session =>
try { try {
runTask(authUser) runTask(authUser)
callback.onExit(0) callback.onExit(0)
@@ -92,7 +92,7 @@ abstract class DefaultGitCommand(val owner: String, val repoName: String) extend
protected def isWritableUser(username: String, repositoryInfo: RepositoryService.RepositoryInfo) protected def isWritableUser(username: String, repositoryInfo: RepositoryService.RepositoryInfo)
(implicit session: Session): Boolean = (implicit session: Session): Boolean =
getAccountByUserName(username) match { getAccountByUserName(username) match {
case Some(account) => hasWritePermission(repositoryInfo.owner, repositoryInfo.name, Some(account)) case Some(account) => hasDeveloperRole(repositoryInfo.owner, repositoryInfo.name, Some(account))
case None => false case None => false
} }

View File

@@ -1,7 +1,8 @@
package gitbucket.core.util package gitbucket.core.util
import gitbucket.core.controller.ControllerBase import gitbucket.core.controller.ControllerBase
import gitbucket.core.service.{RepositoryService, AccountService} import gitbucket.core.service.{AccountService, RepositoryService}
import gitbucket.core.model.Role
import RepositoryService.RepositoryInfo import RepositoryService.RepositoryInfo
import Implicits._ import Implicits._
import ControlUtil._ import ControlUtil._
@@ -40,9 +41,9 @@ trait OwnerAuthenticator { self: ControllerBase with RepositoryService with Acco
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(repository.owner == x.userName) => action(repository) case Some(x) if(repository.owner == x.userName) => action(repository)
case Some(x) if(getGroupMembers(repository.owner).exists { member => // TODO Repository management is allowed for only group managers?
member.userName == x.userName && member.isManager == true case Some(x) if(getGroupMembers(repository.owner).exists { m => m.userName == x.userName && m.isManager == true }) => action(repository)
}) => action(repository) case Some(x) if(getCollaboratorUserNames(paths(0), paths(1), Seq(Role.ADMIN)).contains(x.userName)) => action(repository)
case _ => Unauthorized() case _ => Unauthorized()
} }
} getOrElse NotFound() } getOrElse NotFound()
@@ -86,32 +87,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 => trait ReferrerAuthenticator { self: ControllerBase with RepositoryService with AccountService =>
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 =>
protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) } protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def referrersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } protected def referrersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
@@ -125,7 +103,8 @@ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
context.loginAccount match { context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(paths(0) == x.userName) => 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() case _ => Unauthorized()
} }
} }
@@ -136,9 +115,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(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def readableUsersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) } protected def readableUsersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
@@ -150,7 +129,32 @@ trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService =
case Some(x) if(x.isAdmin) => action(repository) case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(!repository.repository.isPrivate) => 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(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(Role.ADMIN, Role.DEVELOPER)).contains(x.userName)) => action(repository)
case _ => Unauthorized() case _ => Unauthorized()
} }
} getOrElse NotFound() } getOrElse NotFound()

View File

@@ -9,7 +9,10 @@ import scala.collection.mutable.ListBuffer
/** /**
* Provides implicit class which extends java.sql.Connection. * Provides implicit class which extends java.sql.Connection.
* This is used in automatic migration in [[servlet.AutoUpdateListener]]. * This is used in following points:
*
* - Automatic migration in [[gitbucket.core.servlet.InitializeListener]]
* - Data importing / exporting in [[gitbucket.core.controller.SystemSettingsController]] and [[gitbucket.core.controller.FileUploadController]]
*/ */
object JDBCUtil { object JDBCUtil {
@@ -71,8 +74,6 @@ object JDBCUtil {
val bytes = new scala.Array[Byte](1024 * 8) val bytes = new scala.Array[Byte](1024 * 8)
var stringLiteral = false var stringLiteral = false
var count = 0
while({ length = in.read(bytes); length != -1 }){ while({ length = in.read(bytes); length != -1 }){
for(i <- 0 to length - 1){ for(i <- 0 to length - 1){
val c = bytes(i) val c = bytes(i)
@@ -81,13 +82,19 @@ object JDBCUtil {
} }
if(c == ';' && !stringLiteral){ if(c == ';' && !stringLiteral){
val sql = new String(out.toByteArray, "UTF-8") val sql = new String(out.toByteArray, "UTF-8")
conn.update(sql) conn.update(sql.trim)
out = new ByteArrayOutputStream() out = new ByteArrayOutputStream()
} else { } else {
out.write(c) out.write(c)
} }
} }
} }
val remain = out.toByteArray
if(remain.length != 0){
val sql = new String(remain, "UTF-8")
conn.update(sql.trim)
}
} }
conn.commit() conn.commit()

View File

@@ -830,14 +830,16 @@ object JGitUtil {
existIds.toSeq 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 RevWalk(git.getRepository)){ revWalk =>
using(new TreeWalk(git.getRepository)){ treeWalk => using(new TreeWalk(git.getRepository)){ treeWalk =>
val index = treeWalk.addTree(revWalk.parseTree(id)) val index = treeWalk.addTree(revWalk.parseTree(id))
treeWalk.setRecursive(true) treeWalk.setRecursive(true)
val result = new collection.mutable.ListBuffer[T]()
while(treeWalk.next){ while(treeWalk.next){
f(treeWalk.getPathString, treeWalk.getTree(index, classOf[CanonicalTreeParser])) result += f(treeWalk.getPathString, treeWalk.getTree(index, classOf[CanonicalTreeParser]))
} }
result.toSeq
} }
} }
} }

View File

@@ -22,8 +22,10 @@ trait Notifier extends RepositoryService with AccountService with IssuesService
( (
// individual repository's owner // individual repository's owner
issue.userName :: issue.userName ::
// group members of group repository
getGroupMembers(issue.userName).map(_.userName) :::
// collaborators // collaborators
getCollaborators(issue.userName, issue.repositoryName) ::: getCollaboratorUserNames(issue.userName, issue.repositoryName) :::
// participants // participants
issue.openedUserName :: issue.openedUserName ::
getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName) 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 *@param message the message which may contains issue id
* @return the iterator of issue id * @return the iterator of issue id
*/ */
def extractIssueId(message: String): Iterator[String] = def extractIssueId(message: String): Seq[String] =
"(^|\\W)#(\\d+)(\\W|$)".r.findAllIn(message).matchData.map(_.group(2)) "(^|\\W)#(\\d+)(\\W|$)".r
.findAllIn(message).matchData.map(_.group(2)).toSeq.distinct
/** /**
* Extract close issue id like ```close #issueId ``` from the given message. * Extract close issue id like ```close #issueId ``` from the given message.
@@ -95,7 +96,8 @@ object StringUtil {
* @param message the message which may contains close command * @param message the message which may contains close command
* @return the iterator of issue id * @return the iterator of issue id
*/ */
def extractCloseId(message: String): Iterator[String] = 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)) "(?i)(?<!\\w)(?:fix(?:e[sd])?|resolve[sd]?|close[sd]?)\\s+#(\\d+)(?!\\w)".r
.findAllIn(message).matchData.map(_.group(1)).toSeq.distinct
} }

View File

@@ -31,7 +31,7 @@
<label class="strong">Members</label> <label class="strong">Members</label>
<ul id="member-list" class="collaborator"> <ul id="member-list" class="collaborator">
</ul> </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="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(",")"/> <input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/>
<div> <div>
@@ -80,10 +80,9 @@ $(function(){
} }
// check existence // check existence
$.post('@context.path/_user/existence', { $.post('@context.path/_user/existence', { 'userName': userName },
'userName': userName function(data, status){
}, function(data, status){ if(data == 'user'){
if(data == 'true'){
addMemberHTML(userName, false); addMemberHTML(userName, false);
} else { } else {
$('#error-members').text('User does not exist.'); $('#error-members').text('User does not exist.');

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 @import gitbucket.core.view.helpers
@gitbucket.core.account.html.main(account, Nil, "members", isGroupManager){ @gitbucket.core.account.html.main(account, Nil, "members", isGroupManager){
@if(members.isEmpty){ @if(members.isEmpty){
No members No members
} else { } else {
@members.map { userName => @members.map { member =>
<div class="block"> <div class="block">
<div class="block-header"> <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>
</div> </div>
} }

View File

@@ -34,7 +34,7 @@
<label class="strong">Members</label> <label class="strong">Members</label>
<ul id="member-list" class="collaborator"> <ul id="member-list" class="collaborator">
</ul> </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="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(",")"/> <input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/>
<div> <div>
@@ -75,11 +75,9 @@ $(function(){
} }
// check existence // check existence
$.post('@context.path/_user/existence', { $.post('@context.path/_user/existence', { 'userName': userName },
'userName': userName, function(data, status){
'userOnly': true if(data == 'user'){
}, function(data, status){
if(data == 'true'){
addMemberHTML(userName, false); addMemberHTML(userName, false);
} else { } else {
$('#error-members').text('User does not exist.'); $('#error-members').text('User does not exist.');

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;"> <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;"/> <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> </span>
<script> <script>
$(function(){ $(function(){
$('#@id').typeahead({ $('#@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) { 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) { function (data) {
return process(data.options); 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" 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><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 @body
@if(hasWritePermission) { @if(hasWritePermission) {
<li id="create-branch" style="display: none;"> <li id="create-branch" style="display: none;">

View File

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

View File

@@ -1,7 +1,8 @@
@(value : String = "", @(value : String = "",
prefix: String = "", prefix: String = "",
style : String = "", style : String = "",
right : Boolean = false)(body: Html) right : Boolean = false,
filter: String = "")(body: Html)
<div class="btn-group" @if(style.nonEmpty){style="@style"}> <div class="btn-group" @if(style.nonEmpty){style="@style"}>
<button <button
class="dropdown-toggle btn btn-default btn-sm" data-toggle="dropdown"> class="dropdown-toggle btn btn-default btn-sm" data-toggle="dropdown">
@@ -16,6 +17,28 @@
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu@if(right){ pull-right}"> <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 @body
</ul> </ul>
</div> </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, @(issue: gitbucket.core.model.Issue,
reopenable: Boolean, reopenable: Boolean,
hasWritePermission: Boolean, isEditable: Boolean,
isManageable: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers @import gitbucket.core.view.helpers
@if(context.loginAccount.isDefined){ @if(isEditable){
<hr/><br/> <hr/><br/>
<form method="POST" validate="true"> <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 panel-default issue-comment-box">
<div class="panel-body"> <div class="panel-body">
@gitbucket.core.helper.html.preview( @gitbucket.core.helper.html.preview(
@@ -16,7 +16,7 @@
enableRefsLink = true, enableRefsLink = true,
enableLineBreaks = true, enableLineBreaks = true,
enableTaskList = true, enableTaskList = true,
hasWritePermission = hasWritePermission, hasWritePermission = isEditable,
completionContext = "issues", completionContext = "issues",
style = "", style = "",
elastic = true, elastic = true,
@@ -24,7 +24,7 @@
) )
<div class="text-right"> <div class="text-right">
<input type="hidden" name="issueId" value="@issue.issueId"/> <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-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"/> <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], @(issue: Option[gitbucket.core.model.Issue],
comments: List[gitbucket.core.model.Comment], comments: List[gitbucket.core.model.Comment],
hasWritePermission: Boolean, isManageable: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo, repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
pullreq: Option[gitbucket.core.model.PullRequest] = None)(implicit context: gitbucket.core.controller.Context) pullreq: Option[gitbucket.core.model.PullRequest] = None)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers @import gitbucket.core.view.helpers
@import gitbucket.core.model.CommitComment @import gitbucket.core.model.CommitComment
@if(issue.isDefined){ @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 panel-default issue-comment-box">
<div class="panel-heading"> <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"> <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> <a href="#" data-issue-id="@issue.get.issueId"><i class="octicon octicon-pencil" aria-label="Edit"></i></a>
} }
</span> </span>
@@ -24,7 +25,7 @@
enableRefsLink = true, enableRefsLink = true,
enableLineBreaks = true, enableLineBreaks = true,
enableTaskList = true, enableTaskList = true,
hasWritePermission = hasWritePermission hasWritePermission = isManageable
) )
</div> </div>
</div> </div>
@@ -35,9 +36,9 @@
case comment: gitbucket.core.model.IssueComment => { case comment: gitbucket.core.model.IssueComment => {
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch" @if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"
&& comment.action != "commit" && comment.action != "refer"){ && 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 panel-default issue-comment-box" id="comment-@comment.commentId">
<div class="panel-heading"> <div class="panel-heading">
@helpers.avatar(comment.commentedUserName, 20)
@helpers.user(comment.commentedUserName, styleClass="username strong") @helpers.user(comment.commentedUserName, styleClass="username strong")
<span class="muted"> <span class="muted">
@if(comment.action == "comment"){ @if(comment.action == "comment"){
@@ -48,7 +49,7 @@
@gitbucket.core.helper.html.datetimeago(comment.registeredDate) @gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</span> </span>
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" @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"> <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-pencil" aria-label="Edit"></i></a>&nbsp;
<a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-x" aria-label="Remove"></i></a> <a href="#" data-comment-id="@comment.commentId"><i class="octicon octicon-x" aria-label="Remove"></i></a>
@@ -63,7 +64,7 @@
enableRefsLink = true, enableRefsLink = true,
enableLineBreaks = true, enableLineBreaks = true,
enableTaskList = true, enableTaskList = true,
hasWritePermission = hasWritePermission hasWritePermission = isManageable
) )
</div> </div>
</div> </div>
@@ -166,7 +167,7 @@
} }
} }
case comment: CommitComment => { 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> <script>

View File

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

View File

@@ -4,17 +4,20 @@
collaborators: List[String], collaborators: List[String],
milestones: List[(gitbucket.core.model.Milestone, Int, Int)], milestones: List[(gitbucket.core.model.Milestone, Int, Int)],
labels: List[gitbucket.core.model.Label], labels: List[gitbucket.core.model.Label],
hasWritePermission: Boolean, isEditable: Boolean,
isManageable: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers @import gitbucket.core.view.helpers
@gitbucket.core.html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){ @gitbucket.core.html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
@gitbucket.core.html.menu("issues", repository){ @gitbucket.core.html.menu("issues", repository){
<div> <div>
<div class="show-title pull-right"> <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-default" href="#" id="edit">Edit</a>
} }
@if(isEditable){
<a class="btn btn-success" href="@helpers.url(repository)/issues/new">New issue</a> <a class="btn btn-success" href="@helpers.url(repository)/issues/new">New issue</a>
}
</div> </div>
<div class="edit-title pull-right" style="display: none;"> <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> <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> <hr>
<div style="margin-top: 15px;"> <div style="margin-top: 15px;">
<div class="col-md-9"> <div class="col-md-9">
@gitbucket.core.issues.html.commentlist(Some(issue), comments, hasWritePermission, repository) @gitbucket.core.issues.html.commentlist(Some(issue), comments, isManageable, repository)
@gitbucket.core.issues.html.commentform(issue, true, hasWritePermission, repository) @gitbucket.core.issues.html.commentform(issue, true, isEditable, isManageable, repository)
</div> </div>
<div class="col-md-3"> <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>
</div> </div>
} }

View File

@@ -4,14 +4,14 @@
collaborators: List[String], collaborators: List[String],
milestones: List[(gitbucket.core.model.Milestone, Int, Int)], milestones: List[(gitbucket.core.model.Milestone, Int, Int)],
labels: List[gitbucket.core.model.Label], labels: List[gitbucket.core.model.Label],
hasWritePermission: Boolean, isManageable: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers @import gitbucket.core.view.helpers
<div style="margin-bottom: 14px;"> <div style="margin-bottom: 14px;">
<span class="muted small strong">Labels</span> <span class="muted small strong">Labels</span>
@if(hasWritePermission){ @if(isManageable){
<div class="pull-right"> <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 => @labels.map { label =>
<li> <li>
<a href="#" class="toggle-label" data-label-id="@label.labelId"> <a href="#" class="toggle-label" data-label-id="@label.labelId">
@@ -34,9 +34,9 @@
<hr/> <hr/>
<div style="margin-bottom: 14px;"> <div style="margin-bottom: 14px;">
<span class="muted small strong">Milestone</span> <span class="muted small strong">Milestone</span>
@if(hasWritePermission){ @if(isManageable){
<div class="pull-right"> <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> <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, _, _) => @milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
<li> <li>
@@ -74,7 +74,7 @@
<span id="label-milestone"> <span id="label-milestone">
@issue.flatMap(_.milestoneId).map { milestoneId => @issue.flatMap(_.milestoneId).map { milestoneId =>
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) => @milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
<span class="strong small">@milestone.title</span> <a class="strong small username" href="@helpers.url(repository)/issues?milestone=@helpers.urlEncode(milestone.title)&state=open">@milestone.title</a>
} }
}.getOrElse { }.getOrElse {
<span class="muted small">No milestone</span> <span class="muted small">No milestone</span>
@@ -86,9 +86,9 @@
<hr/> <hr/>
<div style="margin-bottom: 14px;"> <div style="margin-bottom: 14px;">
<span class="muted small strong">Assignee</span> <span class="muted small strong">Assignee</span>
@if(hasWritePermission){ @if(isManageable){
<div class="pull-right"> <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> <li><a href="javascript:void(0);" class="assign" data-name=""><i class="octicon octicon-x"></i> Clear assignee</a></li>
@collaborators.map { collaborator => @collaborators.map { collaborator =>
<li> <li>
@@ -210,7 +210,8 @@ $(function(){
$('#label-milestone').html($('<span class="muted small">').text('No milestone')); $('#label-milestone').html($('<span class="muted small">').text('No milestone'));
$('#milestone-progress-area').empty(); $('#milestone-progress-area').empty();
} else { } else {
$('#label-milestone').html($('<span class="strong small">').text(title)); $('#label-milestone').html($('<a class="strong small username">').text(title)
.attr('href', '@helpers.url(repository)/issues?milestone=' + encodeURIComponent(title) + '&state=open'));
if(progress){ if(progress){
$('#milestone-progress-area').html(progress); $('#milestone-progress-area').html(progress);
} }

View File

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

View File

@@ -8,7 +8,7 @@
milestones: List[gitbucket.core.model.Milestone] = Nil, milestones: List[gitbucket.core.model.Milestone] = Nil,
labels: List[gitbucket.core.model.Label] = Nil, labels: List[gitbucket.core.model.Label] = Nil,
repository: Option[gitbucket.core.service.RepositoryService.RepositoryInfo] = None, 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.view.helpers
@import gitbucket.core.service.IssuesService.IssueInfo @import gitbucket.core.service.IssuesService.IssueInfo
@* @*
@@ -110,7 +110,7 @@
</li> </li>
} }
</span> </span>
@if(hasWritePermission){ @if(isManageable){
<span id="table-issues-batchedit"> <span id="table-issues-batchedit">
@gitbucket.core.helper.html.dropdown("Mark as") { @gitbucket.core.helper.html.dropdown("Mark as") {
<li><a href="javascript:void(0);" class="toggle-state" data-id="open">Open</a></li> <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) => @issues.map { case IssueInfo(issue, labels, milestone, commentCount, commitStatus) =>
<tr> <tr>
<td style="padding-top: 12px; padding-bottom: 12px;"> <td style="padding-top: 12px; padding-bottom: 12px;">
@if(hasWritePermission){ @if(isManageable){
<input type="checkbox" value="@issue.issueId"/> <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> <script src="@helpers.assets/vendors/AdminLTE-2.3.6/js/app.js" type="text/javascript"></script>
</head> </head>
<body class="skin-blue"> <body class="skin-blue page-load @if(context.sidebarCollapse){sidebar-collapse}">
<div class="wrapper"> <div class="wrapper">
<header class="main-header"> <header class="main-header">
<a href="@context.path/" class="logo"> <a href="@context.path/" class="logo">
@@ -115,6 +115,9 @@
$('#search').submit(function(){ $('#search').submit(function(){
return $.trim($(this).find('input[name=query]').val()) != ''; return $.trim($(this).find('input[name=query]').val()) != '';
}); });
$(".sidebar-toggle").on('click', function(e){
$.get('@context.path/sidebar-collapse', { collapse: !$('body').hasClass('sidebar-collapse') });
});
}); });
</script> </script>
@PluginRegistry().getJavaScript(context.request.getRequestURI).map { script => @PluginRegistry().getJavaScript(context.request.getRequestURI).map { script =>

View File

@@ -9,11 +9,11 @@
<li @if(active == name){class="active"}> <li @if(active == name){class="active"}>
@if(path.startsWith("http")){ @if(path.startsWith("http")){
<a href="@path" target="_blank"> <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> </a>
} else { } else {
<a href="@helpers.url(repository)@path"> <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> </a>
} }
</li> </li>
@@ -27,24 +27,26 @@
@menuitem("/branches", "branches", "Branches", "git-branch", repository.branchList.length) @menuitem("/branches", "branches", "Branches", "git-branch", repository.branchList.length)
@menuitem("/tags", "tags", "Tags", "tag", repository.tags.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("/issues", "issues", "Issues", "issue-opened", repository.issueCount)
@menuitem("/pulls", "pulls", "Pull Requests", "git-pull-request", repository.pullCount) @menuitem("/pulls", "pulls", "Pull Requests", "git-pull-request", repository.pullCount)
@menuitem("/issues/labels", "labels", "Labels", "tag") @menuitem("/issues/labels", "labels", "Labels", "tag")
@menuitem("/issues/milestones", "milestones", "Milestones", "milestone") @menuitem("/issues/milestones", "milestones", "Milestones", "milestone")
} else { } else {
@repository.repository.externalIssuesUrl.map { externalIssuesUrl => @repository.repository.options.externalIssuesUrl.map { externalIssuesUrl =>
@menuitem(externalIssuesUrl, "issues", "Issues", "issue-opened") @menuitem(externalIssuesUrl, "issues", "Issues", "issue-opened")
} }
} }
@if(repository.repository.enableWiki) { @if(repository.repository.options.wikiOption != "DISABLE") {
@menuitem("/wiki", "wiki", "Wiki", "book") @menuitem("/wiki", "wiki", "Wiki", "book")
} else { } else {
@repository.repository.externalWikiUrl.map { externalWikiUrl => @repository.repository.options.externalWikiUrl.map { externalWikiUrl =>
@menuitem(externalWikiUrl, "wiki", "Wiki", "book") @menuitem(externalWikiUrl, "wiki", "Wiki", "book")
} }
} }
@if(repository.repository.options.allowFork) {
@menuitem("/network/members", "fork", "Forks", "repo-forked", repository.forkedCount) @menuitem("/network/members", "fork", "Forks", "repo-forked", repository.forkedCount)
}
@if(context.loginAccount.isDefined && (context.loginAccount.get.isAdmin || repository.managers.contains(context.loginAccount.get.userName))){ @if(context.loginAccount.isDefined && (context.loginAccount.get.isAdmin || repository.managers.contains(context.loginAccount.get.userName))){
@menuitem("/settings", "settings", "Settings", "tools") @menuitem("/settings", "settings", "Settings", "tools")
} }

View File

@@ -5,12 +5,13 @@
collaborators: List[String], collaborators: List[String],
milestones: List[(gitbucket.core.model.Milestone, Int, Int)], milestones: List[(gitbucket.core.model.Milestone, Int, Int)],
labels: List[gitbucket.core.model.Label], labels: List[gitbucket.core.model.Label],
hasWritePermission: Boolean, isEditable: Boolean,
isManageable: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers @import gitbucket.core.view.helpers
<div class="col-md-9"> <div class="col-md-9">
<div id="comment-list"> <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> </div>
@defining(comments.flatMap { @defining(comments.flatMap {
case comment: gitbucket.core.model.IssueComment => Some(comment) case comment: gitbucket.core.model.IssueComment => Some(comment)
@@ -25,7 +26,7 @@
</div> </div>
</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)){ pullreq.repositoryName == pullreq.requestRepositoryName && repository.branchList.contains(pullreq.requestBranch)){
<div class="issue-comment-box" style="background-color: #d0eeff;"> <div class="issue-comment-box" style="background-color: #d0eeff;">
<div class="box-content"class="issue-content" style="border: 1px solid #87a8c9; padding: 10px;"> <div class="box-content"class="issue-content" style="border: 1px solid #87a8c9; padding: 10px;">
@@ -37,11 +38,11 @@
</div> </div>
</div> </div>
} }
@gitbucket.core.issues.html.commentform(issue, !merged, hasWritePermission, repository) @gitbucket.core.issues.html.commentform(issue, !merged, isEditable, isManageable, repository)
} }
</div> </div>
<div class="col-md-3"> <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>
<script> <script>
$(function(){ $(function(){
@@ -55,7 +56,7 @@ $(function(){
$.get('@helpers.url(repository)/pull/@issue.issueId/mergeguide', function(data){ $('.check-conflict').html(data); }); $.get('@helpers.url(repository)/pull/@issue.issueId/mergeguide', function(data){ $('.check-conflict').html(data); });
} }
@if(hasWritePermission){ @if(isManageable){
$('.delete-branch').click(function(e){ $('.delete-branch').click(function(e){
var branchName = $(e.target).data('name'); var branchName = $(e.target).data('name');
return confirm('Are you sure you want to remove the ' + branchName + ' branch?'); return confirm('Are you sure you want to remove the ' + branchName + ' branch?');

View File

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

View File

@@ -73,7 +73,7 @@
</div> </div>
} else { } else {
<div class="box-content-bottom"> <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> </div>
} }
} }

View File

@@ -1,34 +1,140 @@
@(collaborators: List[String], @(collaborators: List[(gitbucket.core.model.Collaborator, Boolean)],
isGroupRepository: Boolean, isGroupRepository: Boolean,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers @import gitbucket.core.view.helpers
@import gitbucket.core.model.Role
@gitbucket.core.html.main("Settings", Some(repository)){ @gitbucket.core.html.main("Settings", Some(repository)){
@gitbucket.core.html.menu("settings", repository){ @gitbucket.core.html.menu("settings", repository){
@gitbucket.core.settings.html.menu("collaborators", repository){ @gitbucket.core.settings.html.menu("collaborators", repository){
<h3>Manage Collaborators</h3> <form id="form" method="post" action="@helpers.url(repository)/settings/collaborators">
<ul class="collaborator"> <div class="panel panel-default">
@collaborators.map { collaboratorName => <div class="panel-heading strong">Collaborators</div>
<li> <div class="panel-body">
<a href="@helpers.url(collaboratorName)">@collaboratorName</a> <ul id="collaborator-list" class="collaborator">
@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> </ul>
@if(!isGroupRepository){ @gitbucket.core.helper.html.account("userName-collaborator", 200, true, false)
<form method="POST" action="@helpers.url(repository)/settings/collaborators/add" validate="true" autocomplete="off"> <input type="button" class="btn btn-default add" value="Add" id="addCollaborator"/>
<div> <div>
<span class="error" id="error-userName"></span> <span class="error" id="error-collaborator"></span>
</div>
</div>
</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> </div>
@gitbucket.core.helper.html.account("userName", 300)
<input type="submit" class="btn btn-default" value="Add"/>
</form> </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, '@Role.ADMIN.name', '#' + id + '-list');
$('#userName-' + id).val('');
} 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.role', @if(isGroup){'#group-list'}else{'#collaborator-list'});
}
function addListHTML(userName, role, id){
var adminButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="@Role.ADMIN.name" name="' + userName + '">Admin</label>');
if(role == '@Role.ADMIN.name'){
adminButton.addClass('active');
}
var writeButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="@Role.DEVELOPER.name" name="' + userName + '">Developer</label>');
if(role == '@Role.DEVELOPER.name'){
writeButton.addClass('active');
}
var readButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="@Role.GUEST.name" name="' + userName + '">Guest</label>');
if(role == '@Role.GUEST.name'){
readButton.addClass('active');
}
$(id).append($('<li>')
.data('name', userName)
.append($('<div class="btn-group role" 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> <div>
Transfer this repo to another user or to group. Transfer this repo to another user or to group.
<div class="pull-right"> <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"/> <input type="submit" class="btn btn-danger" value="Transfer"/>
<div> <div>
<span id="error-newOwner" class="error"></span> <span id="error-newOwner" class="error"></span>

View File

@@ -39,40 +39,76 @@
</div> </div>
</label> </label>
</fieldset> </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> </div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading strong">Features</div> <div class="panel-heading strong">Issues</div>
<div class="panel-body"> <div class="panel-body">
<fieldset class="form-group"> <fieldset class="form-group">
<label class="checkbox" for="enableIssues"> <div class="radio">
<input type="checkbox" id="enableIssues" name="enableIssues"@if(repository.repository.enableIssues){ checked}/> <label>
Issues<br> <input type="radio" name="issuesOption" value="DISABLE" @if(repository.repository.options.issuesOption == "DISABLE"){ checked}> Disables issues tracking system
<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> </label>
</div>
<div class="radio">
<label>
<input type="radio" name="issuesOption" value="PRIVATE" @if(repository.repository.options.issuesOption == "PRIVATE"){ checked}> Developers 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}> Developers and guests can view, create and comment on isues
</label>
</div>
<div class="radio for-public-repo">
<label>
<input type="radio" name="issuesOption" value="ALL" @if(repository.repository.options.issuesOption == "ALL"){ checked}> All users can view, create and comment on isues
</label>
</div>
<label for="externalIssuesUrl" class="strong">External URL: <label for="externalIssuesUrl" class="strong">External URL:
<span class="normal muted">(Put if you have the external issue tracking system for this project)</span> <span class="normal muted">(Put if you have the external issue tracking system for this project)</span>
</label> </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>
<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> </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> </label>
<label class="checkbox" for="allowWikiEditing"> </div>
<input type="checkbox" id="allowWikiEditing" name="allowWikiEditing"@if(repository.repository.allowWikiEditing){ checked}/> <div class="radio">
Allow read-only users to edit Wiki pages<br> <label>
<input type="radio" name="wikiOption" value="PRIVATE" @if(repository.repository.options.wikiOption == "PRIVATE"){ checked}> Developers can view, create and edit wiki pages
</label> </label>
</div>
<div class="radio">
<label>
<input type="radio" name="wikiOption" value="PUBLIC" @if(repository.repository.options.wikiOption == "PUBLIC"){ checked}> Developers ans guests can view, create and edit wiki pages
</label>
</div>
<div class="radio for-public-repo">
<label>
<input type="radio" name="wikiOption" value="ALL" @if(repository.repository.options.issuesOption == "ALL"){ checked}> All users can view, create and comment on isues
</label>
</div>
<label for="externalWikiUrl" class="strong">External URL: <label for="externalWikiUrl" class="strong">External URL:
<span class="normal muted">(Put if you have the external Wiki for this project)</span> <span class="normal muted">(Put if you have the external Wiki for this project)</span>
</label> </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> </fieldset>
</div> </div>
</div> </div>
@@ -86,15 +122,25 @@
<script> <script>
$(function(){ $(function(){
updateFeatures(); updateFeatures();
$('input[name=isPrivate], input[name=issuesOption], input[name=wikiOption]').click(function(){
$('#enableIssues, #enableWiki').click(function(){
updateFeatures(); updateFeatures();
}); });
}); });
function updateFeatures() { function updateFeatures() {
$('#externalIssuesUrl').prop('disabled', $('#enableIssues').prop('checked')); if($('input[name=isPrivate]:checked').val() == 'false'){
$('#allowWikiEditing').prop('disabled', !$('#enableWiki').prop('checked')); $('.for-public-repo').show();
$('#externalWikiUrl').prop('disabled', $('#enableWiki').prop('checked')); } else {
if($('input[name=issuesOption]:checked').val() == 'ALL'){
$('input[name=issuesOption][value=PUBLIC]').prop('checked', true);
}
if($('input[name=wikiOption]:checked').val() == 'ALL'){
$('input[name=wikiOption][value=PUBLIC]').prop('checked', true);
}
$('.for-public-repo').hide();
}
$('#externalIssuesUrl').prop('disabled', $('input[name=issuesOption]:checked').val() != 'DISABLE');
$('#externalWikiUrl').prop('disabled', $('input[name=wikiOption]:checked').val() != 'DISABLE');
} }
</script> </script>

View File

@@ -3,7 +3,7 @@
to: String, to: String,
diffs: Seq[gitbucket.core.util.JGitUtil.DiffInfo], diffs: Seq[gitbucket.core.util.JGitUtil.DiffInfo],
repository: gitbucket.core.service.RepositoryService.RepositoryInfo, repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean, isEditable: Boolean,
info: Option[Any])(implicit context: gitbucket.core.controller.Context) info: Option[Any])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers @import gitbucket.core.view.helpers
@gitbucket.core.html.main(s"Compare Revisions - ${repository.owner}/${repository.name}", Some(repository)){ @gitbucket.core.html.main(s"Compare Revisions - ${repository.owner}/${repository.name}", Some(repository)){
@@ -27,7 +27,7 @@
<div class="pull-left"> <div class="pull-left">
@gitbucket.core.helper.html.diff(diffs, repository, None, None, false, None, false, false) @gitbucket.core.helper.html.diff(diffs, repository, None, None, false, None, false, false)
</div> </div>
@if(hasWritePermission){ @if(isEditable){
<div> <div>
@if(pageName.isDefined){ @if(pageName.isDefined){
<a href="@helpers.url(repository)/wiki/@helpers.urlEncode(pageName)/_revert/@from...@to" class="btn">Revert Changes</a> <a href="@helpers.url(repository)/wiki/@helpers.urlEncode(pageName)/_revert/@from...@to" class="btn">Revert Changes</a>

View File

@@ -44,7 +44,7 @@
</form> </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(){ $(function(){
try { try {
$('.clickable').dropzone({ $('.clickable').dropzone({

View File

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

View File

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

View File

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

View File

@@ -326,6 +326,12 @@ div.account-image {
margin-bottom: 8px; margin-bottom: 8px;
} }
.dropdown-filter-input {
border: solid 1px #ccc;
margin: 4px;
width: 96%;
}
ul.dropdown-menu { ul.dropdown-menu {
padding: 2px 0; padding: 2px 0;
} }
@@ -338,7 +344,7 @@ ul.dropdown-menu li a {
padding: 2px 10px; padding: 2px 10px;
} }
ul.dropdown-menu :last-child { ul.dropdown-menu li:last-child {
border-bottom: none; border-bottom: none;
} }
@@ -632,12 +638,6 @@ span.simplified-path {
line-height: 15px; line-height: 15px;
} }
#branch-control-input {
border: solid 1px #ccc;
margin: 4px;
width: 96%;
}
.new-branch-name { .new-branch-name {
font-weight: bold; font-weight: bold;
font-size: 1.2em; font-size: 1.2em;
@@ -779,10 +779,6 @@ h4#issueTitle {
padding: 0px; padding: 0px;
} }
div.issue-avatar-image {
float: left;
}
div.issue-participants { div.issue-participants {
margin-bottom: 15px; margin-bottom: 15px;
margin-left: 50px; margin-left: 50px;
@@ -790,7 +786,6 @@ div.issue-participants {
div.issue-comment-box, div.commit-comment-box { div.issue-comment-box, div.commit-comment-box {
margin-bottom: 15px; margin-bottom: 15px;
margin-left: 70px;
} }
div.issue-comment-box > div.panel-body, div.issue-comment-box > div.panel-body,
@@ -879,7 +874,7 @@ li.task-list-item input.task-list-item-checkbox {
.discussion-item { .discussion-item {
position: relative; position: relative;
margin: 15px 0 15px 79px; margin: 15px 0 15px 20px;
padding-left: 25px; padding-left: 25px;
} }
.discussion-item-header { .discussion-item-header {
@@ -1762,3 +1757,14 @@ div.container.blame-container{
width: 20px; width: 20px;
height: 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 // syntax highlighting by google-code-prettify
prettyPrint(); prettyPrint();
// Suppress transition animation on load
$("body").removeClass("page-load");
}); });
function displayErrors(data, elem){ function displayErrors(data, elem){

View File

@@ -5,7 +5,6 @@ import org.eclipse.jgit.transport.{ReceivePack, ReceiveCommand}
import org.eclipse.jgit.lib.ObjectId import org.eclipse.jgit.lib.ObjectId
import gitbucket.core.model.CommitState import gitbucket.core.model.CommitState
import gitbucket.core.service.ProtectedBranchService.{ProtectedBranchReceiveHook, ProtectedBranchInfo} import gitbucket.core.service.ProtectedBranchService.{ProtectedBranchReceiveHook, ProtectedBranchInfo}
import scalaz._, Scalaz._
import org.scalatest.FunSpec import org.scalatest.FunSpec
class ProtectedBranchServiceSpec extends FunSpec with ServiceSpecBase with ProtectedBranchService with CommitStatusService { class ProtectedBranchServiceSpec extends FunSpec with ServiceSpecBase with ProtectedBranchService with CommitStatusService {

View File

@@ -3,7 +3,8 @@ package gitbucket.core.service
import gitbucket.core.model._ import gitbucket.core.model._
import org.scalatest.FunSpec 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) def swap(r: (Issue, PullRequest)) = (r._2 -> r._1)

View File

@@ -9,8 +9,6 @@ import io.github.gitbucket.solidbase.Solidbase
import liquibase.database.core.H2Database import liquibase.database.core.H2Database
import liquibase.database.jvm.JdbcConnection import liquibase.database.jvm.JdbcConnection
import profile.simple._ import profile.simple._
import scalaz._
import Scalaz._
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils

View File

@@ -3,10 +3,11 @@ package gitbucket.core.view
import java.util.Date import java.util.Date
import gitbucket.core.model.Account import gitbucket.core.model.Account
import gitbucket.core.service.{SystemSettingsService, RequestCache} import gitbucket.core.service.{RequestCache, SystemSettingsService}
import gitbucket.core.controller.Context import gitbucket.core.controller.Context
import SystemSettingsService.SystemSettings import SystemSettingsService.SystemSettings
import javax.servlet.http.HttpServletRequest import javax.servlet.http.{HttpServletRequest, HttpSession}
import play.twirl.api.Html import play.twirl.api.Html
import org.scalatest.FunSpec import org.scalatest.FunSpec
import org.scalatest.mock.MockitoSugar import org.scalatest.mock.MockitoSugar
@@ -16,9 +17,11 @@ import org.mockito.Mockito._
class AvatarImageProviderSpec extends FunSpec with MockitoSugar { class AvatarImageProviderSpec extends FunSpec with MockitoSugar {
val request = mock[HttpServletRequest] val request = mock[HttpServletRequest]
val session = mock[HttpSession]
when(request.getRequestURL).thenReturn(new StringBuffer("http://localhost:8080/path.html")) when(request.getRequestURL).thenReturn(new StringBuffer("http://localhost:8080/path.html"))
when(request.getRequestURI).thenReturn("/path.html") when(request.getRequestURI).thenReturn("/path.html")
when(request.getContextPath).thenReturn("") when(request.getContextPath).thenReturn("")
when(request.getSession).thenReturn(session)
describe("getAvatarImageHtml") { describe("getAvatarImageHtml") {
it("should show Gravatar image for no image account if gravatar integration is enabled") { it("should show Gravatar image for no image account if gravatar integration is enabled") {