diff --git a/src/main/scala/gitbucket/core/api/ApiBranchProtection.scala b/src/main/scala/gitbucket/core/api/ApiBranchProtection.scala index 2af86f2..6554e08 100644 --- a/src/main/scala/gitbucket/core/api/ApiBranchProtection.scala +++ b/src/main/scala/gitbucket/core/api/ApiBranchProtection.scala @@ -1,6 +1,6 @@ package gitbucket.core.api -import gitbucket.core.service.ProtectedBrancheService +import gitbucket.core.service.ProtectedBranchService import org.json4s._ /** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */ @@ -12,7 +12,7 @@ /** form for enabling-and-disabling-branch-protection */ case class EnablingAndDisabling(protection: ApiBranchProtection) - def apply(info: ProtectedBrancheService.ProtectedBranchInfo): ApiBranchProtection = ApiBranchProtection( + def apply(info: ProtectedBranchService.ProtectedBranchInfo): ApiBranchProtection = ApiBranchProtection( enabled = info.enabled, required_status_checks = Some(Status(EnforcementLevel(info.enabled, info.includeAdministrators), info.contexts))) val statusNone = Status(Off, Seq.empty) diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala index eaafacc..7a95c42 100644 --- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala +++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala @@ -27,13 +27,13 @@ class PullRequestsController extends PullRequestsControllerBase with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService with CommitsService with ActivityService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator - with CommitStatusService with MergeService with ProtectedBrancheService + with CommitStatusService with MergeService with ProtectedBranchService trait PullRequestsControllerBase extends ControllerBase { self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator - with CommitStatusService with MergeService with ProtectedBrancheService => + with CommitStatusService with MergeService with ProtectedBranchService => private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase]) diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index ac6bd82..2c65df6 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -2,7 +2,7 @@ import gitbucket.core.settings.html import gitbucket.core.model.WebHook -import gitbucket.core.service.{RepositoryService, AccountService, WebHookService, ProtectedBrancheService, CommitStatusService} +import gitbucket.core.service.{RepositoryService, AccountService, WebHookService, ProtectedBranchService, CommitStatusService} import gitbucket.core.service.WebHookService._ import gitbucket.core.util._ import gitbucket.core.util.JGitUtil._ @@ -18,11 +18,11 @@ class RepositorySettingsController extends RepositorySettingsControllerBase - with RepositoryService with AccountService with WebHookService with ProtectedBrancheService with CommitStatusService + with RepositoryService with AccountService with WebHookService with ProtectedBranchService with CommitStatusService with OwnerAuthenticator with UsersAuthenticator trait RepositorySettingsControllerBase extends ControllerBase { - self: RepositoryService with AccountService with WebHookService with ProtectedBrancheService with CommitStatusService + self: RepositoryService with AccountService with WebHookService with ProtectedBranchService with CommitStatusService with OwnerAuthenticator with UsersAuthenticator => // for repository options diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala index 9981033..da9b2d9 100644 --- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala @@ -33,7 +33,7 @@ class RepositoryViewerController extends RepositoryViewerControllerBase with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService - with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBrancheService + with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBranchService /** * The repository viewer. @@ -41,7 +41,7 @@ trait RepositoryViewerControllerBase extends ControllerBase { self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService - with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBrancheService => + with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBranchService => ArchiveCommand.registerFormat("zip", new ZipFormat) ArchiveCommand.registerFormat("tar.gz", new TgzFormat) diff --git a/src/main/scala/gitbucket/core/plugin/CommitHook.scala b/src/main/scala/gitbucket/core/plugin/CommitHook.scala new file mode 100644 index 0000000..bf01e35 --- /dev/null +++ b/src/main/scala/gitbucket/core/plugin/CommitHook.scala @@ -0,0 +1,12 @@ +package gitbucket.core.plugin + +import gitbucket.core.model.Profile._ +import org.eclipse.jgit.transport.ReceiveCommand +import profile.simple._ + +trait CommitHook { + + def hook(owner: String, repository: String, isAllowNonFastForwards: Boolean, command: ReceiveCommand, pusher: String) + (implicit session: Session): Option[String] + +} diff --git a/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala b/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala index b032369..0b87c68 100644 --- a/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala +++ b/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala @@ -6,6 +6,7 @@ import javax.servlet.http.{HttpServletRequest, HttpServletResponse} import gitbucket.core.controller.{Context, ControllerBase} +import gitbucket.core.service.ProtectedBranchService.ProtectedBranchCommitHook import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.service.SystemSettingsService.SystemSettings import gitbucket.core.util.ControlUtil._ @@ -29,6 +30,8 @@ "md" -> MarkdownRenderer, "markdown" -> MarkdownRenderer ) private val repositoryRoutings = new ListBuffer[GitRepositoryRouting] + private val commitHooks = new ListBuffer[CommitHook] + commitHooks += new ProtectedBranchCommitHook() def addPlugin(pluginInfo: PluginInfo): Unit = { plugins += pluginInfo @@ -98,6 +101,12 @@ } } + def addCommitHook(commitHook: CommitHook): Unit = { + commitHooks += commitHook + } + + def getCommitHooks: Seq[CommitHook] = commitHooks.toSeq + private case class GlobalAction( method: String, path: String, diff --git a/src/main/scala/gitbucket/core/service/ProtectedBranchService.scala b/src/main/scala/gitbucket/core/service/ProtectedBranchService.scala new file mode 100644 index 0000000..618106f --- /dev/null +++ b/src/main/scala/gitbucket/core/service/ProtectedBranchService.scala @@ -0,0 +1,121 @@ +package gitbucket.core.service + +import gitbucket.core.model._ +import gitbucket.core.model.Profile._ +import gitbucket.core.plugin.CommitHook +import profile.simple._ + +import org.eclipse.jgit.transport.ReceiveCommand + + +trait ProtectedBranchService { + import ProtectedBranchService._ + private def getProtectedBranchInfoOpt(owner: String, repository: String, branch: String)(implicit session: Session): Option[ProtectedBranchInfo] = + ProtectedBranches + .leftJoin(ProtectedBranchContexts) + .on{ case (pb, c) => pb.byBranch(c.userName, c.repositoryName, c.branch) } + .map{ case (pb, c) => pb -> c.context.? } + .filter(_._1.byPrimaryKey(owner, repository, branch)) + .list + .groupBy(_._1) + .map(p => p._1 -> p._2.flatMap(_._2)) + .map{ case (t1, contexts) => + new ProtectedBranchInfo(t1.userName, t1.repositoryName, true, contexts, t1.statusCheckAdmin) + }.headOption + + def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit session: Session): ProtectedBranchInfo = + getProtectedBranchInfoOpt(owner, repository, branch).getOrElse(ProtectedBranchInfo.disabled(owner, repository)) + + def getProtectedBranchList(owner: String, repository: String)(implicit session: Session): List[String] = + ProtectedBranches.filter(_.byRepository(owner, repository)).map(_.branch).list + + def enableBranchProtection(owner: String, repository: String, branch:String, includeAdministrators: Boolean, contexts: Seq[String]) + (implicit session: Session): Unit = { + disableBranchProtection(owner, repository, branch) + ProtectedBranches.insert(new ProtectedBranch(owner, repository, branch, includeAdministrators && contexts.nonEmpty)) + contexts.map{ context => + ProtectedBranchContexts.insert(new ProtectedBranchContext(owner, repository, branch, context)) + } + } + + def disableBranchProtection(owner: String, repository: String, branch:String)(implicit session: Session): Unit = + ProtectedBranches.filter(_.byPrimaryKey(owner, repository, branch)).delete + +} + +object ProtectedBranchService { + + class ProtectedBranchCommitHook extends CommitHook with ProtectedBranchService { + override def hook(owner: String, repository: String, isAllowNonFastForwards: Boolean, command: ReceiveCommand, pusher: String) + (implicit session: Session): Option[String] = { + val branch = command.getRefName.stripPrefix("refs/heads/") + if(branch != command.getRefName){ + getProtectedBranchInfo(owner, repository, branch).getStopReason(isAllowNonFastForwards, command, pusher) + } else { + None + } + } + } + + + case class ProtectedBranchInfo( + owner: String, + repository: String, + enabled: Boolean, + /** + * Require status checks to pass before merging + * Choose which status checks must pass before branches can be merged into test. + * When enabled, commits must first be pushed to another branch, + * then merged or pushed directly to test after status checks have passed. + */ + contexts: Seq[String], + /** + * Include administrators + * Enforce required status checks for repository administrators. + */ + includeAdministrators: Boolean) extends AccountService with CommitStatusService { + + def isAdministrator(pusher: String)(implicit session: Session): Boolean = + pusher == owner || getGroupMembers(owner).filter(gm => gm.userName == pusher && gm.isManager).nonEmpty + + /** + * Can't be force pushed + * Can't be deleted + * Can't have changes merged into them until required status checks pass + */ + def getStopReason(isAllowNonFastForwards: Boolean, command: ReceiveCommand, pusher: String)(implicit session: Session): Option[String] = { + if(enabled){ + command.getType() match { + case ReceiveCommand.Type.UPDATE|ReceiveCommand.Type.UPDATE_NONFASTFORWARD if isAllowNonFastForwards => + Some("Cannot force-push to a protected branch") + case ReceiveCommand.Type.UPDATE|ReceiveCommand.Type.UPDATE_NONFASTFORWARD if needStatusCheck(pusher) => + unSuccessedContexts(command.getNewId.name) match { + case s if s.size == 1 => Some(s"""Required status check "${s.toSeq(0)}" is expected""") + case s if s.size >= 1 => Some(s"${s.size} of ${contexts.size} required status checks are expected") + case _ => None + } + case ReceiveCommand.Type.DELETE => + Some("Cannot delete a protected branch") + case _ => None + } + }else{ + None + } + } + def unSuccessedContexts(sha1: String)(implicit session: Session): Set[String] = if(contexts.isEmpty){ + Set.empty + } else { + contexts.toSet -- getCommitStatues(owner, repository, sha1).filter(_.state == CommitState.SUCCESS).map(_.context).toSet + } + def needStatusCheck(pusher: String)(implicit session: Session): Boolean = pusher match { + case _ if !enabled => false + case _ if contexts.isEmpty => false + case _ if includeAdministrators => true + case p if isAdministrator(p) => false + case _ => true + } + } + object ProtectedBranchInfo{ + def disabled(owner: String, repository: String): ProtectedBranchInfo = ProtectedBranchInfo(owner, repository, false, Nil, false) + } +} diff --git a/src/main/scala/gitbucket/core/service/ProtectedBrancheService.scala b/src/main/scala/gitbucket/core/service/ProtectedBrancheService.scala deleted file mode 100644 index 97652ae..0000000 --- a/src/main/scala/gitbucket/core/service/ProtectedBrancheService.scala +++ /dev/null @@ -1,117 +0,0 @@ -package gitbucket.core.service - -import gitbucket.core.model.{Collaborator, Repository, Account, CommitState, CommitStatus, ProtectedBranch, ProtectedBranchContext} -import gitbucket.core.model.Profile._ -import gitbucket.core.util.JGitUtil -import profile.simple._ - -import org.eclipse.jgit.transport.ReceiveCommand -import org.eclipse.jgit.transport.ReceivePack -import org.eclipse.jgit.lib.ObjectId - - -trait ProtectedBrancheService { - import ProtectedBrancheService._ - private def getProtectedBranchInfoOpt(owner: String, repository: String, branch: String)(implicit session: Session): Option[ProtectedBranchInfo] = - ProtectedBranches - .leftJoin(ProtectedBranchContexts) - .on{ case (pb, c) => pb.byBranch(c.userName, c.repositoryName, c.branch) } - .map{ case (pb, c) => pb -> c.context.? } - .filter(_._1.byPrimaryKey(owner, repository, branch)) - .list - .groupBy(_._1) - .map(p => p._1 -> p._2.flatMap(_._2)) - .map{ case (t1, contexts) => - new ProtectedBranchInfo(t1.userName, t1.repositoryName, true, contexts, t1.statusCheckAdmin) - }.headOption - - def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit session: Session): ProtectedBranchInfo = - getProtectedBranchInfoOpt(owner, repository, branch).getOrElse(ProtectedBranchInfo.disabled(owner, repository)) - - def getProtectedBranchList(owner: String, repository: String)(implicit session: Session): List[String] = - ProtectedBranches.filter(_.byRepository(owner, repository)).map(_.branch).list - - def enableBranchProtection(owner: String, repository: String, branch:String, includeAdministrators: Boolean, contexts: Seq[String]) - (implicit session: Session): Unit = { - disableBranchProtection(owner, repository, branch) - ProtectedBranches.insert(new ProtectedBranch(owner, repository, branch, includeAdministrators && contexts.nonEmpty)) - contexts.map{ context => - ProtectedBranchContexts.insert(new ProtectedBranchContext(owner, repository, branch, context)) - } - } - - def disableBranchProtection(owner: String, repository: String, branch:String)(implicit session: Session): Unit = - ProtectedBranches.filter(_.byPrimaryKey(owner, repository, branch)).delete - - def getBranchProtectedReason(owner: String, repository: String, isAllowNonFastForwards: Boolean, command: ReceiveCommand, pusher: String) - (implicit session: Session): Option[String] = { - val branch = command.getRefName.stripPrefix("refs/heads/") - if(branch != command.getRefName){ - getProtectedBranchInfo(owner, repository, branch).getStopReason(isAllowNonFastForwards, command, pusher) - } else { - None - } - } -} -object ProtectedBrancheService { - case class ProtectedBranchInfo( - owner: String, - repository: String, - enabled: Boolean, - /** - * Require status checks to pass before merging - * Choose which status checks must pass before branches can be merged into test. - * When enabled, commits must first be pushed to another branch, - * then merged or pushed directly to test after status checks have passed. - */ - contexts: Seq[String], - /** - * Include administrators - * Enforce required status checks for repository administrators. - */ - includeAdministrators: Boolean) extends AccountService with CommitStatusService { - - def isAdministrator(pusher: String)(implicit session: Session): Boolean = - pusher == owner || getGroupMembers(owner).filter(gm => gm.userName == pusher && gm.isManager).nonEmpty - - /** - * Can't be force pushed - * Can't be deleted - * Can't have changes merged into them until required status checks pass - */ - def getStopReason(isAllowNonFastForwards: Boolean, command: ReceiveCommand, pusher: String)(implicit session: Session): Option[String] = { - if(enabled){ - command.getType() match { - case ReceiveCommand.Type.UPDATE|ReceiveCommand.Type.UPDATE_NONFASTFORWARD if isAllowNonFastForwards => - Some("Cannot force-push to a protected branch") - case ReceiveCommand.Type.UPDATE|ReceiveCommand.Type.UPDATE_NONFASTFORWARD if needStatusCheck(pusher) => - unSuccessedContexts(command.getNewId.name) match { - case s if s.size == 1 => Some(s"""Required status check "${s.toSeq(0)}" is expected""") - case s if s.size >= 1 => Some(s"${s.size} of ${contexts.size} required status checks are expected") - case _ => None - } - case ReceiveCommand.Type.DELETE => - Some("Cannot delete a protected branch") - case _ => None - } - }else{ - None - } - } - def unSuccessedContexts(sha1: String)(implicit session: Session): Set[String] = if(contexts.isEmpty){ - Set.empty - } else { - contexts.toSet -- getCommitStatues(owner, repository, sha1).filter(_.state == CommitState.SUCCESS).map(_.context).toSet - } - def needStatusCheck(pusher: String)(implicit session: Session): Boolean = pusher match { - case _ if !enabled => false - case _ if contexts.isEmpty => false - case _ if includeAdministrators => true - case p if isAdministrator(p) => false - case _ => true - } - } - object ProtectedBranchInfo{ - def disabled(owner: String, repository: String): ProtectedBranchInfo = ProtectedBranchInfo(owner, repository, false, Nil, false) - } -} diff --git a/src/main/scala/gitbucket/core/service/PullRequestService.scala b/src/main/scala/gitbucket/core/service/PullRequestService.scala index 789a09d..e90ba3f 100644 --- a/src/main/scala/gitbucket/core/service/PullRequestService.scala +++ b/src/main/scala/gitbucket/core/service/PullRequestService.scala @@ -148,7 +148,7 @@ case class MergeStatus( hasConflict: Boolean, commitStatues:List[CommitStatus], - branchProtection: ProtectedBrancheService.ProtectedBranchInfo, + branchProtection: ProtectedBranchService.ProtectedBranchInfo, branchIsOutOfDate: Boolean, hasUpdatePermission: Boolean, needStatusCheck: Boolean, diff --git a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala index eb9a263..045aef1 100644 --- a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala +++ b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala @@ -111,7 +111,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: Session) extends PostReceiveHook with PreReceiveHook with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService - with WebHookPullRequestService with ProtectedBrancheService { + with WebHookPullRequestService with ProtectedBranchService { private val logger = LoggerFactory.getLogger(classOf[CommitLogHook]) private var existIds: Seq[String] = Nil @@ -119,9 +119,10 @@ def onPreReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = { try { commands.asScala.foreach { command => - - getBranchProtectedReason(owner, repository, receivePack.isAllowNonFastForwards, command, pusher).map { reason => - command.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, reason) + PluginRegistry().getCommitHooks + .flatMap(_.hook(owner, repository, receivePack.isAllowNonFastForwards, command, pusher)) + .headOption.foreach { error => + command.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, error) } } using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>