Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/emergency merge #70

Merged
merged 11 commits into from
May 5, 2020
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
- Workflow Engine to enforce rules before merging pull requests
- Emergency Merge to override configured workflow rules ([#70](https://github.com/scm-manager/scm-review-plugin/pull/70)

## 2.0.0-rc9-2 - 2020-04-14
### Added
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/com/cloudogu/scm/review/PermissionCheck.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public final class PermissionCheck {
public static final String READ_PULL_REQUEST = "readPullRequest";
public static final String COMMENT_PULL_REQUEST = "commentPullRequest";
public static final String MERGE_PULL_REQUEST = "mergePullRequest";
public static final String PERFORM_EMERGENCY_MERGE = "performEmergencyMerge";
public static final String CONFIGURE_PULL_REQUEST = "configurePullRequest";
public static final String CONFIGURE_PERMISSION = "pullRequest";
public static final String WORKFLOW_CONFIG = "workflowConfig";
Expand Down Expand Up @@ -84,6 +85,14 @@ public static void checkMerge(Repository repository) {
RepositoryPermissions.custom(MERGE_PULL_REQUEST, repository).check();
}

public static void checkEmergencyMerge(Repository repository) {
RepositoryPermissions.custom(PERFORM_EMERGENCY_MERGE, repository).check();
}

public static boolean mayPerformEmergencyMerge(Repository repository) {
return RepositoryPermissions.custom(PERFORM_EMERGENCY_MERGE, repository).isPermitted();
}

/**
* A User can modify a comment if he is the author or he has a modify permission
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,11 @@ public String merge(String namespace, String name, String pullRequestId, MergeSt
.method("merge").parameters(namespace, name, pullRequestId).href() + "?strategy=" + strategy;
}

public String emergencyMerge(String namespace, String name, String pullRequestId, MergeStrategy strategy) {
return mergeLinkBuilder
.method("emergencyMerge").parameters(namespace, name, pullRequestId).href() + "?strategy=" + strategy;
}

public String conflicts(String namespace, String name, String pullRequestId) {
return mergeLinkBuilder.method("conflicts").parameters(namespace, name, pullRequestId).href();
}
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/com/cloudogu/scm/review/StatusCheckHook.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
import static java.lang.String.format;
import static sonia.scm.ContextEntry.ContextBuilder.entity;

@EagerSingleton @Extension
@EagerSingleton
@Extension
public class StatusCheckHook {

private static final Logger LOG = LoggerFactory.getLogger(StatusCheckHook.class);
Expand Down Expand Up @@ -103,7 +104,7 @@ private void process(PullRequest pullRequest) {
}

private void processOpen(PullRequest pullRequest) {
if (branchesAreModified(pullRequest)){
if (branchesAreModified(pullRequest)) {
if (isMerged(pullRequest)) {
setMerged(pullRequest);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public class CommentDto extends BasicCommentDto {

private boolean systemComment;
private boolean outdated;
private boolean emergencyMerged;

private String type;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import javax.xml.bind.annotation.XmlRootElement;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static com.cloudogu.scm.review.comment.service.CommentType.COMMENT;
Expand Down Expand Up @@ -62,6 +61,7 @@ public static Comment createComment(String id, String text, String author, Locat
private CommentType type = COMMENT;
private boolean outdated;
private InlineContext context;
private boolean emergencyMerged;

private List<Reply> replies = new ArrayList<>();

Expand Down Expand Up @@ -126,8 +126,15 @@ public InlineContext getContext() {
return this.context;
}


public void setContext(InlineContext context) {
this.context = context;
}

public boolean isEmergencyMerged() {
return emergencyMerged;
}

public void setEmergencyMerged(boolean emergencyMerged) {
this.emergencyMerged = emergencyMerged;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ private void initializeContextFromDiff(Comment comment, PullRequest pullRequest,

List<ContextLine> contextLines =
computeContext(comment, diffResult)
.stream()
.map(ContextLine::copy)
.collect(Collectors.toList());
.stream()
.map(ContextLine::copy)
.collect(Collectors.toList());
comment.setContext(new InlineContext(contextLines));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.cloudogu.scm.review.RepositoryResolver;
import com.cloudogu.scm.review.comment.api.MentionMapper;
import com.cloudogu.scm.review.pullrequest.service.PullRequest;
import com.cloudogu.scm.review.pullrequest.service.PullRequestEmergencyMergedEvent;
import com.cloudogu.scm.review.pullrequest.service.PullRequestMergedEvent;
import com.cloudogu.scm.review.pullrequest.service.PullRequestRejectedEvent;
import com.cloudogu.scm.review.pullrequest.service.PullRequestService;
Expand All @@ -40,8 +41,10 @@
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository;
import sonia.scm.security.KeyGenerator;
import sonia.scm.user.User;

import javax.inject.Inject;
import java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -351,6 +354,16 @@ public void addCommentOnMerge(PullRequestMergedEvent mergedEvent) {
addStatusChangedComment(mergedEvent.getRepository(), mergedEvent.getPullRequest().getId(), SystemCommentType.MERGED);
}

@Subscribe
public void addCommentOnEmergencyMerge(PullRequestEmergencyMergedEvent mergedEvent) {
PullRequest pullRequest = mergedEvent.getPullRequest();
Comment comment = new Comment();
comment.setEmergencyMerged(true);
comment.setComment(pullRequest.getOverrideMessage());

addWithoutPermissionCheck(mergedEvent.getRepository(), pullRequest.getId(), comment);
}

@Subscribe
public void addCommentOnReject(PullRequestRejectedEvent rejectedEvent) {
addStatusChangedComment(rejectedEvent.getRepository(), rejectedEvent.getPullRequest().getId(), getCommentType(rejectedEvent.getCause()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*/
package com.cloudogu.scm.review.pullrequest.api;

import com.cloudogu.scm.review.PermissionCheck;
import com.cloudogu.scm.review.PullRequestMediaType;
import com.cloudogu.scm.review.PullRequestResourceLinks;
import com.cloudogu.scm.review.pullrequest.dto.MergeCheckResultDto;
Expand Down Expand Up @@ -90,7 +91,34 @@ public void merge(
@NotNull @Valid MergeCommitDto mergeCommitDto
) {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
service.merge(namespaceAndName, pullRequestId, mergeCommitDto, strategy);
service.merge(namespaceAndName, pullRequestId, mergeCommitDto, strategy, false);
}

@POST
@Path("{namespace}/{name}/{pullRequestId}/emergency")
@Consumes(PullRequestMediaType.MERGE_COMMAND)
@Operation(summary = "Merge pull request", description = "Merges pull request with selected strategy as emergency merge.", tags = "Pull Request")
@ApiResponse(responseCode = "204", description = "update success")
@ApiResponse(responseCode = "401", description = "not authenticated / invalid credentials")
@ApiResponse(responseCode = "403", description = "not authorized, the current user does not have the \"performEmergencyMerge\" privilege")
@ApiResponse(responseCode = "404", description = "not found, no pull request with the specified id is available")
@ApiResponse(
responseCode = "500",
description = "internal server error",
content = @Content(
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
)
)
public void emergencyMerge(
@PathParam("namespace") String namespace,
@PathParam("name") String name,
@PathParam("pullRequestId") String pullRequestId,
@QueryParam("strategy") MergeStrategy strategy,
@NotNull @Valid MergeCommitDto mergeCommitDto
) {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
service.merge(namespaceAndName, pullRequestId, mergeCommitDto, strategy, true);
}

@POST
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.List;

@Getter
@Setter
@AllArgsConstructor
Expand All @@ -36,4 +38,5 @@ public class MergeCommitDto {
private String commitMessage;
private boolean shouldDeleteSourceBranch;
private String overrideMessage;
private List<String> ignoredMergeObstacles;
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import java.time.Instant;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Getter
Expand Down Expand Up @@ -62,7 +63,8 @@ public class PullRequestDto extends HalRepresentation {
private String sourceRevision;
private String targetRevision;
private Collection<String> markedAsReviewed;

private boolean emergencyMerged;
private List<String> ignoredMergeObstacles;

@Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,11 +221,18 @@ protected void appendLinks(@MappingTarget PullRequestDto target, PullRequest pul
private void appendMergeStrategyLinks(Links.Builder linksBuilder, Repository repository, PullRequest pullRequest) {
try (RepositoryService service = serviceFactory.create(repository)) {
if (service.isSupported(Command.MERGE)) {
List<Link> strategyLinks = service.getMergeCommand().getSupportedMergeStrategies()
List<Link> mergeStrategyLinks = service.getMergeCommand().getSupportedMergeStrategies()
.stream()
.map(strategy -> createStrategyLink(repository.getNamespaceAndName(), pullRequest, strategy))
.map(strategy -> createMergeStrategyLink(repository.getNamespaceAndName(), pullRequest, strategy))
.collect(toList());
linksBuilder.array(strategyLinks);
linksBuilder.array(mergeStrategyLinks);
if (PermissionCheck.mayPerformEmergencyMerge(repository)) {
List<Link> emergencyMergeStrategyLinks = service.getMergeCommand().getSupportedMergeStrategies()
.stream()
.map(strategy -> createEmergencyMergeStrategyLink(repository.getNamespaceAndName(), pullRequest, strategy))
.collect(toList());
linksBuilder.array(emergencyMergeStrategyLinks);
}
}
}
}
Expand All @@ -238,7 +245,7 @@ private ReviewerDto createReviewerDto(DisplayUser user, Boolean approved) {
return new ReviewerDto(user.getId(), user.getDisplayName(), user.getMail(), approved);
}

private Link createStrategyLink(NamespaceAndName namespaceAndName, PullRequest pullRequest, MergeStrategy strategy) {
private Link createMergeStrategyLink(NamespaceAndName namespaceAndName, PullRequest pullRequest, MergeStrategy strategy) {
return Link.linkBuilder("merge", pullRequestResourceLinks.mergeLinks().merge(
namespaceAndName.getNamespace(),
namespaceAndName.getName(),
Expand All @@ -247,4 +254,14 @@ private Link createStrategyLink(NamespaceAndName namespaceAndName, PullRequest p
)
).withName(strategy.name()).build();
}

private Link createEmergencyMergeStrategyLink(NamespaceAndName namespaceAndName, PullRequest pullRequest, MergeStrategy strategy) {
return Link.linkBuilder("emergencyMerge", pullRequestResourceLinks.mergeLinks().emergencyMerge(
namespaceAndName.getNamespace(),
namespaceAndName.getName(),
pullRequest.getId(),
strategy
)
).withName(strategy.name()).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import com.cloudogu.scm.review.PermissionCheck;
import com.cloudogu.scm.review.RepositoryResolver;
import com.cloudogu.scm.review.StatusChangeNotAllowedException;
import com.cloudogu.scm.review.pullrequest.dto.MergeCommitDto;
import com.google.inject.Inject;
import sonia.scm.HandlerEventType;
import sonia.scm.event.ScmEventBus;
Expand Down Expand Up @@ -225,6 +226,17 @@ public void setMerged(Repository repository, String pullRequestId, String overri
}
}

@Override
public void setEmergencyMerged(Repository repository, String pullRequestId, MergeCommitDto mergeCommitDto) {
PullRequest pullRequest = get(repository, pullRequestId);
pullRequest.setOverrideMessage(mergeCommitDto.getOverrideMessage());
pullRequest.setEmergencyMerged(true);
pullRequest.setStatus(MERGED);
pullRequest.setIgnoredMergeObstacles(mergeCommitDto.getIgnoredMergeObstacles());
getStore(repository).update(pullRequest);
eventBus.post(new PullRequestEmergencyMergedEvent(repository, pullRequest));
}

@Override
public void updated(Repository repository, String pullRequestId) {
PullRequest pullRequest = get(repository, pullRequestId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
*/
package com.cloudogu.scm.review.pullrequest.service;

import com.cloudogu.scm.review.PermissionCheck;
import com.cloudogu.scm.review.BranchProtectionHook;
import com.cloudogu.scm.review.PermissionCheck;
import com.cloudogu.scm.review.pullrequest.dto.MergeCommitDto;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.NamespaceAndName;
Expand Down Expand Up @@ -77,7 +77,7 @@ public MergeService(RepositoryServiceFactory serviceFactory, PullRequestService
this.branchProtectionHook = branchProtectionHook;
}

public void merge(NamespaceAndName namespaceAndName, String pullRequestId, MergeCommitDto mergeCommitDto, MergeStrategy strategy) {
public void merge(NamespaceAndName namespaceAndName, String pullRequestId, MergeCommitDto mergeCommitDto, MergeStrategy strategy, boolean emergency) {
eheimbuch marked this conversation as resolved.
Show resolved Hide resolved
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
PullRequest pullRequest = pullRequestService.get(repositoryService.getRepository(), pullRequestId);
Collection<MergeObstacle> obstacles = getObstacles(repositoryService.getRepository(), pullRequest);
Expand All @@ -89,7 +89,7 @@ public void merge(NamespaceAndName namespaceAndName, String pullRequestId, Merge
branchProtectionHook.runPrivileged(
() -> {
MergeCommandBuilder mergeCommand = repositoryService.getMergeCommand();
isAllowedToMerge(repositoryService.getRepository(), mergeCommand, strategy);
isAllowedToMerge(repositoryService.getRepository(), mergeCommand, strategy, emergency);
prepareMergeCommand(mergeCommand, pullRequest, mergeCommitDto, strategy);
MergeCommandResult mergeCommandResult = mergeCommand.executeMerge();

Expand All @@ -99,6 +99,9 @@ public void merge(NamespaceAndName namespaceAndName, String pullRequestId, Merge

pullRequestService.setRevisions(repositoryService.getRepository(), pullRequest.getId(), mergeCommandResult.getTargetRevision(), mergeCommandResult.getRevisionToMerge());
pullRequestService.setMerged(repositoryService.getRepository(), pullRequest.getId(), mergeCommitDto.getOverrideMessage());
if (emergency) {
pullRequestService.setEmergencyMerged(repositoryService.getRepository(), pullRequest.getId(), mergeCommitDto);
}
eheimbuch marked this conversation as resolved.
Show resolved Hide resolved

if (repositoryService.isSupported(Command.BRANCH) && mergeCommitDto.isShouldDeleteSourceBranch()) {
repositoryService.getBranchCommand().delete(pullRequest.getSource());
Expand Down Expand Up @@ -202,8 +205,12 @@ private void assertPullRequestIsOpen(Repository repository, PullRequest pullRequ
}
}

private void isAllowedToMerge(Repository repository, MergeCommandBuilder mergeCommand, MergeStrategy strategy) {
PermissionCheck.checkMerge(repository);
private void isAllowedToMerge(Repository repository, MergeCommandBuilder mergeCommand, MergeStrategy strategy, boolean emergency) {
if (emergency) {
PermissionCheck.checkEmergencyMerge(repository);
} else {
PermissionCheck.checkMerge(repository);
}
if (!mergeCommand.isSupported(strategy)) {
throw new MergeStrategyNotSupportedException(repository, strategy);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.time.Instant;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

Expand Down Expand Up @@ -72,6 +73,8 @@ public class PullRequest {
private String targetRevision;
private Set<ReviewMark> reviewMarks = new HashSet<>();
private String overrideMessage;
private boolean emergencyMerged;
private List<String> ignoredMergeObstacles;

public PullRequest(String id, String source, String target) {
this.id = id;
Expand Down
Loading