Add resource for merges

This commit is contained in:
René Pfeuffer
2018-12-05 13:52:14 +01:00
parent 16eb433618
commit 7900b94011
13 changed files with 283 additions and 9 deletions

View File

@@ -15,7 +15,7 @@ import sonia.scm.repository.spi.MergeCommandRequest;
*
* To actually merge <code>feature_branch</code> into <code>integration_branch</code> do this:
* <pre><code>
* repositoryService.gerMergeCommand()
* repositoryService.getMergeCommand()
* .setBranchToMerge("feature_branch")
* .setTargetBranch("integration_branch")
* .executeMerge();
@@ -33,7 +33,7 @@ import sonia.scm.repository.spi.MergeCommandRequest;
*
* To check whether they can be merged without conflicts beforehand do this:
* <pre><code>
* repositoryService.gerMergeCommand()
* repositoryService.getMergeCommand()
* .setBranchToMerge("feature_branch")
* .setTargetBranch("integration_branch")
* .dryRun()

View File

@@ -363,8 +363,8 @@ public final class RepositoryService implements Closeable {
* by the implementation of the repository service provider.
* @since 2.0.0
*/
public MergeCommandBuilder gerMergeCommand() {
logger.debug("create unbundle command for repository {}",
public MergeCommandBuilder getMergeCommand() {
logger.debug("create merge command for repository {}",
repository.getNamespaceAndName());
return new MergeCommandBuilder(provider.getMergeCommand());

View File

@@ -41,6 +41,8 @@ public class VndMediaType {
public static final String PASSWORD_CHANGE = PREFIX + "passwordChange" + SUFFIX;
@SuppressWarnings("squid:S2068")
public static final String PASSWORD_OVERWRITE = PREFIX + "passwordOverwrite" + SUFFIX;
public static final String MERGE_RESULT = PREFIX + "mergeResult" + SUFFIX;
public static final String MERGE_COMMAND = PREFIX + "mergeCommand" + SUFFIX;
public static final String ME = PREFIX + "me" + SUFFIX;
public static final String SOURCE = PREFIX + "source" + SUFFIX;

View File

@@ -43,6 +43,8 @@ public class MapperModule extends AbstractModule {
bind(ScmViolationExceptionToErrorDtoMapper.class).to(Mappers.getMapper(ScmViolationExceptionToErrorDtoMapper.class).getClass());
bind(ExceptionWithContextToErrorDtoMapper.class).to(Mappers.getMapper(ExceptionWithContextToErrorDtoMapper.class).getClass());
bind(MergeResultToDtoMapper.class).to(Mappers.getMapper(MergeResultToDtoMapper.class).getClass());
// no mapstruct required
bind(UIPluginDtoMapper.class);
bind(UIPluginDtoCollectionMapper.class);

View File

@@ -0,0 +1,14 @@
package sonia.scm.api.v2.resources;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.NotEmpty;
@Getter @Setter
public class MergeCommandDto {
@NotEmpty
private String sourceRevision;
@NotEmpty
private String targetRevision;
}

View File

@@ -0,0 +1,71 @@
package sonia.scm.api.v2.resources;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpStatus;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.api.MergeCommandBuilder;
import sonia.scm.repository.api.MergeCommandResult;
import sonia.scm.repository.api.MergeDryRunCommandResult;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
@Slf4j
public class MergeResource {
private final RepositoryServiceFactory serviceFactory;
private final MergeResultToDtoMapper mapper;
@Inject
public MergeResource(RepositoryServiceFactory serviceFactory, MergeResultToDtoMapper mapper) {
this.serviceFactory = serviceFactory;
this.mapper = mapper;
}
@POST
@Produces(VndMediaType.MERGE_RESULT)
@Consumes(VndMediaType.MERGE_COMMAND)
public Response merge(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid MergeCommandDto mergeCommand) {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
log.info("Merge in Repository {}/{} from {} to {}", namespace, name, mergeCommand.getSourceRevision(), mergeCommand.getTargetRevision());
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
MergeCommandResult mergeCommandResult = createMergeCommand(mergeCommand, repositoryService).executeMerge();
if (mergeCommandResult.isSuccess()) {
return Response.noContent().build();
} else {
return Response.status(HttpStatus.SC_CONFLICT).entity(mapper.map(mergeCommandResult)).build();
}
}
}
@POST
@Path("dry-run/")
public Response dryRun(@PathParam("namespace") String namespace, @PathParam("name") String name, @Valid MergeCommandDto mergeCommand) {
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
log.info("Merge in Repository {}/{} from {} to {}", namespace, name, mergeCommand.getSourceRevision(), mergeCommand.getTargetRevision());
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
MergeDryRunCommandResult mergeCommandResult = createMergeCommand(mergeCommand, repositoryService).dryRun();
if (mergeCommandResult.isMergeable()) {
return Response.noContent().build();
} else {
return Response.status(HttpStatus.SC_CONFLICT).build();
}
}
}
private MergeCommandBuilder createMergeCommand(MergeCommandDto mergeCommand, RepositoryService repositoryService) {
return repositoryService
.getMergeCommand()
.setBranchToMerge(mergeCommand.getSourceRevision())
.setTargetBranch(mergeCommand.getTargetRevision());
}
}

View File

@@ -0,0 +1,12 @@
package sonia.scm.api.v2.resources;
import lombok.Getter;
import lombok.Setter;
import java.util.Collection;
@Getter
@Setter
public class MergeResultDto {
private Collection<String> filesWithConflict;
}

View File

@@ -0,0 +1,9 @@
package sonia.scm.api.v2.resources;
import org.mapstruct.Mapper;
import sonia.scm.repository.api.MergeCommandResult;
@Mapper
public interface MergeResultToDtoMapper {
MergeResultDto map(MergeCommandResult result);
}

View File

@@ -10,7 +10,6 @@ import sonia.scm.repository.RepositoryManager;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
@@ -44,6 +43,7 @@ public class RepositoryResource {
private final Provider<DiffRootResource> diffRootResource;
private final Provider<ModificationsRootResource> modificationsRootResource;
private final Provider<FileHistoryRootResource> fileHistoryRootResource;
private final Provider<MergeResource> mergeResource;
@Inject
public RepositoryResource(
@@ -56,8 +56,8 @@ public class RepositoryResource {
Provider<PermissionRootResource> permissionRootResource,
Provider<DiffRootResource> diffRootResource,
Provider<ModificationsRootResource> modificationsRootResource,
Provider<FileHistoryRootResource> fileHistoryRootResource
) {
Provider<FileHistoryRootResource> fileHistoryRootResource,
Provider<MergeResource> mergeResource) {
this.dtoToRepositoryMapper = dtoToRepositoryMapper;
this.manager = manager;
this.repositoryToDtoMapper = repositoryToDtoMapper;
@@ -71,6 +71,7 @@ public class RepositoryResource {
this.diffRootResource = diffRootResource;
this.modificationsRootResource = modificationsRootResource;
this.fileHistoryRootResource = fileHistoryRootResource;
this.mergeResource = mergeResource;
}
/**
@@ -194,9 +195,12 @@ public class RepositoryResource {
return permissionRootResource.get();
}
@Path("modifications/")
@Path("modifications/")
public ModificationsRootResource modifications() {return modificationsRootResource.get(); }
@Path("merge/")
public MergeResource merge() {return mergeResource.get(); }
private Optional<Response> handleNotArchived(Throwable throwable) {
if (throwable instanceof RepositoryIsNotArchivedException) {
return Optional.of(Response.status(Response.Status.PRECONDITION_FAILED).build());

View File

@@ -0,0 +1,150 @@
package sonia.scm.api.v2.resources;
import com.google.common.io.Resources;
import com.google.inject.util.Providers;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.api.MergeCommandBuilder;
import sonia.scm.repository.api.MergeCommandResult;
import sonia.scm.repository.api.MergeDryRunCommandResult;
import sonia.scm.repository.api.RepositoryService;
import sonia.scm.repository.api.RepositoryServiceFactory;
import sonia.scm.repository.spi.MergeCommand;
import sonia.scm.web.VndMediaType;
import java.net.URL;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class MergeResourceTest extends RepositoryTestBase {
public static final String MERGE_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/merge/";
private Dispatcher dispatcher;
@Mock
private RepositoryServiceFactory serviceFactory;
@Mock
private RepositoryService repositoryService;
@Mock
private MergeCommand mergeCommand;
@InjectMocks
private MergeCommandBuilder mergeCommandBuilder;
private MergeResultToDtoMapperImpl mapper = new MergeResultToDtoMapperImpl();
private MergeResource mergeResource;
@BeforeEach
void init() {
mergeResource = new MergeResource(serviceFactory, mapper);
super.mergeResource = Providers.of(mergeResource);
dispatcher = DispatcherMock.createDispatcher(getRepositoryRootResource());
}
@Test
void shouldHandleIllegalInput() throws Exception {
URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand_invalid.json");
byte[] mergeCommandJson = Resources.toByteArray(url);
MockHttpRequest request = MockHttpRequest
.post(MERGE_URL + "dry-run/")
.content(mergeCommandJson)
.contentType(VndMediaType.MERGE_COMMAND);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(400);
System.out.println(response.getContentAsString());
}
@Nested
class ExecutingMergeCommand {
@BeforeEach
void initRepository() {
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService);
when(repositoryService.getMergeCommand()).thenReturn(mergeCommandBuilder);
}
@Test
void shouldHandleSuccessfulMerge() throws Exception {
when(mergeCommand.merge(any())).thenReturn(MergeCommandResult.success());
URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json");
byte[] mergeCommandJson = Resources.toByteArray(url);
MockHttpRequest request = MockHttpRequest
.post(MERGE_URL)
.content(mergeCommandJson)
.contentType(VndMediaType.MERGE_COMMAND);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(204);
}
@Test
void shouldHandleFailedMerge() throws Exception {
when(mergeCommand.merge(any())).thenReturn(MergeCommandResult.failure(asList("file1", "file2")));
URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json");
byte[] mergeCommandJson = Resources.toByteArray(url);
MockHttpRequest request = MockHttpRequest
.post(MERGE_URL)
.content(mergeCommandJson)
.contentType(VndMediaType.MERGE_COMMAND);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(409);
assertThat(response.getContentAsString()).contains("file1", "file2");
}
@Test
void shouldHandleSuccessfulDryRun() throws Exception {
when(mergeCommand.dryRun(any())).thenReturn(new MergeDryRunCommandResult(true));
URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json");
byte[] mergeCommandJson = Resources.toByteArray(url);
MockHttpRequest request = MockHttpRequest
.post(MERGE_URL + "dry-run/")
.content(mergeCommandJson)
.contentType(VndMediaType.MERGE_COMMAND);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(204);
}
@Test
void shouldHandleFailedDryRun() throws Exception {
when(mergeCommand.dryRun(any())).thenReturn(new MergeDryRunCommandResult(false));
URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json");
byte[] mergeCommandJson = Resources.toByteArray(url);
MockHttpRequest request = MockHttpRequest
.post(MERGE_URL + "dry-run/")
.content(mergeCommandJson)
.contentType(VndMediaType.MERGE_COMMAND);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertThat(response.getStatus()).isEqualTo(409);
}
}
}

View File

@@ -21,6 +21,7 @@ public abstract class RepositoryTestBase {
protected Provider<ModificationsRootResource> modificationsRootResource;
protected Provider<FileHistoryRootResource> fileHistoryRootResource;
protected Provider<RepositoryCollectionResource> repositoryCollectionResource;
protected Provider<MergeResource> mergeResource;
RepositoryRootResource getRepositoryRootResource() {
@@ -36,7 +37,8 @@ public abstract class RepositoryTestBase {
permissionRootResource,
diffRootResource,
modificationsRootResource,
fileHistoryRootResource)), repositoryCollectionResource);
fileHistoryRootResource,
mergeResource)), repositoryCollectionResource);
}

View File

@@ -0,0 +1,4 @@
{
"sourceRevision": "source",
"targetRevision": "target"
}

View File

@@ -0,0 +1,4 @@
{
"sourceRevision": "",
"targetRevision": "target"
}