Merge pull request #1488 from kounoike/pr-add-account-webhook

Add account(group/user)'s webhooks feature.
This commit is contained in:
Naoki Takezoe
2017-06-19 02:13:55 +09:00
committed by GitHub
19 changed files with 640 additions and 100 deletions

View File

@@ -11,4 +11,22 @@
<addPrimaryKey constraintName="IDX_DEPLOY_KEY_PK" tableName="DEPLOY_KEY" columnNames="USER_NAME, REPOSITORY_NAME, DEPLOY_KEY_ID"/> <addPrimaryKey constraintName="IDX_DEPLOY_KEY_PK" tableName="DEPLOY_KEY" columnNames="USER_NAME, REPOSITORY_NAME, DEPLOY_KEY_ID"/>
<addForeignKeyConstraint constraintName="IDX_DEPLOY_KEY_FK0" baseTableName="DEPLOY_KEY" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/> <addForeignKeyConstraint constraintName="IDX_DEPLOY_KEY_FK0" baseTableName="DEPLOY_KEY" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
<createTable tableName="ACCOUNT_WEB_HOOK">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="URL" type="varchar(200)" nullable="false"/>
<column name="TOKEN" type="varchar(100)" nullable="true"/>
<column name="CTYPE" type="varchar(10)" nullable="true"/>
</createTable>
<addPrimaryKey constraintName="IDX_ACCOUNT_WEB_HOOK_PK" tableName="ACCOUNT_WEB_HOOK" columnNames="USER_NAME, URL"/>
<addForeignKeyConstraint constraintName="IDX_ACCOUNT_WEB_HOOK_FK0" baseTableName="ACCOUNT_WEB_HOOK" baseColumnNames="USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
<createTable tableName="ACCOUNT_WEB_HOOK_EVENT">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="URL" type="varchar(200)" nullable="false"/>
<column name="EVENT" type="varchar(30)" nullable="false"/>
</createTable>
</changeSet> </changeSet>

View File

@@ -53,4 +53,14 @@ object ApiRepository{
def forPushPayload(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository = def forPushPayload(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount, urlIsHtmlUrl=true) ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount, urlIsHtmlUrl=true)
def forDummyPayload(owner: ApiUser): ApiRepository =
ApiRepository(
name="dummy",
full_name=s"${owner.login}/dummy",
description="",
watchers=0,
forks=0,
`private`=false,
default_branch="master",
owner=owner)(true)
} }

View File

@@ -2,9 +2,10 @@ package gitbucket.core.controller
import gitbucket.core.account.html import gitbucket.core.account.html
import gitbucket.core.helper import gitbucket.core.helper
import gitbucket.core.model.{GroupMember, Role} import gitbucket.core.model.{GroupMember, Role, WebHook, WebHookContentType, AccountWebHook, RepositoryWebHook, RepositoryWebHookEvent}
import gitbucket.core.plugin.PluginRegistry import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service._ import gitbucket.core.service._
import gitbucket.core.service.WebHookService._
import gitbucket.core.ssh.SshUtil import gitbucket.core.ssh.SshUtil
import gitbucket.core.util.SyntaxSugars._ import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
@@ -16,7 +17,6 @@ import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.scalatra.BadRequest import org.scalatra.BadRequest
class AccountController extends AccountControllerBase class AccountController extends AccountControllerBase
with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
@@ -109,6 +109,47 @@ trait AccountControllerBase extends AccountManagementControllerBase {
"account" -> trim(label("Group/User name", text(required, validAccountName))) "account" -> trim(label("Group/User name", text(required, validAccountName)))
)(AccountForm.apply) )(AccountForm.apply)
// for account web hook url addition.
case class AccountWebHookForm(url: String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])
def accountWebHookForm(update:Boolean) = mapping(
"url" -> trim(label("url", text(required, accountWebHook(update)))),
"events" -> accountWebhookEvents,
"ctype" -> label("ctype", text()),
"token" -> optional(trim(label("token", text(maxlength(100)))))
)(
(url, events, ctype, token) => AccountWebHookForm(url, events, WebHookContentType.valueOf(ctype), token)
)
/**
* Provides duplication check for web hook url. duplicated from RepositorySettingsController.scala
*/
private def accountWebHook(needExists: Boolean): Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
if(getAccountWebHook(params("userName"), value).isDefined != needExists){
Some(if(needExists){
"URL had not been registered yet."
} else {
"URL had been registered already."
})
} else {
None
}
}
private def accountWebhookEvents = new ValueType[Set[WebHook.Event]]{
def convert(name: String, params: Map[String, String], messages: Messages): Set[WebHook.Event] = {
WebHook.Event.values.flatMap { t =>
params.get(name + "." + t.name).map(_ => t)
}.toSet
}
def validate(name: String, params: Map[String, String], messages: Messages): Seq[(String, String)] = if(convert(name,params,messages).isEmpty){
Seq(name -> messages("error.required").format(name))
} else {
Nil
}
}
/** /**
* Displays user information. * Displays user information.
*/ */
@@ -129,6 +170,13 @@ 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 }))
} }
// Webhooks
case "webhooks" =>
gitbucket.core.account.html.webhook(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
getAccountWebHooks(account.userName)
)
// Repositories // Repositories
case _ => { case _ => {
val members = getGroupMembers(account.userName) val members = getGroupMembers(account.userName)
@@ -273,6 +321,106 @@ trait AccountControllerBase extends AccountManagementControllerBase {
redirect(s"/${userName}/_application") redirect(s"/${userName}/_application")
}) })
/**
* Display the account web hook edit page.
*/
get("/:userName/_hooks/new")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName).map { account =>
val webhook = AccountWebHook(userName, "", WebHookContentType.FORM, None)
html.edithooks(webhook, Set(WebHook.Push), account, if (account.isGroupAccount) Nil else getGroupsByUserName(userName), flash.get("info"), true)
}
})
/**
* Add the account web hook URL.
*/
post("/:userName/_hooks/new", accountWebHookForm(false))(oneselfOnly { form =>
val userName = params("userName")
addAccountWebHook(userName, form.url, form.events, form.ctype, form.token)
flash += "info" -> s"Webhook ${form.url} created"
redirect(s"/${userName}?tab=webhooks")
})
/**
* Delete the account web hook URL.
*/
get("/:userName/_hooks/delete")(oneselfOnly {
val userName = params("userName")
deleteAccountWebHook(userName, params("url"))
flash += "info" -> s"Webhook ${params("url")} deleted"
redirect(s"/${userName}?tab=webhooks")
})
/**
* Display the account web hook edit page.
*/
get("/:userName/_hooks/edit")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName).map { account =>
getAccountWebHook(userName, params("url")).map { case (webhook, events) =>
html.edithooks(webhook, events, account, if (account.isGroupAccount) Nil else getGroupsByUserName(userName), flash.get("info"), false)
} getOrElse NotFound()
}
})
/**
* Update account web hook settings.
*/
post("/:userName/_hooks/edit", accountWebHookForm(true))(oneselfOnly { form =>
val userName = params("userName")
updateAccountWebHook(userName, form.url, form.events, form.ctype, form.token)
flash += "info" -> s"webhook ${form.url} updated"
redirect(s"/${userName}?tab=webhooks")
})
/**
* Send the test request to registered account web hook URLs.
*/
ajaxPost("/:userName/_hooks/test")(oneselfOnly {
import scala.collection.JavaConverters._
import scala.concurrent.duration._
import scala.concurrent._
import scala.util.control.NonFatal
import org.apache.http.util.EntityUtils
import scala.concurrent.ExecutionContext.Implicits.global
def _headers(h: Array[org.apache.http.Header]): Array[Array[String]] = h.map { h => Array(h.getName, h.getValue) }
val userName = params("userName")
val url = params("url")
val token = Some(params("token"))
val ctype = WebHookContentType.valueOf(params("ctype"))
val dummyWebHookInfo = RepositoryWebHook(userName, "dummy", url, ctype, token)
val dummyPayload = {
val ownerAccount = getAccountByUserName(userName).get
WebHookPushPayload.createDummyPayload(ownerAccount)
}
val (webHook, json, reqFuture, resFuture) = callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload).head
val toErrorMap: PartialFunction[Throwable, Map[String,String]] = {
case e: java.net.UnknownHostException => Map("error"-> ("Unknown host " + e.getMessage))
case e: java.lang.IllegalArgumentException => Map("error"-> ("invalid url"))
case e: org.apache.http.client.ClientProtocolException => Map("error"-> ("invalid url"))
case NonFatal(e) => Map("error"-> (e.getClass + " "+ e.getMessage))
}
contentType = formats("json")
org.json4s.jackson.Serialization.write(Map(
"url" -> url,
"request" -> Await.result(reqFuture.map(req => Map(
"headers" -> _headers(req.getAllHeaders),
"payload" -> json
)).recover(toErrorMap), 20 seconds),
"responce" -> Await.result(resFuture.map(res => Map(
"status" -> res.getStatusLine(),
"body" -> EntityUtils.toString(res.getEntity()),
"headers" -> _headers(res.getAllHeaders())
)).recover(toErrorMap), 20 seconds)
))
})
get("/register"){ get("/register"){
if(context.settings.allowAccountRegistration){ if(context.settings.allowAccountRegistration){
if(context.loginAccount.isDefined){ if(context.loginAccount.isDefined){

View File

@@ -1,7 +1,7 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.settings.html import gitbucket.core.settings.html
import gitbucket.core.model.WebHook import gitbucket.core.model.{WebHook, RepositoryWebHook}
import gitbucket.core.service._ import gitbucket.core.service._
import gitbucket.core.service.WebHookService._ import gitbucket.core.service.WebHookService._
import gitbucket.core.util._ import gitbucket.core.util._
@@ -221,7 +221,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the web hook edit page. * Display the web hook edit page.
*/ */
get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository => get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository =>
val webhook = WebHook(repository.owner, repository.name, "", WebHookContentType.FORM, None) val webhook = RepositoryWebHook(repository.owner, repository.name, "", WebHookContentType.FORM, None)
html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true) html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true)
}) })
@@ -260,7 +260,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
val url = params("url") val url = params("url")
val token = Some(params("token")) val token = Some(params("token"))
val ctype = WebHookContentType.valueOf(params("ctype")) val ctype = WebHookContentType.valueOf(params("ctype"))
val dummyWebHookInfo = WebHook(repository.owner, repository.name, url, ctype, token) val dummyWebHookInfo = RepositoryWebHook(repository.owner, repository.name, url, ctype, token)
val dummyPayload = { val dummyPayload = {
val ownerAccount = getAccountByUserName(repository.owner).get val ownerAccount = getAccountByUserName(repository.owner).get
val commits = if(JGitUtil.isEmpty(git)) List.empty else git.log val commits = if(JGitUtil.isEmpty(git)) List.empty else git.log

View File

@@ -1,6 +1,6 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.model.{WebHook, WebHookEvent} import gitbucket.core.model.WebHook
import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.WebHookService.WebHookGollumPayload import gitbucket.core.service.WebHookService.WebHookGollumPayload
import gitbucket.core.wiki.html import gitbucket.core.wiki.html

View File

@@ -0,0 +1,25 @@
package gitbucket.core.model
trait AccountWebHookComponent extends TemplateComponent { self: Profile =>
import profile.api._
private implicit val whContentTypeColumnType = MappedColumnType.base[WebHookContentType, String](whct => whct.code , code => WebHookContentType.valueOf(code))
lazy val AccountWebHooks = TableQuery[AccountWebHooks]
class AccountWebHooks(tag: Tag) extends Table[AccountWebHook](tag, "ACCOUNT_WEB_HOOK") with BasicTemplate {
val url = column[String]("URL")
val token = column[Option[String]]("TOKEN")
val ctype = column[WebHookContentType]("CTYPE")
def * = (userName, url, ctype, token) <> ((AccountWebHook.apply _).tupled, AccountWebHook.unapply)
def byPrimaryKey(userName: String, url: String) = (this.userName === userName.bind) && (this.url === url.bind)
}
}
case class AccountWebHook(
userName: String,
url: String,
ctype: WebHookContentType,
token: Option[String]
) extends WebHook

View File

@@ -0,0 +1,34 @@
package gitbucket.core.model
trait AccountWebHookEventComponent extends TemplateComponent {
self: Profile =>
import profile.api._
import gitbucket.core.model.Profile.AccountWebHooks
lazy val AccountWebHookEvents = TableQuery[AccountWebHookEvents]
class AccountWebHookEvents(tag: Tag) extends Table[AccountWebHookEvent](tag, "ACCOUNT_WEB_HOOK_EVENT") with BasicTemplate {
val url = column[String]("URL")
val event = column[WebHook.Event]("EVENT")
def * = (userName, url, event) <> ((AccountWebHookEvent.apply _).tupled, AccountWebHookEvent.unapply)
def byAccountWebHook(userName: String, url: String) = (this.userName === userName.bind) && (this.url === url.bind)
def byAccountWebHook(owner: Rep[String], url: Rep[String]) =
(this.userName === userName) && (this.url === url)
def byAccountWebHook(webhook: AccountWebHooks) =
(this.userName === webhook.userName) && (this.url === webhook.url)
def byPrimaryKey(userName: String, url: String, event: WebHook.Event) =
(this.userName === userName.bind) && (this.url === url.bind) && (this.event === event.bind)
}
}
case class AccountWebHookEvent(
userName: String,
url: String,
event: WebHook.Event
)

View File

@@ -7,6 +7,10 @@ protected[model] trait TemplateComponent { self: Profile =>
val userName = column[String]("USER_NAME") val userName = column[String]("USER_NAME")
val repositoryName = column[String]("REPOSITORY_NAME") val repositoryName = column[String]("REPOSITORY_NAME")
def byAccount(userName: String) = (this.userName === userName.bind)
def byAccount(userName: Rep[String]) = (this.userName === userName)
def byRepository(owner: String, repository: String) = def byRepository(owner: String, repository: String) =
(userName === owner.bind) && (repositoryName === repository.bind) (userName === owner.bind) && (repositoryName === repository.bind)

View File

@@ -15,6 +15,11 @@ trait Profile {
t => new java.util.Date(t.getTime) t => new java.util.Date(t.getTime)
) )
/**
* WebHookBase.Event Column Types
*/
implicit val eventColumnType = MappedColumnType.base[WebHook.Event, String](_.name, WebHook.Event.valueOf(_))
/** /**
* Extends Column to add conditional condition * Extends Column to add conditional condition
*/ */
@@ -52,8 +57,10 @@ trait CoreProfile extends ProfileProvider with Profile
with PullRequestComponent with PullRequestComponent
with RepositoryComponent with RepositoryComponent
with SshKeyComponent with SshKeyComponent
with WebHookComponent with RepositoryWebHookComponent
with WebHookEventComponent with RepositoryWebHookEventComponent
with AccountWebHookComponent
with AccountWebHookEventComponent
with ProtectedBranchComponent with ProtectedBranchComponent
with DeployKeyComponent with DeployKeyComponent

View File

@@ -0,0 +1,27 @@
package gitbucket.core.model
trait RepositoryWebHookComponent extends TemplateComponent { self: Profile =>
import profile.api._
implicit val whContentTypeColumnType = MappedColumnType.base[WebHookContentType, String](whct => whct.code , code => WebHookContentType.valueOf(code))
lazy val RepositoryWebHooks = TableQuery[RepositoryWebHooks]
class RepositoryWebHooks(tag: Tag) extends Table[RepositoryWebHook](tag, "WEB_HOOK") with BasicTemplate {
val url = column[String]("URL")
val token = column[Option[String]]("TOKEN")
val ctype = column[WebHookContentType]("CTYPE")
def * = (userName, repositoryName, url, ctype, token) <> ((RepositoryWebHook.apply _).tupled, RepositoryWebHook.unapply)
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
}
}
case class RepositoryWebHook(
userName: String,
repositoryName: String,
url: String,
ctype: WebHookContentType,
token: Option[String]
) extends WebHook

View File

@@ -0,0 +1,28 @@
package gitbucket.core.model
trait RepositoryWebHookEventComponent extends TemplateComponent { self: Profile =>
import profile.api._
import gitbucket.core.model.Profile.RepositoryWebHooks
lazy val RepositoryWebHookEvents = TableQuery[RepositoryWebHookEvents]
class RepositoryWebHookEvents(tag: Tag) extends Table[RepositoryWebHookEvent](tag, "WEB_HOOK_EVENT") with BasicTemplate {
val url = column[String]("URL")
val event = column[WebHook.Event]("EVENT")
def * = (userName, repositoryName, url, event) <> ((RepositoryWebHookEvent.apply _).tupled, RepositoryWebHookEvent.unapply)
def byRepositoryWebHook(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
def byRepositoryWebHook(owner: Rep[String], repository: Rep[String], url: Rep[String]) =
byRepository(userName, repositoryName) && (this.url === url)
def byRepositoryWebHook(webhook: RepositoryWebHooks) =
byRepository(webhook.userName, webhook.repositoryName) && (this.url === webhook.url)
def byPrimaryKey(owner: String, repository: String, url: String, event: WebHook.Event) = byRepositoryWebHook(owner, repository, url) && (this.event === event.bind)
}
}
case class RepositoryWebHookEvent(
userName: String,
repositoryName: String,
url: String,
event: WebHook.Event
)

View File

@@ -1,22 +1,5 @@
package gitbucket.core.model package gitbucket.core.model
trait WebHookComponent extends TemplateComponent { self: Profile =>
import profile.api._
implicit val whContentTypeColumnType = MappedColumnType.base[WebHookContentType, String](whct => whct.code , code => WebHookContentType.valueOf(code))
lazy val WebHooks = TableQuery[WebHooks]
class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate {
val url = column[String]("URL")
val token = column[Option[String]]("TOKEN")
val ctype = column[WebHookContentType]("CTYPE")
def * = (userName, repositoryName, url, ctype, token) <> ((WebHook.apply _).tupled, WebHook.unapply)
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
}
}
abstract sealed case class WebHookContentType(code: String, ctype: String) abstract sealed case class WebHookContentType(code: String, ctype: String)
object WebHookContentType { object WebHookContentType {
@@ -33,13 +16,11 @@ object WebHookContentType {
def valueOpt(code: String): Option[WebHookContentType] = map.get(code) def valueOpt(code: String): Option[WebHookContentType] = map.get(code)
} }
case class WebHook( trait WebHook{
userName: String, val url: String
repositoryName: String, val ctype: WebHookContentType
url: String, val token: Option[String]
ctype: WebHookContentType, }
token: Option[String]
)
object WebHook { object WebHook {
abstract sealed class Event(val name: String) abstract sealed class Event(val name: String)
@@ -86,6 +67,7 @@ object WebHook {
TeamAdd, TeamAdd,
Watch Watch
) )
private val map: Map[String,Event] = values.map(e => e.name -> e).toMap private val map: Map[String,Event] = values.map(e => e.name -> e).toMap
def valueOf(name: String): Event = map(name) def valueOf(name: String): Event = map(name)
def valueOpt(name: String): Option[Event] = map.get(name) def valueOpt(name: String): Option[Event] = map.get(name)

View File

@@ -1,30 +0,0 @@
package gitbucket.core.model
trait WebHookEventComponent extends TemplateComponent { self: Profile =>
import profile.api._
import gitbucket.core.model.Profile.WebHooks
lazy val WebHookEvents = TableQuery[WebHookEvents]
implicit val typedType = MappedColumnType.base[WebHook.Event, String](_.name, WebHook.Event.valueOf(_))
class WebHookEvents(tag: Tag) extends Table[WebHookEvent](tag, "WEB_HOOK_EVENT") with BasicTemplate {
val url = column[String]("URL")
val event = column[WebHook.Event]("EVENT")
def * = (userName, repositoryName, url, event) <> ((WebHookEvent.apply _).tupled, WebHookEvent.unapply)
def byWebHook(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
def byWebHook(owner: Rep[String], repository: Rep[String], url: Rep[String]) =
byRepository(userName, repositoryName) && (this.url === url)
def byWebHook(webhook: WebHooks) =
byRepository(webhook.userName, webhook.repositoryName) && (this.url === webhook.url)
def byPrimaryKey(owner: String, repository: String, url: String, event: WebHook.Event) = byWebHook(owner, repository, url) && (this.event === event.bind)
}
}
case class WebHookEvent(
userName: String,
repositoryName: String,
url: String,
event: WebHook.Event
)

View File

@@ -59,8 +59,8 @@ trait RepositoryService { self: AccountService =>
(Repositories filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository => (Repositories filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName) Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list val webHooks = RepositoryWebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val webHookEvents = WebHookEvents .filter(_.byRepository(oldUserName, oldRepositoryName)).list val webHookEvents = RepositoryWebHookEvents .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list
@@ -93,8 +93,8 @@ trait RepositoryService { self: AccountService =>
deleteRepository(oldUserName, oldRepositoryName) deleteRepository(oldUserName, oldRepositoryName)
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) RepositoryWebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
WebHookEvents.insertAll(webHookEvents .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) RepositoryWebHookEvents.insertAll(webHookEvents .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Priorities .insertAll(priorities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Priorities .insertAll(priorities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*) IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
@@ -170,8 +170,8 @@ trait RepositoryService { self: AccountService =>
Priorities .filter(_.byRepository(userName, repositoryName)).delete Priorities .filter(_.byRepository(userName, repositoryName)).delete
IssueId .filter(_.byRepository(userName, repositoryName)).delete IssueId .filter(_.byRepository(userName, repositoryName)).delete
Milestones .filter(_.byRepository(userName, repositoryName)).delete Milestones .filter(_.byRepository(userName, repositoryName)).delete
WebHooks .filter(_.byRepository(userName, repositoryName)).delete RepositoryWebHooks .filter(_.byRepository(userName, repositoryName)).delete
WebHookEvents .filter(_.byRepository(userName, repositoryName)).delete RepositoryWebHookEvents .filter(_.byRepository(userName, repositoryName)).delete
DeployKeys .filter(_.byRepository(userName, repositoryName)).delete DeployKeys .filter(_.byRepository(userName, repositoryName)).delete
Repositories .filter(_.byRepository(userName, repositoryName)).delete Repositories .filter(_.byRepository(userName, repositoryName)).delete

View File

@@ -3,7 +3,7 @@ package gitbucket.core.service
import fr.brouillard.oss.security.xhub.XHub import fr.brouillard.oss.security.xhub.XHub
import fr.brouillard.oss.security.xhub.XHub.{XHubConverter, XHubDigest} import fr.brouillard.oss.security.xhub.XHub.{XHubConverter, XHubDigest}
import gitbucket.core.api._ import gitbucket.core.api._
import gitbucket.core.model.{Account, CommitComment, Issue, IssueComment, PullRequest, WebHook, WebHookEvent} import gitbucket.core.model.{Account, CommitComment, Issue, IssueComment, PullRequest, WebHook, RepositoryWebHook, RepositoryWebHookEvent, AccountWebHook, AccountWebHookEvent}
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._ import gitbucket.core.model.Profile.profile.blockingApi._
import org.apache.http.client.utils.URLEncodedUtils import org.apache.http.client.utils.URLEncodedUtils
@@ -32,45 +32,86 @@ trait WebHookService {
private val logger = LoggerFactory.getLogger(classOf[WebHookService]) private val logger = LoggerFactory.getLogger(classOf[WebHookService])
/** get All WebHook informations of repository */ /** get All WebHook informations of repository */
def getWebHooks(owner: String, repository: String)(implicit s: Session): List[(WebHook, Set[WebHook.Event])] = def getWebHooks(owner: String, repository: String)(implicit s: Session): List[(RepositoryWebHook, Set[WebHook.Event])] =
WebHooks.filter(_.byRepository(owner, repository)) RepositoryWebHooks.filter(_.byRepository(owner, repository))
.join(WebHookEvents).on { (w, t) => t.byWebHook(w) } .join(RepositoryWebHookEvents).on { (w, t) => t.byRepositoryWebHook(w) }
.map { case (w, t) => w -> t.event } .map { case (w, t) => w -> t.event }
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).toList.sortBy(_._1.url) .list.groupBy(_._1).mapValues(_.map(_._2).toSet).toList.sortBy(_._1.url)
/** get All WebHook informations of repository event */ /** get All WebHook informations of repository event */
def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[WebHook] = def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[RepositoryWebHook] =
WebHooks.filter(_.byRepository(owner, repository)) RepositoryWebHooks.filter(_.byRepository(owner, repository))
.join(WebHookEvents).on { (wh, whe) => whe.byWebHook(wh) } .join(RepositoryWebHookEvents).on { (wh, whe) => whe.byRepositoryWebHook(wh) }
.filter { case (wh, whe) => whe.event === event.bind } .filter { case (wh, whe) => whe.event === event.bind}
.map{ case (wh, whe) => wh } .map{ case (wh, whe) => wh }
.list.distinct .list.distinct
/** get All WebHook information from repository to url */ /** get All WebHook information from repository to url */
def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(WebHook, Set[WebHook.Event])] = def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(RepositoryWebHook, Set[WebHook.Event])] =
WebHooks RepositoryWebHooks
.filter(_.byPrimaryKey(owner, repository, url)) .filter(_.byPrimaryKey(owner, repository, url))
.join(WebHookEvents).on { (w, t) => t.byWebHook(w) } .join(RepositoryWebHookEvents).on { (w, t) => t.byRepositoryWebHook(w) }
.map { case (w, t) => w -> t.event } .map { case (w, t) => w -> t.event }
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption .list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption
def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = { def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = {
WebHooks insert WebHook(owner, repository, url, ctype, token) RepositoryWebHooks insert RepositoryWebHook(owner, repository, url, ctype, token)
events.map { event: WebHook.Event => events.map { event: WebHook.Event =>
WebHookEvents insert WebHookEvent(owner, repository, url, event) RepositoryWebHookEvents insert RepositoryWebHookEvent(owner, repository, url, event)
} }
} }
def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = { def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = {
WebHooks.filter(_.byPrimaryKey(owner, repository, url)).map(w => (w.ctype, w.token)).update((ctype, token)) RepositoryWebHooks.filter(_.byPrimaryKey(owner, repository, url)).map(w => (w.ctype, w.token)).update((ctype, token))
WebHookEvents.filter(_.byWebHook(owner, repository, url)).delete RepositoryWebHookEvents.filter(_.byRepositoryWebHook(owner, repository, url)).delete
events.map { event: WebHook.Event => events.map { event: WebHook.Event =>
WebHookEvents insert WebHookEvent(owner, repository, url, event) RepositoryWebHookEvents insert RepositoryWebHookEvent(owner, repository, url, event)
} }
} }
def deleteWebHook(owner: String, repository: String, url :String)(implicit s: Session): Unit = def deleteWebHook(owner: String, repository: String, url :String)(implicit s: Session): Unit =
WebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete RepositoryWebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete
/** get All AccountWebHook informations of user */
def getAccountWebHooks(owner: String)(implicit s: Session): List[(AccountWebHook, Set[WebHook.Event])] =
AccountWebHooks.filter(_.byAccount(owner))
.join(AccountWebHookEvents).on { (w, t) => t.byAccountWebHook(w) }
.map { case (w, t) => w -> t.event }
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).toList.sortBy(_._1.url)
/** get All AccountWebHook informations of repository event */
def getAccountWebHooksByEvent(owner: String, event: WebHook.Event)(implicit s: Session): List[AccountWebHook] =
AccountWebHooks.filter(_.byAccount(owner))
.join(AccountWebHookEvents).on { (wh, whe) => whe.byAccountWebHook(wh) }
.filter { case (wh, whe) => whe.event === event.bind}
.map{ case (wh, whe) => wh }
.list.distinct
/** get All AccountWebHook information from repository to url */
def getAccountWebHook(owner: String, url: String)(implicit s: Session): Option[(AccountWebHook, Set[WebHook.Event])] =
AccountWebHooks
.filter(_.byPrimaryKey(owner, url))
.join(AccountWebHookEvents).on { (w, t) => t.byAccountWebHook(w) }
.map { case (w, t) => w -> t.event }
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption
def addAccountWebHook(owner: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = {
AccountWebHooks insert AccountWebHook(owner, url, ctype, token)
events.map { event: WebHook.Event =>
AccountWebHookEvents insert AccountWebHookEvent(owner, url, event)
}
}
def updateAccountWebHook(owner: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = {
AccountWebHooks.filter(_.byPrimaryKey(owner, url)).map(w => (w.ctype, w.token)).update((ctype, token))
AccountWebHookEvents.filter(_.byAccountWebHook(owner, url)).delete
events.map { event: WebHook.Event =>
AccountWebHookEvents insert AccountWebHookEvent(owner, url, event)
}
}
def deleteAccountWebHook(owner: String, url :String)(implicit s: Session): Unit =
AccountWebHooks.filter(_.byPrimaryKey(owner, url)).delete
def callWebHookOf(owner: String, repository: String, event: WebHook.Event)(makePayload: => Option[WebHookPayload]) def callWebHookOf(owner: String, repository: String, event: WebHook.Event)(makePayload: => Option[WebHookPayload])
(implicit s: Session, c: JsonFormat.Context): Unit = { (implicit s: Session, c: JsonFormat.Context): Unit = {
@@ -78,6 +119,10 @@ trait WebHookService {
if(webHooks.nonEmpty){ if(webHooks.nonEmpty){
makePayload.map(callWebHook(event, webHooks, _)) makePayload.map(callWebHook(event, webHooks, _))
} }
val accountWebHooks = getAccountWebHooksByEvent(owner, event)
if(accountWebHooks.nonEmpty){
makePayload.map(callWebHook(event, accountWebHooks, _))
}
} }
def callWebHook(event: WebHook.Event, webHooks: List[WebHook], payload: WebHookPayload) def callWebHook(event: WebHook.Event, webHooks: List[WebHook], payload: WebHookPayload)
@@ -207,7 +252,7 @@ trait WebHookPullRequestService extends WebHookService {
/** @return Map[(issue, issueUser, pullRequest, baseOwner, headOwner), webHooks] */ /** @return Map[(issue, issueUser, pullRequest, baseOwner, headOwner), webHooks] */
def getPullRequestsByRequestForWebhook(userName:String, repositoryName:String, branch:String) def getPullRequestsByRequestForWebhook(userName:String, repositoryName:String, branch:String)
(implicit s: Session): Map[(Issue, Account, PullRequest, Account, Account), List[WebHook]] = (implicit s: Session): Map[(Issue, Account, PullRequest, Account, Account), List[RepositoryWebHook]] =
(for{ (for{
is <- Issues if is.closed === false.bind is <- Issues if is.closed === false.bind
pr <- PullRequests if pr.byPrimaryKey(is.userName, is.repositoryName, is.issueId) pr <- PullRequests if pr.byPrimaryKey(is.userName, is.repositoryName, is.issueId)
@@ -217,8 +262,8 @@ trait WebHookPullRequestService extends WebHookService {
bu <- Accounts if bu.userName === pr.userName bu <- Accounts if bu.userName === pr.userName
ru <- Accounts if ru.userName === pr.requestUserName ru <- Accounts if ru.userName === pr.requestUserName
iu <- Accounts if iu.userName === is.openedUserName iu <- Accounts if iu.userName === is.openedUserName
wh <- WebHooks if wh.byRepository(is.userName , is.repositoryName) wh <- RepositoryWebHooks if wh.byRepository(is.userName , is.repositoryName)
wht <- WebHookEvents if wht.event === WebHook.PullRequest.asInstanceOf[WebHook.Event].bind && wht.byWebHook(wh) wht <- RepositoryWebHookEvents if wht.event === WebHook.PullRequest.asInstanceOf[WebHook.Event].bind && wht.byRepositoryWebHook(wh)
} yield { } yield {
((is, iu, pr, bu, ru), wh) ((is, iu, pr, bu, ru), wh)
}).list.groupBy(_._1).mapValues(_.map(_._2)) }).list.groupBy(_._1).mapValues(_.map(_._2))
@@ -345,6 +390,17 @@ object WebHookService {
repositoryInfo, repositoryInfo,
owner= ApiUser(repositoryOwner)) owner= ApiUser(repositoryOwner))
) )
def createDummyPayload(sender: Account): WebHookPushPayload =
WebHookPushPayload(
pusher = ApiPusher(sender),
sender = ApiUser(sender),
ref = "refs/heads/master",
before = "adc83b19e793491b1c6ea0fd8b46cd9f32e592fc",
after = "adc83b19e793491b1c6ea0fd8b46cd9f32e592fc",
commits = List.empty,
repository = ApiRepository.forDummyPayload(ApiUser(sender))
)
} }
// https://developer.github.com/v3/activity/events/types/#issuesevent // https://developer.github.com/v3/activity/events/types/#issuesevent

View File

@@ -0,0 +1,191 @@
@(webHook: gitbucket.core.model.AccountWebHook,
events: Set[gitbucket.core.model.WebHook.Event],
account: gitbucket.core.model.Account,
groupNames: List[String],
info: Option[Any],
create: Boolean)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@import gitbucket.core.model.WebHook._
@import gitbucket.core.model.WebHookContentType
@check(name: String, event: Event) = {
name="@(name).@event.name" value="on" @if(events(event)){checked}
}
@gitbucket.core.account.html.main(account, groupNames, "webhooks"){
<div class="panel panel-default">
<div class="panel-heading strong">Webhook / Manage webhook</div>
<div class="panel-body">
<form method="POST" validate="true">
<fieldset class="form-group">
<label class="strong">Payload URL</label>
<div>
<span class="error" id="error-url"></span>
</div>
@if(create){
<input type="text" name="url" id="url" value="@webHook.url" class="form-control" style="display: inline; width: 500px; vertical-align: middle;" required />
} else {
<input type="text" value="@webHook.url" class="form-control" style="display: inline; width: 500px; vertical-align: middle;" disabled />
<input type="hidden" value="@webHook.url" name="url" />
}
<button class="btn btn-default" id="test">Test Hook</button>
</fieldset>
<fieldset class="form-group">
<label class="strong">Content type</label>
<div></div>
<select name="ctype">
<option value="@WebHookContentType.FORM.code" @if(webHook.ctype == WebHookContentType.FORM){selected}>@WebHookContentType.FORM.ctype</option>
<option value="@WebHookContentType.JSON.code" @if(webHook.ctype == WebHookContentType.JSON){selected}>@WebHookContentType.JSON.ctype</option>
</select>
</fieldset>
<fieldset class="form-group">
<label class="strong">Security Token</label>
<div></div>
<input type="text" name="token" id="token" placeholder="leave blank for no X-Hub-Signature usage" value="@webHook.token" class="form-control" style="display: inline; width: 500px; vertical-align: middle;" />
</fieldset>
<hr />
<label class="strong">Which events would you like to trigger this webhook?</label>
<div>
<span class="error" id="error-events"></span>
</div>
<!--
<label class="checkbox"><input type="checkbox" @check("events",CommitComment) />Commit comment <small class="help-block">Commit or diff commented on. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",Create) />Create <small class="help-block">Branch, or tag created. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",Delete) />Delete <small class="help-block">Branch, or tag deleted. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",Deployment) />Deployment <small class="help-block">Repository deployed. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",DeploymentStatus) />Deployment status <small class="help-block">Deployment status updated from the API. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",Fork) />Fork <small class="help-block">Repository forked. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",Gollum) />Gollum <small class="help-block">Wiki page updated. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",Member) />Member <small class="help-block">Collaborator added to a non-organization repository. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",PageBuild) />Page build <small class="help-block">Pages site built. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",Public) />Public <small class="help-block">Repository changes from private to public. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",Release) />Release <small class="help-block">Release published in a repository. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",TeamAdd) />Team add <small class="help-block">Team added or modified on a repository. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",Watch) />Watch <small class="help-block">User stars a repository.</small></label>
<label class="checkbox"><input type="checkbox" @check("events",Status) />Status <small class="help-block">Commit status updated from the API. </small> </label>
-->
<label class="checkbox"><input type="checkbox" @check("events",IssueComment) />Issue comment <span class="help-block normal">Issue commented on. </span> </label>
<label class="checkbox"><input type="checkbox" @check("events",Issues) />Issues <span class="help-block normal">Issue opened, closed<!-- , assigned, or labeled -->. </span> </label>
<label class="checkbox"><input type="checkbox" @check("events",PullRequest) />Pull request <span class="help-block normal">Pull request opened, closed<!-- , assigned, labeled -->, or synchronized. </span> </label>
<label class="checkbox"><input type="checkbox" @check("events",PullRequestReviewComment) />Pull request review comment <span class="help-block normal">Pull request diff commented on. </span> </label>
<label class="checkbox"><input type="checkbox" @check("events",Push) />Push <span class="help-block normal">Git push to a repository. </span> </label>
<div class="text-right">
@if(!create){
<input type="submit" class="btn btn-success" value="Update webhook" formaction="@helpers.url(account.userName)/_hooks/edit" />
<a href="@helpers.url(account.userName)/_hooks/delete?url=@helpers.urlEncode(webHook.url)" class="btn btn-danger" onclick="return confirm('delete webhook for @webHook.url ?')">
Delete webhook
</a>
} else {
<input type="submit" class="btn btn-success" value="Add webhook" formaction="@helpers.url(account.userName)/_hooks/new" />
}
</div>
</form>
</div>
</div>
<div class="modal" id="test-report-modal" role="dialog" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>WebHook Test</h3>
</div>
<div class="modal-body" style="max-height: 300px; overflow: auto;">
<p>request to <span id="test-modal-url" style="word-break: break-all; word-wrap: break-word; white-space: pre; white-space: pre-wrap;"></span></p>
<div id="test-report" style="display:none">
<ul class="nav nav-tabs" id="test-report-tab">
<li class="active"><a href="#request">Request</a></li>
<li><a href="#response">Response <span class="badge badge-success" id="res-status"></span></a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="request">
<div id="req-errors" class="alert alert-error">
ERROR<span id="req-errors-body"></span>
</div>
<div id="req-success" style="display:none">
Headers
<pre id="req-headers"></pre>
Payload
<pre id="req-payload"></pre>
</div>
</div>
<div class="tab-pane" id="response">
<div id="res-errors" class="alert alert-error">
ERROR<span id="res-errors-body"></span>
</div>
<div id="res-success" style="display:none">
Headers
<pre id="res-headers"></pre>
Body
<pre id="res-body"></pre>
</div>
</div>
</div>
</div>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<script>
$(function(){
$('#test-report-tab a').click(function (e) {
e.preventDefault();
$(this).tab('show');
});
$('#test').click(function(e){
e.stopPropagation();
e.stopImmediatePropagation();
e.preventDefault();
var url = this.form.url.value;
var token = this.form.token.value;
var ctype = this.form.ctype.value;
if(!/^https?:\/\/.+/.test(url)){
alert("invalid url");
return;
}
$("#test-modal-url").text(url)
$("#test-report-modal").modal('show')
$("#test-report").hide();
var targetUrl = '@helpers.url(account.userName)/_hooks/test?url=' + encodeURIComponent(url) + '&ctype=' + ctype + '&token=';
if (token) {
targetUrl = targetUrl + encodeURIComponent(token);
}
$.ajax({
method:'POST',
url:targetUrl,
success: function(e){
//console.log(e);
$('#test-report-tab a:first').tab('show');
$("#test-report").show();
$("#req-success").toggle(e.request&&!e.request.error);
$("#req-errors").toggle(e.request&&!!e.request.error);
$("#req-errors-body").text(e.request.error);
function headers(h){
h = h["headers"];
return h ? $.map(h, function(h){
return $("<div>").append($('<b>').text(h[0] + ":"),$('<span>').text(" " + h[1]))
}):"";
}
$("#req-headers").html(headers(e.request));
$("#req-payload").text(e.request && e.request.payload ? JSON.stringify(JSON.parse(e.request.payload),undefined,4) : "");
$("#res-success").toggle(e.responce && !e.responce.error);
$("#res-errors").toggle(e.responce && !!e.responce.error);
$("#res-errors-body").text(e.responce.error);
var success = !!(e.responce && e.responce.status && /^2\d\d$/.test(e.responce.status.statusCode));
$("#res-status").text((e.responce && e.responce.status && e.responce.status.statusCode) || "ERROR");
$("#res-status").toggleClass("badge-success", success).toggleClass("badge-important", !success);
$("#res-headers").html(headers(e.responce));
$("#res-body").text(e.responce && e.responce.body ? e.responce.body : "");
},
error:function (e) {
if(e) {
console.log(e.responseText, e);
alert("request error ( http status " + e.status + " error on gitbugket or browser to gitbucket. show details on your javascript console )");
}else{
alert("unknown javascript error (please report to gitbucket team)");
}
$("#test-report-modal").modal('hide')
}
});
return false;
});
})
</script>
}

View File

@@ -43,6 +43,7 @@
} else { } else {
<li@if(active == "activity"){ class="active"}><a href="@helpers.url(account.userName)?tab=activity">Public activity</a></li> <li@if(active == "activity"){ class="active"}><a href="@helpers.url(account.userName)?tab=activity">Public activity</a></li>
} }
<li@if(active == "webhooks"){ class="active"}><a href="@helpers.url(account.userName)?tab=webhooks">Webhooks</a></li>
@gitbucket.core.plugin.PluginRegistry().getProfileTabs.map { tab => @gitbucket.core.plugin.PluginRegistry().getProfileTabs.map { tab =>
@tab(account, context).map { link => @tab(account, context).map { link =>
<li@if(active == link.id){ class="active"}><a href="@context.path/@link.path">@link.label</a></li> <li@if(active == link.id){ class="active"}><a href="@context.path/@link.path">@link.label</a></li>

View File

@@ -0,0 +1,39 @@
@(account: gitbucket.core.model.Account,
groupNames: List[String],
webHooks: List[(gitbucket.core.model.AccountWebHook, Set[gitbucket.core.model.WebHook.Event])])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.account.html.main(account, groupNames, "webhooks"){
<div class="panel panel-default">
<div class="panel-heading strong">
Webhooks
</div>
<div class="panel-body">
<p>
Webhooks allow external services to be notified when certain events happen within your repository.
When the specified events happen, well send a POST request to each of the URLs you provide.
Learn more in <a href="https://github.com/takezoe/gitbucket/wiki/API-WebHook" target="_blank">GitBucket Wiki Webhook Page</a>.
</p>
<a href="@helpers.url(account.userName)/_hooks/new" class="btn btn-success pull-right" style="margin-bottom: 10px;">Add webhook</a>
<table class="table table-condensed" style="margin-bottom:0px;">
@webHooks.map { case (webHook, events) =>
<tr><td style="vertical-align: middle;">
<a href="@helpers.url(account.userName)/_hooks/edit?url=@helpers.urlEncode(webHook.url)" class="css-truncate" style="max-width:360px">
<span class="css-truncate-target">@webHook.url</span>
</a>
<em class="css-truncate" style="max-width: 225px;">(<span class="css-truncate-target">@events.map(_.name).mkString(", ")</span>)</em>
</td><td>
<div class="btn-group pull-right">
<a href="@helpers.url(account.userName)/_hooks/edit?url=@helpers.urlEncode(webHook.url)" class="btn btn-default">
<span class="octicon octicon-pencil"></span>
</a>
<a href="@helpers.url(account.userName)/_hooks/delete?url=@helpers.urlEncode(webHook.url)" class="btn btn-danger" onclick="return confirm('delete webhook for @webHook.url ?')">
<span class="octicon octicon-x"></span>
</a>
</div>
</td></tr>
}
</table>
</div>
</div>
}

View File

@@ -1,6 +1,6 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.model.WebHook import gitbucket.core.model.{WebHook, RepositoryWebHook}
import org.scalatest.FunSuite import org.scalatest.FunSuite
import gitbucket.core.model.WebHookContentType import gitbucket.core.model.WebHookContentType
@@ -47,17 +47,17 @@ class WebHookServiceSpec extends FunSuite with ServiceSpecBase {
val formType = WebHookContentType.FORM val formType = WebHookContentType.FORM
val jsonType = WebHookContentType.JSON val jsonType = WebHookContentType.JSON
service.addWebHook("user1", "repo1", "http://example.com", Set(WebHook.PullRequest), formType, Some("key")) service.addWebHook("user1", "repo1", "http://example.com", Set(WebHook.PullRequest), formType, Some("key"))
assert(service.getWebHooks("user1", "repo1") == List((WebHook("user1","repo1","http://example.com", formType, Some("key")),Set(WebHook.PullRequest)))) assert(service.getWebHooks("user1", "repo1") == List((RepositoryWebHook("user1","repo1","http://example.com", formType, Some("key")),Set(WebHook.PullRequest))))
assert(service.getWebHook("user1", "repo1", "http://example.com") == Some((WebHook("user1","repo1","http://example.com", formType, Some("key")),Set(WebHook.PullRequest)))) assert(service.getWebHook("user1", "repo1", "http://example.com") == Some((RepositoryWebHook("user1","repo1","http://example.com", formType, Some("key")),Set(WebHook.PullRequest))))
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == List((WebHook("user1","repo1","http://example.com", formType, Some("key"))))) assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == List((RepositoryWebHook("user1","repo1","http://example.com", formType, Some("key")))))
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.Push) == Nil) assert(service.getWebHooksByEvent("user1", "repo1", WebHook.Push) == Nil)
assert(service.getWebHook("user1", "repo1", "http://example.com2") == None) assert(service.getWebHook("user1", "repo1", "http://example.com2") == None)
assert(service.getWebHook("user2", "repo1", "http://example.com") == None) assert(service.getWebHook("user2", "repo1", "http://example.com") == None)
assert(service.getWebHook("user1", "repo2", "http://example.com") == None) assert(service.getWebHook("user1", "repo2", "http://example.com") == None)
service.updateWebHook("user1", "repo1", "http://example.com", Set(WebHook.Push, WebHook.Issues), jsonType, Some("key")) service.updateWebHook("user1", "repo1", "http://example.com", Set(WebHook.Push, WebHook.Issues), jsonType, Some("key"))
assert(service.getWebHook("user1", "repo1", "http://example.com") == Some((WebHook("user1","repo1","http://example.com", jsonType, Some("key")),Set(WebHook.Push, WebHook.Issues)))) assert(service.getWebHook("user1", "repo1", "http://example.com") == Some((RepositoryWebHook("user1","repo1","http://example.com", jsonType, Some("key")),Set(WebHook.Push, WebHook.Issues))))
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == Nil) assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == Nil)
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.Push) == List((WebHook("user1","repo1","http://example.com", jsonType, Some("key"))))) assert(service.getWebHooksByEvent("user1", "repo1", WebHook.Push) == List((RepositoryWebHook("user1","repo1","http://example.com", jsonType, Some("key")))))
service.deleteWebHook("user1", "repo1", "http://example.com") service.deleteWebHook("user1", "repo1", "http://example.com")
assert(service.getWebHook("user1", "repo1", "http://example.com") == None) assert(service.getWebHook("user1", "repo1", "http://example.com") == None)
} } } }
@@ -69,11 +69,11 @@ class WebHookServiceSpec extends FunSuite with ServiceSpecBase {
service.addWebHook("user1", "repo1", "http://example.com/2", Set(WebHook.Push), ctype, Some("key")) service.addWebHook("user1", "repo1", "http://example.com/2", Set(WebHook.Push), ctype, Some("key"))
service.addWebHook("user1", "repo1", "http://example.com/3", Set(WebHook.PullRequest,WebHook.Push), ctype, Some("key")) service.addWebHook("user1", "repo1", "http://example.com/3", Set(WebHook.PullRequest,WebHook.Push), ctype, Some("key"))
assert(service.getWebHooks("user1", "repo1") == List( assert(service.getWebHooks("user1", "repo1") == List(
WebHook("user1","repo1","http://example.com/1", ctype, Some("key"))->Set(WebHook.PullRequest), RepositoryWebHook("user1","repo1","http://example.com/1", ctype, Some("key"))->Set(WebHook.PullRequest),
WebHook("user1","repo1","http://example.com/2", ctype, Some("key"))->Set(WebHook.Push), RepositoryWebHook("user1","repo1","http://example.com/2", ctype, Some("key"))->Set(WebHook.Push),
WebHook("user1","repo1","http://example.com/3", ctype, Some("key"))->Set(WebHook.PullRequest,WebHook.Push))) RepositoryWebHook("user1","repo1","http://example.com/3", ctype, Some("key"))->Set(WebHook.PullRequest,WebHook.Push)))
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == List( assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == List(
WebHook("user1","repo1","http://example.com/1", ctype, Some("key")), RepositoryWebHook("user1","repo1","http://example.com/1", ctype, Some("key")),
WebHook("user1","repo1","http://example.com/3", ctype, Some("key")))) RepositoryWebHook("user1","repo1","http://example.com/3", ctype, Some("key"))))
} } } }
} }