feature/can select webhook events

This commit is contained in:
nazoking
2015-10-13 18:48:02 +09:00
parent 30a8cefc37
commit a08a212fdc
12 changed files with 501 additions and 77 deletions

View File

@@ -0,0 +1,22 @@
DROP TABLE IF EXISTS WEB_HOOK_EVENT;
CREATE TABLE WEB_HOOK_EVENT(
USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL,
URL VARCHAR(200) NOT NULL,
EVENT VARCHAR(30) NOT NULL
);
ALTER TABLE WEB_HOOK_EVENT ADD CONSTRAINT IDX_WEB_HOOK_EVENT_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, URL, EVENT);
ALTER TABLE WEB_HOOK_EVENT ADD CONSTRAINT IDX_WEB_HOOK_EVENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, URL) REFERENCES WEB_HOOK (USER_NAME, REPOSITORY_NAME, URL)
ON DELETE CASCADE ON UPDATE CASCADE;
CREATE TEMPORARY TABLE TMP_EVENTS (EVENT VARCHAR(30));
INSERT INTO TMP_EVENTS VALUES ('push'),('issue_comment'),('issues'),('pull_request');
INSERT INTO WEB_HOOK_EVENT (USER_NAME, REPOSITORY_NAME, URL, EVENT)
SELECT USER_NAME, REPOSITORY_NAME, URL, EVENT
FROM WEB_HOOK, TMP_EVENTS;
DROP TABLE TMP_EVENTS;

View File

@@ -14,6 +14,7 @@ import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Constants import org.eclipse.jgit.lib.Constants
import scala.util.{Success, Failure}
class RepositorySettingsController extends RepositorySettingsControllerBase class RepositorySettingsController extends RepositorySettingsControllerBase
@@ -42,10 +43,11 @@ trait RepositorySettingsControllerBase extends ControllerBase {
)(CollaboratorForm.apply) )(CollaboratorForm.apply)
// for web hook url addition // for web hook url addition
case class WebHookForm(url: String) case class WebHookForm(url: String, events: Set[WebHook.Event])
val webHookForm = mapping( def webHookForm(update:Boolean) = mapping(
"url" -> trim(label("url", text(required, webHook))) "url" -> trim(label("url", text(required, webHook(update)))),
"events" -> webhookEvents
)(WebHookForm.apply) )(WebHookForm.apply)
// for transfer ownership // for transfer ownership
@@ -138,14 +140,23 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the web hook page. * Display the web hook page.
*/ */
get("/:owner/:repository/settings/hooks")(ownerOnly { repository => get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
html.hooks(getWebHookURLs(repository.owner, repository.name), flash.get("url"), repository, flash.get("info")) html.hooks(getWebHooks(repository.owner, repository.name), repository, flash.get("info"))
})
/**
* Display the web hook edit page.
*/
get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository =>
val webhook = WebHook(repository.owner, repository.name, "")
html.editHooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true)
}) })
/** /**
* Add the web hook URL. * Add the web hook URL.
*/ */
post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) => post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly { (form, repository) =>
addWebHookURL(repository.owner, repository.name, form.url) addWebHook(repository.owner, repository.name, form.url, form.events)
flash += "info" -> s"Webhook ${form.url} created"
redirect(s"/${repository.owner}/${repository.name}/settings/hooks") redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
}) })
@@ -153,30 +164,71 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Delete the web hook URL. * Delete the web hook URL.
*/ */
get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository => get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository =>
deleteWebHookURL(repository.owner, repository.name, params("url")) deleteWebHook(repository.owner, repository.name, params("url"))
flash += "info" -> s"Webhook ${params("url")} deleted"
redirect(s"/${repository.owner}/${repository.name}/settings/hooks") redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
}) })
/** /**
* Send the test request to registered web hook URLs. * Send the test request to registered web hook URLs.
*/ */
post("/:owner/:repository/settings/hooks/test", webHookForm)(ownerOnly { (form, repository) => ajaxPost("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
import scala.collection.JavaConverters._ 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
val url = params("url")
val commits = if(repository.commitCount == 0) List.empty else git.log val commits = if(repository.commitCount == 0) List.empty else git.log
.add(git.getRepository.resolve(repository.repository.defaultBranch)) .add(git.getRepository.resolve(repository.repository.defaultBranch))
.setMaxCount(3) .setMaxCount(3)
.call.iterator.asScala.map(new CommitInfo(_)) .call.iterator.asScala.map(new CommitInfo(_))
val ownerAccount = getAccountByUserName(repository.owner).get
getAccountByUserName(repository.owner).foreach { ownerAccount => def headers(h: Array[org.apache.http.Header]): Array[Array[String]] = h.map{ h => Array(h.getName, h.getValue) }
callWebHook("push", val toErrorMap:PartialFunction[Throwable, Map[String,String]] = {
List(WebHook(repository.owner, repository.name, form.url)), 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))
}
val (webHook, json, reqFuture, resFuture) = callWebHook(WebHook.Push,
List(WebHook(repository.owner, repository.name, url)),
WebHookPushPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount) WebHookPushPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)
) ).head
} contentType = formats("json")
flash += "url" -> form.url var result = Map(
flash += "info" -> "Test payload deployed!" "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))
org.json4s.jackson.Serialization.write(result)
} }
})
/**
* Display the web hook edit page.
*/
get("/:owner/:repository/settings/hooks/edit/:url")(ownerOnly { repository =>
getWebHook(repository.owner, repository.name, params("url")).map{ case (webhook, events) =>
html.editHooks(webhook, events, repository, flash.get("info"), false)
} getOrElse NotFound
})
/**
* Update web hook settings.
*/
post("/:owner/:repository/settings/hooks/edit/:url", webHookForm(true))(ownerOnly { (form, repository) =>
updateWebHook(repository.owner, repository.name, form.url, form.events)
flash += "info" -> s"webhook ${form.url} updated"
redirect(s"/${repository.owner}/${repository.name}/settings/hooks") redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
}) })
@@ -226,9 +278,28 @@ trait RepositorySettingsControllerBase extends ControllerBase {
/** /**
* Provides duplication check for web hook url. * Provides duplication check for web hook url.
*/ */
private def webHook: Constraint = new Constraint(){ private def webHook(needExists: Boolean): 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] =
getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.") if(getWebHook(params("owner"), params("repository"), value).isDefined != needExists){
Some(if(needExists){
"URL had not been registered yet."
}else{
"URL had been registered already."
})
} else {
None
}
}
private def webhookEvents = 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
}
} }
/** /**

View File

@@ -11,7 +11,7 @@ import gitbucket.core.util.StringUtil._
import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import gitbucket.core.model.{Account, CommitState} import gitbucket.core.model.{Account, CommitState, WebHook}
import gitbucket.core.service.CommitStatusService import gitbucket.core.service.CommitStatusService
import gitbucket.core.service.WebHookService._ import gitbucket.core.service.WebHookService._
import gitbucket.core.view import gitbucket.core.view
@@ -663,7 +663,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
// call web hook // call web hook
callPullRequestWebHookByRequestBranch("synchronize", repository, branch, context.baseUrl, loginAccount) callPullRequestWebHookByRequestBranch("synchronize", repository, branch, context.baseUrl, loginAccount)
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId)) val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
callWebHookOf(repository.owner, repository.name, "push") { callWebHookOf(repository.owner, repository.name, WebHook.Push) {
getAccountByUserName(repository.owner).map{ ownerAccount => getAccountByUserName(repository.owner).map{ ownerAccount =>
WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount) WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount)
} }

View File

@@ -48,6 +48,7 @@ trait CoreProfile extends ProfileProvider with Profile
with RepositoryComponent with RepositoryComponent
with SshKeyComponent with SshKeyComponent
with WebHookComponent with WebHookComponent
with WebHookEventComponent
with PluginComponent with PluginComponent
object Profile extends CoreProfile object Profile extends CoreProfile

View File

@@ -7,7 +7,7 @@ trait WebHookComponent extends TemplateComponent { self: Profile =>
class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate { class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate {
val url = column[String]("URL") val url = column[String]("URL")
def * = (userName, repositoryName, url) <> (WebHook.tupled, WebHook.unapply) def * = (userName, repositoryName, url) <> ((WebHook.apply _).tupled, WebHook.unapply)
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind) def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
} }
@@ -18,3 +18,32 @@ case class WebHook(
repositoryName: String, repositoryName: String,
url: String url: String
) )
object WebHook {
sealed class Event(var name: String)
case object CommitComment extends Event("commit_comment")
case object Create extends Event("create")
case object Delete extends Event("delete")
case object Deployment extends Event("deployment")
case object DeploymentStatus extends Event("deployment_status")
case object Fork extends Event("fork")
case object Gollum extends Event("gollum")
case object IssueComment extends Event("issue_comment")
case object Issues extends Event("issues")
case object Member extends Event("member")
case object PageBuild extends Event("page_build")
case object Public extends Event("public")
case object PullRequest extends Event("pull_request")
case object PullRequestReviewComment extends Event("pull_request_review_comment")
case object Push extends Event("push")
case object Release extends Event("release")
case object Status extends Event("status")
case object TeamAdd extends Event("team_add")
case object Watch extends Event("watch")
object Event{
val values = List(CommitComment,Create,Delete,Deployment,DeploymentStatus,Fork,Gollum,IssueComment,Issues,Member,PageBuild,Public,PullRequest,PullRequestReviewComment,Push,Release,Status,TeamAdd,Watch)
private val map:Map[String,Event] = values.map(e => e.name -> e).toMap
def valueOf(name: String): Event = map(name)
def valueOpt(name: String): Option[Event] = map.get(name)
}
}

View File

@@ -0,0 +1,30 @@
package gitbucket.core.model
trait WebHookEventComponent extends TemplateComponent { self: Profile =>
import profile.simple._
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: Column[String], repository: Column[String], url: Column[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

@@ -1,7 +1,7 @@
package gitbucket.core.service package gitbucket.core.service
import gitbucket.core.api._ import gitbucket.core.api._
import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment} import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment, WebHookEvent}
import gitbucket.core.model.Profile._ import gitbucket.core.model.Profile._
import profile.simple._ import profile.simple._
import gitbucket.core.util.JGitUtil.CommitInfo import gitbucket.core.util.JGitUtil.CommitInfo
@@ -13,6 +13,9 @@ import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.message.BasicNameValuePair import org.apache.http.message.BasicNameValuePair
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import scala.concurrent._
import org.apache.http.HttpRequest
import org.apache.http.HttpResponse
trait WebHookService { trait WebHookService {
@@ -20,46 +23,91 @@ trait WebHookService {
private val logger = LoggerFactory.getLogger(classOf[WebHookService]) private val logger = LoggerFactory.getLogger(classOf[WebHookService])
def getWebHookURLs(owner: String, repository: String)(implicit s: Session): List[WebHook] = /** get All WebHook informations of repository */
WebHooks.filter(_.byRepository(owner, repository)).sortBy(_.url).list def getWebHooks(owner: String, repository: String)(implicit s: Session): List[(WebHook, Set[WebHook.Event])] =
WebHooks.filter(_.byRepository(owner, repository))
.innerJoin(WebHookEvents).on { (w, t) => t.byWebHook(w) }
.map{ case (w,t) => w -> t.event }
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).toList.sortBy(_._1.url)
def addWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit = /** get All WebHook informations of repository event */
def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[WebHook] =
WebHookEvents.filter(t => t.byRepository(owner, repository) && t.event === event.bind)
.list.map(t => WebHook(t.userName, t.repositoryName, t.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])] =
WebHooks
.filter(_.byPrimaryKey(owner, repository, url))
.innerJoin(WebHookEvents).on { (w, t) => t.byWebHook(w) }
.map{ case (w,t) => w -> t.event }
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption
def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = {
WebHooks insert WebHook(owner, repository, url) WebHooks insert WebHook(owner, repository, url)
events.toSet.map{ event: WebHook.Event =>
WebHookEvents insert WebHookEvent(owner, repository, url, event)
}
}
def deleteWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit = def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = {
WebHookEvents.filter(_.byWebHook(owner, repository, url)).delete
events.toSet.map{ event: WebHook.Event =>
WebHookEvents insert WebHookEvent(owner, repository, url, event)
}
}
def deleteWebHook(owner: String, repository: String, url :String)(implicit s: Session): Unit =
WebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete WebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete
def callWebHookOf(owner: String, repository: String, eventName: String)(makePayload: => Option[WebHookPayload])(implicit s: Session, c: JsonFormat.Context): Unit = { def callWebHookOf(owner: String, repository: String, event: WebHook.Event)(makePayload: => Option[WebHookPayload])(implicit s: Session, c: JsonFormat.Context): Unit = {
val webHookURLs = getWebHookURLs(owner, repository) val webHooks = getWebHooksByEvent(owner, repository, event)
if(webHookURLs.nonEmpty){ if(webHooks.nonEmpty){
makePayload.map(callWebHook(eventName, webHookURLs, _)) makePayload.map(callWebHook(event, webHooks, _))
} }
} }
def callWebHook(eventName: String, webHookURLs: List[WebHook], payload: WebHookPayload)(implicit c: JsonFormat.Context): Unit = { def callWebHook(event: WebHook.Event, webHookURLs: List[WebHook], payload: WebHookPayload)(implicit c: JsonFormat.Context): List[(WebHook, String, Future[HttpRequest], Future[HttpResponse])] = {
import org.apache.http.client.methods.HttpPost
import org.apache.http.impl.client.HttpClientBuilder import org.apache.http.impl.client.HttpClientBuilder
import scala.concurrent._
import ExecutionContext.Implicits.global import ExecutionContext.Implicits.global
import org.apache.http.protocol.HttpContext
import org.apache.http.client.methods.HttpPost
if(webHookURLs.nonEmpty){ if(webHookURLs.nonEmpty){
val json = JsonFormat(payload) val json = JsonFormat(payload)
val httpClient = HttpClientBuilder.create.build
webHookURLs.foreach { webHookUrl => webHookURLs.map { webHookUrl =>
val reqPromise = Promise[HttpRequest]
val f = Future { val f = Future {
logger.debug(s"start web hook invocation for ${webHookUrl}") val itcp = new org.apache.http.HttpRequestInterceptor{
def process(res: HttpRequest, ctx: HttpContext): Unit = {
reqPromise.success(res)
}
}
try{
val httpClient = HttpClientBuilder.create.addInterceptorLast(itcp).build
logger.debug(s"start web hook invocation for ${webHookUrl.url}")
val httpPost = new HttpPost(webHookUrl.url) val httpPost = new HttpPost(webHookUrl.url)
httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded") httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded")
httpPost.addHeader("X-Github-Event", eventName) httpPost.addHeader("X-Github-Event", event.name)
httpPost.addHeader("X-Github-Delivery", java.util.UUID.randomUUID().toString)
val params: java.util.List[NameValuePair] = new java.util.ArrayList() val params: java.util.List[NameValuePair] = new java.util.ArrayList()
params.add(new BasicNameValuePair("payload", json)) params.add(new BasicNameValuePair("payload", json))
httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8")) httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8"))
httpClient.execute(httpPost) val res = httpClient.execute(httpPost)
httpPost.releaseConnection() httpPost.releaseConnection()
logger.debug(s"end web hook invocation for ${webHookUrl}") logger.debug(s"end web hook invocation for ${webHookUrl}")
res
}catch{
case e:Throwable => {
if(!reqPromise.isCompleted){
reqPromise.failure(e)
}
throw e
}
}
} }
f.onSuccess { f.onSuccess {
case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}") case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}")
@@ -67,9 +115,12 @@ trait WebHookService {
f.onFailure { f.onFailure {
case t => logger.error(s"Failed: web hook request to ${webHookUrl.url}", t) case t => logger.error(s"Failed: web hook request to ${webHookUrl.url}", t)
} }
(webHookUrl, json, reqPromise.future, f)
} }
} else {
Nil
} }
logger.debug("end callWebHook") // logger.debug("end callWebHook")
} }
} }
@@ -80,7 +131,7 @@ trait WebHookPullRequestService extends WebHookService {
import WebHookService._ import WebHookService._
// https://developer.github.com/v3/activity/events/types/#issuesevent // https://developer.github.com/v3/activity/events/types/#issuesevent
def callIssuesWebHook(action: String, repository: RepositoryService.RepositoryInfo, issue: Issue, baseUrl: String, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = { def callIssuesWebHook(action: String, repository: RepositoryService.RepositoryInfo, issue: Issue, baseUrl: String, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
callWebHookOf(repository.owner, repository.name, "issues"){ callWebHookOf(repository.owner, repository.name, WebHook.Issues){
val users = getAccountsByUserNames(Set(repository.owner, issue.openedUserName), Set(sender)) val users = getAccountsByUserNames(Set(repository.owner, issue.openedUserName), Set(sender))
for{ for{
repoOwner <- users.get(repository.owner) repoOwner <- users.get(repository.owner)
@@ -98,7 +149,7 @@ trait WebHookPullRequestService extends WebHookService {
def callPullRequestWebHook(action: String, repository: RepositoryService.RepositoryInfo, issueId: Int, baseUrl: String, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = { def callPullRequestWebHook(action: String, repository: RepositoryService.RepositoryInfo, issueId: Int, baseUrl: String, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
import WebHookService._ import WebHookService._
callWebHookOf(repository.owner, repository.name, "pull_request"){ callWebHookOf(repository.owner, repository.name, WebHook.PullRequest){
for{ for{
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId) (issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender)) users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
@@ -134,6 +185,7 @@ trait WebHookPullRequestService extends WebHookService {
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 <- WebHooks if wh.byRepository(is.userName , is.repositoryName)
wht <- WebHookEvents if wht.event === WebHook.PullRequest.asInstanceOf[WebHook.Event].bind && wht.byWebHook(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))
@@ -154,7 +206,7 @@ trait WebHookPullRequestService extends WebHookService {
baseRepository = baseRepo, baseRepository = baseRepo,
baseOwner = baseOwner, baseOwner = baseOwner,
sender = sender) sender = sender)
callWebHook("pull_request", webHooks, payload) callWebHook(WebHook.PullRequest, webHooks, payload)
} }
} }
} }
@@ -164,7 +216,7 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
import WebHookService._ import WebHookService._
def callIssueCommentWebHook(repository: RepositoryService.RepositoryInfo, issue: Issue, issueCommentId: Int, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = { def callIssueCommentWebHook(repository: RepositoryService.RepositoryInfo, issue: Issue, issueCommentId: Int, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
callWebHookOf(repository.owner, repository.name, "issue_comment"){ callWebHookOf(repository.owner, repository.name, WebHook.IssueComment){
for{ for{
issueComment <- getComment(repository.owner, repository.name, issueCommentId.toString()) issueComment <- getComment(repository.owner, repository.name, issueCommentId.toString())
users = getAccountsByUserNames(Set(issue.openedUserName, repository.owner, issueComment.commentedUserName), Set(sender)) users = getAccountsByUserNames(Set(issue.openedUserName, repository.owner, issueComment.commentedUserName), Set(sender))

View File

@@ -21,6 +21,7 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version. * The history of versions. A head of this sequence is the current BitBucket version.
*/ */
val versions = Seq( val versions = Seq(
new Version(3, 8),
new Version(3, 7) with SystemSettingsService { new Version(3, 7) with SystemSettingsService {
override def update(conn: Connection, cl: ClassLoader): Unit = { override def update(conn: Connection, cl: ClassLoader): Unit = {
super.update(conn, cl) super.update(conn, cl)

View File

@@ -3,7 +3,7 @@ package gitbucket.core.servlet
import java.io.File import java.io.File
import gitbucket.core.api import gitbucket.core.api
import gitbucket.core.model.Session import gitbucket.core.model.{Session, WebHook}
import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry} import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry}
import gitbucket.core.service.IssuesService.IssueSearchCondition import gitbucket.core.service.IssuesService.IssueSearchCondition
import gitbucket.core.service.WebHookService._ import gitbucket.core.service.WebHookService._
@@ -200,7 +200,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
} }
// call web hook // call web hook
callWebHookOf(owner, repository, "push"){ callWebHookOf(owner, repository, WebHook.Push){
for(pusherAccount <- getAccountByUserName(pusher); for(pusherAccount <- getAccountByUserName(pusher);
ownerAccount <- getAccountByUserName(owner)) yield { ownerAccount <- getAccountByUserName(owner)) yield {
WebHookPushPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount) WebHookPushPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount)

View File

@@ -0,0 +1,165 @@
@(webHook: gitbucket.core.model.WebHook,
events: Set[gitbucket.core.model.WebHook.Event],
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
info: Option[Any],
create: Boolean)(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core.view.helpers._
@import gitbucket.core.model.WebHook._
@check(name: String, event: Event)={
name="@(name).@event.name" value="on" @if(events(event)){checked}
}
@html.main("Settings", Some(repository)){
@html.menu("settings", repository){
@menu("hooks", repository){
@helper.html.information(info)
<div class="box">
<div class="box-header">
Webhook / Manage webhook
</div>
<div class="box-content">
<form method="POST" validate="true">
<div>
<span class="error" id="error-url"></span>
</div>
<label class="strong">Payload URL</label>
@if(create){
<input type="text" name="url" id="url" value="@webHook.url" class="input-xxlarge" style="margin-bottom: 0px;" required />
}else{
<input type="text" value="@webHook.url" style="margin-bottom: 0px;" class="input-xxlarge" disabled />
<input type="hidden" value="@webHook.url" name="url" />
}
<button class="btn" id="test">Test Hook</button>
<hr />
<div>
<span class="error" id="error-events"></span>
</div>
<label class="strong">Which events would you like to trigger this webhook?</label>
<!--
<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",PullRequestReviewComment) />Pull Request review comment <small class="help-block">Pull Request diff commented on. </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 <small class="help-block">Issue commented on. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",Issues) />Issues <small class="help-block">Issue opened, closed<!-- , assigned, or labeled -->. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",PullRequest) />Pull Request <small class="help-block">Pull Request opened, closed<!-- , assigned, labeled -->, or synchronized. </small> </label>
<label class="checkbox"><input type="checkbox" @check("events",Push) />Push <small class="help-block">Git push to a repository. </small> </label>
<hr />
@if(!create){
<input type="submit" class="btn btn-success" value="Update webhook" formaction="@url(repository)/settings/hooks/edit/@urlEncode(webHook.url)" />
<a href="@url(repository)/settings/hooks/delete?url=@urlEncode(webHook.url)" class="btn text-error" onclick="return confirm('delete webhook for @webHook.url ?')">
Delete webhook
</a>
}else{
<input type="submit" class="btn" value="Add webhook" formaction="@url(repository)/settings/hooks/new" />
}
</form>
</div>
</div>
<div class="modal hide" id="test-report-modal">
<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">
<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="#RESPONCE">RESPONCE <span class="badge badge-success" id="res-status"></span></a></li>
</ul>
<div class="tab-content" style="max-height: 300px;">
<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="RESPONCE">
<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>
<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;
if(!/^https?:\/\/.+/.test(url)){
alert("invalid url");
return;
}
$("#test-modal-url").text(url)
$("#test-report-modal").modal('show')
$("#test-report").hide();
$.ajax({
method:'POST',
url:'@url(repository)/settings/hooks/test?url='+encodeURIComponent(url),
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 : "");
},
});
return false;
});
})
</script>
}
}
}

View File

@@ -1,5 +1,4 @@
@(webHooks: List[gitbucket.core.model.WebHook], @(webHooks: List[(gitbucket.core.model.WebHook, Set[gitbucket.core.model.WebHook.Event])],
enteredUrl: Option[Any],
repository: gitbucket.core.service.RepositoryService.RepositoryInfo, repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
info: Option[Any])(implicit context: gitbucket.core.controller.Context) info: Option[Any])(implicit context: gitbucket.core.controller.Context)
@import context._ @import context._
@@ -8,20 +7,42 @@
@html.menu("settings", repository){ @html.menu("settings", repository){
@menu("hooks", repository){ @menu("hooks", repository){
@helper.html.information(info) @helper.html.information(info)
<h3>WebHook URLs</h3>
<ul> <div class="box">
@webHooks.map { webHook => <div class="box-header">
<li>@webHook.url <a href="@url(repository)/settings/hooks/delete?url=@urlEncode(webHook.url)" class="remove">(remove)</a></li> <a href="@url(repository)/settings/hooks/new" class="btn btn-small pull-right">Add webhook</a>
} Webhooks
</ul>
<form method="POST" validate="true">
<div>
<span class="error" id="error-url"></span>
</div> </div>
<input type="text" name="url" id="url" value="@enteredUrl" style="width: 300px; margin-bottom: 0px;"/> <div class="box-content">
<input type="submit" class="btn" formaction="@url(repository)/settings/hooks/add" value="Add"/> <p>
<input type="submit" class="btn" formaction="@url(repository)/settings/hooks/test" value="Test Hook"/> Webhooks allow external services to be notified when certain events happen within your repository.
</form> 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>
<table class="table table-condensed" style="margin-bottom:0">
@webHooks.map { case (webHook, events) =>
<tr><td style="vertical-align: middle;">
<a href="@url(repository)/settings/hooks/edit/@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">
<a href="@url(repository)/settings/hooks/edit/@urlEncode(webHook.url)" class="btn btn-small">
<span class="octicon octicon-pencil"></span>
</a>
<a href="@url(repository)/settings/hooks/delete?url=@urlEncode(webHook.url)" class="btn btn-small 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,7 @@
package gitbucket.core.service package gitbucket.core.service
import org.specs2.mutable.Specification import org.specs2.mutable.Specification
import gitbucket.core.model.WebHook
class WebHookServiceSpec extends Specification with ServiceSpecBase { class WebHookServiceSpec extends Specification with ServiceSpecBase {
@@ -16,12 +17,12 @@ class WebHookServiceSpec extends Specification with ServiceSpecBase {
val (issue3, pullreq3) = generateNewPullRequest("user3/repo3/master3", "user2/repo2/master2", loginUser="root") val (issue3, pullreq3) = generateNewPullRequest("user3/repo3/master3", "user2/repo2/master2", loginUser="root")
val (issue32, pullreq32) = generateNewPullRequest("user3/repo3/master32", "user2/repo2/master2", loginUser="root") val (issue32, pullreq32) = generateNewPullRequest("user3/repo3/master32", "user2/repo2/master2", loginUser="root")
generateNewPullRequest("user2/repo2/master2", "user1/repo1/master2") generateNewPullRequest("user2/repo2/master2", "user1/repo1/master2")
service.addWebHookURL("user1", "repo1", "webhook1-1") service.addWebHook("user1", "repo1", "webhook1-1", Set(WebHook.PullRequest))
service.addWebHookURL("user1", "repo1", "webhook1-2") service.addWebHook("user1", "repo1", "webhook1-2", Set(WebHook.PullRequest))
service.addWebHookURL("user2", "repo2", "webhook2-1") service.addWebHook("user2", "repo2", "webhook2-1", Set(WebHook.PullRequest))
service.addWebHookURL("user2", "repo2", "webhook2-2") service.addWebHook("user2", "repo2", "webhook2-2", Set(WebHook.PullRequest))
service.addWebHookURL("user3", "repo3", "webhook3-1") service.addWebHook("user3", "repo3", "webhook3-1", Set(WebHook.PullRequest))
service.addWebHookURL("user3", "repo3", "webhook3-2") service.addWebHook("user3", "repo3", "webhook3-2", Set(WebHook.PullRequest))
service.getPullRequestsByRequestForWebhook("user1","repo1","master1") must_== Map.empty service.getPullRequestsByRequestForWebhook("user1","repo1","master1") must_== Map.empty
@@ -41,4 +42,35 @@ class WebHookServiceSpec extends Specification with ServiceSpecBase {
r2((issue32, issueUser, pullreq32, user3, user2)) must_== Set("webhook3-1","webhook3-2") r2((issue32, issueUser, pullreq32, user3, user2)) must_== Set("webhook3-1","webhook3-2")
} } } }
} }
"add and get and update and delete" in { withTestDB { implicit session =>
val user1 = generateNewUserWithDBRepository("user1","repo1")
service.addWebHook("user1", "repo1", "http://example.com", Set(WebHook.PullRequest))
service.getWebHooks("user1", "repo1") must_== List((WebHook("user1","repo1","http://example.com"),Set(WebHook.PullRequest)))
service.getWebHook("user1", "repo1", "http://example.com") must_== Some((WebHook("user1","repo1","http://example.com"),Set(WebHook.PullRequest)))
service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) must_== List((WebHook("user1","repo1","http://example.com")))
service.getWebHooksByEvent("user1", "repo1", WebHook.Push) must_== Nil
service.getWebHook("user1", "repo1", "http://example.com2") must_== None
service.getWebHook("user2", "repo1", "http://example.com") must_== None
service.getWebHook("user1", "repo2", "http://example.com") must_== None
service.updateWebHook("user1", "repo1", "http://example.com", Set(WebHook.Push, WebHook.Issues))
service.getWebHook("user1", "repo1", "http://example.com") must_== Some((WebHook("user1","repo1","http://example.com"),Set(WebHook.Push, WebHook.Issues)))
service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) must_== Nil
service.getWebHooksByEvent("user1", "repo1", WebHook.Push) must_== List((WebHook("user1","repo1","http://example.com")))
service.deleteWebHook("user1", "repo1", "http://example.com")
service.getWebHook("user1", "repo1", "http://example.com") must_== None
} }
"getWebHooks, getWebHooksByEvent" in { withTestDB { implicit session =>
val user1 = generateNewUserWithDBRepository("user1","repo1")
service.addWebHook("user1", "repo1", "http://example.com/1", Set(WebHook.PullRequest))
service.addWebHook("user1", "repo1", "http://example.com/2", Set(WebHook.Push))
service.addWebHook("user1", "repo1", "http://example.com/3", Set(WebHook.PullRequest,WebHook.Push))
service.getWebHooks("user1", "repo1") must_== List(
WebHook("user1","repo1","http://example.com/1")->Set(WebHook.PullRequest),
WebHook("user1","repo1","http://example.com/2")->Set(WebHook.Push),
WebHook("user1","repo1","http://example.com/3")->Set(WebHook.PullRequest,WebHook.Push))
service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) must_== List(
WebHook("user1","repo1","http://example.com/1"),
WebHook("user1","repo1","http://example.com/3"))
} }
} }