diff --git a/src/main/resources/update/3_10.sql b/src/main/resources/update/3_10.sql new file mode 100644 index 0000000..721690a --- /dev/null +++ b/src/main/resources/update/3_10.sql @@ -0,0 +1,25 @@ +DROP TABLE IF EXISTS PROTECTED_BRANCH; + +CREATE TABLE PROTECTED_BRANCH( + USER_NAME VARCHAR(100) NOT NULL, + REPOSITORY_NAME VARCHAR(100) NOT NULL, + BRANCH VARCHAR(100) NOT NULL, + STATUS_CHECK_ADMIN BOOLEAN NOT NULL DEFAULT false +); + +ALTER TABLE PROTECTED_BRANCH ADD CONSTRAINT IDX_PROTECTED_BRANCH_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, BRANCH); +ALTER TABLE PROTECTED_BRANCH ADD CONSTRAINT IDX_PROTECTED_BRANCH_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME) + ON DELETE CASCADE ON UPDATE CASCADE; + + +DROP TABLE IF EXISTS PROTECTED_BRANCH_REQUIRE_CONTEXT; +CREATE TABLE PROTECTED_BRANCH_REQUIRE_CONTEXT( + USER_NAME VARCHAR(100) NOT NULL, + REPOSITORY_NAME VARCHAR(100) NOT NULL, + BRANCH VARCHAR(100) NOT NULL, + CONTEXT VARCHAR(255) NOT NULL +); + +ALTER TABLE PROTECTED_BRANCH_REQUIRE_CONTEXT ADD CONSTRAINT IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, BRANCH, CONTEXT); +ALTER TABLE PROTECTED_BRANCH_REQUIRE_CONTEXT ADD CONSTRAINT IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, BRANCH) REFERENCES PROTECTED_BRANCH (USER_NAME, REPOSITORY_NAME, BRANCH) + ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/main/scala/gitbucket/core/api/ApiBranch.scala b/src/main/scala/gitbucket/core/api/ApiBranch.scala new file mode 100644 index 0000000..cf6805b --- /dev/null +++ b/src/main/scala/gitbucket/core/api/ApiBranch.scala @@ -0,0 +1,16 @@ +package gitbucket.core.api + +import gitbucket.core.util.RepositoryName + +/** + * https://developer.github.com/v3/repos/#get-branch + * https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection + */ +case class ApiBranch( + name: String, + // commit: ApiBranchCommit, + protection: ApiBranchProtection)(repositoryName:RepositoryName) extends FieldSerializable { + def _links = Map( + "self" -> ApiPath(s"/api/v3/repos/${repositoryName.fullName}/branches/${name}"), + "html" -> ApiPath(s"/${repositoryName.fullName}/tree/${name}")) +} diff --git a/src/main/scala/gitbucket/core/api/ApiBranchProtection.scala b/src/main/scala/gitbucket/core/api/ApiBranchProtection.scala new file mode 100644 index 0000000..2af86f2 --- /dev/null +++ b/src/main/scala/gitbucket/core/api/ApiBranchProtection.scala @@ -0,0 +1,47 @@ +package gitbucket.core.api + +import gitbucket.core.service.ProtectedBrancheService +import org.json4s._ + +/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */ +case class ApiBranchProtection(enabled: Boolean, required_status_checks: Option[ApiBranchProtection.Status]){ + def status: ApiBranchProtection.Status = required_status_checks.getOrElse(ApiBranchProtection.statusNone) +} + +object ApiBranchProtection{ + /** form for enabling-and-disabling-branch-protection */ + case class EnablingAndDisabling(protection: ApiBranchProtection) + + def apply(info: ProtectedBrancheService.ProtectedBranchInfo): ApiBranchProtection = ApiBranchProtection( + enabled = info.enabled, + required_status_checks = Some(Status(EnforcementLevel(info.enabled, info.includeAdministrators), info.contexts))) + val statusNone = Status(Off, Seq.empty) + case class Status(enforcement_level: EnforcementLevel, contexts: Seq[String]) + sealed class EnforcementLevel(val name: String) + case object Off extends EnforcementLevel("off") + case object NonAdmins extends EnforcementLevel("non_admins") + case object Everyone extends EnforcementLevel("everyone") + object EnforcementLevel { + def apply(enabled: Boolean, includeAdministrators: Boolean): EnforcementLevel = if(enabled){ + if(includeAdministrators){ + Everyone + }else{ + NonAdmins + } + }else{ + Off + } + } + + implicit val enforcementLevelSerializer = new CustomSerializer[EnforcementLevel](format => ( + { + case JString("off") => Off + case JString("non_admins") => NonAdmins + case JString("everyone") => Everyone + }, + { + case x: EnforcementLevel => JString(x.name) + } + )) +} + diff --git a/src/main/scala/gitbucket/core/api/JsonFormat.scala b/src/main/scala/gitbucket/core/api/JsonFormat.scala index bd9c3bf..0733ba4 100644 --- a/src/main/scala/gitbucket/core/api/JsonFormat.scala +++ b/src/main/scala/gitbucket/core/api/JsonFormat.scala @@ -30,7 +30,8 @@ FieldSerializer[ApiCombinedCommitStatus]() + FieldSerializer[ApiPullRequest.Commit]() + FieldSerializer[ApiIssue]() + - FieldSerializer[ApiComment]() + FieldSerializer[ApiComment]() + + ApiBranchProtection.enforcementLevelSerializer def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](format => ( diff --git a/src/main/scala/gitbucket/core/controller/ControllerBase.scala b/src/main/scala/gitbucket/core/controller/ControllerBase.scala index 63949ab..f98e9cb 100644 --- a/src/main/scala/gitbucket/core/controller/ControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/ControllerBase.scala @@ -28,7 +28,7 @@ with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations with SystemSettingsService { - implicit val jsonFormats = DefaultFormats + implicit val jsonFormats = gitbucket.core.api.JsonFormat.jsonFormats // TODO Scala 2.11 // // Don't set content type via Accept header. diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala index 8cbb317..eaafacc 100644 --- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala +++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala @@ -1,7 +1,7 @@ package gitbucket.core.controller import gitbucket.core.api._ -import gitbucket.core.model.{Account, CommitState, Repository, PullRequest, Issue} +import gitbucket.core.model.{Account, CommitStatus, CommitState, Repository, PullRequest, Issue, WebHook} import gitbucket.core.pulls.html import gitbucket.core.service.CommitStatusService import gitbucket.core.service.MergeService @@ -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 CommitStatusService with MergeService with ProtectedBrancheService 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 CommitStatusService with MergeService with ProtectedBrancheService => private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase]) @@ -119,7 +119,8 @@ commits, diffs, hasWritePermission(owner, name, context.loginAccount), - repository) + repository, + flash.toMap.map(f => f._1 -> f._2.toString)) } } } getOrElse NotFound @@ -166,22 +167,34 @@ } getOrElse NotFound }) - ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository => + ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository => params("id").toIntOpt.flatMap{ issueId => val owner = repository.owner val name = repository.name getPullRequest(owner, name, issueId) map { case(issue, pullreq) => - val statuses = getCommitStatues(owner, name, pullreq.commitIdTo) - val hasConfrict = LockUtil.lock(s"${owner}/${name}"){ + val hasConflict = LockUtil.lock(s"${owner}/${name}"){ checkConflict(owner, name, pullreq.branch, issueId) } - val hasProblem = hasConfrict || (!statuses.isEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS) + val hasMergePermission = hasWritePermission(owner, name, context.loginAccount) + val branchProtection = getProtectedBranchInfo(owner, name, pullreq.branch) + val mergeStatus = PullRequestService.MergeStatus( + hasConflict = hasConflict, + commitStatues = getCommitStatues(owner, name, pullreq.commitIdTo), + branchProtection = branchProtection, + branchIsOutOfDate = JGitUtil.getShaByRef(owner, name, pullreq.branch) != Some(pullreq.commitIdFrom), + needStatusCheck = context.loginAccount.map{ u => + branchProtection.needStatusCheck(u.userName) + }.getOrElse(true), + hasUpdatePermission = hasWritePermission(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount) && + context.loginAccount.map{ u => + !getProtectedBranchInfo(pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch).needStatusCheck(u.userName) + }.getOrElse(false), + hasMergePermission = hasMergePermission, + commitIdTo = pullreq.commitIdTo) html.mergeguide( - hasConfrict, - hasProblem, + mergeStatus, issue, pullreq, - statuses, repository, getRepository(pullreq.requestUserName, pullreq.requestRepositoryName, context.baseUrl).get) } @@ -203,6 +216,75 @@ } getOrElse NotFound }) + post("/:owner/:repository/pull/:id/update_branch")(referrersOnly { baseRepository => + (for { + issueId <- params("id").toIntOpt + loginAccount <- context.loginAccount + (issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId) + owner = pullreq.requestUserName + name = pullreq.requestRepositoryName + if hasWritePermission(owner, name, context.loginAccount) + } yield { + val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch) + if(branchProtection.needStatusCheck(loginAccount.userName)){ + flash += "error" -> s"branch ${pullreq.requestBranch} is protected need status check." + } else { + val repository = getRepository(owner, name, context.baseUrl).get + LockUtil.lock(s"${owner}/${name}"){ + val alias = if(pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName){ + pullreq.branch + }else{ + s"${pullreq.userName}:${pullreq.branch}" + } + val existIds = using(Git.open(Directory.getRepositoryDir(owner, name))) { git => JGitUtil.getAllCommitIds(git) }.toSet + pullRemote(owner, name, pullreq.requestBranch, pullreq.userName, pullreq.repositoryName, pullreq.branch, loginAccount, + "Merge branch '${alias}' into ${pullreq.requestBranch}") match { + case None => // conflict + flash += "error" -> s"Can't automatic merging branch '${alias}' into ${pullreq.requestBranch}." + case Some(oldId) => + // update pull request + updatePullRequests(owner, name, pullreq.requestBranch) + + using(Git.open(Directory.getRepositoryDir(owner, name))) { git => + // after update branch + + val newCommitId = git.getRepository.resolve(s"refs/heads/${pullreq.requestBranch}") + val commits = git.log.addRange(oldId, newCommitId).call.iterator.asScala.map(c => new JGitUtil.CommitInfo(c)).toList + + commits.foreach{ commit => + if(!existIds.contains(commit.id)){ + createIssueComment(owner, name, commit) + } + } + + // record activity + recordPushActivity(owner, name, loginAccount.userName, pullreq.branch, commits) + + // close issue by commit message + if(pullreq.requestBranch == repository.repository.defaultBranch){ + commits.map{ commit => + closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name) + } + } + + // call web hook + callPullRequestWebHookByRequestBranch("synchronize", repository, pullreq.requestBranch, baseUrl, loginAccount) + callWebHookOf(owner, name, WebHook.Push) { + for { + ownerAccount <- getAccountByUserName(owner) + } yield { + WebHookService.WebHookPushPayload(git, loginAccount, pullreq.requestBranch, repository, commits, ownerAccount, oldId = oldId, newId = newCommitId) + } + } + } + flash += "info" -> s"Merge branch '${alias}' into ${pullreq.requestBranch}" + } + } + redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") + } + }) getOrElse NotFound + }) + post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) => params("id").toIntOpt.flatMap { issueId => val owner = repository.owner @@ -528,4 +610,15 @@ repository, hasWritePermission(owner, repoName, context.loginAccount)) } + + // TODO: same as gitbucket.core.servlet.CommitLogHook ... + private def createIssueComment(owner: String, repository: String, commit: CommitInfo) = { + StringUtil.extractIssueId(commit.fullMessage).foreach { issueId => + if(getIssue(owner, repository, issueId).isDefined){ + getAccountByMailAddress(commit.committerEmailAddress).foreach { account => + createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit") + } + } + } + } } diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index 7a22d34..1db2f5d 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} +import gitbucket.core.service.{RepositoryService, AccountService, WebHookService, ProtectedBrancheService, CommitStatusService} import gitbucket.core.service.WebHookService._ import gitbucket.core.util._ import gitbucket.core.util.JGitUtil._ @@ -18,23 +18,29 @@ class RepositorySettingsController extends RepositorySettingsControllerBase - with RepositoryService with AccountService with WebHookService + with RepositoryService with AccountService with WebHookService with ProtectedBrancheService with CommitStatusService with OwnerAuthenticator with UsersAuthenticator trait RepositorySettingsControllerBase extends ControllerBase { - self: RepositoryService with AccountService with WebHookService + self: RepositoryService with AccountService with WebHookService with ProtectedBrancheService with CommitStatusService 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,61 @@ 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") }) - + + /** branch settings */ + get("/:owner/:repository/settings/branches")(ownerOnly { repository => + val protecteions = getProtectedBranchList(repository.owner, repository.name) + html.branches(repository, protecteions, flash.get("info")) + }); + + /** Update default branch */ + 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") + } + }) + + /** Branch protection for branch */ + get("/:owner/:repository/settings/branches/:branch")(ownerOnly { repository => + import gitbucket.core.api._ + val branch = params("branch") + if(repository.branchList.find(_ == branch).isEmpty){ + redirect(s"/${repository.owner}/${repository.name}/settings/branches") + }else{ + val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch)) + val lastWeeks = getRecentStatuesContexts(repository.owner, repository.name, org.joda.time.LocalDateTime.now.minusWeeks(1).toDate).toSet + val knownContexts = (lastWeeks ++ protection.status.contexts).toSeq.sortBy(identity) + html.branchprotection(repository, branch, protection, knownContexts, flash.get("info")) + } + }); + + /** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */ + patch("/api/v3/repos/:owner/:repo/branches/:branch")(ownerOnly { repository => + import gitbucket.core.api._ + (for{ + branch <- params.get("branch") if repository.branchList.find(_ == branch).isDefined + protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection) + } yield { + if(protection.enabled){ + enableBranchProtection(repository.owner, repository.name, branch, protection.status.enforcement_level == ApiBranchProtection.Everyone, protection.status.contexts) + }else{ + disableBranchProtection(repository.owner, repository.name, branch) + } + JsonFormat(ApiBranch(branch, protection)(RepositoryName(repository))) + }) getOrElse NotFound + }) + /** * Display the Collaborators page. */ diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala index 155204b..9981033 100644 --- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala @@ -14,7 +14,6 @@ import gitbucket.core.util.Implicits._ import gitbucket.core.util.Directory._ import gitbucket.core.model.{Account, CommitState, WebHook} -import gitbucket.core.service.CommitStatusService import gitbucket.core.service.WebHookService._ import gitbucket.core.view import gitbucket.core.view.helpers @@ -34,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 WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBrancheService /** * The repository viewer. @@ -42,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 WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBrancheService => ArchiveCommand.registerFormat("zip", new ZipFormat) ArchiveCommand.registerFormat("tar.gz", new TgzFormat) @@ -222,12 +221,15 @@ get("/:owner/:repository/new/*")(collaboratorsOnly { repository => val (branch, path) = splitPath(repository, multiParams("splat").head) + val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName) html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList, - None, JGitUtil.ContentInfo("text", None, Some("UTF-8"))) + None, JGitUtil.ContentInfo("text", None, Some("UTF-8")), + protectedBranch) }) get("/:owner/:repository/edit/*")(collaboratorsOnly { repository => val (branch, path) = splitPath(repository, multiParams("splat").head) + val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName) using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch)) @@ -235,7 +237,8 @@ getPathObjectId(git, path, revCommit).map { objectId => val paths = path.split("/") html.editor(branch, repository, paths.take(paths.size - 1).toList, Some(paths.last), - JGitUtil.getContentInfo(git, path, objectId)) + JGitUtil.getContentInfo(git, path, objectId), + protectedBranch) } getOrElse NotFound } }) @@ -486,6 +489,7 @@ * Displays branches. */ get("/:owner/:repository/branches")(referrersOnly { repository => + val protectedBranches = getProtectedBranchList(repository.owner, repository.name).toSet val branches = JGitUtil.getBranches( owner = repository.owner, name = repository.name, @@ -493,7 +497,7 @@ origin = repository.repository.originUserName.isEmpty ) .sortBy(br => (br.mergeInfo.isEmpty, br.commitTime)) - .map(br => br -> getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId)) + .map(br => (br, getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId), protectedBranches.contains(br.name))) .reverse html.branches(branches, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository) diff --git a/src/main/scala/gitbucket/core/model/BasicTemplate.scala b/src/main/scala/gitbucket/core/model/BasicTemplate.scala index db3e1b4..2b13764 100644 --- a/src/main/scala/gitbucket/core/model/BasicTemplate.scala +++ b/src/main/scala/gitbucket/core/model/BasicTemplate.scala @@ -54,4 +54,9 @@ byRepository(userName, repositoryName) && (this.commitId === commitId) } + trait BranchTemplate extends BasicTemplate{ self: Table[_] => + val branch = column[String]("BRANCH") + def byBranch(owner: String, repository: String, branchName: String) = byRepository(owner, repository) && (branch === branchName.bind) + def byBranch(owner: Column[String], repository: Column[String], branchName: Column[String]) = byRepository(owner, repository) && (this.branch === branchName) + } } diff --git a/src/main/scala/gitbucket/core/model/CommitStatus.scala b/src/main/scala/gitbucket/core/model/CommitStatus.scala index 87b74f1..75ed261 100644 --- a/src/main/scala/gitbucket/core/model/CommitStatus.scala +++ b/src/main/scala/gitbucket/core/model/CommitStatus.scala @@ -19,7 +19,7 @@ val creator = column[String]("CREATOR") val registeredDate = column[java.util.Date]("REGISTERED_DATE") val updatedDate = column[java.util.Date]("UPDATED_DATE") - def * = (commitStatusId, userName, repositoryName, commitId, context, state, targetUrl, description, creator, registeredDate, updatedDate) <> (CommitStatus.tupled, CommitStatus.unapply) + def * = (commitStatusId, userName, repositoryName, commitId, context, state, targetUrl, description, creator, registeredDate, updatedDate) <> ((CommitStatus.apply _).tupled, CommitStatus.unapply) def byPrimaryKey(id: Int) = commitStatusId === id.bind } } @@ -38,7 +38,20 @@ registeredDate: java.util.Date, updatedDate: java.util.Date ) - +object CommitStatus { + def pending(owner: String, repository: String, context: String) = CommitStatus( + commitStatusId = 0, + userName = owner, + repositoryName = repository, + commitId = "", + context = context, + state = CommitState.PENDING, + targetUrl = None, + description = Some("Waiting for status to be reported"), + creator = "", + registeredDate = new java.util.Date(), + updatedDate = new java.util.Date()) +} sealed abstract class CommitState(val name: String) diff --git a/src/main/scala/gitbucket/core/model/Profile.scala b/src/main/scala/gitbucket/core/model/Profile.scala index e173630..0badf86 100644 --- a/src/main/scala/gitbucket/core/model/Profile.scala +++ b/src/main/scala/gitbucket/core/model/Profile.scala @@ -50,5 +50,6 @@ with WebHookComponent with WebHookEventComponent with PluginComponent + with ProtectedBranchComponent object Profile extends CoreProfile diff --git a/src/main/scala/gitbucket/core/model/ProtectedBranch.scala b/src/main/scala/gitbucket/core/model/ProtectedBranch.scala new file mode 100644 index 0000000..4700a04 --- /dev/null +++ b/src/main/scala/gitbucket/core/model/ProtectedBranch.scala @@ -0,0 +1,37 @@ +package gitbucket.core.model + +import scala.slick.lifted.MappedTo +import scala.slick.jdbc._ + +trait ProtectedBranchComponent extends TemplateComponent { self: Profile => + import profile.simple._ + import self._ + + lazy val ProtectedBranches = TableQuery[ProtectedBranches] + class ProtectedBranches(tag: Tag) extends Table[ProtectedBranch](tag, "PROTECTED_BRANCH") with BranchTemplate { + val statusCheckAdmin = column[Boolean]("STATUS_CHECK_ADMIN") + def * = (userName, repositoryName, branch, statusCheckAdmin) <> (ProtectedBranch.tupled, ProtectedBranch.unapply) + def byPrimaryKey(userName: String, repositoryName: String, branch: String) = byBranch(userName, repositoryName, branch) + def byPrimaryKey(userName: Column[String], repositoryName: Column[String], branch: Column[String]) = byBranch(userName, repositoryName, branch) + } + + lazy val ProtectedBranchContexts = TableQuery[ProtectedBranchContexts] + class ProtectedBranchContexts(tag: Tag) extends Table[ProtectedBranchContext](tag, "PROTECTED_BRANCH_REQUIRE_CONTEXT") with BranchTemplate { + val context = column[String]("CONTEXT") + def * = (userName, repositoryName, branch, context) <> (ProtectedBranchContext.tupled, ProtectedBranchContext.unapply) + } +} + + +case class ProtectedBranch( + userName: String, + repositoryName: String, + branch: String, + statusCheckAdmin: Boolean) + + +case class ProtectedBranchContext( + userName: String, + repositoryName: String, + branch: String, + context: String) diff --git a/src/main/scala/gitbucket/core/service/CommitStatusService.scala b/src/main/scala/gitbucket/core/service/CommitStatusService.scala index 2ebea2b..411aeea 100644 --- a/src/main/scala/gitbucket/core/service/CommitStatusService.scala +++ b/src/main/scala/gitbucket/core/service/CommitStatusService.scala @@ -7,7 +7,8 @@ import gitbucket.core.util.Implicits._ import gitbucket.core.util.StringUtil._ import gitbucket.core.service.RepositoryService.RepositoryInfo - +import org.joda.time.LocalDateTime +import gitbucket.core.model.Profile.dateColumnType trait CommitStatusService { /** insert or update */ @@ -42,6 +43,9 @@ def getCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session) :List[CommitStatus] = byCommitStatues(userName, repositoryName, sha).list + def getRecentStatuesContexts(userName: String, repositoryName: String, time: java.util.Date)(implicit s: Session) :List[String] = + CommitStatuses.filter(t => t.byRepository(userName, repositoryName)).filter(t => t.updatedDate > time.bind).groupBy(_.context).map(_._1).list + def getCommitStatuesWithCreator(userName: String, repositoryName: String, sha: String)(implicit s: Session) :List[(CommitStatus, Account)] = byCommitStatues(userName, repositoryName, sha).innerJoin(Accounts) .filter{ case (t,a) => t.creator === a.userName }.list diff --git a/src/main/scala/gitbucket/core/service/MergeService.scala b/src/main/scala/gitbucket/core/service/MergeService.scala index 6614fd4..f09211c 100644 --- a/src/main/scala/gitbucket/core/service/MergeService.scala +++ b/src/main/scala/gitbucket/core/service/MergeService.scala @@ -10,10 +10,9 @@ import org.eclipse.jgit.api.Git import org.eclipse.jgit.transport.RefSpec import org.eclipse.jgit.errors.NoMergeBaseException -import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent} +import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent, Repository} import org.eclipse.jgit.revwalk.RevWalk - trait MergeService { import MergeService._ /** @@ -52,26 +51,30 @@ /** * Checks whether conflict will be caused in merging. Returns true if conflict will be caused. */ - def checkConflict(userName: String, repositoryName: String, branch: String, - requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = { - using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git => - val remoteRefName = s"refs/heads/${branch}" - val tmpRefName = s"refs/merge-check/${userName}/${branch}" + def tryMergeRemote(localUserName: String, localRepositoryName: String, localBranch: String, + remoteUserName: String, remoteRepositoryName: String, remoteBranch: String): Option[(ObjectId, ObjectId, ObjectId)] = { + using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git => + val remoteRefName = s"refs/heads/${remoteBranch}" + val tmpRefName = s"refs/remote-temp/${remoteUserName}/${remoteRepositoryName}/${remoteBranch}" val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true) try { // fetch objects from origin repository branch git.fetch - .setRemote(getRepositoryDir(userName, repositoryName).toURI.toString) + .setRemote(getRepositoryDir(remoteUserName, remoteRepositoryName).toURI.toString) .setRefSpecs(refSpec) .call // merge conflict check val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true) - val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${requestBranch}") + val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${localBranch}") val mergeTip = git.getRepository.resolve(tmpRefName) try { - !merger.merge(mergeBaseTip, mergeTip) + if(merger.merge(mergeBaseTip, mergeTip)){ + Some((merger.getResultTreeId, mergeBaseTip, mergeTip)) + }else{ + None + } } catch { - case e: NoMergeBaseException => true + case e: NoMergeBaseException => None } } finally { val refUpdate = git.getRepository.updateRef(refSpec.getDestination) @@ -80,8 +83,54 @@ } } } + /** + * Checks whether conflict will be caused in merging. Returns true if conflict will be caused. + */ + def checkConflict(userName: String, repositoryName: String, branch: String, + requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = + tryMergeRemote(userName, repositoryName, branch, requestUserName, requestRepositoryName, requestBranch).isEmpty + + def pullRemote(localUserName: String, localRepositoryName: String, localBranch: String, + remoteUserName: String, remoteRepositoryName: String, remoteBranch: String, + loginAccount: Account, message: String): Option[ObjectId] = { + tryMergeRemote(localUserName, localRepositoryName, localBranch, remoteUserName, remoteRepositoryName, remoteBranch).map{ case (newTreeId, oldBaseId, oldHeadId) => + using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git => + val committer = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress) + val newCommit = Util.createMergeCommit(git.getRepository, newTreeId, committer, message, Seq(oldBaseId, oldHeadId)) + Util.updateRefs(git.getRepository, s"refs/heads/${localBranch}", newCommit, false, committer, Some("merge")) + } + oldBaseId + } + } + } object MergeService{ + object Util{ + // return treeId + def createMergeCommit(repository: Repository, treeId: ObjectId, committer: PersonIdent, message: String, parents: Seq[ObjectId]): ObjectId = { + val mergeCommit = new CommitBuilder() + mergeCommit.setTreeId(treeId) + mergeCommit.setParentIds(parents:_*) + mergeCommit.setAuthor(committer) + mergeCommit.setCommitter(committer) + mergeCommit.setMessage(message) + // insertObject and got mergeCommit Object Id + val inserter = repository.newObjectInserter + val mergeCommitId = inserter.insert(mergeCommit) + inserter.flush() + inserter.close() + mergeCommitId + } + def updateRefs(repository: Repository, ref: String, newObjectId: ObjectId, force: Boolean, committer: PersonIdent, refLogMessage: Option[String] = None):Unit = { + // update refs + val refUpdate = repository.updateRef(ref) + refUpdate.setNewObjectId(newObjectId) + refUpdate.setForceUpdate(force) + refUpdate.setRefLogIdent(committer) + refLogMessage.map(refUpdate.setRefLogMessage(_, true)) + refUpdate.update() + } + } case class MergeCacheInfo(git:Git, branch:String, issueId:Int){ val repository = git.getRepository val mergedBranchName = s"refs/pull/${issueId}/merge" @@ -120,12 +169,7 @@ def updateBranch(treeId:ObjectId, message:String, branchName:String){ // creates merge commit val mergeCommitId = createMergeCommit(treeId, committer, message) - // update refs - val refUpdate = repository.updateRef(branchName) - refUpdate.setNewObjectId(mergeCommitId) - refUpdate.setForceUpdate(true) - refUpdate.setRefLogIdent(committer) - refUpdate.update() + Util.updateRefs(repository, branchName, mergeCommitId, true, committer) } if(!conflicted){ updateBranch(merger.getResultTreeId, s"Merge ${mergeTip.name} into ${mergeBaseTip.name}", mergedBranchName) @@ -145,28 +189,12 @@ // creates merge commit val mergeCommitId = createMergeCommit(mergeResultCommit.getTree().getId(), committer, message) // update refs - val refUpdate = repository.updateRef(s"refs/heads/${branch}") - refUpdate.setNewObjectId(mergeCommitId) - refUpdate.setForceUpdate(false) - refUpdate.setRefLogIdent(committer) - refUpdate.setRefLogMessage("merged", true) - refUpdate.update() + Util.updateRefs(repository, s"refs/heads/${branch}", mergeCommitId, false, committer, Some("merged")) } // return treeId - private def createMergeCommit(treeId:ObjectId, committer:PersonIdent, message:String) = { - val mergeCommit = new CommitBuilder() - mergeCommit.setTreeId(treeId) - mergeCommit.setParentIds(Array[ObjectId](mergeBaseTip, mergeTip): _*) - mergeCommit.setAuthor(committer) - mergeCommit.setCommitter(committer) - mergeCommit.setMessage(message) - // insertObject and got mergeCommit Object Id - val inserter = repository.newObjectInserter - val mergeCommitId = inserter.insert(mergeCommit) - inserter.flush() - inserter.close() - mergeCommitId - } + private def createMergeCommit(treeId: ObjectId, committer: PersonIdent, message: String) = + Util.createMergeCommit(repository, treeId, committer, message, Seq[ObjectId](mergeBaseTip, mergeTip)) + private def parseCommit(id:ObjectId) = using(new RevWalk( repository ))(_.parseCommit(id)) } } \ No newline at end of file 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..cfbe9e3 --- /dev/null +++ b/src/main/scala/gitbucket/core/service/ProtectedBrancheService.scala @@ -0,0 +1,114 @@ +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 67db3c8..789a09d 100644 --- a/src/main/scala/gitbucket/core/service/PullRequestService.scala +++ b/src/main/scala/gitbucket/core/service/PullRequestService.scala @@ -1,6 +1,6 @@ package gitbucket.core.service -import gitbucket.core.model.{Account, Issue, PullRequest, WebHook} +import gitbucket.core.model.{Account, Issue, PullRequest, WebHook, CommitStatus, CommitState} import gitbucket.core.model.Profile._ import gitbucket.core.util.JGitUtil import profile.simple._ @@ -145,4 +145,29 @@ case class PullRequestCount(userName: String, count: Int) + case class MergeStatus( + hasConflict: Boolean, + commitStatues:List[CommitStatus], + branchProtection: ProtectedBrancheService.ProtectedBranchInfo, + branchIsOutOfDate: Boolean, + hasUpdatePermission: Boolean, + needStatusCheck: Boolean, + hasMergePermission: Boolean, + commitIdTo: String){ + + val statuses: List[CommitStatus] = + commitStatues ++ (branchProtection.contexts.toSet -- commitStatues.map(_.context).toSet).map(CommitStatus.pending(branchProtection.owner, branchProtection.repository, _)) + val hasRequiredStatusProblem = needStatusCheck && branchProtection.contexts.exists(context => statuses.find(_.context == context).map(_.state) != Some(CommitState.SUCCESS)) + val hasProblem = hasRequiredStatusProblem || hasConflict || (!statuses.isEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS) + val canUpdate = branchIsOutOfDate && !hasConflict + val canMerge = hasMergePermission && !hasConflict && !hasRequiredStatusProblem + lazy val commitStateSummary:(CommitState, String) = { + val stateMap = statuses.groupBy(_.state) + val state = CommitState.combine(stateMap.keySet) + val summary = stateMap.map{ case (keyState, states) => states.size+" "+keyState.name }.mkString(", ") + state -> summary + } + lazy val statusesAndRequired:List[(CommitStatus, Boolean)] = statuses.map{ s => s -> branchProtection.contexts.exists(_==s.context) } + lazy val isAllSuccess = commitStateSummary._1==CommitState.SUCCESS + } } diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala index 971ff4a..8e74642 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala @@ -46,18 +46,20 @@ (Repositories filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository => Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName) - val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list - val webHookEvents = WebHookEvents .filter(_.byRepository(oldUserName, oldRepositoryName)).list - val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list - val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list - val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list - val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list - val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list - val issueComments = IssueComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list - val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list - val commitComments = CommitComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list - val commitStatuses = CommitStatuses.filter(_.byRepository(oldUserName, oldRepositoryName)).list - val collaborators = Collaborators .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val webHookEvents = WebHookEvents .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val issueComments = IssueComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val commitComments = CommitComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val commitStatuses = CommitStatuses .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val collaborators = Collaborators .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val protectedBranches = ProtectedBranches .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val protectedBranchContexts = ProtectedBranchContexts.filter(_.byRepository(oldUserName, oldRepositoryName)).list Repositories.filter { t => (t.originUserName === oldUserName.bind) && (t.originRepositoryName === oldRepositoryName.bind) @@ -90,11 +92,13 @@ } )} :_*) - PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) - IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) - Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) - CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) - CommitStatuses.insertAll(commitStatuses.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + CommitComments .insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + CommitStatuses .insertAll(commitStatuses.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + ProtectedBranches .insertAll(protectedBranches.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + ProtectedBranchContexts.insertAll(protectedBranchContexts.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) // Update source repository of pull requests PullRequests.filter { t => @@ -309,11 +313,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..f92207c 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.isAllowNonFastForwards, command, pusher).map{ reason => + command.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, reason) + } + } using(Git.open(Directory.getRepositoryDir(owner, repository))) { git => existIds = JGitUtil.getAllCommitIds(git) } diff --git a/src/main/scala/gitbucket/core/view/helpers.scala b/src/main/scala/gitbucket/core/view/helpers.scala index a09edcb..b61c0d6 100644 --- a/src/main/scala/gitbucket/core/view/helpers.scala +++ b/src/main/scala/gitbucket/core/view/helpers.scala @@ -289,10 +289,10 @@ } def commitStateIcon(state: CommitState) = Html(state match { - case CommitState.PENDING => "●" - case CommitState.SUCCESS => "✔" - case CommitState.ERROR => "×" - case CommitState.FAILURE => "×" + case CommitState.PENDING => """""" + case CommitState.SUCCESS => """""" + case CommitState.ERROR => """""" + case CommitState.FAILURE => """""" }) def commitStateText(state: CommitState, commitId:String) = state match { diff --git a/src/main/twirl/gitbucket/core/pulls/conversation.scala.html b/src/main/twirl/gitbucket/core/pulls/conversation.scala.html index 6b13496..a4e4749 100644 --- a/src/main/twirl/gitbucket/core/pulls/conversation.scala.html +++ b/src/main/twirl/gitbucket/core/pulls/conversation.scala.html @@ -20,7 +20,7 @@ case comment: gitbucket.core.model.IssueComment => Some(comment) case other => None }.exists(_.action == "merge")){ merged => - @if(hasWritePermission && !issue.closed){ + @if(!issue.closed){