diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java new file mode 100644 index 0000000000..6c4274ee15 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryContentInitializer.java @@ -0,0 +1,72 @@ +package sonia.scm.repository; + +import com.google.common.io.ByteSource; +import sonia.scm.plugin.ExtensionPoint; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Use this {@link RepositoryContentInitializer} to create new files with custom content + * which will be included in the initial commit of the new repository + */ +@ExtensionPoint +public interface RepositoryContentInitializer { + + /** + * + * @param context add content to this context in order to commit files in the initial repository commit + * @throws IOException + */ + void initialize(InitializerContext context) throws IOException; + + /** + * Use this {@link InitializerContext} to create new files on repository initialization + * which will be included in the first commit + */ + interface InitializerContext { + + /** + * @return repository to which this initializerContext belongs to + */ + Repository getRepository(); + + /** + * create new file which will be included in initial repository commit + * @param path path of new file + * @return + */ + CreateFile create(String path); + } + + /** + * Use this to apply content to new files which should be committed on repository initialization + */ + interface CreateFile { + + /** + * Applies content to new file + * @param content content of file as string + * @return {@link InitializerContext} + * @throws IOException + */ + InitializerContext from(String content) throws IOException; + + /** + * Applies content to new file + * @param input content of file as input stream + * @return {@link InitializerContext} + * @throws IOException + */ + InitializerContext from(InputStream input) throws IOException; + + /** + * Applies content to new file + * @param byteSource content of file as byte source + * @return {@link InitializerContext} + * @throws IOException + */ + InitializerContext from(ByteSource byteSource) throws IOException; + + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java index 05c1babe03..85fb814df4 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/ModifyCommandBuilder.java @@ -102,7 +102,7 @@ public class ModifyCommandBuilder { public String execute() { AuthorUtil.setAuthorIfNotAvailable(request); try { - Preconditions.checkArgument(request.isValid(), "commit message, branch and at least one request are required"); + Preconditions.checkArgument(request.isValid(), "commit message and at least one request are required"); return command.execute(request); } finally { try { diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java index d61a48508d..d89feb823b 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/SimpleGitWorkdirFactoryTest.java @@ -70,6 +70,18 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase { } } + @Test + public void shouldCheckoutDefaultBranch() { + SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider); + + try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext(), null)) { + assertThat(new File(workingCopy.getWorkingRepository().getWorkTree(), "a.txt")) + .exists() + .isFile() + .hasContent("a\nline for blame"); + } + } + @Test public void cloneFromPoolShouldNotBeReused() { SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider); diff --git a/scm-ui/ui-webapp/src/repos/modules/repos.ts b/scm-ui/ui-webapp/src/repos/modules/repos.ts index be09379c71..dbea9c24fd 100644 --- a/scm-ui/ui-webapp/src/repos/modules/repos.ts +++ b/scm-ui/ui-webapp/src/repos/modules/repos.ts @@ -158,7 +158,7 @@ export function fetchRepoFailure(namespace: string, name: string, error: Error): export function createRepo(link: string, repository: Repository, initRepository: boolean, callback?: (repo: Repository) => void) { return function(dispatch: any) { dispatch(createRepoPending()); - const repoLink = initRepository ? link + "?initRepository=true" : link; + const repoLink = initRepository ? link + "?initialize=true" : link; return apiClient .post(repoLink, repository, CONTENT_TYPE) .then(response => { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index 8b351aa46a..d27b598646 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -7,6 +7,7 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import org.apache.shiro.SecurityUtils; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryInitializer; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryPermission; import sonia.scm.search.SearchRequest; @@ -24,7 +25,7 @@ import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; - +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Predicate; import static com.google.common.base.Strings.isNullOrEmpty; @@ -38,13 +39,15 @@ public class RepositoryCollectionResource { private final RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper; private final RepositoryDtoToRepositoryMapper dtoToRepositoryMapper; private final ResourceLinks resourceLinks; + private final RepositoryInitializer repositoryInitializer; @Inject - public RepositoryCollectionResource(RepositoryManager manager, RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper, RepositoryDtoToRepositoryMapper dtoToRepositoryMapper, ResourceLinks resourceLinks) { + public RepositoryCollectionResource(RepositoryManager manager, RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper, RepositoryDtoToRepositoryMapper dtoToRepositoryMapper, ResourceLinks resourceLinks, RepositoryInitializer repositoryInitializer) { this.adapter = new CollectionResourceManagerAdapter<>(manager, Repository.class); this.repositoryCollectionToDtoMapper = repositoryCollectionToDtoMapper; this.dtoToRepositoryMapper = dtoToRepositoryMapper; this.resourceLinks = resourceLinks; + this.repositoryInitializer = repositoryInitializer; } /** @@ -68,10 +71,10 @@ public class RepositoryCollectionResource { @ResponseCode(code = 500, condition = "internal server error") }) public Response getAll(@DefaultValue("0") @QueryParam("page") int page, - @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, - @QueryParam("sortBy") String sortBy, - @DefaultValue("false") @QueryParam("desc") boolean desc, - @DefaultValue("") @QueryParam("q") String search + @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, + @QueryParam("sortBy") String sortBy, + @DefaultValue("false") @QueryParam("desc") boolean desc, + @DefaultValue("") @QueryParam("q") String search ) { return adapter.getAll(page, pageSize, createSearchPredicate(search), sortBy, desc, pageResult -> repositoryCollectionToDtoMapper.map(page, pageSize, pageResult)); @@ -81,7 +84,7 @@ public class RepositoryCollectionResource { * Creates a new repository. * * Note: This method requires "repository" privilege. The namespace of the given repository will - * be ignored and set by the configured namespace strategy. + * be ignored and set by the configured namespace strategy. * * @param repository The repository to be created. * @return A response with the link to the new repository (if created successfully). @@ -98,10 +101,18 @@ public class RepositoryCollectionResource { }) @TypeHint(TypeHint.NO_CONTENT.class) @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created repository")) - public Response create(@Valid RepositoryDto repository) { - return adapter.create(repository, + public Response create(@Valid RepositoryDto repository, @QueryParam("initialize") boolean initialize) { + AtomicReference reference = new AtomicReference<>(); + Response response = adapter.create(repository, () -> createModelObjectFromDto(repository), - r -> resourceLinks.repository().self(r.getNamespace(), r.getName())); + r -> { + reference.set(r); + return resourceLinks.repository().self(r.getNamespace(), r.getName()); + }); + if (initialize) { + repositoryInitializer.initialize(reference.get()); + } + return response; } private Repository createModelObjectFromDto(@Valid RepositoryDto repositoryDto) { diff --git a/scm-webapp/src/main/java/sonia/scm/repository/ReadmeRepositoryContentInitializer.java b/scm-webapp/src/main/java/sonia/scm/repository/ReadmeRepositoryContentInitializer.java new file mode 100644 index 0000000000..bc31763288 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/ReadmeRepositoryContentInitializer.java @@ -0,0 +1,23 @@ +package sonia.scm.repository; + +import com.google.common.base.Strings; +import sonia.scm.Priority; +import sonia.scm.plugin.Extension; + +import java.io.IOException; + +@Extension +@Priority(1) // should always be the first, so that plugins can overwrite the readme.md +public class ReadmeRepositoryContentInitializer implements RepositoryContentInitializer { + @Override + public void initialize(InitializerContext context) throws IOException { + Repository repository = context.getRepository(); + + String content = "# " + repository.getName(); + String description = repository.getDescription(); + if (!Strings.isNullOrEmpty(description)) { + content += "\n\n" + description; + } + context.create("README.md").from(content); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/RepositoryInitializer.java b/scm-webapp/src/main/java/sonia/scm/repository/RepositoryInitializer.java new file mode 100644 index 0000000000..fd1558b5d7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/RepositoryInitializer.java @@ -0,0 +1,101 @@ +package sonia.scm.repository; + +import com.google.common.io.ByteSource; +import com.google.common.io.CharSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.Priorities; +import sonia.scm.repository.api.ModifyCommandBuilder; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Set; + +@Singleton +public class RepositoryInitializer { + + private static final Logger LOG = LoggerFactory.getLogger(RepositoryInitializer.class); + + private final RepositoryServiceFactory serviceFactory; + private final Iterable contentInitializers; + + @Inject + public RepositoryInitializer(RepositoryServiceFactory serviceFactory, Set contentInitializerSet) { + this.serviceFactory = serviceFactory; + this.contentInitializers = Priorities.sortInstances(contentInitializerSet); + } + + public void initialize(Repository repository) { + try (RepositoryService service = serviceFactory.create(repository)) { + ModifyCommandBuilder modifyCommandBuilder = service.getModifyCommand(); + + InitializerContextImpl initializerContext = new InitializerContextImpl(repository, modifyCommandBuilder); + + for (RepositoryContentInitializer initializer : contentInitializers) { + initializer.initialize(initializerContext); + } + + modifyCommandBuilder.setCommitMessage("initialize repository"); + String revision = modifyCommandBuilder.execute(); + LOG.info("initialized repository {} as revision {}", repository.getNamespaceAndName(), revision); + + } catch (IOException e) { + throw new InternalRepositoryException(repository, "failed to initialize repository", e); + } + } + + private class InitializerContextImpl implements RepositoryContentInitializer.InitializerContext { + + private final Repository repository; + private final ModifyCommandBuilder builder; + + InitializerContextImpl(Repository repository, ModifyCommandBuilder builder) { + this.repository = repository; + this.builder = builder; + } + + @Override + public Repository getRepository() { + return repository; + } + + @Override + public RepositoryContentInitializer.CreateFile create(String path) { + return new CreateFileImpl(this, builder.createFile(path).setOverwrite(true)); + } + } + + private class CreateFileImpl implements RepositoryContentInitializer.CreateFile { + + private final RepositoryContentInitializer.InitializerContext initializerContext; + private final ModifyCommandBuilder.WithOverwriteFlagContentLoader contentLoader; + + CreateFileImpl(RepositoryContentInitializer.InitializerContext initializerContext, ModifyCommandBuilder.WithOverwriteFlagContentLoader contentLoader) { + this.initializerContext = initializerContext; + this.contentLoader = contentLoader; + } + + @Override + public RepositoryContentInitializer.InitializerContext from(String content) throws IOException { + return from(CharSource.wrap(content).asByteSource(StandardCharsets.UTF_8)); + } + + @Override + public RepositoryContentInitializer.InitializerContext from(InputStream input) throws IOException { + contentLoader.withData(input); + return initializerContext; + } + + @Override + public RepositoryContentInitializer.InitializerContext from(ByteSource byteSource) throws IOException { + contentLoader.withData(byteSource); + return initializerContext; + } + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index f4c49be693..fc0de8fe5c 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -18,6 +18,7 @@ import org.mockito.Mock; import sonia.scm.PageResult; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryInitializer; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; @@ -76,6 +77,8 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { private ScmPathInfoStore scmPathInfoStore; @Mock private ScmPathInfo uriInfo; + @Mock + private RepositoryInitializer repositoryInitializer; @Captor private ArgumentCaptor> filterCaptor; @@ -95,7 +98,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { super.dtoToRepositoryMapper = dtoToRepositoryMapper; super.manager = repositoryManager; RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks); - super.repositoryCollectionResource = Providers.of(new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks)); + super.repositoryCollectionResource = Providers.of(new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks, repositoryInitializer)); dispatcher.addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(any(Repository.class))).thenReturn(service); when(scmPathInfoStore.get()).thenReturn(uriInfo); @@ -288,6 +291,32 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { assertEquals(HttpServletResponse.SC_CREATED, response.getStatus()); assertEquals("/v2/repositories/otherspace/repo", response.getOutputHeaders().get("Location").get(0).toString()); verify(repositoryManager).create(any(Repository.class)); + verify(repositoryInitializer, never()).initialize(any(Repository.class)); + } + + @Test + public void shouldCreateNewRepositoryAndInitialize() throws Exception { + when(repositoryManager.create(any())).thenAnswer(invocation -> invocation.getArgument(0)); + + URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); + byte[] repositoryJson = Resources.toByteArray(url); + + MockHttpRequest request = MockHttpRequest + .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "?initialize=true") + .contentType(VndMediaType.REPOSITORY) + .content(repositoryJson); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_CREATED, response.getStatus()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Repository.class); + verify(repositoryInitializer).initialize(captor.capture()); + + Repository repository = captor.getValue(); + assertEquals("space", repository.getNamespace()); + assertEquals("repo", repository.getName()); } @Test diff --git a/scm-webapp/src/test/java/sonia/scm/repository/ReadmeRepositoryContentInitializerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/ReadmeRepositoryContentInitializerTest.java new file mode 100644 index 0000000000..49cd8dc9e1 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/ReadmeRepositoryContentInitializerTest.java @@ -0,0 +1,52 @@ +package sonia.scm.repository; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ReadmeRepositoryContentInitializerTest { + + @Mock + private RepositoryContentInitializer.InitializerContext context; + + @Mock + private RepositoryContentInitializer.CreateFile createFile; + + private Repository repository; + + private ReadmeRepositoryContentInitializer initializer = new ReadmeRepositoryContentInitializer(); + + @BeforeEach + void setUpContext() { + repository = RepositoryTestData.createHeartOfGold("hg"); + when(context.getRepository()).thenReturn(repository); + when(context.create("README.md")).thenReturn(createFile); + } + + @Test + void shouldCreateReadme() throws IOException { + initializer.initialize(context); + + verify(createFile).from("# HeartOfGold\n\n" + repository.getDescription()); + } + + @Test + void shouldCreateReadmeWithoutDescription() throws IOException { + repository.setDescription(null); + + initializer.initialize(context); + + verify(createFile).from("# HeartOfGold"); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/RepositoryInitializerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/RepositoryInitializerTest.java new file mode 100644 index 0000000000..7d5a511338 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/RepositoryInitializerTest.java @@ -0,0 +1,181 @@ +package sonia.scm.repository; + +import com.google.common.collect.ImmutableSet; +import com.google.common.io.ByteSource; +import com.google.common.io.ByteStreams; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.Priority; +import sonia.scm.repository.api.ModifyCommandBuilder; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RepositoryInitializerTest { + + @Mock + private RepositoryServiceFactory repositoryServiceFactory; + + @Mock + private RepositoryService repositoryService; + + @Mock(answer = Answers.RETURNS_SELF) + private ModifyCommandBuilder modifyCommand; + + private final Repository repository = RepositoryTestData.createHeartOfGold("git"); + + @BeforeEach + void setUpModifyCommand() { + when(repositoryServiceFactory.create(repository)).thenReturn(repositoryService); + when(repositoryService.getModifyCommand()).thenReturn(modifyCommand); + } + + @Test + void shouldCallRepositoryContentInitializer() throws IOException { + ModifyCommandBuilder.WithOverwriteFlagContentLoader readmeContentLoader = mockContentLoader("README.md"); + ModifyCommandBuilder.WithOverwriteFlagContentLoader licenseContentLoader = mockContentLoader("LICENSE.txt"); + + Set repositoryContentInitializers = ImmutableSet.of( + new ReadmeContentInitializer(), + new LicenseContentInitializer() + ); + + RepositoryInitializer initializer = new RepositoryInitializer(repositoryServiceFactory, repositoryContentInitializers); + initializer.initialize(repository); + + verifyFileCreation(readmeContentLoader, "# HeartOfGold"); + verifyFileCreation(licenseContentLoader, "MIT"); + + verify(modifyCommand).setCommitMessage("initialize repository"); + verify(modifyCommand).execute(); + + verify(repositoryService).close(); + } + + @Test + void shouldCallRepositoryContentInitializerWithInputStream() throws IOException { + ModifyCommandBuilder.WithOverwriteFlagContentLoader contentLoader = mockContentLoader("awesome.txt"); + + Set repositoryContentInitializers = ImmutableSet.of( + new StreamingContentInitializer() + ); + + RepositoryInitializer initializer = new RepositoryInitializer(repositoryServiceFactory, repositoryContentInitializers); + initializer.initialize(repository); + + verifyFileCreationWithStream(contentLoader, "awesome"); + + verify(modifyCommand).setCommitMessage("initialize repository"); + verify(modifyCommand).execute(); + + verify(repositoryService).close(); + } + + @Test + void shouldRespectPriorityOrder() throws IOException { + ModifyCommandBuilder.WithOverwriteFlagContentLoader contentLoader = mock(ModifyCommandBuilder.WithOverwriteFlagContentLoader.class); + when(contentLoader.setOverwrite(true)).thenReturn(contentLoader); + + when(modifyCommand.createFile(anyString())).thenReturn(contentLoader); + + AtomicReference reference = new AtomicReference<>(); + when(contentLoader.withData(any(ByteSource.class))).thenAnswer(ic -> { + ByteSource byteSource = ic.getArgument(0); + reference.set(byteSource.asCharSource(StandardCharsets.UTF_8).read()); + return modifyCommand; + }); + + Set repositoryContentInitializers = ImmutableSet.of( + new LicenseContentInitializer(), + new ReadmeContentInitializer() + ); + + RepositoryInitializer initializer = new RepositoryInitializer(repositoryServiceFactory, repositoryContentInitializers); + initializer.initialize(repository); + + assertThat(reference.get()).isEqualTo("MIT"); + } + + @Test + void shouldCloseRepositoryServiceOnException() throws IOException { + ModifyCommandBuilder.WithOverwriteFlagContentLoader contentLoader = mockContentLoader("README.md"); + doThrow(new IOException("epic fail")).when(contentLoader).withData(any(ByteSource.class)); + + RepositoryInitializer initializer = new RepositoryInitializer(repositoryServiceFactory, ImmutableSet.of(new ReadmeContentInitializer())); + assertThrows(InternalRepositoryException.class, () -> initializer.initialize(repository)); + + verify(repositoryService).close(); + } + + private ModifyCommandBuilder.WithOverwriteFlagContentLoader mockContentLoader(String path) { + ModifyCommandBuilder.WithOverwriteFlagContentLoader contentLoader = mock(ModifyCommandBuilder.WithOverwriteFlagContentLoader.class); + doReturn(contentLoader).when(modifyCommand).createFile(path); + when(contentLoader.setOverwrite(true)).thenReturn(contentLoader); + return contentLoader; + } + + private void verifyFileCreation(ModifyCommandBuilder.WithOverwriteFlagContentLoader contentLoader, String expectedContent) throws IOException { + ArgumentCaptor captor = ArgumentCaptor.forClass(ByteSource.class); + verify(contentLoader).withData(captor.capture()); + String content = captor.getValue().asCharSource(StandardCharsets.UTF_8).read(); + assertThat(content).isEqualTo(expectedContent); + } + + private void verifyFileCreationWithStream(ModifyCommandBuilder.WithOverwriteFlagContentLoader contentLoader, String expectedContent) throws IOException { + ArgumentCaptor captor = ArgumentCaptor.forClass(InputStream.class); + verify(contentLoader).withData(captor.capture()); + byte[] bytes = ByteStreams.toByteArray(captor.getValue()); + assertThat(new String(bytes, StandardCharsets.UTF_8)).isEqualTo(expectedContent); + } + + @Priority(1) + private static class ReadmeContentInitializer implements RepositoryContentInitializer { + + @Override + public void initialize(InitializerContext context) throws IOException { + Repository repository = context.getRepository(); + context.create("README.md").from("# " + repository.getName()); + } + } + + @Priority(2) + private static class LicenseContentInitializer implements RepositoryContentInitializer { + + @Override + public void initialize(InitializerContext context) throws IOException { + context.create("LICENSE.txt").from("MIT"); + } + } + + private static class StreamingContentInitializer implements RepositoryContentInitializer { + + @Override + public void initialize(InitializerContext context) throws IOException { + context.create("awesome.txt").from(new ByteArrayInputStream("awesome".getBytes(StandardCharsets.UTF_8))); + } + } + +}