mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-03-06 12:20:56 +01:00
Merge with 2.0.0-m3
This commit is contained in:
8
pom.xml
8
pom.xml
@@ -439,6 +439,13 @@
|
||||
<artifactId>smp-maven-plugin</artifactId>
|
||||
<version>1.0.0-alpha-6</version>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<version>2.8.2</version>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
|
||||
@@ -633,7 +640,6 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<version>2.7</version>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package sonia.scm;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
public class IllegalIdentifierChangeException extends BadRequestException {
|
||||
|
||||
private static final String CODE = "thbsUFokjk";
|
||||
|
||||
public IllegalIdentifierChangeException(ContextEntry.ContextBuilder context, String message) {
|
||||
super(context.build(), message);
|
||||
}
|
||||
|
||||
public IllegalIdentifierChangeException(String message) {
|
||||
super(Collections.emptyList(), message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return CODE;
|
||||
}
|
||||
}
|
||||
@@ -66,5 +66,5 @@ public enum Command
|
||||
/**
|
||||
* @since 2.0
|
||||
*/
|
||||
MODIFICATIONS, MERGE, DIFF_RESULT, BRANCH;
|
||||
MODIFICATIONS, MERGE, DIFF_RESULT, BRANCH, MODIFY;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.io.ByteSource;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.io.Files;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.repository.spi.ModifyCommand;
|
||||
import sonia.scm.repository.spi.ModifyCommandRequest;
|
||||
import sonia.scm.repository.util.WorkdirProvider;
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Use this {@link ModifyCommandBuilder} to make file changes to the head of a branch. You can
|
||||
* <ul>
|
||||
* <li>create new files ({@link #createFile(String)} (with the option to overwrite a file, if it already exists; by
|
||||
* default a {@link sonia.scm.AlreadyExistsException} will be thrown)</li>
|
||||
* <li>modify existing files ({@link #modifyFile(String)}</li>
|
||||
* <li>delete existing files ({@link #deleteFile(String)}</li>
|
||||
* <li>move/rename existing files ({@link #moveFile(String, String)}</li>
|
||||
* </ul>
|
||||
*
|
||||
* You can collect multiple changes before they are executed with a call to {@link #execute()}.
|
||||
*
|
||||
* <p>Example:
|
||||
* <pre>
|
||||
* commandBuilder
|
||||
* .setBranch("feature/branch")
|
||||
* .setCommitMessage("make some changes")
|
||||
* .setAuthor(new Person())
|
||||
* .createFile("file/to/create").withData(inputStream)
|
||||
* .deleteFile("old/file/to/delete")
|
||||
* .execute();
|
||||
* </pre>
|
||||
* </p>
|
||||
*/
|
||||
public class ModifyCommandBuilder {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ModifyCommandBuilder.class);
|
||||
|
||||
private final ModifyCommand command;
|
||||
private final File workdir;
|
||||
|
||||
private final ModifyCommandRequest request = new ModifyCommandRequest();
|
||||
|
||||
ModifyCommandBuilder(ModifyCommand command, WorkdirProvider workdirProvider) {
|
||||
this.command = command;
|
||||
this.workdir = workdirProvider.createNewWorkdir();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the branch that should be modified. The new commit will be made for this branch.
|
||||
* @param branchToModify The branch to modify.
|
||||
* @return This builder instance.
|
||||
*/
|
||||
public ModifyCommandBuilder setBranchToModify(String branchToModify) {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new file. The content of the file will be specified in a subsequent call to
|
||||
* {@link ContentLoader#withData(ByteSource)} or {@link ContentLoader#withData(InputStream)}.
|
||||
* By default, an {@link sonia.scm.AlreadyExistsException} will be thrown, when there already
|
||||
* exists a file with the given path. You can disable this setting
|
||||
* {@link WithOverwriteFlagContentLoader#setOverwrite(boolean)} to <code>true</code>.
|
||||
* @param path The path and the name of the file that should be created.
|
||||
* @return The loader to specify the content of the new file.
|
||||
*/
|
||||
public WithOverwriteFlagContentLoader createFile(String path) {
|
||||
return new WithOverwriteFlagContentLoader(
|
||||
(content, overwrite) -> request.addRequest(new ModifyCommandRequest.CreateFileRequest(path, content, overwrite))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify an existing file. The new content of the file will be specified in a subsequent call to
|
||||
* {@link ContentLoader#withData(ByteSource)} or {@link ContentLoader#withData(InputStream)}.
|
||||
* @param path The path and the name of the file that should be modified.
|
||||
* @return The loader to specify the new content of the file.
|
||||
*/
|
||||
public SimpleContentLoader modifyFile(String path) {
|
||||
return new SimpleContentLoader(
|
||||
content -> request.addRequest(new ModifyCommandRequest.ModifyFileRequest(path, content))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an existing file.
|
||||
* @param path The path and the name of the file that should be deleted.
|
||||
* @return This builder instance.
|
||||
*/
|
||||
public ModifyCommandBuilder deleteFile(String path) {
|
||||
request.addRequest(new ModifyCommandRequest.DeleteFileRequest(path));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an existing file.
|
||||
* @param sourcePath The path and the name of the file that should be moved.
|
||||
* @param targetPath The new path and name the file should be moved to.
|
||||
* @return This builder instance.
|
||||
*/
|
||||
public ModifyCommandBuilder moveFile(String sourcePath, String targetPath) {
|
||||
request.addRequest(new ModifyCommandRequest.MoveFileRequest(sourcePath, targetPath));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the changes and create a new commit with the given message and author.
|
||||
* @return The revision of the new commit.
|
||||
*/
|
||||
public String execute() {
|
||||
try {
|
||||
Preconditions.checkArgument(request.isValid(), "commit message, branch and at least one request are required");
|
||||
return command.execute(request);
|
||||
} finally {
|
||||
try {
|
||||
IOUtil.delete(workdir);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("could not delete temporary workdir '{}'", workdir, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the commit message for the new commit.
|
||||
* @return This builder instance.
|
||||
*/
|
||||
public ModifyCommandBuilder setCommitMessage(String message) {
|
||||
request.setCommitMessage(message);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the author for the new commit.
|
||||
* @return This builder instance.
|
||||
*/
|
||||
public ModifyCommandBuilder setAuthor(Person author) {
|
||||
request.setAuthor(author);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the branch the changes should be made upon.
|
||||
* @return This builder instance.
|
||||
*/
|
||||
public ModifyCommandBuilder setBranch(String branch) {
|
||||
request.setBranch(branch);
|
||||
return this;
|
||||
}
|
||||
|
||||
public interface ContentLoader {
|
||||
/**
|
||||
* Specify the data of the file using a {@link ByteSource}.
|
||||
*
|
||||
* @return The builder instance.
|
||||
* @throws IOException If the data could not be read.
|
||||
*/
|
||||
ModifyCommandBuilder withData(ByteSource data) throws IOException;
|
||||
|
||||
/**
|
||||
* Specify the data of the file using an {@link InputStream}.
|
||||
* @return The builder instance.
|
||||
* @throws IOException If the data could not be read.
|
||||
*/
|
||||
ModifyCommandBuilder withData(InputStream data) throws IOException;
|
||||
}
|
||||
|
||||
public class SimpleContentLoader implements ContentLoader {
|
||||
|
||||
private final Consumer<File> contentConsumer;
|
||||
|
||||
private SimpleContentLoader(Consumer<File> contentConsumer) {
|
||||
this.contentConsumer = contentConsumer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModifyCommandBuilder withData(ByteSource data) throws IOException {
|
||||
File content = loadData(data);
|
||||
contentConsumer.accept(content);
|
||||
return ModifyCommandBuilder.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModifyCommandBuilder withData(InputStream data) throws IOException {
|
||||
File content = loadData(data);
|
||||
contentConsumer.accept(content);
|
||||
return ModifyCommandBuilder.this;
|
||||
}
|
||||
}
|
||||
|
||||
public class WithOverwriteFlagContentLoader implements ContentLoader {
|
||||
|
||||
private final ContentLoader contentLoader;
|
||||
private boolean overwrite = false;
|
||||
|
||||
private WithOverwriteFlagContentLoader(BiConsumer<File, Boolean> contentConsumer) {
|
||||
this.contentLoader = new SimpleContentLoader(file -> contentConsumer.accept(file, overwrite));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this to <code>true</code> to overwrite the file if it already exists. Otherwise an
|
||||
* {@link sonia.scm.AlreadyExistsException} will be thrown.
|
||||
* @return This loader instance.
|
||||
*/
|
||||
public WithOverwriteFlagContentLoader setOverwrite(boolean overwrite) {
|
||||
this.overwrite = overwrite;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModifyCommandBuilder withData(ByteSource data) throws IOException {
|
||||
return contentLoader.withData(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModifyCommandBuilder withData(InputStream data) throws IOException {
|
||||
return contentLoader.withData(data);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage") // Files only used internal
|
||||
private File loadData(ByteSource data) throws IOException {
|
||||
File file = createTemporaryFile();
|
||||
data.copyTo(Files.asByteSink(file));
|
||||
return file;
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnstableApiUsage") // Files and ByteStreams only used internal
|
||||
private File loadData(InputStream data) throws IOException {
|
||||
File file = createTemporaryFile();
|
||||
try (OutputStream out = Files.asByteSink(file).openBufferedStream()) {
|
||||
ByteStreams.copy(data, out);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
private File createTemporaryFile() throws IOException {
|
||||
return File.createTempFile("upload-", "", workdir);
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import sonia.scm.repository.PreProcessorUtil;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.spi.RepositoryServiceProvider;
|
||||
import sonia.scm.repository.util.WorkdirProvider;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
@@ -91,22 +92,25 @@ public final class RepositoryService implements Closeable {
|
||||
private final RepositoryServiceProvider provider;
|
||||
private final Repository repository;
|
||||
private final Set<ScmProtocolProvider> protocolProviders;
|
||||
private final WorkdirProvider workdirProvider;
|
||||
|
||||
/**
|
||||
* Constructs a new {@link RepositoryService}. This constructor should only
|
||||
* be called from the {@link RepositoryServiceFactory}.
|
||||
* @param cacheManager cache manager
|
||||
* @param cacheManager cache manager
|
||||
* @param provider implementation for {@link RepositoryServiceProvider}
|
||||
* @param repository the repository
|
||||
* @param workdirProvider
|
||||
*/
|
||||
RepositoryService(CacheManager cacheManager,
|
||||
RepositoryServiceProvider provider, Repository repository,
|
||||
PreProcessorUtil preProcessorUtil, Set<ScmProtocolProvider> protocolProviders) {
|
||||
RepositoryServiceProvider provider, Repository repository,
|
||||
PreProcessorUtil preProcessorUtil, Set<ScmProtocolProvider> protocolProviders, WorkdirProvider workdirProvider) {
|
||||
this.cacheManager = cacheManager;
|
||||
this.provider = provider;
|
||||
this.repository = repository;
|
||||
this.preProcessorUtil = preProcessorUtil;
|
||||
this.protocolProviders = protocolProviders;
|
||||
this.workdirProvider = workdirProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -399,6 +403,27 @@ public final class RepositoryService implements Closeable {
|
||||
return new MergeCommandBuilder(provider.getMergeCommand());
|
||||
}
|
||||
|
||||
/**
|
||||
* The modify command makes changes to the head of a branch. It is possible to
|
||||
* <ul>
|
||||
* <li>create new files</li>
|
||||
* <li>delete existing files</li>
|
||||
* <li>modify/replace files</li>
|
||||
* <li>move files</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return instance of {@link ModifyCommandBuilder}
|
||||
* @throws CommandNotSupportedException if the command is not supported
|
||||
* by the implementation of the repository service provider.
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public ModifyCommandBuilder getModifyCommand() {
|
||||
LOG.debug("create modify command for repository {}",
|
||||
repository.getNamespaceAndName());
|
||||
|
||||
return new ModifyCommandBuilder(provider.getModifyCommand(), workdirProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the command is supported by the repository service.
|
||||
*
|
||||
|
||||
@@ -61,6 +61,7 @@ import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.spi.RepositoryServiceProvider;
|
||||
import sonia.scm.repository.spi.RepositoryServiceResolver;
|
||||
import sonia.scm.repository.util.WorkdirProvider;
|
||||
import sonia.scm.security.ScmSecurityException;
|
||||
|
||||
import java.util.Set;
|
||||
@@ -135,13 +136,14 @@ public final class RepositoryServiceFactory
|
||||
* @param resolvers a set of {@link RepositoryServiceResolver}
|
||||
* @param preProcessorUtil helper object for pre processor handling
|
||||
*
|
||||
* @param workdirProvider
|
||||
* @since 1.21
|
||||
*/
|
||||
@Inject
|
||||
public RepositoryServiceFactory(ScmConfiguration configuration,
|
||||
CacheManager cacheManager, RepositoryManager repositoryManager,
|
||||
Set<RepositoryServiceResolver> resolvers, PreProcessorUtil preProcessorUtil,
|
||||
Set<ScmProtocolProvider> protocolProviders)
|
||||
CacheManager cacheManager, RepositoryManager repositoryManager,
|
||||
Set<RepositoryServiceResolver> resolvers, PreProcessorUtil preProcessorUtil,
|
||||
Set<ScmProtocolProvider> protocolProviders, WorkdirProvider workdirProvider)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
this.cacheManager = cacheManager;
|
||||
@@ -149,6 +151,7 @@ public final class RepositoryServiceFactory
|
||||
this.resolvers = resolvers;
|
||||
this.preProcessorUtil = preProcessorUtil;
|
||||
this.protocolProviders = protocolProviders;
|
||||
this.workdirProvider = workdirProvider;
|
||||
|
||||
ScmEventBus.getInstance().register(new CacheClearHook(cacheManager));
|
||||
}
|
||||
@@ -256,7 +259,7 @@ public final class RepositoryServiceFactory
|
||||
}
|
||||
|
||||
service = new RepositoryService(cacheManager, provider, repository,
|
||||
preProcessorUtil, protocolProviders);
|
||||
preProcessorUtil, protocolProviders, workdirProvider);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -373,4 +376,6 @@ public final class RepositoryServiceFactory
|
||||
private final Set<RepositoryServiceResolver> resolvers;
|
||||
|
||||
private Set<ScmProtocolProvider> protocolProviders;
|
||||
|
||||
private final WorkdirProvider workdirProvider;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public interface ModifyCommand {
|
||||
|
||||
String execute(ModifyCommandRequest request);
|
||||
|
||||
interface Worker {
|
||||
void delete(String toBeDeleted);
|
||||
|
||||
void create(String toBeCreated, File file, boolean overwrite) throws IOException;
|
||||
|
||||
void modify(String path, File file);
|
||||
|
||||
void move(String sourcePath, String targetPath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.Validateable;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ModifyCommandRequest implements Resetable, Validateable {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ModifyCommandRequest.class);
|
||||
|
||||
private final List<PartialRequest> requests = new ArrayList<>();
|
||||
|
||||
private Person author;
|
||||
private String commitMessage;
|
||||
private String branch;
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
requests.clear();
|
||||
author = null;
|
||||
commitMessage = null;
|
||||
branch = null;
|
||||
}
|
||||
|
||||
public void addRequest(PartialRequest request) {
|
||||
this.requests.add(request);
|
||||
}
|
||||
|
||||
public void setAuthor(Person author) {
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
public void setCommitMessage(String commitMessage) {
|
||||
this.commitMessage = commitMessage;
|
||||
}
|
||||
|
||||
public void setBranch(String branch) {
|
||||
this.branch = branch;
|
||||
}
|
||||
|
||||
public List<PartialRequest> getRequests() {
|
||||
return Collections.unmodifiableList(requests);
|
||||
}
|
||||
|
||||
public Person getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public String getCommitMessage() {
|
||||
return commitMessage;
|
||||
}
|
||||
|
||||
public String getBranch() {
|
||||
return branch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return StringUtils.isNotEmpty(commitMessage) && StringUtils.isNotEmpty(branch) && !requests.isEmpty();
|
||||
}
|
||||
|
||||
public interface PartialRequest {
|
||||
void execute(ModifyCommand.Worker worker) throws IOException;
|
||||
}
|
||||
|
||||
public static class DeleteFileRequest implements PartialRequest {
|
||||
private final String path;
|
||||
|
||||
public DeleteFileRequest(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(ModifyCommand.Worker worker) {
|
||||
worker.delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
public static class MoveFileRequest implements PartialRequest {
|
||||
private final String sourcePath;
|
||||
private final String targetPath;
|
||||
|
||||
public MoveFileRequest(String sourcePath, String targetPath) {
|
||||
this.sourcePath = sourcePath;
|
||||
this.targetPath = targetPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(ModifyCommand.Worker worker) {
|
||||
worker.move(sourcePath, targetPath);
|
||||
}
|
||||
}
|
||||
|
||||
private abstract static class ContentModificationRequest implements PartialRequest {
|
||||
|
||||
private final File content;
|
||||
|
||||
ContentModificationRequest(File content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
File getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
void cleanup() {
|
||||
try {
|
||||
IOUtil.delete(content);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("could not delete temporary file {}", content, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class CreateFileRequest extends ContentModificationRequest {
|
||||
|
||||
private final String path;
|
||||
private final boolean overwrite;
|
||||
|
||||
public CreateFileRequest(String path, File content, boolean overwrite) {
|
||||
super(content);
|
||||
this.path = path;
|
||||
this.overwrite = overwrite;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(ModifyCommand.Worker worker) throws IOException {
|
||||
worker.create(path, getContent(), overwrite);
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
public static class ModifyFileRequest extends ContentModificationRequest {
|
||||
|
||||
private final String path;
|
||||
|
||||
public ModifyFileRequest(String path, File content) {
|
||||
super(content);
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(ModifyCommand.Worker worker) {
|
||||
worker.modify(path, getContent());
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,4 +275,12 @@ public abstract class RepositoryServiceProvider implements Closeable
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.MERGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
*/
|
||||
public ModifyCommand getModifyCommand()
|
||||
{
|
||||
throw new CommandNotSupportedException(Command.MODIFY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,29 +7,21 @@ import sonia.scm.repository.Repository;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
public abstract class SimpleWorkdirFactory<R, C> implements WorkdirFactory<R, C> {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SimpleWorkdirFactory.class);
|
||||
|
||||
private final File poolDirectory;
|
||||
private final WorkdirProvider workdirProvider;
|
||||
|
||||
public SimpleWorkdirFactory() {
|
||||
this(new File(System.getProperty("scm.workdir" , System.getProperty("java.io.tmpdir")), "scm-work"));
|
||||
}
|
||||
|
||||
public SimpleWorkdirFactory(File poolDirectory) {
|
||||
this.poolDirectory = poolDirectory;
|
||||
if (!poolDirectory.exists() && !poolDirectory.mkdirs()) {
|
||||
throw new IllegalStateException("could not create pool directory " + poolDirectory);
|
||||
}
|
||||
public SimpleWorkdirFactory(WorkdirProvider workdirProvider) {
|
||||
this.workdirProvider = workdirProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WorkingCopy<R> createWorkingCopy(C context) {
|
||||
try {
|
||||
File directory = createNewWorkdir();
|
||||
File directory = workdirProvider.createNewWorkdir();
|
||||
ParentAndClone<R> parentAndClone = cloneRepository(context, directory);
|
||||
return new WorkingCopy<>(parentAndClone.getClone(), parentAndClone.getParent(), this::close, directory);
|
||||
} catch (IOException e) {
|
||||
@@ -45,10 +37,6 @@ public abstract class SimpleWorkdirFactory<R, C> implements WorkdirFactory<R, C>
|
||||
|
||||
protected abstract ParentAndClone<R> cloneRepository(C context, File target) throws IOException;
|
||||
|
||||
private File createNewWorkdir() throws IOException {
|
||||
return Files.createTempDirectory(poolDirectory.toPath(),"workdir").toFile();
|
||||
}
|
||||
|
||||
private void close(R repository) {
|
||||
try {
|
||||
closeRepository(repository);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package sonia.scm.repository.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
public class WorkdirProvider {
|
||||
|
||||
private final File poolDirectory;
|
||||
|
||||
public WorkdirProvider() {
|
||||
this(new File(System.getProperty("scm.workdir" , System.getProperty("java.io.tmpdir")), "scm-work"));
|
||||
}
|
||||
|
||||
public WorkdirProvider(File poolDirectory) {
|
||||
this.poolDirectory = poolDirectory;
|
||||
if (!poolDirectory.exists() && !poolDirectory.mkdirs()) {
|
||||
throw new IllegalStateException("could not create pool directory " + poolDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
public File createNewWorkdir() {
|
||||
try {
|
||||
return Files.createTempDirectory(poolDirectory.toPath(),"workdir").toFile();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("could not create temporary workdir", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package sonia.scm.repository.api;
|
||||
|
||||
import com.google.common.io.ByteSource;
|
||||
import com.sun.org.apache.xpath.internal.operations.Bool;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junitpioneer.jupiter.TempDirectory;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.repository.spi.ModifyCommand;
|
||||
import sonia.scm.repository.spi.ModifyCommandRequest;
|
||||
import sonia.scm.repository.util.WorkdirProvider;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@ExtendWith(TempDirectory.class)
|
||||
class ModifyCommandBuilderTest {
|
||||
|
||||
@Mock
|
||||
ModifyCommand command;
|
||||
@Mock
|
||||
WorkdirProvider workdirProvider;
|
||||
@Mock
|
||||
ModifyCommand.Worker worker;
|
||||
|
||||
ModifyCommandBuilder commandBuilder;
|
||||
Path workdir;
|
||||
|
||||
@BeforeEach
|
||||
void initWorkdir(@TempDirectory.TempDir Path temp) throws IOException {
|
||||
workdir = Files.createDirectory(temp.resolve("workdir"));
|
||||
lenient().when(workdirProvider.createNewWorkdir()).thenReturn(workdir.toFile());
|
||||
commandBuilder = new ModifyCommandBuilder(command, workdirProvider);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void initRequestCaptor() {
|
||||
when(command.execute(any())).thenAnswer(
|
||||
invocation -> {
|
||||
ModifyCommandRequest request = invocation.getArgument(0);
|
||||
for (ModifyCommandRequest.PartialRequest r : request.getRequests()) {
|
||||
r.execute(worker);
|
||||
}
|
||||
return "target";
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnTargetRevisionFromCommit() {
|
||||
String targetRevision = initCommand()
|
||||
.deleteFile("toBeDeleted")
|
||||
.execute();
|
||||
|
||||
assertThat(targetRevision).isEqualTo("target");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExecuteDelete() throws IOException {
|
||||
initCommand()
|
||||
.deleteFile("toBeDeleted")
|
||||
.execute();
|
||||
|
||||
verify(worker).delete("toBeDeleted");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExecuteMove() throws IOException {
|
||||
initCommand()
|
||||
.moveFile("source", "target")
|
||||
.execute();
|
||||
|
||||
verify(worker).move("source", "target");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExecuteCreateWithByteSourceContent() throws IOException {
|
||||
ArgumentCaptor<String> nameCaptor = ArgumentCaptor.forClass(String.class);
|
||||
List<String> contentCaptor = new ArrayList<>();
|
||||
doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), anyBoolean());
|
||||
|
||||
initCommand()
|
||||
.createFile("toBeCreated").withData(ByteSource.wrap("content".getBytes()))
|
||||
.execute();
|
||||
|
||||
assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated");
|
||||
assertThat(contentCaptor).contains("content");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExecuteCreateWithInputStreamContent() throws IOException {
|
||||
ArgumentCaptor<String> nameCaptor = ArgumentCaptor.forClass(String.class);
|
||||
List<String> contentCaptor = new ArrayList<>();
|
||||
doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), anyBoolean());
|
||||
|
||||
initCommand()
|
||||
.createFile("toBeCreated").withData(new ByteArrayInputStream("content".getBytes()))
|
||||
.execute();
|
||||
|
||||
assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated");
|
||||
assertThat(contentCaptor).contains("content");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExecuteCreateWithOverwriteFalseAsDefault() throws IOException {
|
||||
ArgumentCaptor<String> nameCaptor = ArgumentCaptor.forClass(String.class);
|
||||
ArgumentCaptor<Boolean> overwriteCaptor = ArgumentCaptor.forClass(Boolean.class);
|
||||
List<String> contentCaptor = new ArrayList<>();
|
||||
doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), overwriteCaptor.capture());
|
||||
|
||||
initCommand()
|
||||
.createFile("toBeCreated").withData(new ByteArrayInputStream("content".getBytes()))
|
||||
.execute();
|
||||
|
||||
assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated");
|
||||
assertThat(overwriteCaptor.getValue()).isFalse();
|
||||
assertThat(contentCaptor).contains("content");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExecuteCreateWithOverwriteIfSet() throws IOException {
|
||||
ArgumentCaptor<String> nameCaptor = ArgumentCaptor.forClass(String.class);
|
||||
ArgumentCaptor<Boolean> overwriteCaptor = ArgumentCaptor.forClass(Boolean.class);
|
||||
List<String> contentCaptor = new ArrayList<>();
|
||||
doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), overwriteCaptor.capture());
|
||||
|
||||
initCommand()
|
||||
.createFile("toBeCreated").setOverwrite(true).withData(new ByteArrayInputStream("content".getBytes()))
|
||||
.execute();
|
||||
|
||||
assertThat(nameCaptor.getValue()).isEqualTo("toBeCreated");
|
||||
assertThat(overwriteCaptor.getValue()).isTrue();
|
||||
assertThat(contentCaptor).contains("content");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExecuteCreateMultipleTimes() throws IOException {
|
||||
ArgumentCaptor<String> nameCaptor = ArgumentCaptor.forClass(String.class);
|
||||
List<String> contentCaptor = new ArrayList<>();
|
||||
doAnswer(new ExtractContent(contentCaptor)).when(worker).create(nameCaptor.capture(), any(), anyBoolean());
|
||||
|
||||
initCommand()
|
||||
.createFile("toBeCreated_1").withData(new ByteArrayInputStream("content_1".getBytes()))
|
||||
.createFile("toBeCreated_2").withData(new ByteArrayInputStream("content_2".getBytes()))
|
||||
.execute();
|
||||
|
||||
List<String> createdNames = nameCaptor.getAllValues();
|
||||
assertThat(createdNames.get(0)).isEqualTo("toBeCreated_1");
|
||||
assertThat(createdNames.get(1)).isEqualTo("toBeCreated_2");
|
||||
assertThat(contentCaptor).contains("content_1", "content_2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExecuteModify() throws IOException {
|
||||
ArgumentCaptor<String> nameCaptor = ArgumentCaptor.forClass(String.class);
|
||||
List<String> contentCaptor = new ArrayList<>();
|
||||
doAnswer(new ExtractContent(contentCaptor)).when(worker).modify(nameCaptor.capture(), any());
|
||||
|
||||
initCommand()
|
||||
.modifyFile("toBeModified").withData(ByteSource.wrap("content".getBytes()))
|
||||
.execute();
|
||||
|
||||
assertThat(nameCaptor.getValue()).isEqualTo("toBeModified");
|
||||
assertThat(contentCaptor).contains("content");
|
||||
}
|
||||
|
||||
private ModifyCommandBuilder initCommand() {
|
||||
return commandBuilder
|
||||
.setBranch("branch")
|
||||
.setCommitMessage("message")
|
||||
.setAuthor(new Person());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteTemporaryFiles(@TempDirectory.TempDir Path temp) throws IOException {
|
||||
ArgumentCaptor<String> nameCaptor = ArgumentCaptor.forClass(String.class);
|
||||
ArgumentCaptor<File> fileCaptor = ArgumentCaptor.forClass(File.class);
|
||||
doNothing().when(worker).modify(nameCaptor.capture(), fileCaptor.capture());
|
||||
|
||||
initCommand()
|
||||
.modifyFile("toBeModified").withData(ByteSource.wrap("content".getBytes()))
|
||||
.execute();
|
||||
|
||||
assertThat(Files.list(temp)).isEmpty();
|
||||
}
|
||||
|
||||
private static class ExtractContent implements Answer {
|
||||
private final List<String> contentCaptor;
|
||||
|
||||
public ExtractContent(List<String> contentCaptor) {
|
||||
this.contentCaptor = contentCaptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object answer(InvocationOnMock invocation) throws Throwable {
|
||||
return contentCaptor.add(Files.readAllLines(((File) invocation.getArgument(1)).toPath()).get(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ public class RepositoryServiceTest {
|
||||
|
||||
@Test
|
||||
public void shouldReturnMatchingProtocolsFromProvider() {
|
||||
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()));
|
||||
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null);
|
||||
Stream<ScmProtocol> supportedProtocols = repositoryService.getSupportedProtocols();
|
||||
|
||||
assertThat(sizeOf(supportedProtocols.collect(Collectors.toList()))).isEqualTo(1);
|
||||
@@ -32,7 +32,7 @@ public class RepositoryServiceTest {
|
||||
|
||||
@Test
|
||||
public void shouldFindKnownProtocol() {
|
||||
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()));
|
||||
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null);
|
||||
|
||||
HttpScmProtocol protocol = repositoryService.getProtocol(HttpScmProtocol.class);
|
||||
|
||||
@@ -41,7 +41,7 @@ public class RepositoryServiceTest {
|
||||
|
||||
@Test
|
||||
public void shouldFailForUnknownProtocol() {
|
||||
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()));
|
||||
RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
repositoryService.getProtocol(UnknownScmProtocol.class);
|
||||
|
||||
@@ -28,7 +28,8 @@ public class SimpleWorkdirFactoryTest {
|
||||
|
||||
@Before
|
||||
public void initFactory() throws IOException {
|
||||
simpleWorkdirFactory = new SimpleWorkdirFactory<Closeable, Context>(temporaryFolder.newFolder()) {
|
||||
WorkdirProvider workdirProvider = new WorkdirProvider(temporaryFolder.newFolder());
|
||||
simpleWorkdirFactory = new SimpleWorkdirFactory<Closeable, Context>(workdirProvider) {
|
||||
@Override
|
||||
protected Repository getScmRepository(Context context) {
|
||||
return REPOSITORY;
|
||||
|
||||
@@ -35,15 +35,33 @@ package sonia.scm.repository.spi;
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.api.errors.RefNotFoundException;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.GitUtil;
|
||||
import sonia.scm.repository.GitWorkdirFactory;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.repository.util.WorkingCopy;
|
||||
import sonia.scm.user.User;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
import static sonia.scm.NotFoundException.notFound;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
@@ -51,7 +69,7 @@ import java.util.Optional;
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class AbstractGitCommand
|
||||
class AbstractGitCommand
|
||||
{
|
||||
|
||||
/**
|
||||
@@ -66,7 +84,7 @@ public class AbstractGitCommand
|
||||
* @param context
|
||||
* @param repository
|
||||
*/
|
||||
protected AbstractGitCommand(GitContext context,
|
||||
AbstractGitCommand(GitContext context,
|
||||
sonia.scm.repository.Repository repository)
|
||||
{
|
||||
this.repository = repository;
|
||||
@@ -83,12 +101,12 @@ public class AbstractGitCommand
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
protected Repository open() throws IOException
|
||||
Repository open() throws IOException
|
||||
{
|
||||
return context.open();
|
||||
}
|
||||
|
||||
protected ObjectId getCommitOrDefault(Repository gitRepository, String requestedCommit) throws IOException {
|
||||
ObjectId getCommitOrDefault(Repository gitRepository, String requestedCommit) throws IOException {
|
||||
ObjectId commit;
|
||||
if ( Strings.isNullOrEmpty(requestedCommit) ) {
|
||||
commit = getDefaultBranch(gitRepository);
|
||||
@@ -98,7 +116,7 @@ public class AbstractGitCommand
|
||||
return commit;
|
||||
}
|
||||
|
||||
protected ObjectId getDefaultBranch(Repository gitRepository) throws IOException {
|
||||
ObjectId getDefaultBranch(Repository gitRepository) throws IOException {
|
||||
Ref ref = getBranchOrDefault(gitRepository, null);
|
||||
if (ref == null) {
|
||||
return null;
|
||||
@@ -107,7 +125,7 @@ public class AbstractGitCommand
|
||||
}
|
||||
}
|
||||
|
||||
protected Ref getBranchOrDefault(Repository gitRepository, String requestedBranch) throws IOException {
|
||||
Ref getBranchOrDefault(Repository gitRepository, String requestedBranch) throws IOException {
|
||||
if ( Strings.isNullOrEmpty(requestedBranch) ) {
|
||||
String defaultBranchName = context.getConfig().getDefaultBranch();
|
||||
if (!Strings.isNullOrEmpty(defaultBranchName)) {
|
||||
@@ -122,6 +140,120 @@ public class AbstractGitCommand
|
||||
}
|
||||
}
|
||||
|
||||
<R, W extends GitCloneWorker<R>> R inClone(Function<Git, W> workerSupplier, GitWorkdirFactory workdirFactory) {
|
||||
try (WorkingCopy<Repository> workingCopy = workdirFactory.createWorkingCopy(context)) {
|
||||
Repository repository = workingCopy.getWorkingRepository();
|
||||
logger.debug("cloned repository to folder {}", repository.getWorkTree());
|
||||
return workerSupplier.apply(new Git(repository)).run();
|
||||
} catch (IOException e) {
|
||||
throw new InternalRepositoryException(context.getRepository(), "could not clone repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
ObjectId resolveRevisionOrThrowNotFound(Repository repository, String revision) throws IOException {
|
||||
ObjectId resolved = repository.resolve(revision);
|
||||
if (resolved == null) {
|
||||
throw notFound(entity("Revision", revision).in(context.getRepository()));
|
||||
} else {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class GitCloneWorker<R> {
|
||||
private final Git clone;
|
||||
|
||||
GitCloneWorker(Git clone) {
|
||||
this.clone = clone;
|
||||
}
|
||||
|
||||
abstract R run() throws IOException;
|
||||
|
||||
Git getClone() {
|
||||
return clone;
|
||||
}
|
||||
|
||||
void checkOutBranch(String branchName) throws IOException {
|
||||
try {
|
||||
clone.checkout().setName(branchName).call();
|
||||
} catch (RefNotFoundException e) {
|
||||
logger.trace("could not checkout branch {} directly; trying to create local branch", branchName, e);
|
||||
checkOutTargetAsNewLocalBranch(branchName);
|
||||
} catch (GitAPIException e) {
|
||||
throw new InternalRepositoryException(context.getRepository(), "could not checkout branch: " + branchName, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkOutTargetAsNewLocalBranch(String branchName) throws IOException {
|
||||
try {
|
||||
ObjectId targetRevision = resolveRevision(branchName);
|
||||
clone.checkout().setStartPoint(targetRevision.getName()).setName(branchName).setCreateBranch(true).call();
|
||||
} catch (RefNotFoundException e) {
|
||||
logger.debug("could not checkout branch {} as local branch", branchName, e);
|
||||
throw notFound(entity("Revision", branchName).in(context.getRepository()));
|
||||
} catch (GitAPIException e) {
|
||||
throw new InternalRepositoryException(context.getRepository(), "could not checkout branch as local branch: " + branchName, e);
|
||||
}
|
||||
}
|
||||
|
||||
ObjectId resolveRevision(String revision) throws IOException {
|
||||
ObjectId resolved = clone.getRepository().resolve(revision);
|
||||
if (resolved == null) {
|
||||
return resolveRevisionOrThrowNotFound(clone.getRepository(), "origin/" + revision);
|
||||
} else {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
void failIfNotChanged(Supplier<RuntimeException> doThrow) {
|
||||
try {
|
||||
if (clone.status().call().isClean()) {
|
||||
throw doThrow.get();
|
||||
}
|
||||
} catch (GitAPIException e) {
|
||||
throw new InternalRepositoryException(context.getRepository(), "could not read status of repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
Optional<RevCommit> doCommit(String message, Person author) {
|
||||
Person authorToUse = determineAuthor(author);
|
||||
try {
|
||||
if (!clone.status().call().isClean()) {
|
||||
return of(clone.commit()
|
||||
.setAuthor(authorToUse.getName(), authorToUse.getMail())
|
||||
.setMessage(message)
|
||||
.call());
|
||||
} else {
|
||||
return empty();
|
||||
}
|
||||
} catch (GitAPIException e) {
|
||||
throw new InternalRepositoryException(context.getRepository(), "could not commit changes", e);
|
||||
}
|
||||
}
|
||||
|
||||
void push() {
|
||||
try {
|
||||
clone.push().call();
|
||||
} catch (GitAPIException e) {
|
||||
throw new IntegrateChangesFromWorkdirException(repository,
|
||||
"could not push changes into central repository", e);
|
||||
}
|
||||
logger.debug("pushed changes");
|
||||
}
|
||||
|
||||
private Person determineAuthor(Person author) {
|
||||
if (author == null) {
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
User user = subject.getPrincipals().oneByType(User.class);
|
||||
String name = user.getDisplayName();
|
||||
String email = user.getMail();
|
||||
logger.debug("no author set; using logged in user: {} <{}>", name, email);
|
||||
return new Person(name, email);
|
||||
} else {
|
||||
return author;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.MergeCommand.FastForwardMode;
|
||||
import org.eclipse.jgit.api.MergeResult;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.api.errors.RefNotFoundException;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.merge.MergeStrategy;
|
||||
@@ -17,18 +14,12 @@ import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.GitWorkdirFactory;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.api.MergeCommandResult;
|
||||
import sonia.scm.repository.api.MergeDryRunCommandResult;
|
||||
import sonia.scm.repository.util.WorkingCopy;
|
||||
import sonia.scm.user.User;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.MessageFormat;
|
||||
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
import static sonia.scm.NotFoundException.notFound;
|
||||
|
||||
public class GitMergeCommand extends AbstractGitCommand implements MergeCommand {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GitMergeCommand.class);
|
||||
@@ -47,13 +38,7 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
|
||||
|
||||
@Override
|
||||
public MergeCommandResult merge(MergeCommandRequest request) {
|
||||
try (WorkingCopy<Repository> workingCopy = workdirFactory.createWorkingCopy(context)) {
|
||||
Repository repository = workingCopy.getWorkingRepository();
|
||||
logger.debug("cloned repository to folder {}", repository.getWorkTree());
|
||||
return new MergeWorker(repository, request).merge();
|
||||
} catch (IOException e) {
|
||||
throw new InternalRepositoryException(context.getRepository(), "could not clone repository for merge", e);
|
||||
}
|
||||
return inClone(clone -> new MergeWorker(clone, request), workdirFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -70,32 +55,23 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
|
||||
}
|
||||
}
|
||||
|
||||
private ObjectId resolveRevisionOrThrowNotFound(Repository repository, String revision) throws IOException {
|
||||
ObjectId resolved = repository.resolve(revision);
|
||||
if (resolved == null) {
|
||||
throw notFound(entity("Revision", revision).in(context.getRepository()));
|
||||
} else {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
private class MergeWorker {
|
||||
private class MergeWorker extends GitCloneWorker<MergeCommandResult> {
|
||||
|
||||
private final String target;
|
||||
private final String toMerge;
|
||||
private final Person author;
|
||||
private final Git clone;
|
||||
private final String messageTemplate;
|
||||
|
||||
private MergeWorker(Repository clone, MergeCommandRequest request) {
|
||||
private MergeWorker(Git clone, MergeCommandRequest request) {
|
||||
super(clone);
|
||||
this.target = request.getTargetBranch();
|
||||
this.toMerge = request.getBranchToMerge();
|
||||
this.author = request.getAuthor();
|
||||
this.messageTemplate = request.getMessageTemplate();
|
||||
this.clone = new Git(clone);
|
||||
}
|
||||
|
||||
private MergeCommandResult merge() throws IOException {
|
||||
@Override
|
||||
MergeCommandResult run() throws IOException {
|
||||
checkOutTargetBranch();
|
||||
MergeResult result = doMergeInClone();
|
||||
if (result.getMergeStatus().isSuccessful()) {
|
||||
@@ -108,33 +84,14 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
|
||||
}
|
||||
|
||||
private void checkOutTargetBranch() throws IOException {
|
||||
try {
|
||||
clone.checkout().setName(target).call();
|
||||
} catch (RefNotFoundException e) {
|
||||
logger.trace("could not checkout target branch {} for merge directly; trying to create local branch", target, e);
|
||||
checkOutTargetAsNewLocalBranch();
|
||||
} catch (GitAPIException e) {
|
||||
throw new InternalRepositoryException(context.getRepository(), "could not checkout target branch for merge: " + target, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkOutTargetAsNewLocalBranch() throws IOException {
|
||||
try {
|
||||
ObjectId targetRevision = resolveRevision(target);
|
||||
clone.checkout().setStartPoint(targetRevision.getName()).setName(target).setCreateBranch(true).call();
|
||||
} catch (RefNotFoundException e) {
|
||||
logger.debug("could not checkout target branch {} for merge as local branch", target, e);
|
||||
throw notFound(entity("Revision", target).in(context.getRepository()));
|
||||
} catch (GitAPIException e) {
|
||||
throw new InternalRepositoryException(context.getRepository(), "could not checkout target branch for merge as local branch: " + target, e);
|
||||
}
|
||||
checkOutBranch(target);
|
||||
}
|
||||
|
||||
private MergeResult doMergeInClone() throws IOException {
|
||||
MergeResult result;
|
||||
try {
|
||||
ObjectId sourceRevision = resolveRevision(toMerge);
|
||||
result = clone.merge()
|
||||
result = getClone().merge()
|
||||
.setFastForward(FastForwardMode.NO_FF)
|
||||
.setCommit(false) // we want to set the author manually
|
||||
.include(toMerge, sourceRevision)
|
||||
@@ -147,17 +104,7 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
|
||||
|
||||
private void doCommit() {
|
||||
logger.debug("merged branch {} into {}", toMerge, target);
|
||||
Person authorToUse = determineAuthor();
|
||||
try {
|
||||
if (!clone.status().call().isClean()) {
|
||||
clone.commit()
|
||||
.setAuthor(authorToUse.getName(), authorToUse.getMail())
|
||||
.setMessage(MessageFormat.format(determineMessageTemplate(), toMerge, target))
|
||||
.call();
|
||||
}
|
||||
} catch (GitAPIException e) {
|
||||
throw new InternalRepositoryException(context.getRepository(), "could not commit merge between branch " + toMerge + " and " + target, e);
|
||||
}
|
||||
doCommit(MessageFormat.format(determineMessageTemplate(), toMerge, target), author);
|
||||
}
|
||||
|
||||
private String determineMessageTemplate() {
|
||||
@@ -168,41 +115,9 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
|
||||
}
|
||||
}
|
||||
|
||||
private Person determineAuthor() {
|
||||
if (author == null) {
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
User user = subject.getPrincipals().oneByType(User.class);
|
||||
String name = user.getDisplayName();
|
||||
String email = user.getMail();
|
||||
logger.debug("no author set; using logged in user: {} <{}>", name, email);
|
||||
return new Person(name, email);
|
||||
} else {
|
||||
return author;
|
||||
}
|
||||
}
|
||||
|
||||
private void push() {
|
||||
try {
|
||||
clone.push().call();
|
||||
} catch (GitAPIException e) {
|
||||
throw new IntegrateChangesFromWorkdirException(repository,
|
||||
"could not push merged branch " + target + " into central repository", e);
|
||||
}
|
||||
logger.debug("pushed merged branch {}", target);
|
||||
}
|
||||
|
||||
private MergeCommandResult analyseFailure(MergeResult result) {
|
||||
logger.info("could not merged branch {} into {} due to conflict in paths {}", toMerge, target, result.getConflicts().keySet());
|
||||
return MergeCommandResult.failure(result.getConflicts().keySet());
|
||||
}
|
||||
|
||||
private ObjectId resolveRevision(String revision) throws IOException {
|
||||
ObjectId resolved = clone.getRepository().resolve(revision);
|
||||
if (resolved == null) {
|
||||
return resolveRevisionOrThrowNotFound(clone.getRepository(), "origin/" + revision);
|
||||
} else {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import sonia.scm.BadRequestException;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.repository.GitWorkdirFactory;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
|
||||
import static sonia.scm.AlreadyExistsException.alreadyExists;
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
|
||||
public class GitModifyCommand extends AbstractGitCommand implements ModifyCommand {
|
||||
|
||||
private final GitWorkdirFactory workdirFactory;
|
||||
|
||||
GitModifyCommand(GitContext context, Repository repository, GitWorkdirFactory workdirFactory) {
|
||||
super(context, repository);
|
||||
this.workdirFactory = workdirFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(ModifyCommandRequest request) {
|
||||
return inClone(clone -> new ModifyWorker(clone, request), workdirFactory);
|
||||
}
|
||||
|
||||
private class ModifyWorker extends GitCloneWorker<String> implements Worker {
|
||||
|
||||
private final File workDir;
|
||||
private final ModifyCommandRequest request;
|
||||
|
||||
ModifyWorker(Git clone, ModifyCommandRequest request) {
|
||||
super(clone);
|
||||
this.workDir = clone.getRepository().getWorkTree();
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
@Override
|
||||
String run() throws IOException {
|
||||
checkOutBranch(request.getBranch());
|
||||
for (ModifyCommandRequest.PartialRequest r : request.getRequests()) {
|
||||
r.execute(this);
|
||||
}
|
||||
failIfNotChanged(NoChangesMadeException::new);
|
||||
Optional<RevCommit> revCommit = doCommit(request.getCommitMessage(), request.getAuthor());
|
||||
push();
|
||||
return revCommit.orElseThrow(NoChangesMadeException::new).name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create(String toBeCreated, File file, boolean overwrite) throws IOException {
|
||||
Path targetFile = new File(workDir, toBeCreated).toPath();
|
||||
Files.createDirectories(targetFile.getParent());
|
||||
if (overwrite) {
|
||||
Files.copy(file.toPath(), targetFile, REPLACE_EXISTING);
|
||||
} else {
|
||||
try {
|
||||
Files.copy(file.toPath(), targetFile);
|
||||
} catch (FileAlreadyExistsException e) {
|
||||
throw alreadyExists(entity("file", toBeCreated).in("branch", request.getBranch()).in(context.getRepository()));
|
||||
}
|
||||
}
|
||||
try {
|
||||
getClone().add().addFilepattern(toBeCreated).call();
|
||||
} catch (GitAPIException e) {
|
||||
throwInternalRepositoryException("could not add new file to index", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String toBeDeleted) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void modify(String path, File file) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void move(String sourcePath, String targetPath) {
|
||||
|
||||
}
|
||||
|
||||
private class NoChangesMadeException extends BadRequestException {
|
||||
public NoChangesMadeException() {
|
||||
super(ContextEntry.ContextBuilder.entity(context.getRepository()).build(), "no changes detected to branch " + ModifyWorker.this.request.getBranch());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return "40RaYIeeR1";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String throwInternalRepositoryException(String message, Exception e) {
|
||||
throw new InternalRepositoryException(context.getRepository(), message, e);
|
||||
}
|
||||
}
|
||||
@@ -268,6 +268,11 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
|
||||
return new GitMergeCommand(context, repository, handler.getWorkdirFactory());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModifyCommand getModifyCommand() {
|
||||
return new GitModifyCommand(context, repository, handler.getWorkdirFactory());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Feature> getSupportedFeatures() {
|
||||
return FEATURES;
|
||||
|
||||
@@ -7,16 +7,16 @@ import org.eclipse.jgit.transport.ScmTransportProtocol;
|
||||
import sonia.scm.repository.GitWorkdirFactory;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.util.SimpleWorkdirFactory;
|
||||
import sonia.scm.repository.util.WorkdirProvider;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.File;
|
||||
|
||||
public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory<Repository, GitContext> implements GitWorkdirFactory {
|
||||
|
||||
public SimpleGitWorkdirFactory() {
|
||||
}
|
||||
|
||||
SimpleGitWorkdirFactory(File poolDirectory) {
|
||||
super(poolDirectory);
|
||||
@Inject
|
||||
public SimpleGitWorkdirFactory(WorkdirProvider workdirProvider) {
|
||||
super(workdirProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.api.BranchRequest;
|
||||
import sonia.scm.repository.util.WorkdirProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
@@ -25,7 +26,7 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase {
|
||||
branchRequest.setParentBranch(source.getName());
|
||||
branchRequest.setNewBranch("new_branch");
|
||||
|
||||
new GitBranchCommand(context, repository, new SimpleGitWorkdirFactory()).branch(branchRequest);
|
||||
new GitBranchCommand(context, repository, new SimpleGitWorkdirFactory(new WorkdirProvider())).branch(branchRequest);
|
||||
|
||||
Branch newBranch = findBranch(context, "new_branch");
|
||||
Assertions.assertThat(newBranch.getRevision()).isEqualTo(source.getRevision());
|
||||
@@ -45,7 +46,7 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase {
|
||||
BranchRequest branchRequest = new BranchRequest();
|
||||
branchRequest.setNewBranch("new_branch");
|
||||
|
||||
new GitBranchCommand(context, repository, new SimpleGitWorkdirFactory()).branch(branchRequest);
|
||||
new GitBranchCommand(context, repository, new SimpleGitWorkdirFactory(new WorkdirProvider())).branch(branchRequest);
|
||||
|
||||
Assertions.assertThat(readBranches(context)).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty();
|
||||
}
|
||||
|
||||
@@ -10,29 +10,17 @@ import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.PersonIdent;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.transport.ScmTransportProtocol;
|
||||
import org.eclipse.jgit.transport.Transport;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.repository.GitRepositoryHandler;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.repository.PreProcessorUtil;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.api.HookContextFactory;
|
||||
import sonia.scm.repository.api.MergeCommandResult;
|
||||
import sonia.scm.repository.api.MergeDryRunCommandResult;
|
||||
import sonia.scm.repository.util.WorkdirProvider;
|
||||
import sonia.scm.user.User;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static com.google.inject.util.Providers.of;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret")
|
||||
public class GitMergeCommandTest extends AbstractGitCommandTestBase {
|
||||
@@ -244,6 +232,6 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
|
||||
}
|
||||
|
||||
private GitMergeCommand createCommand() {
|
||||
return new GitMergeCommand(createContext(), repository, new SimpleGitWorkdirFactory());
|
||||
return new GitMergeCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import com.github.sdorra.shiro.ShiroRule;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.errors.CorruptObjectException;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.ObjectReader;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.revwalk.RevWalk;
|
||||
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import sonia.scm.AlreadyExistsException;
|
||||
import sonia.scm.BadRequestException;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.repository.util.WorkdirProvider;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret")
|
||||
public class GitModifyCommandTest extends AbstractGitCommandTestBase {
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
@Rule
|
||||
public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule();
|
||||
@Rule
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
|
||||
@Test
|
||||
public void shouldCreateCommit() throws IOException, GitAPIException {
|
||||
File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile();
|
||||
|
||||
GitModifyCommand command = createCommand();
|
||||
|
||||
ModifyCommandRequest request = new ModifyCommandRequest();
|
||||
request.setBranch("master");
|
||||
request.setCommitMessage("test commit");
|
||||
request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile, false));
|
||||
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
|
||||
|
||||
String newRef = command.execute(request);
|
||||
|
||||
try (Git git = new Git(createContext().open())) {
|
||||
RevCommit lastCommit = getLastCommit(git);
|
||||
assertThat(lastCommit.getFullMessage()).isEqualTo("test commit");
|
||||
assertThat(lastCommit.getAuthorIdent().getName()).isEqualTo("Dirk Gently");
|
||||
assertThat(newRef).isEqualTo(lastCommit.toObjectId().name());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateNewFile() throws IOException, GitAPIException {
|
||||
File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile();
|
||||
|
||||
GitModifyCommand command = createCommand();
|
||||
|
||||
ModifyCommandRequest request = new ModifyCommandRequest();
|
||||
request.setBranch("master");
|
||||
request.setCommitMessage("test commit");
|
||||
request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile, false));
|
||||
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
|
||||
|
||||
command.execute(request);
|
||||
|
||||
TreeAssertions assertions = canonicalTreeParser -> assertThat(canonicalTreeParser.findFile("new_file")).isTrue();
|
||||
|
||||
assertInTree(assertions);
|
||||
}
|
||||
|
||||
@Test(expected = AlreadyExistsException.class)
|
||||
public void shouldFailIfOverwritingExistingFileWithoutOverwriteFlag() throws IOException, GitAPIException {
|
||||
File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile();
|
||||
|
||||
GitModifyCommand command = createCommand();
|
||||
|
||||
ModifyCommandRequest request = new ModifyCommandRequest();
|
||||
request.setBranch("master");
|
||||
request.setCommitMessage("test commit");
|
||||
request.addRequest(new ModifyCommandRequest.CreateFileRequest("a.txt", newFile, false));
|
||||
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
|
||||
|
||||
command.execute(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldOverwriteExistingFileIfOverwriteFlagSet() throws IOException, GitAPIException {
|
||||
File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile();
|
||||
|
||||
GitModifyCommand command = createCommand();
|
||||
|
||||
ModifyCommandRequest request = new ModifyCommandRequest();
|
||||
request.setBranch("master");
|
||||
request.setCommitMessage("test commit");
|
||||
request.addRequest(new ModifyCommandRequest.CreateFileRequest("a.txt", newFile, true));
|
||||
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
|
||||
|
||||
command.execute(request);
|
||||
|
||||
TreeAssertions assertions = canonicalTreeParser -> assertThat(canonicalTreeParser.findFile("a.txt")).isTrue();
|
||||
|
||||
assertInTree(assertions);
|
||||
}
|
||||
|
||||
@Test(expected = BadRequestException.class)
|
||||
public void shouldFailIfNoChangesMade() throws IOException, GitAPIException {
|
||||
File newFile = Files.write(temporaryFolder.newFile().toPath(), "b\n".getBytes()).toFile();
|
||||
|
||||
GitModifyCommand command = createCommand();
|
||||
|
||||
ModifyCommandRequest request = new ModifyCommandRequest();
|
||||
request.setBranch("master");
|
||||
request.setCommitMessage("test commit");
|
||||
request.addRequest(new ModifyCommandRequest.CreateFileRequest("b.txt", newFile, true));
|
||||
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
|
||||
|
||||
command.execute(request);
|
||||
}
|
||||
|
||||
private void assertInTree(TreeAssertions assertions) throws IOException, GitAPIException {
|
||||
try (Git git = new Git(createContext().open())) {
|
||||
RevCommit lastCommit = getLastCommit(git);
|
||||
try (RevWalk walk = new RevWalk(git.getRepository())) {
|
||||
RevCommit commit = walk.parseCommit(lastCommit);
|
||||
ObjectId treeId = commit.getTree().getId();
|
||||
try (ObjectReader reader = git.getRepository().newObjectReader()) {
|
||||
assertions.checkAssertions(new CanonicalTreeParser(null, reader, treeId));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private RevCommit getLastCommit(Git git) throws GitAPIException {
|
||||
return git.log().setMaxCount(1).call().iterator().next();
|
||||
}
|
||||
|
||||
private GitModifyCommand createCommand() {
|
||||
return new GitModifyCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider()));
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface TreeAssertions {
|
||||
void checkAssertions(CanonicalTreeParser treeParser) throws CorruptObjectException;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import sonia.scm.repository.GitRepositoryHandler;
|
||||
import sonia.scm.repository.PreProcessorUtil;
|
||||
import sonia.scm.repository.RepositoryManager;
|
||||
import sonia.scm.repository.api.HookContextFactory;
|
||||
import sonia.scm.repository.util.WorkdirProvider;
|
||||
import sonia.scm.repository.util.WorkingCopy;
|
||||
|
||||
import java.io.File;
|
||||
@@ -29,19 +30,21 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
|
||||
|
||||
// keep this so that it will not be garbage collected (Transport keeps this in a week reference)
|
||||
private ScmTransportProtocol proto;
|
||||
private WorkdirProvider workdirProvider;
|
||||
|
||||
@Before
|
||||
public void bindScmProtocol() {
|
||||
public void bindScmProtocol() throws IOException {
|
||||
HookContextFactory hookContextFactory = new HookContextFactory(mock(PreProcessorUtil.class));
|
||||
HookEventFacade hookEventFacade = new HookEventFacade(of(mock(RepositoryManager.class)), hookContextFactory);
|
||||
GitRepositoryHandler gitRepositoryHandler = mock(GitRepositoryHandler.class);
|
||||
proto = new ScmTransportProtocol(of(hookEventFacade), of(gitRepositoryHandler));
|
||||
Transport.register(proto);
|
||||
workdirProvider = new WorkdirProvider(temporaryFolder.newFolder());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyPoolShouldCreateNewWorkdir() throws IOException {
|
||||
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
|
||||
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider);
|
||||
File masterRepo = createRepositoryDirectory();
|
||||
|
||||
try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext())) {
|
||||
@@ -59,7 +62,7 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
|
||||
|
||||
@Test
|
||||
public void cloneFromPoolShouldNotBeReused() throws IOException {
|
||||
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
|
||||
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider);
|
||||
|
||||
File firstDirectory;
|
||||
try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext())) {
|
||||
@@ -73,7 +76,7 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
|
||||
|
||||
@Test
|
||||
public void cloneFromPoolShouldBeDeletedOnClose() throws IOException {
|
||||
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
|
||||
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider);
|
||||
|
||||
File directory;
|
||||
try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext())) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.aragost.javahg.Repository;
|
||||
import com.aragost.javahg.commands.CloneCommand;
|
||||
import com.aragost.javahg.commands.PullCommand;
|
||||
import sonia.scm.repository.util.SimpleWorkdirFactory;
|
||||
import sonia.scm.repository.util.WorkdirProvider;
|
||||
import sonia.scm.web.HgRepositoryEnvironmentBuilder;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -18,7 +19,8 @@ public class SimpleHgWorkdirFactory extends SimpleWorkdirFactory<Repository, HgC
|
||||
private final Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder;
|
||||
|
||||
@Inject
|
||||
public SimpleHgWorkdirFactory(Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder) {
|
||||
public SimpleHgWorkdirFactory(Provider<HgRepositoryEnvironmentBuilder> hgRepositoryEnvironmentBuilder, WorkdirProvider workdirProvider) {
|
||||
super(workdirProvider);
|
||||
this.hgRepositoryEnvironmentBuilder = hgRepositoryEnvironmentBuilder;
|
||||
}
|
||||
@Override
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.junit.Test;
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.HgTestUtil;
|
||||
import sonia.scm.repository.api.BranchRequest;
|
||||
import sonia.scm.repository.util.WorkdirProvider;
|
||||
import sonia.scm.web.HgRepositoryEnvironmentBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -20,7 +21,7 @@ public class HgBranchCommandTest extends AbstractHgCommandTestBase {
|
||||
HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder =
|
||||
new HgRepositoryEnvironmentBuilder(handler, HgTestUtil.createHookManager());
|
||||
|
||||
SimpleHgWorkdirFactory workdirFactory = new SimpleHgWorkdirFactory(Providers.of(hgRepositoryEnvironmentBuilder)) {
|
||||
SimpleHgWorkdirFactory workdirFactory = new SimpleHgWorkdirFactory(Providers.of(hgRepositoryEnvironmentBuilder), new WorkdirProvider()) {
|
||||
@Override
|
||||
public void configure(PullCommand pullCommand) {
|
||||
// we do not want to configure http hooks in this unit test
|
||||
|
||||
@@ -125,7 +125,7 @@ class FileTree extends React.Component<Props> {
|
||||
<th className="is-hidden-mobile">
|
||||
{t("sources.file-tree.description")}
|
||||
</th>
|
||||
{binder.hasExtension("sourceView.right") && (
|
||||
{binder.hasExtension("repos.sources.tree.row.right") && (
|
||||
<th className="is-hidden-mobile" />
|
||||
)}
|
||||
</tr>
|
||||
|
||||
@@ -92,11 +92,11 @@ class FileTreeLeaf extends React.Component<Props> {
|
||||
>
|
||||
{file.description}
|
||||
</td>
|
||||
{binder.hasExtension("sourceView.right") && (
|
||||
{binder.hasExtension("repos.sources.tree.row.right") && (
|
||||
<td>
|
||||
{!file.directory && (
|
||||
<ExtensionPoint
|
||||
name="sourceView.right"
|
||||
name="repos.sources.tree.row.right"
|
||||
props={{ file }}
|
||||
renderAll={true}
|
||||
/>
|
||||
|
||||
@@ -100,7 +100,7 @@ class Content extends React.Component<Props, State> {
|
||||
<div className={classes.marginInHeader}>{selector}</div>
|
||||
<ButtonGroup>
|
||||
<ExtensionPoint
|
||||
name="fileView.actionbar.right"
|
||||
name="repos.sources.content.actionbar"
|
||||
props={{ file }}
|
||||
renderAll={true}
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,7 @@ package sonia.scm.api.v2.resources;
|
||||
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import sonia.scm.ConcurrentModificationException;
|
||||
import sonia.scm.IllegalIdentifierChangeException;
|
||||
import sonia.scm.Manager;
|
||||
import sonia.scm.ModelObject;
|
||||
import sonia.scm.NotFoundException;
|
||||
@@ -11,8 +12,6 @@ import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
|
||||
|
||||
/**
|
||||
* Adapter from resource http endpoints to managers, for Single resources (e.g. {@code /user/name}).
|
||||
*
|
||||
@@ -55,7 +54,7 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject,
|
||||
MODEL_OBJECT existingModelObject = reader.get();
|
||||
MODEL_OBJECT changedModelObject = applyChanges.apply(existingModelObject);
|
||||
if (!hasSameKey.test(changedModelObject)) {
|
||||
return Response.status(BAD_REQUEST).entity("illegal change of id").build();
|
||||
throw new IllegalIdentifierChangeException("illegal change of id");
|
||||
}
|
||||
else if (modelObjectWasModifiedConcurrently(existingModelObject, changedModelObject)) {
|
||||
throw new ConcurrentModificationException(type, keyExtractor.apply(existingModelObject));
|
||||
|
||||
@@ -59,6 +59,9 @@ import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
@@ -68,6 +71,7 @@ import java.util.Set;
|
||||
*
|
||||
* TODO don't mix nio and io
|
||||
*/
|
||||
@SuppressWarnings("squid:S3725") // performance is not critical, for this type of checks
|
||||
public final class PluginProcessor
|
||||
{
|
||||
|
||||
@@ -171,7 +175,11 @@ public final class PluginProcessor
|
||||
|
||||
extract(archives);
|
||||
|
||||
List<Path> dirs = collectPluginDirectories(pluginDirectory);
|
||||
List<Path> dirs =
|
||||
collectPluginDirectories(pluginDirectory)
|
||||
.stream()
|
||||
.filter(isPluginDirectory())
|
||||
.collect(toList());
|
||||
|
||||
logger.debug("process {} directories: {}", dirs.size(), dirs);
|
||||
|
||||
@@ -194,6 +202,10 @@ public final class PluginProcessor
|
||||
return ImmutableSet.copyOf(wrappers);
|
||||
}
|
||||
|
||||
private Predicate<Path> isPluginDirectory() {
|
||||
return dir -> Files.exists(dir.resolve(DIRECTORY_METAINF).resolve("scm").resolve("plugin.xml"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
|
||||
@@ -167,6 +167,14 @@
|
||||
"CHRM7IQzo1": {
|
||||
"displayName": "Änderung fehlgeschlagen",
|
||||
"description": "Die Änderung ist fehlgeschlagen. Bitte wenden Sie sich an ihren Administrator für weitere Hinweise."
|
||||
},
|
||||
"thbsUFokjk": {
|
||||
"displayName": "Unerlaubte Änderung eines Schlüsselwerts",
|
||||
"description": "Ein Schlüsselwert wurde unerlaubterweise geändert."
|
||||
},
|
||||
"40RaYIeeR1": {
|
||||
"displayName": "Es wurden keine Änderungen durchgeführt",
|
||||
"description": "Das Repository wurde nicht verändert. Daher konnte kein neuer Commit erzeugt werden."
|
||||
}
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
|
||||
@@ -167,6 +167,14 @@
|
||||
"CHRM7IQzo1": {
|
||||
"displayName": "Change failed",
|
||||
"description": "The change failed. Please contact your administrator for further assistance."
|
||||
},
|
||||
"thbsUFokjk": {
|
||||
"displayName": "Illegal change of an identifier",
|
||||
"description": "A identifier value has been changed in the entity. This is not allowed."
|
||||
},
|
||||
"40RaYIeeR1": {
|
||||
"displayName": "No changes were made",
|
||||
"description": "No changes were made to the files of the repository. Therefor no new commit could be created."
|
||||
}
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
|
||||
@@ -134,6 +134,16 @@ public class PluginProcessorTest
|
||||
assertThat(plugin.getId(), is(PLUGIN_A.id));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCollectPluginsAndDoNotFailOnNonPluginDirectories() throws IOException {
|
||||
new File(pluginDirectory, "some-directory").mkdirs();
|
||||
|
||||
copySmp(PLUGIN_A);
|
||||
InstalledPlugin plugin = collectAndGetFirst();
|
||||
|
||||
assertThat(plugin.getId(), is(PLUGIN_A.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user