Compare commits

...

1 Commits

Author SHA1 Message Date
takezoe
a791e937b6 Support for revert pull request 2025-08-13 08:06:05 +09:00
9 changed files with 219 additions and 74 deletions

View File

@@ -237,7 +237,7 @@
<addForeignKeyConstraint constraintName="IDX_ISSUE_ID_FK1" baseTableName="ISSUE_ID" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
<!--================================================================================================-->
<!-- ISSUE_ID -->
<!-- ISSUE_LABEL -->
<!--================================================================================================-->
<createTable tableName="ISSUE_LABEL">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>

View File

@@ -29,4 +29,12 @@
<addPrimaryKey constraintName="IDX_PROTECTED_BRANCH_RESTRICTION_PK" tableName="PROTECTED_BRANCH_RESTRICTION" columnNames="USER_NAME, REPOSITORY_NAME, BRANCH, ALLOWED_USER"/>
<addForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_RESTRICTION_FK0" baseTableName="PROTECTED_BRANCH_RESTRICTION" baseColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" referencedTableName="PROTECTED_BRANCH" referencedColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" onDelete="CASCADE" onUpdate="CASCADE"/>
<addForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_RESTRICTION_FK1" baseTableName="PROTECTED_BRANCH_RESTRICTION" baseColumnNames="ALLOWED_USER" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
<!--================================================================================================-->
<!-- PULL_REQUEST -->
<!--================================================================================================-->
<addColumn tableName="PULL_REQUEST">
<column name="MERGED_COMMIT_IDS" type="text" nullable="true"/>
</addColumn>
</changeSet>

View File

@@ -13,8 +13,10 @@ import gitbucket.core.util.Implicits.*
import gitbucket.core.util.*
import org.scalatra.forms.*
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevWalk
import org.scalatra.BadRequest
import java.nio.file.Files
import scala.util.Using
class PullRequestsController
@@ -247,41 +249,43 @@ trait PullRequestsControllerBase extends ControllerBase {
})
get("/:owner/:repository/pull/:id/delete_branch")(readableUsersOnly { baseRepository =>
(for {
issueId <- params("id").toIntOpt
loginAccount <- context.loginAccount
case (issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId)
owner = pullreq.requestUserName
name = pullreq.requestRepositoryName
if hasDeveloperRole(owner, name, context.loginAccount)
} yield {
val repository = getRepository(owner, name).get
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch)
if (branchProtection.enabled) {
flash.update("error", s"branch ${pullreq.requestBranch} is protected.")
} else {
if (repository.repository.defaultBranch != pullreq.requestBranch) {
val userName = context.loginAccount.get.userName
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
git.branchDelete().setForce(true).setBranchNames(pullreq.requestBranch).call()
val deleteBranchInfo = DeleteBranchInfo(repository.owner, repository.name, userName, pullreq.requestBranch)
recordActivity(deleteBranchInfo)
}
createComment(
baseRepository.owner,
baseRepository.name,
userName,
issueId,
pullreq.requestBranch,
"delete_branch"
)
context.withLoginAccount { _ =>
(for {
issueId <- params("id").toIntOpt
case (issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId)
owner = pullreq.requestUserName
name = pullreq.requestRepositoryName
if hasDeveloperRole(owner, name, context.loginAccount)
} yield {
val repository = getRepository(owner, name).get
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch)
if (branchProtection.enabled) {
flash.update("error", s"branch ${pullreq.requestBranch} is protected.")
} else {
flash.update("error", s"""Can't delete the default branch "${pullreq.requestBranch}".""")
if (repository.repository.defaultBranch != pullreq.requestBranch) {
val userName = context.loginAccount.get.userName
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
git.branchDelete().setForce(true).setBranchNames(pullreq.requestBranch).call()
val deleteBranchInfo =
DeleteBranchInfo(repository.owner, repository.name, userName, pullreq.requestBranch)
recordActivity(deleteBranchInfo)
}
createComment(
baseRepository.owner,
baseRepository.name,
userName,
issueId,
pullreq.requestBranch,
"delete_branch"
)
} else {
flash.update("error", s"""Can't delete the default branch "${pullreq.requestBranch}".""")
}
}
}
redirect(s"/${baseRepository.owner}/${baseRepository.name}/pull/${issueId}")
}) getOrElse NotFound()
redirect(s"/${baseRepository.owner}/${baseRepository.name}/pull/${issueId}")
}) getOrElse NotFound()
}
})
post("/:owner/:repository/pull/:id/update_branch")(readableUsersOnly { baseRepository =>
@@ -361,8 +365,11 @@ trait PullRequestsControllerBase extends ControllerBase {
form.isDraft,
context.settings
) match {
case Right(objectId) => redirect(s"/${repository.owner}/${repository.name}/pull/$issueId")
case Left(message) => Some(BadRequest(message))
case Right(result) =>
updateMergedCommitIds(repository.owner, repository.name, issueId, result.mergedCommitId)
redirect(s"/${repository.owner}/${repository.name}/pull/$issueId")
case Left(message) =>
Some(BadRequest(message))
}
} getOrElse NotFound()
}
@@ -722,6 +729,107 @@ trait PullRequestsControllerBase extends ControllerBase {
)
}
post("/:owner/:repository/pull/:id/revert")(writableUsersOnly { repository =>
context.withLoginAccount { loginAccount =>
(for {
issueId <- params.get("id").map(_.toInt)
(issue, pullreq) <- getPullRequest(repository.owner, repository.name, issueId) if issue.closed
} yield {
val baseBranch = pullreq.branch
val revertBranch = s"revert-pr-$issueId-${System.currentTimeMillis()}"
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
try {
// Create a new branch from base
JGitUtil.createBranch(git, baseBranch, revertBranch)
// TODO Call webhook ???
val tempDir = Files.createTempDirectory("jgit-revert-")
val revertCommitName =
try {
// Clone bare repository
Using.resource(
Git.cloneRepository
.setURI(getRepositoryDir(repository.owner, repository.name).getAbsolutePath)
.setDirectory(tempDir.toFile)
.setBranch(revertBranch)
.setBare(false)
.setNoCheckout(false)
.call()
) { git =>
// Get commit Ids to be reverted
val commitsToRevert = Using.resource(new RevWalk(git.getRepository)) { revWalk =>
pullreq.mergedCommitIds
.map(
_.split(",")
.map { mergedCommitId =>
revWalk.parseCommit(git.getRepository.resolve(mergedCommitId))
}
.toSeq
.reverse
)
.getOrElse(Nil)
}
// revert
var revert = git.revert
commitsToRevert.foreach { id =>
revert = revert.include(id)
}
val newCommit = revert.call()
if (newCommit != null) {
System.out.println("Reverted commit created: " + newCommit.getName)
git.push.call()
Some(newCommit.getName)
} else {
System.out.println("Revert resulted in conflicts.")
None
}
}
} finally {
FileUtil.deleteRecursively(tempDir.toFile)
}
revertCommitName match {
case Some(revertCommitName) =>
val newIssueId = insertIssue(
owner = repository.owner,
repository = repository.name,
loginUser = loginAccount.userName,
title = s"Revert #${issueId}",
content = Some(s"Revert #${issueId}"),
milestoneId = None,
priorityId = None,
isPullRequest = true
)
createPullRequest(
originRepository = repository,
issueId = newIssueId,
originBranch = baseBranch,
requestUserName = repository.owner,
requestRepositoryName = repository.name,
requestBranch = revertBranch,
commitIdFrom = baseBranch,
commitIdTo = revertCommitName,
isDraft = false,
loginAccount = loginAccount,
settings = context.settings
)
redirect(s"/${repository.owner}/${repository.name}/pull/$newIssueId")
case None =>
BadRequest("Failed to create revert commit.")
}
} catch {
case ex: Exception =>
ex.printStackTrace()
BadRequest(s"Revert failed: ${ex.getMessage}")
}
}
}) getOrElse NotFound()
}
})
/**
* Tests whether an logged-in user can manage pull requests.
*/
@@ -740,5 +848,4 @@ trait PullRequestsControllerBase extends ControllerBase {
case "DISABLE" => false
}
}
}

View File

@@ -13,6 +13,7 @@ trait PullRequestComponent extends TemplateComponent { self: Profile =>
val commitIdFrom = column[String]("COMMIT_ID_FROM")
val commitIdTo = column[String]("COMMIT_ID_TO")
val isDraft = column[Boolean]("IS_DRAFT")
val mergedCommitIds = column[String]("MERGED_COMMIT_IDS")
def * =
(
userName,
@@ -24,12 +25,13 @@ trait PullRequestComponent extends TemplateComponent { self: Profile =>
requestBranch,
commitIdFrom,
commitIdTo,
isDraft
isDraft,
mergedCommitIds.?
).mapTo[PullRequest]
def byPrimaryKey(userName: String, repositoryName: String, issueId: Int) =
def byPrimaryKey(userName: String, repositoryName: String, issueId: Int): Rep[Boolean] =
byIssue(userName, repositoryName, issueId)
def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], issueId: Rep[Int]) =
def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], issueId: Rep[Int]): Rep[Boolean] =
byIssue(userName, repositoryName, issueId)
}
}
@@ -44,5 +46,6 @@ case class PullRequest(
requestBranch: String,
commitIdFrom: String,
commitIdTo: String,
isDraft: Boolean
isDraft: Boolean,
mergedCommitIds: Option[String]
)

View File

@@ -7,7 +7,7 @@ import gitbucket.core.plugin.{PluginRegistry, ReceiveHook}
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.util.Directory._
import gitbucket.core.util.{JGitUtil, LockUtil}
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.model.Profile.profile.blockingApi.*
import gitbucket.core.model.activity.{CloseIssueInfo, MergeInfo, PushInfo}
import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.service.WebHookService.WebHookPushPayload
@@ -19,14 +19,14 @@ import org.eclipse.jgit.errors.NoMergeBaseException
import org.eclipse.jgit.lib.{CommitBuilder, ObjectId, PersonIdent, Repository}
import org.eclipse.jgit.revwalk.{RevCommit, RevWalk}
import scala.jdk.CollectionConverters._
import scala.jdk.CollectionConverters.*
import scala.util.Using
trait MergeService {
self: AccountService & ActivityService & IssuesService & RepositoryService & PullRequestService &
WebHookPullRequestService & WebHookService =>
import MergeService._
import MergeService.*
/**
* Checks whether conflict will be caused in merging within pull request.
@@ -61,15 +61,16 @@ trait MergeService {
repository: RepositoryInfo,
branch: String,
issueId: Int,
commits: Seq[RevCommit],
message: String,
loginAccount: Account,
settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context): ObjectId = {
)(implicit s: Session, c: JsonFormat.Context): MergeResult = {
val beforeCommitId = git.getRepository.resolve(s"refs/heads/${branch}")
val afterCommitId = new MergeCacheInfo(git, repository.owner, repository.name, branch, issueId, getReceiveHooks())
.merge(message, new PersonIdent(loginAccount.fullName, loginAccount.mailAddress), loginAccount.userName)
callWebHook(git, repository, branch, beforeCommitId, afterCommitId, loginAccount, settings)
afterCommitId
val mergeResult = new MergeCacheInfo(git, repository.owner, repository.name, branch, issueId, getReceiveHooks())
.merge(message, new PersonIdent(loginAccount.fullName, loginAccount.mailAddress), loginAccount.userName, commits)
callWebHook(git, repository, branch, beforeCommitId, mergeResult.newCommitId, loginAccount, settings)
mergeResult
}
/** rebase to the head of the pull request branch */
@@ -81,13 +82,13 @@ trait MergeService {
commits: Seq[RevCommit],
loginAccount: Account,
settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context): ObjectId = {
)(implicit s: Session, c: JsonFormat.Context): MergeResult = {
val beforeCommitId = git.getRepository.resolve(s"refs/heads/${branch}")
val afterCommitId =
val mergeResult =
new MergeCacheInfo(git, repository.owner, repository.name, branch, issueId, getReceiveHooks())
.rebase(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress), loginAccount.userName, commits)
callWebHook(git, repository, branch, beforeCommitId, afterCommitId, loginAccount, settings)
afterCommitId
callWebHook(git, repository, branch, beforeCommitId, mergeResult.newCommitId, loginAccount, settings)
mergeResult
}
/** squash commits in the pull request and append it */
@@ -99,13 +100,13 @@ trait MergeService {
message: String,
loginAccount: Account,
settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context): ObjectId = {
)(implicit s: Session, c: JsonFormat.Context): MergeResult = {
val beforeCommitId = git.getRepository.resolve(s"refs/heads/${branch}")
val afterCommitId =
val mergeResult =
new MergeCacheInfo(git, repository.owner, repository.name, branch, issueId, getReceiveHooks())
.squash(message, new PersonIdent(loginAccount.fullName, loginAccount.mailAddress), loginAccount.userName)
callWebHook(git, repository, branch, beforeCommitId, afterCommitId, loginAccount, settings)
afterCommitId
callWebHook(git, repository, branch, beforeCommitId, mergeResult.newCommitId, loginAccount, settings)
mergeResult
}
private def callWebHook(
@@ -337,7 +338,7 @@ trait MergeService {
strategy: String,
isDraft: Boolean,
settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context, context: Context): Either[String, ObjectId] = {
)(implicit s: Session, c: JsonFormat.Context, context: Context): Either[String, MergeResult] = {
if (!isDraft) {
if (repository.repository.options.mergeOptions.split(",").contains(strategy)) {
LockUtil.lock(s"${repository.owner}/${repository.name}") {
@@ -493,7 +494,7 @@ trait MergeService {
commits: Seq[Seq[CommitInfo]],
receiveHooks: Seq[ReceiveHook],
settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context): Option[ObjectId] = {
)(implicit s: Session, c: JsonFormat.Context): Option[MergeResult] = {
val revCommits = Using
.resource(new RevWalk(git.getRepository)) { revWalk =>
commits.flatten.map { commit =>
@@ -510,6 +511,7 @@ trait MergeService {
repository,
pullRequest.branch,
issue.issueId,
revCommits,
s"Merge pull request #${issue.issueId} from ${pullRequest.requestUserName}/${pullRequest.requestBranch}\n\n" + message,
loginAccount,
settings
@@ -600,13 +602,13 @@ object MergeService {
private val mergedBranchName = s"refs/pull/${issueId}/merge"
private val conflictedBranchName = s"refs/pull/${issueId}/conflict"
lazy val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${branch}")
lazy val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
lazy val mergeBaseTip: ObjectId = git.getRepository.resolve(s"refs/heads/${branch}")
lazy val mergeTip: ObjectId = git.getRepository.resolve(s"refs/pull/${issueId}/head")
def checkConflictCache(): Option[Option[String]] = {
Option(git.getRepository.resolve(mergedBranchName))
.flatMap { merged =>
if (parseCommit(merged).getParents().toSet == Set(mergeBaseTip, mergeTip)) {
if (parseCommit(merged).getParents.toSet == Set(mergeBaseTip, mergeTip)) {
// merged branch exists
Some(None)
} else {
@@ -615,7 +617,7 @@ object MergeService {
}
.orElse(Option(git.getRepository.resolve(conflictedBranchName)).flatMap { conflicted =>
val commit = parseCommit(conflicted)
if (commit.getParents().toSet == Set(mergeBaseTip, mergeTip)) {
if (commit.getParents.toSet == Set(mergeBaseTip, mergeTip)) {
// conflict branch exists
Some(Some(commit.getFullMessage))
} else {
@@ -651,14 +653,16 @@ object MergeService {
None
} else {
val message = createConflictMessage(mergeTip, mergeBaseTip, merger)
_updateBranch(mergeTipCommit.getTree().getId(), message, conflictedBranchName)
_updateBranch(mergeTipCommit.getTree.getId, message, conflictedBranchName)
git.branchDelete().setForce(true).setBranchNames(mergedBranchName).call()
Some(message)
}
}
// update branch from cache
def merge(message: String, committer: PersonIdent, pusher: String)(implicit s: Session): ObjectId = {
def merge(message: String, committer: PersonIdent, pusher: String, commits: Seq[RevCommit])(implicit
s: Session
): MergeResult = {
if (checkConflict().isDefined) {
throw new RuntimeException("This pull request can't merge automatically.")
}
@@ -666,7 +670,7 @@ object MergeService {
throw new RuntimeException(s"Not found branch ${mergedBranchName}")
})
// creates merge commit
val mergeCommitId = createMergeCommit(mergeResultCommit.getTree().getId(), committer, message)
val mergeCommitId = createMergeCommit(mergeResultCommit.getTree.getId, committer, message)
val refName = s"refs/heads/${branch}"
val currentObjectId = git.getRepository.resolve(refName)
@@ -690,10 +694,10 @@ object MergeService {
hook.postReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true)
}
objectId
MergeResult(objectId, commits.map(_.name()))
}
def rebase(committer: PersonIdent, pusher: String, commits: Seq[RevCommit])(implicit s: Session): ObjectId = {
def rebase(committer: PersonIdent, pusher: String, commits: Seq[RevCommit])(implicit s: Session): MergeResult = {
if (checkConflict().isDefined) {
throw new RuntimeException("This pull request can't merge automatically.")
}
@@ -713,11 +717,13 @@ object MergeService {
val mergeBaseTipCommit = Using.resource(new RevWalk(git.getRepository))(_.parseCommit(mergeBaseTip))
var previousId = mergeBaseTipCommit.getId
val mergedCommitIds = Seq.newBuilder[String]
Using.resource(git.getRepository.newObjectInserter) { inserter =>
commits.foreach { commit =>
val nextCommit = _cloneCommit(commit, previousId, mergeBaseTipCommit.getId)
previousId = inserter.insert(nextCommit)
mergedCommitIds += previousId.name()
}
inserter.flush()
}
@@ -745,10 +751,10 @@ object MergeService {
hook.postReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true)
}
objectId
MergeResult(objectId, mergedCommitIds.result())
}
def squash(message: String, committer: PersonIdent, pusher: String)(implicit s: Session): ObjectId = {
def squash(message: String, committer: PersonIdent, pusher: String)(implicit s: Session): MergeResult = {
if (checkConflict().isDefined) {
throw new RuntimeException("This pull request can't merge automatically.")
}
@@ -804,7 +810,7 @@ object MergeService {
hook.postReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true)
}
objectId
MergeResult(objectId, Seq(newCommitId.name()))
}
// return treeId
@@ -823,4 +829,5 @@ object MergeService {
mergeResults.asScala.map { case (key, _) => "- `" + key + "`\n" }.mkString
}
case class MergeResult(newCommitId: ObjectId, mergedCommitId: Seq[String])
}

View File

@@ -63,6 +63,15 @@ trait PullRequestService {
.update((baseBranch, commitIdTo))
}
def updateMergedCommitIds(owner: String, repository: String, issueId: Int, mergedCommitIds: Seq[String])(implicit
s: Session
): Unit = {
PullRequests
.filter(_.byPrimaryKey(owner, repository, issueId))
.map(pr => pr.mergedCommitIds)
.update(mergedCommitIds.mkString(","))
}
def getPullRequestCountGroupByUser(closed: Boolean, owner: Option[String], repository: Option[String])(implicit
s: Session
): List[PullRequestCount] =
@@ -126,7 +135,8 @@ trait PullRequestService {
requestBranch,
commitIdFrom,
commitIdTo,
isDraft
isDraft,
None
)
// fetch requested branch
@@ -408,11 +418,10 @@ trait PullRequestService {
.find(x => x.oldPath == file)
.map { diff =>
(diff.oldContent, diff.newContent) match {
case (Some(oldContent), Some(newContent)) => {
case (Some(oldContent), Some(newContent)) =>
val oldLines = convertLineSeparator(oldContent, "LF").split("\n")
val newLines = convertLineSeparator(newContent, "LF").split("\n")
file -> Option(DiffUtils.diff(oldLines.toList.asJava, newLines.toList.asJava))
}
case _ =>
file -> None
}

View File

@@ -81,4 +81,13 @@
});
});
</script>
@if(issue.closed && pullreq.mergedCommitIds.isDefined){
<div class="issue-comment-box" style="margin-bottom: 20px;">
<div style="padding: 10px;">
<form method="post" action="@helpers.url(repository)/pull/@issue.issueId/revert" style="display:inline;">
<button type="submit" class="btn btn-warning pull-right" style="margin-left: 10px;">Revert</button>
</form>
</div>
</div>
}
}

View File

@@ -154,7 +154,8 @@ object ApiSpecModels {
requestBranch = "new-topic",
commitIdFrom = sha1,
commitIdTo = sha1,
isDraft = true
isDraft = true,
mergedCommitIds = None
)
val commitComment: CommitComment = CommitComment(

View File

@@ -171,6 +171,7 @@ class MergeServiceSpec extends AnyFunSpec with ServiceSpecBase {
repository,
branch,
issueId,
Seq(git.getRepository.parseCommit(commitId)),
"merged",
context.loginAccount.get,
context.settings