mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-14 17:26:22 +01:00
Create REST endpoint for branch creation
This commit is contained in:
@@ -3,9 +3,12 @@ package sonia.scm.api.v2.resources;
|
||||
import com.google.inject.Inject;
|
||||
import de.otto.edison.hal.Embedded;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Link;
|
||||
import de.otto.edison.hal.Links;
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@@ -25,22 +28,36 @@ public class BranchCollectionToDtoMapper {
|
||||
this.branchToDtoMapper = branchToDtoMapper;
|
||||
}
|
||||
|
||||
public HalRepresentation map(String namespace, String name, Collection<Branch> branches) {
|
||||
return new HalRepresentation(createLinks(namespace, name), embedDtos(getBranchDtoList(namespace, name, branches)));
|
||||
public HalRepresentation map(Repository repository, Collection<Branch> branches) {
|
||||
return new HalRepresentation(
|
||||
createLinks(repository),
|
||||
embedDtos(getBranchDtoList(repository.getNamespace(), repository.getName(), branches)));
|
||||
}
|
||||
|
||||
public List<BranchDto> getBranchDtoList(String namespace, String name, Collection<Branch> branches) {
|
||||
return branches.stream().map(branch -> branchToDtoMapper.map(branch, new NamespaceAndName(namespace, name))).collect(toList());
|
||||
}
|
||||
|
||||
private Links createLinks(String namespace, String name) {
|
||||
private Links createLinks(Repository repository) {
|
||||
String namespace = repository.getNamespace();
|
||||
String name = repository.getName();
|
||||
String baseUrl = resourceLinks.branchCollection().self(namespace, name);
|
||||
|
||||
Links.Builder linksBuilder = linkingTo()
|
||||
.with(Links.linkingTo().self(baseUrl).build());
|
||||
Links.Builder linksBuilder = linkingTo().with(createSelfLink(baseUrl));
|
||||
if (RepositoryPermissions.push(repository).isPermitted()) {
|
||||
linksBuilder.single(createCreateLink(namespace, name));
|
||||
}
|
||||
return linksBuilder.build();
|
||||
}
|
||||
|
||||
private Links createSelfLink(String baseUrl) {
|
||||
return Links.linkingTo().self(baseUrl).build();
|
||||
}
|
||||
|
||||
private Link createCreateLink(String namespace, String name) {
|
||||
return Link.link("create", resourceLinks.branch().create(namespace, name));
|
||||
}
|
||||
|
||||
private Embedded embedDtos(List<BranchDto> dtos) {
|
||||
return embeddedBuilder()
|
||||
.with("branches", dtos)
|
||||
|
||||
@@ -6,10 +6,19 @@ import de.otto.edison.hal.Links;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.constraints.Pattern;
|
||||
|
||||
@Getter @Setter @NoArgsConstructor
|
||||
public class BranchDto extends HalRepresentation {
|
||||
|
||||
private static final String VALID_CHARACTERS_AT_START_AND_END = "\\w-,;\\]{}@&+=$#`|<>";
|
||||
private static final String VALID_CHARACTERS = VALID_CHARACTERS_AT_START_AND_END + "/.";
|
||||
static final String VALID_BRANCH_NAMES = "[" + VALID_CHARACTERS_AT_START_AND_END + "]([" + VALID_CHARACTERS + "]*[" + VALID_CHARACTERS_AT_START_AND_END + "])?";
|
||||
|
||||
@NotEmpty @Length(min = 1, max=100) @Pattern(regexp = VALID_BRANCH_NAMES)
|
||||
private String name;
|
||||
private String revision;
|
||||
private boolean defaultBranch;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeader;
|
||||
import com.webcohesion.enunciate.metadata.rs.ResponseHeaders;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import com.webcohesion.enunciate.metadata.rs.TypeHint;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.PageResult;
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.Branches;
|
||||
@@ -18,15 +19,20 @@ 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.DefaultValue;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
import static sonia.scm.AlreadyExistsException.alreadyExists;
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
import static sonia.scm.NotFoundException.notFound;
|
||||
|
||||
@@ -38,12 +44,15 @@ public class BranchRootResource {
|
||||
|
||||
private final BranchChangesetCollectionToDtoMapper branchChangesetCollectionToDtoMapper;
|
||||
|
||||
private final ResourceLinks resourceLinks;
|
||||
|
||||
@Inject
|
||||
public BranchRootResource(RepositoryServiceFactory serviceFactory, BranchToBranchDtoMapper branchToDtoMapper, BranchCollectionToDtoMapper branchCollectionToDtoMapper, BranchChangesetCollectionToDtoMapper changesetCollectionToDtoMapper) {
|
||||
public BranchRootResource(RepositoryServiceFactory serviceFactory, BranchToBranchDtoMapper branchToDtoMapper, BranchCollectionToDtoMapper branchCollectionToDtoMapper, BranchChangesetCollectionToDtoMapper changesetCollectionToDtoMapper, ResourceLinks resourceLinks) {
|
||||
this.serviceFactory = serviceFactory;
|
||||
this.branchToDtoMapper = branchToDtoMapper;
|
||||
this.branchCollectionToDtoMapper = branchCollectionToDtoMapper;
|
||||
this.branchChangesetCollectionToDtoMapper = changesetCollectionToDtoMapper;
|
||||
this.resourceLinks = resourceLinks;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,12 +109,7 @@ public class BranchRootResource {
|
||||
@DefaultValue("0") @QueryParam("page") int page,
|
||||
@DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException {
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
boolean branchExists = repositoryService.getBranchesCommand()
|
||||
.getBranches()
|
||||
.getBranches()
|
||||
.stream()
|
||||
.anyMatch(branch -> branchName.equals(branch.getName()));
|
||||
if (!branchExists){
|
||||
if (!branchExists(branchName, repositoryService)){
|
||||
throw notFound(entity(Branch.class, branchName).in(Repository.class, namespace + "/" + name));
|
||||
}
|
||||
Repository repository = repositoryService.getRepository();
|
||||
@@ -125,6 +129,50 @@ public class BranchRootResource {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new branch.
|
||||
*
|
||||
* @param namespace the namespace of the repository
|
||||
* @param name the name of the repository
|
||||
* @param branchName The branch to be created.
|
||||
* @return A response with the link to the new branch (if created successfully).
|
||||
*/
|
||||
@POST
|
||||
@Path("")
|
||||
@Consumes(VndMediaType.BRANCH)
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 201, condition = "create success"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"push\" privilege"),
|
||||
@ResponseCode(code = 409, condition = "conflict, a user with this name already exists"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(TypeHint.NO_CONTENT.class)
|
||||
@ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created branch"))
|
||||
public Response create(@PathParam("namespace") String namespace,
|
||||
@PathParam("name") String name,
|
||||
@Valid BranchDto branchToCreate) throws IOException {
|
||||
NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name);
|
||||
String branchName = branchToCreate.getName();
|
||||
try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) {
|
||||
if (branchExists(branchName, repositoryService)) {
|
||||
throw alreadyExists(entity(Branch.class, branchName).in(Repository.class, namespace + "/" + name));
|
||||
}
|
||||
Repository repository = repositoryService.getRepository();
|
||||
RepositoryPermissions.push(repository).check();
|
||||
Branch newBranch = repositoryService.getBranchCommand().branch(branchName);
|
||||
return Response.created(URI.create(resourceLinks.branch().self(namespaceAndName, newBranch.getName()))).build();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean branchExists(String branchName, RepositoryService repositoryService) throws IOException {
|
||||
return repositoryService.getBranchesCommand()
|
||||
.getBranches()
|
||||
.getBranches()
|
||||
.stream()
|
||||
.anyMatch(branch -> branchName.equals(branch.getName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the branches for a repository.
|
||||
*
|
||||
@@ -141,14 +189,14 @@ public class BranchRootResource {
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 400, condition = "branches not supported for given repository"),
|
||||
@ResponseCode(code = 401, condition = "not authenticated / invalid credentials"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"group\" privilege"),
|
||||
@ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"read repository\" privilege"),
|
||||
@ResponseCode(code = 404, condition = "not found, no repository found for the given namespace and name"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name) throws IOException {
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
Branches branches = repositoryService.getBranchesCommand().getBranches();
|
||||
return Response.ok(branchCollectionToDtoMapper.map(namespace, name, branches.getBranches())).build();
|
||||
return Response.ok(branchCollectionToDtoMapper.map(repositoryService.getRepository(), branches.getBranches())).build();
|
||||
} catch (CommandNotSupportedException ex) {
|
||||
return Response.status(Response.Status.BAD_REQUEST).build();
|
||||
}
|
||||
|
||||
@@ -386,6 +386,9 @@ class ResourceLinks {
|
||||
return branchLinkBuilder.method("getRepositoryResource").parameters(namespaceAndName.getNamespace(), namespaceAndName.getName()).method("branches").parameters().method("history").parameters(branch).href();
|
||||
}
|
||||
|
||||
public String create(String namespace, String name) {
|
||||
return branchLinkBuilder.method("getRepositoryResource").parameters(namespace, name).method("branches").parameters().method("create").parameters().href();
|
||||
}
|
||||
}
|
||||
|
||||
public IncomingLinks incoming() {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package sonia.scm.api.v2.resources;
|
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class BranchDtoTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"v",
|
||||
"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890",
|
||||
"val#x",
|
||||
"val&x",
|
||||
"val+",
|
||||
"val,kill",
|
||||
"val.kill",
|
||||
"val;kill",
|
||||
"val<kill",
|
||||
"val=",
|
||||
"val>kill",
|
||||
"val@",
|
||||
"val]id",
|
||||
"val`id",
|
||||
"valid#",
|
||||
"valid.t",
|
||||
"val{",
|
||||
"val{d",
|
||||
"val{}d",
|
||||
"val|kill",
|
||||
"val}"
|
||||
})
|
||||
void shouldAcceptValidBranchName(String branchName) {
|
||||
assertTrue(branchName.matches(BranchDto.VALID_BRANCH_NAMES));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"",
|
||||
".val",
|
||||
"val.",
|
||||
"/val",
|
||||
"val/",
|
||||
"val id"
|
||||
})
|
||||
void shouldRejectInvalidBranchName(String branchName) {
|
||||
assertFalse(branchName.matches(BranchDto.VALID_BRANCH_NAMES));
|
||||
}
|
||||
}
|
||||
@@ -25,10 +25,12 @@ import sonia.scm.repository.ChangesetPagingResult;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.BranchCommandBuilder;
|
||||
import sonia.scm.repository.api.BranchesCommandBuilder;
|
||||
import sonia.scm.repository.api.LogCommandBuilder;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Instant;
|
||||
@@ -49,6 +51,7 @@ public class BranchRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
public static final String BRANCH_PATH = "space/repo/branches/master";
|
||||
public static final String BRANCH_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + BRANCH_PATH;
|
||||
public static final String REVISION = "revision";
|
||||
private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
|
||||
|
||||
private final URI baseUri = URI.create("/");
|
||||
@@ -60,6 +63,8 @@ public class BranchRootResourceTest extends RepositoryTestBase {
|
||||
private RepositoryService service;
|
||||
@Mock
|
||||
private BranchesCommandBuilder branchesCommandBuilder;
|
||||
@Mock
|
||||
private BranchCommandBuilder branchCommandBuilder;
|
||||
|
||||
@Mock
|
||||
private LogCommandBuilder logCommandBuilder;
|
||||
@@ -89,10 +94,10 @@ public class BranchRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
|
||||
@Before
|
||||
public void prepareEnvironment() throws Exception {
|
||||
public void prepareEnvironment() {
|
||||
changesetCollectionToDtoMapper = new BranchChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks);
|
||||
BranchCollectionToDtoMapper branchCollectionToDtoMapper = new BranchCollectionToDtoMapper(branchToDtoMapper, resourceLinks);
|
||||
branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper, changesetCollectionToDtoMapper);
|
||||
branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper, changesetCollectionToDtoMapper, resourceLinks);
|
||||
super.branchRootResource = Providers.of(branchRootResource);
|
||||
dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource());
|
||||
when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service);
|
||||
@@ -100,6 +105,7 @@ public class BranchRootResourceTest extends RepositoryTestBase {
|
||||
when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo"));
|
||||
|
||||
when(service.getBranchesCommand()).thenReturn(branchesCommandBuilder);
|
||||
when(service.getBranchCommand()).thenReturn(branchCommandBuilder);
|
||||
when(service.getLogCommand()).thenReturn(logCommandBuilder);
|
||||
subjectThreadState.bind();
|
||||
ThreadContext.bind(subject);
|
||||
@@ -125,7 +131,7 @@ public class BranchRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
@Test
|
||||
public void shouldFindExistingBranch() throws Exception {
|
||||
when(branchesCommandBuilder.getBranches()).thenReturn(new Branches(Branch.normalBranch("master", "revision")));
|
||||
when(branchesCommandBuilder.getBranches()).thenReturn(new Branches(createBranch("master")));
|
||||
|
||||
MockHttpRequest request = MockHttpRequest.get(BRANCH_URL);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
@@ -139,13 +145,12 @@ public class BranchRootResourceTest extends RepositoryTestBase {
|
||||
|
||||
@Test
|
||||
public void shouldFindHistory() throws Exception {
|
||||
String id = "revision_123";
|
||||
Instant creationDate = Instant.now();
|
||||
String authorName = "name";
|
||||
String authorEmail = "em@i.l";
|
||||
String commit = "my branch commit";
|
||||
ChangesetPagingResult changesetPagingResult = mock(ChangesetPagingResult.class);
|
||||
List<Changeset> changesetList = Lists.newArrayList(new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit));
|
||||
List<Changeset> changesetList = Lists.newArrayList(new Changeset(REVISION, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit));
|
||||
when(changesetPagingResult.getChangesets()).thenReturn(changesetList);
|
||||
when(changesetPagingResult.getTotal()).thenReturn(1);
|
||||
when(logCommandBuilder.setPagingStart(anyInt())).thenReturn(logCommandBuilder);
|
||||
@@ -153,7 +158,7 @@ public class BranchRootResourceTest extends RepositoryTestBase {
|
||||
when(logCommandBuilder.setBranch(anyString())).thenReturn(logCommandBuilder);
|
||||
when(logCommandBuilder.getChangesets()).thenReturn(changesetPagingResult);
|
||||
Branches branches = mock(Branches.class);
|
||||
List<Branch> branchList = Lists.newArrayList(Branch.normalBranch("master",id));
|
||||
List<Branch> branchList = Lists.newArrayList(createBranch("master"));
|
||||
when(branches.getBranches()).thenReturn(branchList);
|
||||
when(branchesCommandBuilder.getBranches()).thenReturn(branches);
|
||||
MockHttpRequest request = MockHttpRequest.get(BRANCH_URL + "/changesets/");
|
||||
@@ -161,9 +166,51 @@ public class BranchRootResourceTest extends RepositoryTestBase {
|
||||
dispatcher.invoke(request, response);
|
||||
assertEquals(200, response.getStatus());
|
||||
log.info("Response :{}", response.getContentAsString());
|
||||
assertTrue(response.getContentAsString().contains(String.format("\"id\":\"%s\"", id)));
|
||||
assertTrue(response.getContentAsString().contains(String.format("\"id\":\"%s\"", REVISION)));
|
||||
assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName)));
|
||||
assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail)));
|
||||
assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateNewBranch() throws Exception {
|
||||
when(branchesCommandBuilder.getBranches()).thenReturn(new Branches());
|
||||
when(branchCommandBuilder.branch("new_branch")).thenReturn(createBranch("new_branch"));
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/branches/")
|
||||
.content("{\"name\": \"new_branch\"}".getBytes())
|
||||
.contentType(VndMediaType.BRANCH);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(201, response.getStatus());
|
||||
assertEquals(
|
||||
URI.create("/v2/repositories/space/repo/branches/new_branch"),
|
||||
response.getOutputHeaders().getFirst("Location"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotCreateExistingBranchAgain() throws Exception {
|
||||
when(branchesCommandBuilder.getBranches()).thenReturn(new Branches(createBranch("existing_branch")));
|
||||
when(branchCommandBuilder.branch("new_branch")).thenReturn(createBranch("new_branch"));
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo/branches/")
|
||||
.content("{\"name\": \"new_branch\"}".getBytes())
|
||||
.contentType(VndMediaType.BRANCH);
|
||||
MockHttpResponse response = new MockHttpResponse();
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertEquals(201, response.getStatus());
|
||||
assertEquals(
|
||||
URI.create("/v2/repositories/space/repo/branches/new_branch"),
|
||||
response.getOutputHeaders().getFirst("Location"));
|
||||
}
|
||||
|
||||
private Branch createBranch(String existing_branch) {
|
||||
return Branch.normalBranch(existing_branch, REVISION);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user