diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index 7a22d34..46f4b83 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -26,15 +26,21 @@ with OwnerAuthenticator with UsersAuthenticator => // for repository options - case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean) + case class OptionsForm(repositoryName: String, description: Option[String], isPrivate: Boolean) val optionsForm = mapping( "repositoryName" -> trim(label("Repository Name", text(required, maxlength(40), identifier, renameRepositoryName))), "description" -> trim(label("Description" , optional(text()))), - "defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))), "isPrivate" -> trim(label("Repository Type", boolean())) )(OptionsForm.apply) + // for default branch + case class DefaultBranchForm(defaultBranch: String) + + val defaultBranchForm = mapping( + "defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))) + )(DefaultBranchForm.apply) + // for collaborator addition case class CollaboratorForm(userName: String) @@ -75,12 +81,10 @@ * Save the repository options. */ post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) => - val defaultBranch = if(repository.branchList.isEmpty) "master" else form.defaultBranch saveRepositoryOptions( repository.owner, repository.name, form.description, - defaultBranch, repository.repository.parentUserName.map { _ => repository.repository.isPrivate } getOrElse form.isPrivate @@ -98,14 +102,28 @@ FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName)) } } - // Change repository HEAD - using(Git.open(getRepositoryDir(repository.owner, form.repositoryName))) { git => - git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + defaultBranch) - } flash += "info" -> "Repository settings has been updated." redirect(s"/${repository.owner}/${form.repositoryName}/settings/options") }) - + + get("/:owner/:repository/settings/branches")(ownerOnly { repository => + html.branches(repository, flash.get("info")) + }); + + post("/:owner/:repository/settings/update_default_branch", defaultBranchForm)(ownerOnly { (form, repository) => + if(repository.branchList.find(_ == form.defaultBranch).isEmpty){ + redirect(s"/${repository.owner}/${repository.name}/settings/options") + }else{ + saveRepositoryDefaultBranch(repository.owner, repository.name, form.defaultBranch) + // Change repository HEAD + using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => + git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + form.defaultBranch) + } + flash += "info" -> "Repository default branch has been updated." + redirect(s"/${repository.owner}/${repository.name}/settings/branches") + } + }) + /** * Display the Collaborators page. */ diff --git a/src/main/scala/gitbucket/core/service/ProtectedBrancheService.scala b/src/main/scala/gitbucket/core/service/ProtectedBrancheService.scala new file mode 100644 index 0000000..572e081 --- /dev/null +++ b/src/main/scala/gitbucket/core/service/ProtectedBrancheService.scala @@ -0,0 +1,86 @@ +package gitbucket.core.service + +import gitbucket.core.model.{Collaborator, Repository, Account, CommitState} +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._ + def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit session: Session): Option[ProtectedBranchInfo] = { + // TODO: mock + if(owner == "root" && repository == "test58" && branch == "hoge2"){ + Some(new ProtectedBranchInfo(owner, repository, Seq.empty, false)) + }else{ + None + } + } + + def getBranchProtectedReason(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)(implicit session: Session): Option[String] = { + val branch = command.getRefName.stripPrefix("refs/heads/") + if(branch != command.getRefName){ + getProtectedBranchInfo(owner, repository, branch).flatMap(_.getStopReason(receivePack, command, pusher)) + }else{ + None + } + } +} +object ProtectedBrancheService { + class ProtectedBranchInfo( + owner: String, + repository: String, + /** + * 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. + */ + requireStatusChecksToPass: 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(receivePack: ReceivePack, command: ReceiveCommand, pusher: String)(implicit session: Session): Option[String] = { + command.getType() match { + case ReceiveCommand.Type.UPDATE|ReceiveCommand.Type.UPDATE_NONFASTFORWARD if receivePack.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 ${requireStatusChecksToPass.size} required status checks are expected") + case _ => None + } + case ReceiveCommand.Type.DELETE => + Some("Cannot delete a protected branch") + case _ => None + } + } + def unSuccessedContexts(sha1: String)(implicit session: Session): Set[String] = if(requireStatusChecksToPass.isEmpty){ + Set.empty + } else { + requireStatusChecksToPass.toSet -- getCommitStatues(owner, repository, sha1).filter(_.state == CommitState.SUCCESS).map(_.context).toSet + } + def needStatusCheck(pusher: String)(implicit session: Session): Boolean = + if(requireStatusChecksToPass.isEmpty){ + false + }else if(includeAdministrators){ + true + }else{ + !isAdministrator(pusher) + } + } +} diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala index 971ff4a..ffef183 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala @@ -309,11 +309,17 @@ /** * Save repository options. */ - def saveRepositoryOptions(userName: String, repositoryName: String, - description: Option[String], defaultBranch: String, isPrivate: Boolean)(implicit s: Session): Unit = + def saveRepositoryOptions(userName: String, repositoryName: String, + description: Option[String], isPrivate: Boolean)(implicit s: Session): Unit = Repositories.filter(_.byRepository(userName, repositoryName)) - .map { r => (r.description.?, r.defaultBranch, r.isPrivate, r.updatedDate) } - .update (description, defaultBranch, isPrivate, currentDate) + .map { r => (r.description.?, r.isPrivate, r.updatedDate) } + .update (description, isPrivate, currentDate) + + def saveRepositoryDefaultBranch(userName: String, repositoryName: String, + defaultBranch: String)(implicit s: Session): Unit = + Repositories.filter(_.byRepository(userName, repositoryName)) + .map { r => r.defaultBranch } + .update (defaultBranch) /** * Add collaborator to the repository. diff --git a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala index a2866f1..ab967c1 100644 --- a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala +++ b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala @@ -111,13 +111,18 @@ 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 WebHookPullRequestService with ProtectedBrancheService { private val logger = LoggerFactory.getLogger(classOf[CommitLogHook]) private var existIds: Seq[String] = Nil def onPreReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = { try { + commands.asScala.foreach { command => + getBranchProtectedReason(owner, repository, receivePack, command, pusher).map{ reason => + command.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, reason) + } + } using(Git.open(Directory.getRepositoryDir(owner, repository))) { git => existIds = JGitUtil.getAllCommitIds(git) } @@ -134,6 +139,7 @@ using(Git.open(Directory.getRepositoryDir(owner, repository))) { git => val pushedIds = scala.collection.mutable.Set[String]() commands.asScala.foreach { command => + println(s"onPostReceive commandType: ${command.getType}, refName: ${command.getRefName}") logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}") implicit val apiContext = api.JsonFormat.Context(baseUrl) val refName = command.getRefName.split("/") diff --git a/src/main/twirl/gitbucket/core/settings/branches.scala.html b/src/main/twirl/gitbucket/core/settings/branches.scala.html new file mode 100644 index 0000000..ed57bbf --- /dev/null +++ b/src/main/twirl/gitbucket/core/settings/branches.scala.html @@ -0,0 +1,37 @@ +@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + info: Option[Any])(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@import gitbucket.core.model.WebHook._ +@html.main("Branches", Some(repository)){ + @html.menu("settings", repository){ + @menu("branches", repository){ + @if(repository.branchList.isEmpty){ +
+
+

+

You don’t have any branches

+

Before you can edit branch settings, you need to add a branch.

+
+
+ }else{ + @helper.html.information(info) +
+
Default branch
+
+

The default branch is considered the “base” branch in your repository, against which all pull requests and code commits are automatically made, unless you specify a different branch.

+
+ + + +
+
+
+ } + } + } +} \ No newline at end of file diff --git a/src/main/twirl/gitbucket/core/settings/menu.scala.html b/src/main/twirl/gitbucket/core/settings/menu.scala.html index 13fd262..2734226 100644 --- a/src/main/twirl/gitbucket/core/settings/menu.scala.html +++ b/src/main/twirl/gitbucket/core/settings/menu.scala.html @@ -11,6 +11,11 @@ Collaborators + @if(!repository.branchList.isEmpty){ + + Branches + + } Service Hooks diff --git a/src/main/twirl/gitbucket/core/settings/options.scala.html b/src/main/twirl/gitbucket/core/settings/options.scala.html index 0cbc8b9..d7cb3fc 100644 --- a/src/main/twirl/gitbucket/core/settings/options.scala.html +++ b/src/main/twirl/gitbucket/core/settings/options.scala.html @@ -18,22 +18,6 @@ -
- - - @if(repository.branchList.isEmpty){ - - } - -