mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-02 19:45:51 +01:00
Merge with 2.0.0-m3
This commit is contained in:
2
pom.xml
2
pom.xml
@@ -842,7 +842,7 @@
|
||||
<shiro.version>1.4.0</shiro.version>
|
||||
|
||||
<!-- repository libraries -->
|
||||
<jgit.version>v5.4.0.201906121030-r-scm1</jgit.version>
|
||||
<jgit.version>v5.4.0.201906121030-r-scm2</jgit.version>
|
||||
<svnkit.version>1.9.0-scm3</svnkit.version>
|
||||
|
||||
<!-- util libraries -->
|
||||
|
||||
18
scm-core/src/main/java/sonia/scm/NoChangesMadeException.java
Normal file
18
scm-core/src/main/java/sonia/scm/NoChangesMadeException.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package sonia.scm;
|
||||
|
||||
import sonia.scm.repository.Repository;
|
||||
|
||||
public class NoChangesMadeException extends BadRequestException {
|
||||
public NoChangesMadeException(Repository repository, String branch) {
|
||||
super(ContextEntry.ContextBuilder.entity(repository).build(), "no changes detected to branch " + branch);
|
||||
}
|
||||
|
||||
public NoChangesMadeException(Repository repository) {
|
||||
super(ContextEntry.ContextBuilder.entity(repository).build(), "no changes detected");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return "40RaYIeeR1";
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,13 @@ public interface PluginManager {
|
||||
*/
|
||||
List<AvailablePlugin> getAvailable();
|
||||
|
||||
/**
|
||||
* Returns all updatable plugins.
|
||||
*
|
||||
* @return a list of updatable plugins.
|
||||
*/
|
||||
List<InstalledPlugin> getUpdatable();
|
||||
|
||||
/**
|
||||
* Installs the plugin with the given name from the list of available plugins.
|
||||
*
|
||||
@@ -93,4 +100,14 @@ public interface PluginManager {
|
||||
* Install all pending plugins and restart the scm context.
|
||||
*/
|
||||
void executePendingAndRestart();
|
||||
|
||||
/**
|
||||
* Cancel all pending plugins.
|
||||
*/
|
||||
void cancelPending();
|
||||
|
||||
/**
|
||||
* Update all installed plugins.
|
||||
*/
|
||||
void updateAll();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package sonia.scm.repository;
|
||||
|
||||
import sonia.scm.BadRequestException;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
@SuppressWarnings("squid:MaximumInheritanceDepth")
|
||||
public class NoCommonHistoryException extends BadRequestException {
|
||||
|
||||
public NoCommonHistoryException() {
|
||||
this("no common history");
|
||||
}
|
||||
|
||||
public NoCommonHistoryException(String message) {
|
||||
super(emptyList(), message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return "4iRct4avG1";
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,6 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.Feature;
|
||||
import sonia.scm.repository.spi.DiffCommand;
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
@@ -103,16 +102,12 @@ public final class DiffCommandBuilder extends AbstractDiffCommandBuilder<DiffCom
|
||||
* Passes the difference of the given parameter to the outputstream.
|
||||
*
|
||||
*
|
||||
* @param outputStream outputstream for the difference
|
||||
*
|
||||
* @return {@code this}
|
||||
* @return A consumer that expects the output stream for the difference
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
public DiffCommandBuilder retrieveContent(OutputStream outputStream) throws IOException {
|
||||
getDiffResult(outputStream);
|
||||
|
||||
return this;
|
||||
public OutputStreamConsumer retrieveContent() throws IOException {
|
||||
return getDiffResult();
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
@@ -125,21 +120,10 @@ public final class DiffCommandBuilder extends AbstractDiffCommandBuilder<DiffCom
|
||||
* @throws IOException
|
||||
*/
|
||||
public String getContent() throws IOException {
|
||||
String content = null;
|
||||
ByteArrayOutputStream baos = null;
|
||||
|
||||
try
|
||||
{
|
||||
baos = new ByteArrayOutputStream();
|
||||
getDiffResult(baos);
|
||||
content = baos.toString();
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||
getDiffResult();
|
||||
return baos.toString();
|
||||
}
|
||||
finally
|
||||
{
|
||||
IOUtil.close(baos);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
//~--- set methods ----------------------------------------------------------
|
||||
@@ -169,25 +153,25 @@ public final class DiffCommandBuilder extends AbstractDiffCommandBuilder<DiffCom
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param outputStream
|
||||
*
|
||||
* @throws IOException
|
||||
* @return
|
||||
*/
|
||||
private void getDiffResult(OutputStream outputStream) throws IOException {
|
||||
Preconditions.checkNotNull(outputStream, "OutputStream is required");
|
||||
private OutputStreamConsumer getDiffResult() throws IOException {
|
||||
Preconditions.checkArgument(request.isValid(),
|
||||
"path and/or revision is required");
|
||||
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
logger.debug("create diff for {}", request);
|
||||
}
|
||||
logger.debug("create diff for {}", request);
|
||||
|
||||
diffCommand.getDiffResult(request, outputStream);
|
||||
return diffCommand.getDiffResult(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
DiffCommandBuilder self() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface OutputStreamConsumer {
|
||||
void accept(OutputStream outputStream) throws IOException;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.google.common.base.Preconditions;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.repository.spi.MergeCommand;
|
||||
import sonia.scm.repository.spi.MergeCommandRequest;
|
||||
import sonia.scm.repository.util.AuthorUtil;
|
||||
|
||||
/**
|
||||
* Use this {@link MergeCommandBuilder} to merge two branches of a repository ({@link #executeMerge()}) or to check if
|
||||
@@ -126,6 +127,7 @@ public class MergeCommandBuilder {
|
||||
* @return The result of the merge.
|
||||
*/
|
||||
public MergeCommandResult executeMerge() {
|
||||
AuthorUtil.setAuthorIfNotAvailable(request);
|
||||
Preconditions.checkArgument(request.isValid(), "revision to merge and target revision is required");
|
||||
return mergeCommand.merge(request);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ 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.AuthorUtil;
|
||||
import sonia.scm.repository.util.WorkdirProvider;
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
@@ -94,22 +95,12 @@ public class ModifyCommandBuilder {
|
||||
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() {
|
||||
AuthorUtil.setAuthorIfNotAvailable(request);
|
||||
try {
|
||||
Preconditions.checkArgument(request.isValid(), "commit message, branch and at least one request are required");
|
||||
return command.execute(request);
|
||||
|
||||
@@ -33,8 +33,9 @@
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import sonia.scm.repository.api.DiffCommandBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -49,10 +50,9 @@ public interface DiffCommand
|
||||
*
|
||||
*
|
||||
* @param request
|
||||
* @param output
|
||||
*
|
||||
* @throws IOException
|
||||
* @throws RuntimeException
|
||||
* @return
|
||||
*/
|
||||
public void getDiffResult(DiffCommandRequest request, OutputStream output) throws IOException;
|
||||
DiffCommandBuilder.OutputStreamConsumer getDiffResult(DiffCommandRequest request) throws IOException;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ import com.google.common.base.Objects;
|
||||
import com.google.common.base.Strings;
|
||||
import sonia.scm.Validateable;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.repository.util.AuthorUtil.CommandWithAuthor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class MergeCommandRequest implements Validateable, Resetable, Serializable, Cloneable {
|
||||
public class MergeCommandRequest implements Validateable, Resetable, Serializable, Cloneable, CommandWithAuthor {
|
||||
|
||||
private static final long serialVersionUID = -2650236557922431528L;
|
||||
|
||||
|
||||
@@ -13,7 +13,5 @@ public interface ModifyCommand {
|
||||
void create(String toBeCreated, File file, boolean overwrite) throws IOException;
|
||||
|
||||
void modify(String path, File file) throws IOException;
|
||||
|
||||
void move(String sourcePath, String targetPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.Validateable;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.repository.util.AuthorUtil.CommandWithAuthor;
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
import java.io.File;
|
||||
@@ -13,7 +14,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ModifyCommandRequest implements Resetable, Validateable {
|
||||
public class ModifyCommandRequest implements Resetable, Validateable, CommandWithAuthor {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ModifyCommandRequest.class);
|
||||
|
||||
@@ -94,21 +95,6 @@ public class ModifyCommandRequest implements Resetable, Validateable {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import sonia.scm.ContextEntry;
|
||||
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.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
|
||||
import static sonia.scm.AlreadyExistsException.alreadyExists;
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
import static sonia.scm.NotFoundException.notFound;
|
||||
|
||||
/**
|
||||
* This "interface" is not really intended to be used as an interface but rather as
|
||||
* a base class to reduce code redundancy in Worker instances.
|
||||
*/
|
||||
public interface ModifyWorkerHelper extends ModifyCommand.Worker {
|
||||
|
||||
@Override
|
||||
default void delete(String toBeDeleted) throws IOException {
|
||||
Path fileToBeDeleted = new File(getWorkDir(), toBeDeleted).toPath();
|
||||
try {
|
||||
Files.delete(fileToBeDeleted);
|
||||
} catch (NoSuchFileException e) {
|
||||
throw notFound(createFileContext(toBeDeleted));
|
||||
}
|
||||
doScmDelete(toBeDeleted);
|
||||
}
|
||||
|
||||
void doScmDelete(String toBeDeleted);
|
||||
|
||||
@Override
|
||||
default void create(String toBeCreated, File file, boolean overwrite) throws IOException {
|
||||
Path targetFile = new File(getWorkDir(), toBeCreated).toPath();
|
||||
createDirectories(targetFile);
|
||||
if (overwrite) {
|
||||
Files.move(file.toPath(), targetFile, REPLACE_EXISTING);
|
||||
} else {
|
||||
try {
|
||||
Files.move(file.toPath(), targetFile);
|
||||
} catch (FileAlreadyExistsException e) {
|
||||
throw alreadyExists(createFileContext(toBeCreated));
|
||||
}
|
||||
}
|
||||
addFileToScm(toBeCreated, targetFile);
|
||||
}
|
||||
|
||||
default void modify(String path, File file) throws IOException {
|
||||
Path targetFile = new File(getWorkDir(), path).toPath();
|
||||
createDirectories(targetFile);
|
||||
if (!targetFile.toFile().exists()) {
|
||||
throw notFound(createFileContext(path));
|
||||
}
|
||||
Files.move(file.toPath(), targetFile, REPLACE_EXISTING);
|
||||
addFileToScm(path, targetFile);
|
||||
}
|
||||
|
||||
void addFileToScm(String name, Path file);
|
||||
|
||||
default ContextEntry.ContextBuilder createFileContext(String path) {
|
||||
ContextEntry.ContextBuilder contextBuilder = entity("file", path);
|
||||
if (!StringUtils.isEmpty(getBranch())) {
|
||||
contextBuilder.in("branch", getBranch());
|
||||
}
|
||||
contextBuilder.in(getRepository());
|
||||
return contextBuilder;
|
||||
}
|
||||
|
||||
default void createDirectories(Path targetFile) throws IOException {
|
||||
try {
|
||||
Files.createDirectories(targetFile.getParent());
|
||||
} catch (FileAlreadyExistsException e) {
|
||||
throw alreadyExists(createFileContext(targetFile.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
File getWorkDir();
|
||||
|
||||
Repository getRepository();
|
||||
|
||||
String getBranch();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package sonia.scm.repository.util;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.user.User;
|
||||
|
||||
public class AuthorUtil {
|
||||
|
||||
public static void setAuthorIfNotAvailable(CommandWithAuthor request) {
|
||||
if (request.getAuthor() == null) {
|
||||
request.setAuthor(createAuthorFromSubject());
|
||||
}
|
||||
}
|
||||
|
||||
private static Person createAuthorFromSubject() {
|
||||
Subject subject = SecurityUtils.getSubject();
|
||||
User user = subject.getPrincipals().oneByType(User.class);
|
||||
String name = user.getDisplayName();
|
||||
String email = user.getMail();
|
||||
return new Person(name, email);
|
||||
}
|
||||
|
||||
public interface CommandWithAuthor {
|
||||
Person getAuthor();
|
||||
|
||||
void setAuthor(Person person);
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,10 @@ public abstract class SimpleWorkdirFactory<R, C> implements WorkdirFactory<R, C>
|
||||
}
|
||||
|
||||
@Override
|
||||
public WorkingCopy<R> createWorkingCopy(C context) {
|
||||
public WorkingCopy<R> createWorkingCopy(C context, String initialBranch) {
|
||||
try {
|
||||
File directory = workdirProvider.createNewWorkdir();
|
||||
ParentAndClone<R> parentAndClone = cloneRepository(context, directory);
|
||||
ParentAndClone<R> parentAndClone = cloneRepository(context, directory, initialBranch);
|
||||
return new WorkingCopy<>(parentAndClone.getClone(), parentAndClone.getParent(), this::close, directory);
|
||||
} catch (IOException e) {
|
||||
throw new InternalRepositoryException(getScmRepository(context), "could not clone repository in temporary directory", e);
|
||||
@@ -35,7 +35,7 @@ public abstract class SimpleWorkdirFactory<R, C> implements WorkdirFactory<R, C>
|
||||
// We do allow implementations to throw arbitrary exceptions here, so that we can handle them in close
|
||||
protected abstract void closeRepository(R repository) throws Exception;
|
||||
|
||||
protected abstract ParentAndClone<R> cloneRepository(C context, File target) throws IOException;
|
||||
protected abstract ParentAndClone<R> cloneRepository(C context, File target, String initialBranch) throws IOException;
|
||||
|
||||
private void close(R repository) {
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package sonia.scm.repository.util;
|
||||
|
||||
public interface WorkdirFactory<R, C> {
|
||||
WorkingCopy<R> createWorkingCopy(C context);
|
||||
WorkingCopy<R> createWorkingCopy(C context, String initialBranch);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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;
|
||||
@@ -85,15 +84,6 @@ class ModifyCommandBuilderTest {
|
||||
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);
|
||||
|
||||
@@ -26,6 +26,8 @@ public class SimpleWorkdirFactoryTest {
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
private SimpleWorkdirFactory<Closeable, Context> simpleWorkdirFactory;
|
||||
|
||||
private String initialBranchForLastCloneCall;
|
||||
|
||||
@Before
|
||||
public void initFactory() throws IOException {
|
||||
WorkdirProvider workdirProvider = new WorkdirProvider(temporaryFolder.newFolder());
|
||||
@@ -41,7 +43,8 @@ public class SimpleWorkdirFactoryTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ParentAndClone<Closeable> cloneRepository(Context context, File target) {
|
||||
protected ParentAndClone<Closeable> cloneRepository(Context context, File target, String initialBranch) {
|
||||
initialBranchForLastCloneCall = initialBranch;
|
||||
return new ParentAndClone<>(parent, clone);
|
||||
}
|
||||
};
|
||||
@@ -50,7 +53,7 @@ public class SimpleWorkdirFactoryTest {
|
||||
@Test
|
||||
public void shouldCreateParentAndClone() {
|
||||
Context context = new Context();
|
||||
try (WorkingCopy<Closeable> workingCopy = simpleWorkdirFactory.createWorkingCopy(context)) {
|
||||
try (WorkingCopy<Closeable> workingCopy = simpleWorkdirFactory.createWorkingCopy(context, null)) {
|
||||
assertThat(workingCopy.getCentralRepository()).isSameAs(parent);
|
||||
assertThat(workingCopy.getWorkingRepository()).isSameAs(clone);
|
||||
}
|
||||
@@ -59,7 +62,7 @@ public class SimpleWorkdirFactoryTest {
|
||||
@Test
|
||||
public void shouldCloseParent() throws IOException {
|
||||
Context context = new Context();
|
||||
try (WorkingCopy<Closeable> workingCopy = simpleWorkdirFactory.createWorkingCopy(context)) {}
|
||||
try (WorkingCopy<Closeable> workingCopy = simpleWorkdirFactory.createWorkingCopy(context, null)) {}
|
||||
|
||||
verify(parent).close();
|
||||
}
|
||||
@@ -67,10 +70,18 @@ public class SimpleWorkdirFactoryTest {
|
||||
@Test
|
||||
public void shouldCloseClone() throws IOException {
|
||||
Context context = new Context();
|
||||
try (WorkingCopy<Closeable> workingCopy = simpleWorkdirFactory.createWorkingCopy(context)) {}
|
||||
try (WorkingCopy<Closeable> workingCopy = simpleWorkdirFactory.createWorkingCopy(context, null)) {}
|
||||
|
||||
verify(clone).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldPropagateInitialBranch() {
|
||||
Context context = new Context();
|
||||
try (WorkingCopy<Closeable> workingCopy = simpleWorkdirFactory.createWorkingCopy(context, "some")) {
|
||||
assertThat(initialBranchForLastCloneCall).isEqualTo("some");
|
||||
}
|
||||
}
|
||||
|
||||
private static class Context {}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,10 @@ import com.google.common.collect.Multimap;
|
||||
import org.eclipse.jgit.api.FetchCommand;
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.attributes.Attribute;
|
||||
import org.eclipse.jgit.attributes.Attributes;
|
||||
import org.eclipse.jgit.diff.DiffFormatter;
|
||||
import org.eclipse.jgit.lfs.LfsPointer;
|
||||
import org.eclipse.jgit.lib.AnyObjectId;
|
||||
import org.eclipse.jgit.lib.Constants;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
@@ -55,6 +58,7 @@ import org.eclipse.jgit.transport.FetchResult;
|
||||
import org.eclipse.jgit.transport.RefSpec;
|
||||
import org.eclipse.jgit.treewalk.TreeWalk;
|
||||
import org.eclipse.jgit.util.FS;
|
||||
import org.eclipse.jgit.util.LfsFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.ContextEntry;
|
||||
@@ -65,10 +69,12 @@ import sonia.scm.web.GitUserAgentProvider;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
@@ -79,7 +85,7 @@ import static java.util.Optional.of;
|
||||
*/
|
||||
public final class GitUtil
|
||||
{
|
||||
|
||||
|
||||
private static final GitUserAgentProvider GIT_USER_AGENT_PROVIDER = new GitUserAgentProvider();
|
||||
|
||||
/** Field description */
|
||||
@@ -325,14 +331,14 @@ public final class GitUtil
|
||||
|
||||
return branch;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the provided reference name is a branch name.
|
||||
*
|
||||
*
|
||||
* @param refName reference name
|
||||
*
|
||||
*
|
||||
* @return {@code true} if the name is a branch name
|
||||
*
|
||||
*
|
||||
* @since 1.50
|
||||
*/
|
||||
public static boolean isBranch(String refName)
|
||||
@@ -611,11 +617,11 @@ public final class GitUtil
|
||||
|
||||
/**
|
||||
* Returns the name of the tag or {@code null} if the the ref is not a tag.
|
||||
*
|
||||
*
|
||||
* @param refName ref name
|
||||
*
|
||||
*
|
||||
* @return name of tag or {@link null}
|
||||
*
|
||||
*
|
||||
* @since 1.50
|
||||
*/
|
||||
public static String getTagName(String refName)
|
||||
@@ -688,7 +694,7 @@ public final class GitUtil
|
||||
{
|
||||
//J-
|
||||
return fs.resolve(dir, DIRECTORY_OBJETCS).exists()
|
||||
&& fs.resolve(dir, DIRECTORY_REFS).exists()
|
||||
&& fs.resolve(dir, DIRECTORY_REFS).exists()
|
||||
&&!fs.resolve(dir, DIRECTORY_DOTGIT).exists();
|
||||
//J+
|
||||
}
|
||||
@@ -727,7 +733,26 @@ public final class GitUtil
|
||||
mergeBaseWalk.setRevFilter(RevFilter.MERGE_BASE);
|
||||
mergeBaseWalk.markStart(mergeBaseWalk.lookupCommit(revision1));
|
||||
mergeBaseWalk.markStart(mergeBaseWalk.parseCommit(revision2));
|
||||
return mergeBaseWalk.next().getId();
|
||||
RevCommit ancestor = mergeBaseWalk.next();
|
||||
if (ancestor == null) {
|
||||
String msg = "revisions %s and %s are not related and therefore do not have a common ancestor";
|
||||
throw new NoCommonHistoryException(String.format(msg, revision1.name(), revision2.name()));
|
||||
}
|
||||
return ancestor.getId();
|
||||
}
|
||||
}
|
||||
|
||||
public static Optional<LfsPointer> getLfsPointer(org.eclipse.jgit.lib.Repository repo, String path, RevCommit commit, TreeWalk treeWalk) throws IOException {
|
||||
Attributes attributes = LfsFactory.getAttributesForPath(repo, path, commit);
|
||||
|
||||
Attribute filter = attributes.get("filter");
|
||||
if (filter != null && "lfs".equals(filter.getValue())) {
|
||||
ObjectId blobId = treeWalk.getObjectId(0);
|
||||
try (InputStream is = repo.open(blobId, Constants.OBJ_BLOB).openStream()) {
|
||||
return of(LfsPointer.parseLfsPointer(is));
|
||||
}
|
||||
} else {
|
||||
return empty();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -140,8 +140,8 @@ class AbstractGitCommand
|
||||
}
|
||||
}
|
||||
|
||||
<R, W extends GitCloneWorker<R>> R inClone(Function<Git, W> workerSupplier, GitWorkdirFactory workdirFactory) {
|
||||
try (WorkingCopy<Repository> workingCopy = workdirFactory.createWorkingCopy(context)) {
|
||||
<R, W extends GitCloneWorker<R>> R inClone(Function<Git, W> workerSupplier, GitWorkdirFactory workdirFactory, String initialBranch) {
|
||||
try (WorkingCopy<Repository> workingCopy = workdirFactory.createWorkingCopy(context, initialBranch)) {
|
||||
Repository repository = workingCopy.getWorkingRepository();
|
||||
logger.debug("cloned repository to folder {}", repository.getWorkTree());
|
||||
return workerSupplier.apply(new Git(repository)).run();
|
||||
|
||||
@@ -55,7 +55,7 @@ final class Differ implements AutoCloseable {
|
||||
if (!Strings.isNullOrEmpty(request.getAncestorChangeset()))
|
||||
{
|
||||
ObjectId otherRevision = repository.resolve(request.getAncestorChangeset());
|
||||
ObjectId ancestorId = computeCommonAncestor(repository, revision, otherRevision);
|
||||
ObjectId ancestorId = GitUtil.computeCommonAncestor(repository, revision, otherRevision);
|
||||
RevTree tree = walk.parseCommit(ancestorId).getTree();
|
||||
treeWalk.addTree(tree);
|
||||
}
|
||||
@@ -82,10 +82,6 @@ final class Differ implements AutoCloseable {
|
||||
return new Differ(commit, walk, treeWalk);
|
||||
}
|
||||
|
||||
private static ObjectId computeCommonAncestor(org.eclipse.jgit.lib.Repository repository, ObjectId revision1, ObjectId revision2) throws IOException {
|
||||
return GitUtil.computeCommonAncestor(repository, revision1, revision2);
|
||||
}
|
||||
|
||||
private Diff diff() throws IOException {
|
||||
List<DiffEntry> entries = DiffEntry.scan(treeWalk);
|
||||
return new Diff(commit, entries);
|
||||
@@ -115,4 +111,5 @@ final class Differ implements AutoCloseable {
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -58,11 +58,8 @@ public class GitBranchCommand extends AbstractGitCommand implements BranchComman
|
||||
|
||||
@Override
|
||||
public Branch branch(BranchRequest request) {
|
||||
try (WorkingCopy<org.eclipse.jgit.lib.Repository> workingCopy = workdirFactory.createWorkingCopy(context)) {
|
||||
try (WorkingCopy<org.eclipse.jgit.lib.Repository> workingCopy = workdirFactory.createWorkingCopy(context, request.getParentBranch())) {
|
||||
Git clone = new Git(workingCopy.getWorkingRepository());
|
||||
if (request.getParentBranch() != null) {
|
||||
clone.checkout().setName("origin/" + request.getParentBranch()).call();
|
||||
}
|
||||
Ref ref = clone.branchCreate().setName(request.getNewBranch()).call();
|
||||
Iterable<PushResult> call = clone.push().add(request.getNewBranch()).call();
|
||||
StreamSupport.stream(call.spliterator(), false)
|
||||
|
||||
@@ -38,6 +38,7 @@ package sonia.scm.repository.spi;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import org.eclipse.jgit.lfs.LfsPointer;
|
||||
import org.eclipse.jgit.lib.Constants;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.ObjectLoader;
|
||||
@@ -57,13 +58,17 @@ import sonia.scm.repository.GitSubModuleParser;
|
||||
import sonia.scm.repository.GitUtil;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.SubRepository;
|
||||
import sonia.scm.store.Blob;
|
||||
import sonia.scm.store.BlobStore;
|
||||
import sonia.scm.util.Util;
|
||||
import sonia.scm.web.lfs.LfsBlobStoreFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
import static sonia.scm.NotFoundException.notFound;
|
||||
@@ -86,18 +91,20 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(GitBrowseCommand.class);
|
||||
private final LfsBlobStoreFactory lfsBlobStoreFactory;
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
* @param context
|
||||
* @param context
|
||||
* @param repository
|
||||
* @param lfsBlobStoreFactory
|
||||
*/
|
||||
public GitBrowseCommand(GitContext context, Repository repository)
|
||||
public GitBrowseCommand(GitContext context, Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory)
|
||||
{
|
||||
super(context, repository);
|
||||
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
@@ -167,7 +174,7 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
* @throws IOException
|
||||
*/
|
||||
private FileObject createFileObject(org.eclipse.jgit.lib.Repository repo,
|
||||
BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk)
|
||||
BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk)
|
||||
throws IOException {
|
||||
|
||||
FileObject file = new FileObject();
|
||||
@@ -195,7 +202,6 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
ObjectLoader loader = repo.open(treeWalk.getObjectId(0));
|
||||
|
||||
file.setDirectory(loader.getType() == Constants.OBJ_TREE);
|
||||
file.setLength(loader.getSize());
|
||||
|
||||
// don't show message and date for directories to improve performance
|
||||
if (!file.isDirectory() &&!request.isDisableLastCommit())
|
||||
@@ -203,6 +209,16 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
logger.trace("fetch last commit for {} at {}", path, revId.getName());
|
||||
RevCommit commit = getLatestCommit(repo, revId, path);
|
||||
|
||||
Optional<LfsPointer> lfsPointer = GitUtil.getLfsPointer(repo, path, commit, treeWalk);
|
||||
|
||||
if (lfsPointer.isPresent()) {
|
||||
BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
|
||||
Blob blob = lfsBlobStore.get(lfsPointer.get().getOid().getName());
|
||||
file.setLength(blob.getSize());
|
||||
} else {
|
||||
file.setLength(loader.getSize());
|
||||
}
|
||||
|
||||
if (commit != null)
|
||||
{
|
||||
file.setLastModified(GitUtil.getCommitTime(commit));
|
||||
@@ -232,7 +248,7 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
* @return
|
||||
*/
|
||||
private RevCommit getLatestCommit(org.eclipse.jgit.lib.Repository repo,
|
||||
ObjectId revId, String path)
|
||||
ObjectId revId, String path)
|
||||
{
|
||||
RevCommit result = null;
|
||||
RevWalk walk = null;
|
||||
@@ -339,7 +355,7 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
}
|
||||
|
||||
private FileObject findFirstMatch(org.eclipse.jgit.lib.Repository repo,
|
||||
BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException {
|
||||
BrowseCommandRequest request, ObjectId revId, TreeWalk treeWalk) throws IOException {
|
||||
String[] pathElements = request.getPath().split("/");
|
||||
int currentDepth = 0;
|
||||
int limit = pathElements.length;
|
||||
@@ -364,7 +380,7 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String,
|
||||
SubRepository> getSubRepositories(org.eclipse.jgit.lib.Repository repo,
|
||||
ObjectId revision)
|
||||
ObjectId revision)
|
||||
throws IOException {
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
@@ -375,7 +391,7 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
Map<String, SubRepository> subRepositories;
|
||||
try ( ByteArrayOutputStream baos = new ByteArrayOutputStream() )
|
||||
{
|
||||
new GitCatCommand(context, repository).getContent(repo, revision,
|
||||
new GitCatCommand(context, repository, lfsBlobStoreFactory).getContent(repo, revision,
|
||||
PATH_MODULES, baos);
|
||||
subRepositories = GitSubModuleParser.parse(baos.toString());
|
||||
}
|
||||
@@ -389,7 +405,7 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
}
|
||||
|
||||
private SubRepository getSubRepository(org.eclipse.jgit.lib.Repository repo,
|
||||
ObjectId revId, String path)
|
||||
ObjectId revId, String path)
|
||||
throws IOException {
|
||||
Map<String, SubRepository> subRepositories = subrepositoryCache.get(revId);
|
||||
|
||||
@@ -410,7 +426,7 @@ public class GitBrowseCommand extends AbstractGitCommand
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
|
||||
/** sub repository cache */
|
||||
private final Map<ObjectId, Map<String, SubRepository>> subrepositoryCache = Maps.newHashMap();
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.eclipse.jgit.errors.MissingObjectException;
|
||||
import org.eclipse.jgit.lfs.LfsPointer;
|
||||
import org.eclipse.jgit.lib.Constants;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.ObjectLoader;
|
||||
@@ -45,13 +46,18 @@ import org.eclipse.jgit.treewalk.filter.PathFilter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.GitUtil;
|
||||
import sonia.scm.store.Blob;
|
||||
import sonia.scm.store.BlobStore;
|
||||
import sonia.scm.util.IOUtil;
|
||||
import sonia.scm.util.Util;
|
||||
import sonia.scm.web.lfs.LfsBlobStoreFactory;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Optional;
|
||||
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
import static sonia.scm.NotFoundException.notFound;
|
||||
@@ -61,15 +67,18 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GitCatCommand.class);
|
||||
|
||||
public GitCatCommand(GitContext context, sonia.scm.repository.Repository repository) {
|
||||
private final LfsBlobStoreFactory lfsBlobStoreFactory;
|
||||
|
||||
public GitCatCommand(GitContext context, sonia.scm.repository.Repository repository, LfsBlobStoreFactory lfsBlobStoreFactory) {
|
||||
super(context, repository);
|
||||
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getCatResult(CatCommandRequest request, OutputStream output) throws IOException {
|
||||
logger.debug("try to read content for {}", request);
|
||||
try (ClosableObjectLoaderContainer closableObjectLoaderContainer = getLoader(request)) {
|
||||
closableObjectLoaderContainer.objectLoader.copyTo(output);
|
||||
try (Loader closableObjectLoaderContainer = getLoader(request)) {
|
||||
closableObjectLoaderContainer.copyTo(output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,18 +89,18 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
|
||||
}
|
||||
|
||||
void getContent(org.eclipse.jgit.lib.Repository repo, ObjectId revId, String path, OutputStream output) throws IOException {
|
||||
try (ClosableObjectLoaderContainer closableObjectLoaderContainer = getLoader(repo, revId, path)) {
|
||||
closableObjectLoaderContainer.objectLoader.copyTo(output);
|
||||
try (Loader closableObjectLoaderContainer = getLoader(repo, revId, path)) {
|
||||
closableObjectLoaderContainer.copyTo(output);
|
||||
}
|
||||
}
|
||||
|
||||
private ClosableObjectLoaderContainer getLoader(CatCommandRequest request) throws IOException {
|
||||
private Loader getLoader(CatCommandRequest request) throws IOException {
|
||||
org.eclipse.jgit.lib.Repository repo = open();
|
||||
ObjectId revId = getCommitOrDefault(repo, request.getRevision());
|
||||
return getLoader(repo, revId, request.getPath());
|
||||
}
|
||||
|
||||
private ClosableObjectLoaderContainer getLoader(Repository repo, ObjectId revId, String path) throws IOException {
|
||||
private Loader getLoader(Repository repo, ObjectId revId, String path) throws IOException {
|
||||
TreeWalk treeWalk = new TreeWalk(repo);
|
||||
treeWalk.setRecursive(Util.nonNull(path).contains("/"));
|
||||
|
||||
@@ -116,21 +125,67 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
|
||||
treeWalk.setFilter(PathFilter.create(path));
|
||||
|
||||
if (treeWalk.next() && treeWalk.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) {
|
||||
ObjectId blobId = treeWalk.getObjectId(0);
|
||||
ObjectLoader loader = repo.open(blobId);
|
||||
|
||||
return new ClosableObjectLoaderContainer(loader, treeWalk, revWalk);
|
||||
Optional<LfsPointer> lfsPointer = GitUtil.getLfsPointer(repo, path, entry, treeWalk);
|
||||
if (lfsPointer.isPresent()) {
|
||||
return loadFromLfsStore(treeWalk, revWalk, lfsPointer.get());
|
||||
} else {
|
||||
return loadFromGit(repo, treeWalk, revWalk);
|
||||
}
|
||||
} else {
|
||||
throw notFound(entity("Path", path).in("Revision", revId.getName()).in(repository));
|
||||
}
|
||||
}
|
||||
|
||||
private static class ClosableObjectLoaderContainer implements Closeable {
|
||||
private Loader loadFromGit(Repository repo, TreeWalk treeWalk, RevWalk revWalk) throws IOException {
|
||||
ObjectId blobId = treeWalk.getObjectId(0);
|
||||
ObjectLoader loader = repo.open(blobId);
|
||||
|
||||
return new GitObjectLoaderWrapper(loader, treeWalk, revWalk);
|
||||
}
|
||||
|
||||
private Loader loadFromLfsStore(TreeWalk treeWalk, RevWalk revWalk, LfsPointer lfsPointer) throws IOException {
|
||||
BlobStore lfsBlobStore = lfsBlobStoreFactory.getLfsBlobStore(repository);
|
||||
Blob blob = lfsBlobStore.get(lfsPointer.getOid().getName());
|
||||
GitUtil.release(revWalk);
|
||||
GitUtil.release(treeWalk);
|
||||
return new BlobLoader(blob);
|
||||
}
|
||||
|
||||
private interface Loader extends Closeable {
|
||||
void copyTo(OutputStream output) throws IOException;
|
||||
|
||||
InputStream openStream() throws IOException;
|
||||
}
|
||||
|
||||
private static class BlobLoader implements Loader {
|
||||
private final InputStream inputStream;
|
||||
|
||||
private BlobLoader(Blob blob) throws IOException {
|
||||
this.inputStream = blob.getInputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyTo(OutputStream output) throws IOException {
|
||||
IOUtil.copy(inputStream, output);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream openStream() {
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
this.inputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static class GitObjectLoaderWrapper implements Loader {
|
||||
private final ObjectLoader objectLoader;
|
||||
private final TreeWalk treeWalk;
|
||||
private final RevWalk revWalk;
|
||||
|
||||
private ClosableObjectLoaderContainer(ObjectLoader objectLoader, TreeWalk treeWalk, RevWalk revWalk) {
|
||||
private GitObjectLoaderWrapper(ObjectLoader objectLoader, TreeWalk treeWalk, RevWalk revWalk) {
|
||||
this.objectLoader = objectLoader;
|
||||
this.treeWalk = treeWalk;
|
||||
this.revWalk = revWalk;
|
||||
@@ -141,14 +196,22 @@ public class GitCatCommand extends AbstractGitCommand implements CatCommand {
|
||||
GitUtil.release(revWalk);
|
||||
GitUtil.release(treeWalk);
|
||||
}
|
||||
|
||||
public void copyTo(OutputStream output) throws IOException {
|
||||
this.objectLoader.copyTo(output);
|
||||
}
|
||||
|
||||
public InputStream openStream() throws IOException {
|
||||
return objectLoader.openStream();
|
||||
}
|
||||
}
|
||||
|
||||
private static class InputStreamWrapper extends FilterInputStream {
|
||||
|
||||
private final ClosableObjectLoaderContainer container;
|
||||
private final Loader container;
|
||||
|
||||
private InputStreamWrapper(ClosableObjectLoaderContainer container) throws IOException {
|
||||
super(container.objectLoader.openStream());
|
||||
private InputStreamWrapper(Loader container) throws IOException {
|
||||
super(container.openStream());
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,15 +31,12 @@
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import org.eclipse.jgit.diff.DiffEntry;
|
||||
import org.eclipse.jgit.diff.DiffFormatter;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.DiffCommandBuilder;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -52,22 +49,25 @@ public class GitDiffCommand extends AbstractGitCommand implements DiffCommand {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getDiffResult(DiffCommandRequest request, OutputStream output) throws IOException {
|
||||
public DiffCommandBuilder.OutputStreamConsumer getDiffResult(DiffCommandRequest request) throws IOException {
|
||||
@SuppressWarnings("squid:S2095") // repository will be closed with the RepositoryService
|
||||
org.eclipse.jgit.lib.Repository repository = open();
|
||||
try (DiffFormatter formatter = new DiffFormatter(new BufferedOutputStream(output))) {
|
||||
formatter.setRepository(repository);
|
||||
|
||||
Differ.Diff diff = Differ.diff(repository, request);
|
||||
Differ.Diff diff = Differ.diff(repository, request);
|
||||
|
||||
for (DiffEntry e : diff.getEntries()) {
|
||||
if (!e.getOldId().equals(e.getNewId())) {
|
||||
formatter.format(e);
|
||||
return output -> {
|
||||
try (DiffFormatter formatter = new DiffFormatter(output)) {
|
||||
formatter.setRepository(repository);
|
||||
|
||||
for (DiffEntry e : diff.getEntries()) {
|
||||
if (!e.getOldId().equals(e.getNewId())) {
|
||||
formatter.format(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formatter.flush();
|
||||
}
|
||||
formatter.flush();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import com.google.common.io.ByteStreams;
|
||||
import org.eclipse.jgit.attributes.FilterCommand;
|
||||
import org.eclipse.jgit.attributes.FilterCommandRegistry;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.util.FS;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.plugin.Extension;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.ServletContextEvent;
|
||||
import javax.servlet.ServletContextListener;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static java.nio.file.StandardOpenOption.CREATE;
|
||||
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
|
||||
|
||||
@Extension
|
||||
public class GitLfsFilterContextListener implements ServletContextListener {
|
||||
|
||||
public static final String GITCONFIG = "[filter \"lfs\"]\n" +
|
||||
"clean = git-lfs clean -- %f\n" +
|
||||
"smudge = git-lfs smudge -- %f\n" +
|
||||
"process = git-lfs filter-process\n" +
|
||||
"required = true\n";
|
||||
public static final Pattern COMMAND_NAME_PATTERN = Pattern.compile("git-lfs (smudge|clean) -- .*");
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GitLfsFilterContextListener.class);
|
||||
|
||||
private final SCMContextProvider contextProvider;
|
||||
|
||||
@Inject
|
||||
public GitLfsFilterContextListener(SCMContextProvider contextProvider) {
|
||||
this.contextProvider = contextProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void contextInitialized(ServletContextEvent sce) {
|
||||
Path gitconfig = contextProvider.getBaseDirectory().toPath().resolve("gitconfig");
|
||||
try {
|
||||
Files.write(gitconfig, GITCONFIG.getBytes(Charset.defaultCharset()), TRUNCATE_EXISTING, CREATE);
|
||||
FS.DETECTED.setGitSystemConfig(gitconfig.toFile());
|
||||
LOG.info("wrote git config file: {}", gitconfig);
|
||||
} catch (IOException e) {
|
||||
LOG.error("could not write git config in path {}; git lfs support may not work correctly", gitconfig, e);
|
||||
}
|
||||
FilterCommandRegistry.register(COMMAND_NAME_PATTERN, NoOpFilterCommand::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void contextDestroyed(ServletContextEvent sce) {
|
||||
FilterCommandRegistry.unregister(COMMAND_NAME_PATTERN);
|
||||
}
|
||||
|
||||
private static class NoOpFilterCommand extends FilterCommand {
|
||||
NoOpFilterCommand(Repository db, InputStream in, OutputStream out) {
|
||||
super(in, out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int run() throws IOException {
|
||||
ByteStreams.copy(in, out);
|
||||
in.close();
|
||||
out.close();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
|
||||
|
||||
@Override
|
||||
public MergeCommandResult merge(MergeCommandRequest request) {
|
||||
return inClone(clone -> new MergeWorker(clone, request), workdirFactory);
|
||||
return inClone(clone -> new MergeWorker(clone, request), workdirFactory, request.getTargetBranch());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -72,7 +72,6 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
|
||||
|
||||
@Override
|
||||
MergeCommandResult run() throws IOException {
|
||||
checkOutTargetBranch();
|
||||
MergeResult result = doMergeInClone();
|
||||
if (result.getMergeStatus().isSuccessful()) {
|
||||
doCommit();
|
||||
@@ -83,10 +82,6 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
|
||||
}
|
||||
}
|
||||
|
||||
private void checkOutTargetBranch() throws IOException {
|
||||
checkOutBranch(target);
|
||||
}
|
||||
|
||||
private MergeResult doMergeInClone() throws IOException {
|
||||
MergeResult result;
|
||||
try {
|
||||
|
||||
@@ -1,47 +1,46 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import com.google.common.util.concurrent.Striped;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.lib.Constants;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.attributes.FilterCommandRegistry;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import sonia.scm.BadRequestException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.ConcurrentModificationException;
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.NoChangesMadeException;
|
||||
import sonia.scm.repository.GitWorkdirFactory;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.web.lfs.LfsBlobStoreFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
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;
|
||||
import static sonia.scm.NotFoundException.notFound;
|
||||
import static sonia.scm.ScmConstraintViolationException.Builder.doThrow;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
|
||||
public class GitModifyCommand extends AbstractGitCommand implements ModifyCommand {
|
||||
|
||||
private final GitWorkdirFactory workdirFactory;
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GitModifyCommand.class);
|
||||
private static final Striped<Lock> REGISTER_LOCKS = Striped.lock(5);
|
||||
|
||||
GitModifyCommand(GitContext context, Repository repository, GitWorkdirFactory workdirFactory) {
|
||||
private final GitWorkdirFactory workdirFactory;
|
||||
private final LfsBlobStoreFactory lfsBlobStoreFactory;
|
||||
|
||||
GitModifyCommand(GitContext context, Repository repository, GitWorkdirFactory workdirFactory, LfsBlobStoreFactory lfsBlobStoreFactory) {
|
||||
super(context, repository);
|
||||
this.workdirFactory = workdirFactory;
|
||||
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(ModifyCommandRequest request) {
|
||||
return inClone(clone -> new ModifyWorker(clone, request), workdirFactory);
|
||||
return inClone(clone -> new ModifyWorker(clone, request), workdirFactory, request.getBranch());
|
||||
}
|
||||
|
||||
private class ModifyWorker extends GitCloneWorker<String> implements Worker {
|
||||
private class ModifyWorker extends GitCloneWorker<String> implements ModifyWorkerHelper {
|
||||
|
||||
private final File workDir;
|
||||
private final ModifyCommandRequest request;
|
||||
@@ -54,58 +53,43 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman
|
||||
|
||||
@Override
|
||||
String run() throws IOException {
|
||||
if (!StringUtils.isEmpty(request.getBranch())) {
|
||||
checkOutBranch(request.getBranch());
|
||||
}
|
||||
Ref head = getClone().getRepository().exactRef(Constants.HEAD);
|
||||
doThrow().violation("branch has to be a valid branch, no revision", "branch", request.getBranch()).when(head == null || !head.isSymbolic());
|
||||
getClone().getRepository().getFullBranch();
|
||||
if (!StringUtils.isEmpty(request.getExpectedRevision())) {
|
||||
if (!request.getExpectedRevision().equals(getCurrentRevision().getName())) {
|
||||
throw new ConcurrentModificationException("branch", request.getBranch() == null? "default": request.getBranch());
|
||||
}
|
||||
if (!StringUtils.isEmpty(request.getExpectedRevision())
|
||||
&& !request.getExpectedRevision().equals(getCurrentRevision().getName())) {
|
||||
throw new ConcurrentModificationException("branch", request.getBranch() == null ? "default" : request.getBranch());
|
||||
}
|
||||
for (ModifyCommandRequest.PartialRequest r : request.getRequests()) {
|
||||
r.execute(this);
|
||||
}
|
||||
failIfNotChanged(NoChangesMadeException::new);
|
||||
failIfNotChanged(() -> new NoChangesMadeException(repository, ModifyWorker.this.request.getBranch()));
|
||||
Optional<RevCommit> revCommit = doCommit(request.getCommitMessage(), request.getAuthor());
|
||||
push();
|
||||
return revCommit.orElseThrow(NoChangesMadeException::new).name();
|
||||
return revCommit.orElseThrow(() -> new NoChangesMadeException(repository, ModifyWorker.this.request.getBranch())).name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create(String toBeCreated, File file, boolean overwrite) throws IOException {
|
||||
Path targetFile = new File(workDir, toBeCreated).toPath();
|
||||
createDirectories(targetFile);
|
||||
if (overwrite) {
|
||||
Files.move(file.toPath(), targetFile, REPLACE_EXISTING);
|
||||
} else {
|
||||
public void addFileToScm(String name, Path file) {
|
||||
addToGitWithLfsSupport(name, file);
|
||||
}
|
||||
|
||||
private void addToGitWithLfsSupport(String path, Path targetFile) {
|
||||
REGISTER_LOCKS.get(targetFile).lock();
|
||||
try {
|
||||
LfsBlobStoreCleanFilterFactory cleanFilterFactory = new LfsBlobStoreCleanFilterFactory(lfsBlobStoreFactory, repository, targetFile);
|
||||
|
||||
String registerKey = "git-lfs clean -- '" + path + "'";
|
||||
LOG.debug("register lfs filter command factory for command '{}'", registerKey);
|
||||
FilterCommandRegistry.register(registerKey, cleanFilterFactory::createFilter);
|
||||
try {
|
||||
Files.move(file.toPath(), targetFile);
|
||||
} catch (FileAlreadyExistsException e) {
|
||||
throw alreadyExists(createFileContext(toBeCreated));
|
||||
addFileToGit(path);
|
||||
} catch (GitAPIException e) {
|
||||
throwInternalRepositoryException("could not add file to index", e);
|
||||
} finally {
|
||||
LOG.debug("unregister lfs filter command factory for command \"{}\"", registerKey);
|
||||
FilterCommandRegistry.unregister(registerKey);
|
||||
}
|
||||
}
|
||||
try {
|
||||
addFileToGit(toBeCreated);
|
||||
} catch (GitAPIException e) {
|
||||
throwInternalRepositoryException("could not add new file to index", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void modify(String path, File file) throws IOException {
|
||||
Path targetFile = new File(workDir, path).toPath();
|
||||
createDirectories(targetFile);
|
||||
if (!targetFile.toFile().exists()) {
|
||||
throw notFound(createFileContext(path));
|
||||
}
|
||||
Files.move(file.toPath(), targetFile, REPLACE_EXISTING);
|
||||
try {
|
||||
addFileToGit(path);
|
||||
} catch (GitAPIException e) {
|
||||
throwInternalRepositoryException("could not add new file to index", e);
|
||||
} finally {
|
||||
REGISTER_LOCKS.get(targetFile).unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,13 +98,7 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String toBeDeleted) throws IOException {
|
||||
Path fileToBeDeleted = new File(workDir, toBeDeleted).toPath();
|
||||
try {
|
||||
Files.delete(fileToBeDeleted);
|
||||
} catch (NoSuchFileException e) {
|
||||
throw notFound(createFileContext(toBeDeleted));
|
||||
}
|
||||
public void doScmDelete(String toBeDeleted) {
|
||||
try {
|
||||
getClone().rm().addFilepattern(removeStartingPathSeparators(toBeDeleted)).call();
|
||||
} catch (GitAPIException e) {
|
||||
@@ -128,6 +106,21 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getWorkDir() {
|
||||
return workDir;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Repository getRepository() {
|
||||
return repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBranch() {
|
||||
return request.getBranch();
|
||||
}
|
||||
|
||||
private String removeStartingPathSeparators(String path) {
|
||||
while (path.startsWith(File.separator)) {
|
||||
path = path.substring(1);
|
||||
@@ -135,41 +128,8 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman
|
||||
return path;
|
||||
}
|
||||
|
||||
private void createDirectories(Path targetFile) throws IOException {
|
||||
try {
|
||||
Files.createDirectories(targetFile.getParent());
|
||||
} catch (FileAlreadyExistsException e) {
|
||||
throw alreadyExists(createFileContext(targetFile.toString()));
|
||||
}
|
||||
private String throwInternalRepositoryException(String message, Exception e) {
|
||||
throw new InternalRepositoryException(context.getRepository(), message, e);
|
||||
}
|
||||
|
||||
private ContextEntry.ContextBuilder createFileContext(String path) {
|
||||
ContextEntry.ContextBuilder contextBuilder = entity("file", path);
|
||||
if (!StringUtils.isEmpty(request.getBranch())) {
|
||||
contextBuilder.in("branch", request.getBranch());
|
||||
}
|
||||
contextBuilder.in(context.getRepository());
|
||||
return contextBuilder;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import sonia.scm.repository.Feature;
|
||||
import sonia.scm.repository.GitRepositoryHandler;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.Command;
|
||||
import sonia.scm.web.lfs.LfsBlobStoreFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.EnumSet;
|
||||
@@ -76,9 +77,10 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider) {
|
||||
public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory) {
|
||||
this.handler = handler;
|
||||
this.repository = repository;
|
||||
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
|
||||
this.context = new GitContext(handler.getDirectory(repository.getId()), repository, storeProvider);
|
||||
}
|
||||
|
||||
@@ -143,7 +145,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
|
||||
@Override
|
||||
public BrowseCommand getBrowseCommand()
|
||||
{
|
||||
return new GitBrowseCommand(context, repository);
|
||||
return new GitBrowseCommand(context, repository, lfsBlobStoreFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,7 +157,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
|
||||
@Override
|
||||
public CatCommand getCatCommand()
|
||||
{
|
||||
return new GitCatCommand(context, repository);
|
||||
return new GitCatCommand(context, repository, lfsBlobStoreFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -271,7 +273,7 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
|
||||
|
||||
@Override
|
||||
public ModifyCommand getModifyCommand() {
|
||||
return new GitModifyCommand(context, repository, handler.getWorkdirFactory());
|
||||
return new GitModifyCommand(context, repository, handler.getWorkdirFactory(), lfsBlobStoreFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -281,11 +283,13 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
private GitContext context;
|
||||
private final GitContext context;
|
||||
|
||||
/** Field description */
|
||||
private GitRepositoryHandler handler;
|
||||
private final GitRepositoryHandler handler;
|
||||
|
||||
/** Field description */
|
||||
private Repository repository;
|
||||
private final Repository repository;
|
||||
|
||||
private final LfsBlobStoreFactory lfsBlobStoreFactory;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.repository.GitRepositoryHandler;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.web.lfs.LfsBlobStoreFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -49,11 +50,13 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver {
|
||||
|
||||
private final GitRepositoryHandler handler;
|
||||
private final GitRepositoryConfigStoreProvider storeProvider;
|
||||
private final LfsBlobStoreFactory lfsBlobStoreFactory;
|
||||
|
||||
@Inject
|
||||
public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider) {
|
||||
public GitRepositoryServiceResolver(GitRepositoryHandler handler, GitRepositoryConfigStoreProvider storeProvider, LfsBlobStoreFactory lfsBlobStoreFactory) {
|
||||
this.handler = handler;
|
||||
this.storeProvider = storeProvider;
|
||||
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -61,7 +64,7 @@ public class GitRepositoryServiceResolver implements RepositoryServiceResolver {
|
||||
GitRepositoryServiceProvider provider = null;
|
||||
|
||||
if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) {
|
||||
provider = new GitRepositoryServiceProvider(handler, repository, storeProvider);
|
||||
provider = new GitRepositoryServiceProvider(handler, repository, storeProvider, lfsBlobStoreFactory);
|
||||
}
|
||||
|
||||
return provider;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import com.google.common.io.ByteStreams;
|
||||
import org.eclipse.jgit.attributes.FilterCommand;
|
||||
import org.eclipse.jgit.lfs.LfsPointer;
|
||||
import org.eclipse.jgit.lfs.lib.AnyLongObjectId;
|
||||
import org.eclipse.jgit.lfs.lib.Constants;
|
||||
import org.eclipse.jgit.lfs.lib.LongObjectId;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.store.Blob;
|
||||
import sonia.scm.store.BlobStore;
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.DigestOutputStream;
|
||||
|
||||
/**
|
||||
* Adapted version of JGit's {@link org.eclipse.jgit.lfs.CleanFilter} to write the
|
||||
* lfs file directly to the lfs blob store.
|
||||
*/
|
||||
class LfsBlobStoreCleanFilter extends FilterCommand {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(LfsBlobStoreCleanFilter.class);
|
||||
|
||||
private final BlobStore lfsBlobStore;
|
||||
private final Path targetFile;
|
||||
|
||||
LfsBlobStoreCleanFilter(InputStream in, OutputStream out, BlobStore lfsBlobStore, Path targetFile) {
|
||||
super(in, out);
|
||||
this.lfsBlobStore = lfsBlobStore;
|
||||
this.targetFile = targetFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
// Suppress warning for RuntimeException after check for wrong size, because mathematicians say this will never happen
|
||||
@SuppressWarnings("squid:S00112")
|
||||
public int run() throws IOException {
|
||||
LOG.debug("running scm lfs filter for file {}", targetFile);
|
||||
DigestOutputStream digestOutputStream = createDigestStream();
|
||||
try {
|
||||
long size = ByteStreams.copy(in, digestOutputStream);
|
||||
AnyLongObjectId loid = LongObjectId.fromRaw(digestOutputStream.getMessageDigest().digest());
|
||||
String hash = loid.getName();
|
||||
|
||||
Blob existingBlob = lfsBlobStore.get(hash);
|
||||
if (existingBlob != null) {
|
||||
LOG.debug("found existing lfs blob for oid {}", hash);
|
||||
long blobSize = existingBlob.getSize();
|
||||
if (blobSize != size) {
|
||||
throw new RuntimeException("lfs entry already exists for loid " + hash + " but has wrong size");
|
||||
}
|
||||
} else {
|
||||
LOG.debug("uploading new lfs blob for oid {}", hash);
|
||||
Blob newBlob = lfsBlobStore.create(hash);
|
||||
OutputStream outputStream = newBlob.getOutputStream();
|
||||
Files.copy(targetFile, outputStream);
|
||||
newBlob.commit();
|
||||
}
|
||||
|
||||
LfsPointer lfsPointer = new LfsPointer(loid, size);
|
||||
lfsPointer.encode(out);
|
||||
return -1;
|
||||
} finally {
|
||||
IOUtil.close(digestOutputStream);
|
||||
IOUtil.close(in);
|
||||
IOUtil.close(out);
|
||||
}
|
||||
}
|
||||
|
||||
private DigestOutputStream createDigestStream() {
|
||||
return new DigestOutputStream(new OutputStream() {
|
||||
@Override
|
||||
public void write(int b) {
|
||||
// no further target here, we are just interested in the digest
|
||||
}
|
||||
}, Constants.newMessageDigest());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import sonia.scm.web.lfs.LfsBlobStoreFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Path;
|
||||
|
||||
class LfsBlobStoreCleanFilterFactory {
|
||||
|
||||
private final LfsBlobStoreFactory blobStoreFactory;
|
||||
private final sonia.scm.repository.Repository repository;
|
||||
private final Path targetFile;
|
||||
|
||||
LfsBlobStoreCleanFilterFactory(LfsBlobStoreFactory blobStoreFactory, sonia.scm.repository.Repository repository, Path targetFile) {
|
||||
this.blobStoreFactory = blobStoreFactory;
|
||||
this.repository = repository;
|
||||
this.targetFile = targetFile;
|
||||
}
|
||||
|
||||
@SuppressWarnings("squid:S1172") // suppress unused parameter to keep the api compatible to jgit's FilterCommandFactory
|
||||
LfsBlobStoreCleanFilter createFilter(Repository db, InputStream in, OutputStream out) {
|
||||
return new LfsBlobStoreCleanFilter(in, out, blobStoreFactory.getLfsBlobStore(repository), targetFile);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package sonia.scm.repository.spi;
|
||||
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.lib.Constants;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.transport.ScmTransportProtocol;
|
||||
import sonia.scm.repository.GitWorkdirFactory;
|
||||
@@ -11,6 +13,10 @@ import sonia.scm.repository.util.WorkdirProvider;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
import static sonia.scm.NotFoundException.notFound;
|
||||
|
||||
public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory<Repository, GitContext> implements GitWorkdirFactory {
|
||||
|
||||
@@ -20,14 +26,23 @@ public class SimpleGitWorkdirFactory extends SimpleWorkdirFactory<Repository, Gi
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParentAndClone<Repository> cloneRepository(GitContext context, File target) {
|
||||
public ParentAndClone<Repository> cloneRepository(GitContext context, File target, String initialBranch) {
|
||||
try {
|
||||
return new ParentAndClone<>(null, Git.cloneRepository()
|
||||
Repository clone = Git.cloneRepository()
|
||||
.setURI(createScmTransportProtocolUri(context.getDirectory()))
|
||||
.setDirectory(target)
|
||||
.setBranch(initialBranch)
|
||||
.call()
|
||||
.getRepository());
|
||||
} catch (GitAPIException e) {
|
||||
.getRepository();
|
||||
|
||||
Ref head = clone.exactRef(Constants.HEAD);
|
||||
|
||||
if (head == null || !head.isSymbolic() || (initialBranch != null && !head.getTarget().getName().endsWith(initialBranch))) {
|
||||
throw notFound(entity("Branch", initialBranch).in(context.getRepository()));
|
||||
}
|
||||
|
||||
return new ParentAndClone<>(null, clone);
|
||||
} catch (GitAPIException | IOException e) {
|
||||
throw new InternalRepositoryException(context.getRepository(), "could not clone working copy of repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +171,6 @@ public class GitBrowseCommandTest extends AbstractGitCommandTestBase {
|
||||
}
|
||||
|
||||
private GitBrowseCommand createCommand() {
|
||||
return new GitBrowseCommand(createContext(), repository);
|
||||
return new GitBrowseCommand(createContext(), repository, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,12 +39,18 @@ import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.repository.GitRepositoryConfig;
|
||||
import sonia.scm.store.Blob;
|
||||
import sonia.scm.store.BlobStore;
|
||||
import sonia.scm.web.lfs.LfsBlobStoreFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link GitCatCommand}.
|
||||
@@ -136,7 +142,7 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase {
|
||||
CatCommandRequest request = new CatCommandRequest();
|
||||
request.setPath("b.txt");
|
||||
|
||||
InputStream catResultStream = new GitCatCommand(createContext(), repository).getCatResultStream(request);
|
||||
InputStream catResultStream = new GitCatCommand(createContext(), repository, null).getCatResultStream(request);
|
||||
|
||||
assertEquals('b', catResultStream.read());
|
||||
assertEquals('\n', catResultStream.read());
|
||||
@@ -145,13 +151,38 @@ public class GitCatCommandTest extends AbstractGitCommandTestBase {
|
||||
catResultStream.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLfsStream() throws IOException {
|
||||
LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class);
|
||||
BlobStore blobStore = mock(BlobStore.class);
|
||||
Blob blob = mock(Blob.class);
|
||||
when(lfsBlobStoreFactory.getLfsBlobStore(repository)).thenReturn(blobStore);
|
||||
when(blobStore.get("d2252bd9fde1bb2ae7531b432c48262c3cbe4df4376008986980de40a7c9cf8b"))
|
||||
.thenReturn(blob);
|
||||
when(blob.getInputStream()).thenReturn(new ByteArrayInputStream(new byte[]{'i', 's'}));
|
||||
|
||||
CatCommandRequest request = new CatCommandRequest();
|
||||
request.setRevision("lfs-test");
|
||||
request.setPath("lfs-image.png");
|
||||
|
||||
InputStream catResultStream = new GitCatCommand(createContext(), repository, lfsBlobStoreFactory)
|
||||
.getCatResultStream(request);
|
||||
|
||||
assertEquals('i', catResultStream.read());
|
||||
assertEquals('s', catResultStream.read());
|
||||
|
||||
assertEquals(-1, catResultStream.read());
|
||||
|
||||
catResultStream.close();
|
||||
}
|
||||
|
||||
private String execute(CatCommandRequest request) throws IOException {
|
||||
String content = null;
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
try
|
||||
{
|
||||
new GitCatCommand(createContext(), repository).getCatResult(request,
|
||||
new GitCatCommand(createContext(), repository, null).getCatResult(request,
|
||||
baos);
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -44,7 +44,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase {
|
||||
DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
|
||||
diffCommandRequest.setRevision("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
gitDiffCommand.getDiffResult(diffCommandRequest, output);
|
||||
gitDiffCommand.getDiffResult(diffCommandRequest).accept(output);
|
||||
assertEquals(DIFF_FILE_A + DIFF_FILE_B, output.toString());
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase {
|
||||
DiffCommandRequest diffCommandRequest = new DiffCommandRequest();
|
||||
diffCommandRequest.setRevision("test-branch");
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
gitDiffCommand.getDiffResult(diffCommandRequest, output);
|
||||
gitDiffCommand.getDiffResult(diffCommandRequest).accept(output);
|
||||
assertEquals(DIFF_FILE_A + DIFF_FILE_B, output.toString());
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase {
|
||||
diffCommandRequest.setRevision("test-branch");
|
||||
diffCommandRequest.setPath("a.txt");
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
gitDiffCommand.getDiffResult(diffCommandRequest, output);
|
||||
gitDiffCommand.getDiffResult(diffCommandRequest).accept(output);
|
||||
assertEquals(DIFF_FILE_A, output.toString());
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase {
|
||||
diffCommandRequest.setRevision("master");
|
||||
diffCommandRequest.setAncestorChangeset("test-branch");
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
gitDiffCommand.getDiffResult(diffCommandRequest, output);
|
||||
gitDiffCommand.getDiffResult(diffCommandRequest).accept(output);
|
||||
assertEquals(DIFF_FILE_A_MULTIPLE_REVISIONS + DIFF_FILE_F_MULTIPLE_REVISIONS, output.toString());
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ public class GitDiffCommandTest extends AbstractGitCommandTestBase {
|
||||
diffCommandRequest.setAncestorChangeset("test-branch");
|
||||
diffCommandRequest.setPath("a.txt");
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
gitDiffCommand.getDiffResult(diffCommandRequest, output);
|
||||
gitDiffCommand.getDiffResult(diffCommandRequest).accept(output);
|
||||
assertEquals(DIFF_FILE_A_MULTIPLE_REVISIONS, output.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,14 @@ import sonia.scm.NotFoundException;
|
||||
import sonia.scm.ScmConstraintViolationException;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.repository.util.WorkdirProvider;
|
||||
import sonia.scm.web.lfs.LfsBlobStoreFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret")
|
||||
public class GitModifyCommandTest extends AbstractGitCommandTestBase {
|
||||
@@ -37,6 +39,8 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase {
|
||||
@Rule
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
|
||||
private final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class);
|
||||
|
||||
@Test
|
||||
public void shouldCreateCommit() throws IOException, GitAPIException {
|
||||
File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile();
|
||||
@@ -263,8 +267,8 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase {
|
||||
command.execute(request);
|
||||
}
|
||||
|
||||
@Test(expected = ScmConstraintViolationException.class)
|
||||
public void shouldFailWithConstraintViolationIfBranchIsNoBranch() throws IOException {
|
||||
@Test(expected = NotFoundException.class)
|
||||
public void shouldFailWithNotFoundExceptionIfBranchIsNoBranch() throws IOException {
|
||||
File newFile = Files.write(temporaryFolder.newFile().toPath(), "irrelevant\n".getBytes()).toFile();
|
||||
|
||||
GitModifyCommand command = createCommand();
|
||||
@@ -296,7 +300,7 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase {
|
||||
}
|
||||
|
||||
private GitModifyCommand createCommand() {
|
||||
return new GitModifyCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider()));
|
||||
return new GitModifyCommand(createContext(), repository, new SimpleGitWorkdirFactory(new WorkdirProvider()), lfsBlobStoreFactory);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
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.revwalk.RevCommit;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.repository.util.WorkdirProvider;
|
||||
import sonia.scm.store.Blob;
|
||||
import sonia.scm.store.BlobStore;
|
||||
import sonia.scm.web.lfs.LfsBlobStoreFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
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 GitModifyCommand_LFSTest extends AbstractGitCommandTestBase {
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
@Rule
|
||||
public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule();
|
||||
@Rule
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
|
||||
private final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class);
|
||||
|
||||
@Before
|
||||
public void registerFilter() {
|
||||
new GitLfsFilterContextListener(contextProvider).contextInitialized(null);
|
||||
}
|
||||
|
||||
@After
|
||||
public void unregisterFilter() {
|
||||
new GitLfsFilterContextListener(contextProvider).contextDestroyed(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateCommit() throws IOException, GitAPIException {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
String newRef = createCommit("new_lfs.png", "new content", "fe32608c9ef5b6cf7e3f946480253ff76f24f4ec0678f3d0f07f9844cbff9601", outputStream);
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
assertThat(outputStream.toString()).isEqualTo("new content");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateSecondCommits() throws IOException, GitAPIException {
|
||||
createCommit("new_lfs.png", "new content", "fe32608c9ef5b6cf7e3f946480253ff76f24f4ec0678f3d0f07f9844cbff9601", new ByteArrayOutputStream());
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
String newRef = createCommit("more_lfs.png", "more content", "2c2316737c9313956dfc0083da3a2a62ce259f66484f3e26440f0d1b02dd4128", outputStream);
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
assertThat(outputStream.toString()).isEqualTo("more content");
|
||||
}
|
||||
|
||||
private String createCommit(String fileName, String content, String hashOfContent, ByteArrayOutputStream outputStream) throws IOException {
|
||||
BlobStore blobStore = mock(BlobStore.class);
|
||||
Blob blob = mock(Blob.class);
|
||||
when(lfsBlobStoreFactory.getLfsBlobStore(any())).thenReturn(blobStore);
|
||||
when(blobStore.create(hashOfContent)).thenReturn(blob);
|
||||
when(blobStore.get(hashOfContent)).thenReturn(null, blob);
|
||||
when(blob.getOutputStream()).thenReturn(outputStream);
|
||||
when(blob.getSize()).thenReturn((long) content.length());
|
||||
|
||||
File newFile = Files.write(temporaryFolder.newFile().toPath(), content.getBytes()).toFile();
|
||||
|
||||
GitModifyCommand command = createCommand();
|
||||
|
||||
ModifyCommandRequest request = new ModifyCommandRequest();
|
||||
request.setCommitMessage("test commit");
|
||||
request.addRequest(new ModifyCommandRequest.CreateFileRequest(fileName, newFile, false));
|
||||
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
|
||||
|
||||
return command.execute(request);
|
||||
}
|
||||
|
||||
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()), lfsBlobStoreFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getZippedRepositoryResource() {
|
||||
return "sonia/scm/repository/spi/scm-git-spi-lfs-test.zip";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
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.ScmConstraintViolationException;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.repository.util.WorkdirProvider;
|
||||
import sonia.scm.web.lfs.LfsBlobStoreFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret")
|
||||
public class GitModifyCommand_withEmptyRepositoryTest extends AbstractGitCommandTestBase {
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
@Rule
|
||||
public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule();
|
||||
@Rule
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
|
||||
private final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class);
|
||||
|
||||
@Test
|
||||
public void shouldCreateNewFileInEmptyRepository() throws IOException, GitAPIException {
|
||||
File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile();
|
||||
|
||||
GitModifyCommand command = createCommand();
|
||||
|
||||
ModifyCommandRequest request = new ModifyCommandRequest();
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getZippedRepositoryResource() {
|
||||
return "sonia/scm/repository/spi/scm-git-empty-repo.zip";
|
||||
}
|
||||
|
||||
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()), lfsBlobStoreFactory);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface TreeAssertions {
|
||||
void checkAssertions(CanonicalTreeParser treeParser) throws CorruptObjectException;
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,6 @@ import java.io.IOException;
|
||||
import static com.google.inject.util.Providers.of;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
|
||||
|
||||
@@ -43,11 +41,11 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyPoolShouldCreateNewWorkdir() throws IOException {
|
||||
public void emptyPoolShouldCreateNewWorkdir() {
|
||||
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider);
|
||||
File masterRepo = createRepositoryDirectory();
|
||||
|
||||
try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext())) {
|
||||
try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext(), null)) {
|
||||
|
||||
assertThat(workingCopy.getDirectory())
|
||||
.exists()
|
||||
@@ -61,25 +59,37 @@ public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cloneFromPoolShouldNotBeReused() throws IOException {
|
||||
public void shouldCheckoutInitialBranch() {
|
||||
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider);
|
||||
|
||||
try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext(), "test-branch")) {
|
||||
assertThat(new File(workingCopy.getWorkingRepository().getWorkTree(), "a.txt"))
|
||||
.exists()
|
||||
.isFile()
|
||||
.hasContent("a and b");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cloneFromPoolShouldNotBeReused() {
|
||||
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider);
|
||||
|
||||
File firstDirectory;
|
||||
try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext())) {
|
||||
try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext(), null)) {
|
||||
firstDirectory = workingCopy.getDirectory();
|
||||
}
|
||||
try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext())) {
|
||||
try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext(), null)) {
|
||||
File secondDirectory = workingCopy.getDirectory();
|
||||
assertThat(secondDirectory).isNotEqualTo(firstDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cloneFromPoolShouldBeDeletedOnClose() throws IOException {
|
||||
public void cloneFromPoolShouldBeDeletedOnClose() {
|
||||
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(workdirProvider);
|
||||
|
||||
File directory;
|
||||
try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext())) {
|
||||
try (WorkingCopy<Repository> workingCopy = factory.createWorkingCopy(createContext(), null)) {
|
||||
directory = workingCopy.getWorkingRepository().getWorkTree();
|
||||
}
|
||||
assertThat(directory).doesNotExist();
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -33,19 +33,15 @@ package sonia.scm.repository.spi;
|
||||
import com.aragost.javahg.Changeset;
|
||||
import com.aragost.javahg.commands.CommitCommand;
|
||||
import com.aragost.javahg.commands.PullCommand;
|
||||
import com.aragost.javahg.commands.UpdateCommand;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.BranchRequest;
|
||||
import sonia.scm.repository.util.WorkingCopy;
|
||||
import sonia.scm.user.User;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Mercurial implementation of the {@link BranchCommand}.
|
||||
* Note that this creates an empty commit to "persist" the new branch.
|
||||
@@ -63,11 +59,9 @@ public class HgBranchCommand extends AbstractCommand implements BranchCommand {
|
||||
|
||||
@Override
|
||||
public Branch branch(BranchRequest request) {
|
||||
try (WorkingCopy<com.aragost.javahg.Repository> workingCopy = workdirFactory.createWorkingCopy(getContext())) {
|
||||
try (WorkingCopy<com.aragost.javahg.Repository> workingCopy = workdirFactory.createWorkingCopy(getContext(), request.getParentBranch())) {
|
||||
com.aragost.javahg.Repository repository = workingCopy.getWorkingRepository();
|
||||
|
||||
checkoutParentBranchIfSpecified(request, repository);
|
||||
|
||||
Changeset emptyChangeset = createNewBranchWithEmptyCommit(request, repository);
|
||||
|
||||
LOG.debug("Created new branch '{}' in repository {} with changeset {}",
|
||||
@@ -79,16 +73,6 @@ public class HgBranchCommand extends AbstractCommand implements BranchCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private void checkoutParentBranchIfSpecified(BranchRequest request, com.aragost.javahg.Repository repository) {
|
||||
if (request.getParentBranch() != null) {
|
||||
try {
|
||||
UpdateCommand.on(repository).rev(request.getParentBranch()).execute();
|
||||
} catch (IOException e) {
|
||||
throw new InternalRepositoryException(getRepository(), "Could not check out parent branch " + request.getParentBranch(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Changeset createNewBranchWithEmptyCommit(BranchRequest request, com.aragost.javahg.Repository repository) {
|
||||
com.aragost.javahg.commands.BranchCommand.on(repository).set(request.getNewBranch());
|
||||
User currentUser = SecurityUtils.getSubject().getPrincipals().oneByType(User.class);
|
||||
|
||||
@@ -39,13 +39,12 @@ import com.google.common.base.Strings;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.io.Closeables;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.DiffCommandBuilder;
|
||||
import sonia.scm.repository.api.DiffFormat;
|
||||
import sonia.scm.repository.spi.javahg.HgDiffInternalCommand;
|
||||
import sonia.scm.web.HgUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
@@ -71,41 +70,36 @@ public class HgDiffCommand extends AbstractCommand implements DiffCommand
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public void getDiffResult(DiffCommandRequest request, OutputStream output)
|
||||
throws IOException
|
||||
public DiffCommandBuilder.OutputStreamConsumer getDiffResult(DiffCommandRequest request)
|
||||
{
|
||||
com.aragost.javahg.Repository hgRepo = open();
|
||||
return output -> {
|
||||
com.aragost.javahg.Repository hgRepo = open();
|
||||
|
||||
HgDiffInternalCommand cmd = HgDiffInternalCommand.on(hgRepo);
|
||||
DiffFormat format = request.getFormat();
|
||||
HgDiffInternalCommand cmd = HgDiffInternalCommand.on(hgRepo);
|
||||
DiffFormat format = request.getFormat();
|
||||
|
||||
if (format == DiffFormat.GIT)
|
||||
{
|
||||
cmd.git();
|
||||
}
|
||||
|
||||
cmd.change(HgUtil.getRevision(request.getRevision()));
|
||||
|
||||
InputStream inputStream = null;
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
if (!Strings.isNullOrEmpty(request.getPath()))
|
||||
if (format == DiffFormat.GIT)
|
||||
{
|
||||
inputStream = cmd.stream(hgRepo.file(request.getPath()));
|
||||
}
|
||||
else
|
||||
{
|
||||
inputStream = cmd.stream();
|
||||
cmd.git();
|
||||
}
|
||||
|
||||
ByteStreams.copy(inputStream, output);
|
||||
cmd.change(HgUtil.getRevision(request.getRevision()));
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
Closeables.close(inputStream, true);
|
||||
}
|
||||
InputStream inputStream = null;
|
||||
|
||||
try {
|
||||
|
||||
if (!Strings.isNullOrEmpty(request.getPath())) {
|
||||
inputStream = cmd.stream(hgRepo.file(request.getPath()));
|
||||
} else {
|
||||
inputStream = cmd.stream();
|
||||
}
|
||||
|
||||
ByteStreams.copy(inputStream, output);
|
||||
|
||||
} finally {
|
||||
Closeables.close(inputStream, true);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import com.aragost.javahg.Changeset;
|
||||
import com.aragost.javahg.Repository;
|
||||
import com.aragost.javahg.commands.CommitCommand;
|
||||
import com.aragost.javahg.commands.ExecutionException;
|
||||
import com.aragost.javahg.commands.PullCommand;
|
||||
import com.aragost.javahg.commands.RemoveCommand;
|
||||
import com.aragost.javahg.commands.StatusCommand;
|
||||
import sonia.scm.NoChangesMadeException;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.util.WorkingCopy;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
public class HgModifyCommand implements ModifyCommand {
|
||||
|
||||
private HgCommandContext context;
|
||||
private final HgWorkdirFactory workdirFactory;
|
||||
|
||||
public HgModifyCommand(HgCommandContext context, HgWorkdirFactory workdirFactory) {
|
||||
this.context = context;
|
||||
this.workdirFactory = workdirFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(ModifyCommandRequest request) {
|
||||
|
||||
try (WorkingCopy<com.aragost.javahg.Repository> workingCopy = workdirFactory.createWorkingCopy(context, request.getBranch())) {
|
||||
Repository workingRepository = workingCopy.getWorkingRepository();
|
||||
request.getRequests().forEach(
|
||||
partialRequest -> {
|
||||
try {
|
||||
partialRequest.execute(new ModifyWorkerHelper() {
|
||||
|
||||
@Override
|
||||
public void addFileToScm(String name, Path file) {
|
||||
try {
|
||||
addFileToHg(file.toFile());
|
||||
} catch (ExecutionException e) {
|
||||
throwInternalRepositoryException("could not add new file to index", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doScmDelete(String toBeDeleted) {
|
||||
RemoveCommand.on(workingRepository).execute(toBeDeleted);
|
||||
}
|
||||
|
||||
@Override
|
||||
public sonia.scm.repository.Repository getRepository() {
|
||||
return context.getScmRepository();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBranch() {
|
||||
return request.getBranch();
|
||||
}
|
||||
|
||||
public File getWorkDir() {
|
||||
return workingRepository.getDirectory();
|
||||
}
|
||||
|
||||
private void addFileToHg(File file) {
|
||||
workingRepository.workingCopy().add(file.getAbsolutePath());
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
throwInternalRepositoryException("could not execute command on repository", e);
|
||||
}
|
||||
}
|
||||
);
|
||||
if (StatusCommand.on(workingRepository).lines().isEmpty()) {
|
||||
throw new NoChangesMadeException(context.getScmRepository());
|
||||
}
|
||||
CommitCommand.on(workingRepository).user(String.format("%s <%s>", request.getAuthor().getName(), request.getAuthor().getMail())).message(request.getCommitMessage()).execute();
|
||||
List<Changeset> execute = pullModifyChangesToCentralRepository(request, workingCopy);
|
||||
return execute.get(0).getNode();
|
||||
} catch (ExecutionException e) {
|
||||
throwInternalRepositoryException("could not execute command on repository", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<Changeset> pullModifyChangesToCentralRepository(ModifyCommandRequest request, WorkingCopy<com.aragost.javahg.Repository> workingCopy) {
|
||||
try {
|
||||
com.aragost.javahg.commands.PullCommand pullCommand = PullCommand.on(workingCopy.getCentralRepository());
|
||||
workdirFactory.configure(pullCommand);
|
||||
return pullCommand.execute(workingCopy.getDirectory().getAbsolutePath());
|
||||
} catch (Exception e) {
|
||||
throw new IntegrateChangesFromWorkdirException(context.getScmRepository(),
|
||||
String.format("Could not pull modify changes from working copy to central repository for branch %s", request.getBranch()),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private String throwInternalRepositoryException(String message, Exception e) {
|
||||
throw new InternalRepositoryException(context.getScmRepository(), message, e);
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,8 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider
|
||||
Command.INCOMING,
|
||||
Command.OUTGOING,
|
||||
Command.PUSH,
|
||||
Command.PULL
|
||||
Command.PULL,
|
||||
Command.MODIFY
|
||||
);
|
||||
//J+
|
||||
|
||||
@@ -77,7 +78,7 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
HgRepositoryServiceProvider(HgRepositoryHandler handler,
|
||||
HgHookManager hookManager, Repository repository)
|
||||
HgHookManager hookManager, Repository repository)
|
||||
{
|
||||
this.repository = repository;
|
||||
this.handler = handler;
|
||||
@@ -238,6 +239,11 @@ public class HgRepositoryServiceProvider extends RepositoryServiceProvider
|
||||
return new HgPushCommand(handler, context, repository);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModifyCommand getModifyCommand() {
|
||||
return new HgModifyCommand(context, handler.getWorkdirFactory());
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
|
||||
@@ -47,12 +47,12 @@ import sonia.scm.repository.Repository;
|
||||
public class HgRepositoryServiceResolver implements RepositoryServiceResolver
|
||||
{
|
||||
|
||||
private HgRepositoryHandler handler;
|
||||
private HgHookManager hookManager;
|
||||
private final HgRepositoryHandler handler;
|
||||
private final HgHookManager hookManager;
|
||||
|
||||
@Inject
|
||||
public HgRepositoryServiceResolver(HgRepositoryHandler handler,
|
||||
HgHookManager hookManager)
|
||||
HgHookManager hookManager)
|
||||
{
|
||||
this.handler = handler;
|
||||
this.hookManager = hookManager;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import com.aragost.javahg.BaseRepository;
|
||||
import com.aragost.javahg.Repository;
|
||||
import com.aragost.javahg.commands.CloneCommand;
|
||||
import com.aragost.javahg.commands.PullCommand;
|
||||
import com.aragost.javahg.commands.flags.CloneCommandFlags;
|
||||
import sonia.scm.repository.util.SimpleWorkdirFactory;
|
||||
import sonia.scm.repository.util.WorkdirProvider;
|
||||
import sonia.scm.web.HgRepositoryEnvironmentBuilder;
|
||||
@@ -24,12 +26,19 @@ public class SimpleHgWorkdirFactory extends SimpleWorkdirFactory<Repository, HgC
|
||||
this.hgRepositoryEnvironmentBuilder = hgRepositoryEnvironmentBuilder;
|
||||
}
|
||||
@Override
|
||||
public ParentAndClone<Repository> cloneRepository(HgCommandContext context, File target) throws IOException {
|
||||
public ParentAndClone<Repository> cloneRepository(HgCommandContext context, File target, String initialBranch) throws IOException {
|
||||
BiConsumer<sonia.scm.repository.Repository, Map<String, String>> repositoryMapBiConsumer =
|
||||
(repository, environment) -> hgRepositoryEnvironmentBuilder.get().buildFor(repository, null, environment);
|
||||
Repository centralRepository = context.openWithSpecialEnvironment(repositoryMapBiConsumer);
|
||||
CloneCommand.on(centralRepository).execute(target.getAbsolutePath());
|
||||
return new ParentAndClone<>(centralRepository, Repository.open(target));
|
||||
CloneCommand cloneCommand = CloneCommandFlags.on(centralRepository);
|
||||
if (initialBranch != null) {
|
||||
cloneCommand.updaterev(initialBranch);
|
||||
}
|
||||
cloneCommand.execute(target.getAbsolutePath());
|
||||
|
||||
BaseRepository clone = Repository.open(target);
|
||||
|
||||
return new ParentAndClone<>(centralRepository, clone);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#
|
||||
# Copyright (c) 2010, Sebastian Sdorra
|
||||
# All rights reserved.
|
||||
# aLL rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# rEDistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
@@ -34,7 +34,7 @@ Prints date, size and last message of files.
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
from mercurial import cmdutil,util
|
||||
from mercurial import scmutil
|
||||
|
||||
cmdtable = {}
|
||||
|
||||
@@ -122,7 +122,7 @@ class File_Object:
|
||||
return result
|
||||
|
||||
class File_Walker:
|
||||
|
||||
|
||||
def __init__(self, sub_repositories, visitor):
|
||||
self.visitor = visitor
|
||||
self.sub_repositories = sub_repositories
|
||||
@@ -273,7 +273,7 @@ class File_Viewer:
|
||||
('t', 'transport', False, 'format the output for command server'),
|
||||
])
|
||||
def fileview(ui, repo, **opts):
|
||||
revCtx = repo[opts["revision"]]
|
||||
revCtx = scmutil.revsingle(repo, opts["revision"])
|
||||
subrepos = {}
|
||||
if not opts["disableSubRepositoryDetection"]:
|
||||
subrepos = collect_sub_repositories(revCtx)
|
||||
|
||||
@@ -2,7 +2,7 @@ package sonia.scm.repository.spi;
|
||||
|
||||
import com.aragost.javahg.commands.PullCommand;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import sonia.scm.repository.Branch;
|
||||
import sonia.scm.repository.HgTestUtil;
|
||||
@@ -10,30 +10,48 @@ import sonia.scm.repository.api.BranchRequest;
|
||||
import sonia.scm.repository.util.WorkdirProvider;
|
||||
import sonia.scm.web.HgRepositoryEnvironmentBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class HgBranchCommandTest extends AbstractHgCommandTestBase {
|
||||
@Test
|
||||
public void shouldCreateBranch() throws IOException {
|
||||
Assertions.assertThat(readBranches()).filteredOn(b -> b.getName().equals("new_branch")).isEmpty();
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class HgBranchCommandTest extends AbstractHgCommandTestBase {
|
||||
|
||||
private SimpleHgWorkdirFactory workdirFactory;
|
||||
|
||||
@Before
|
||||
public void initWorkdirFactory() {
|
||||
HgRepositoryEnvironmentBuilder hgRepositoryEnvironmentBuilder =
|
||||
new HgRepositoryEnvironmentBuilder(handler, HgTestUtil.createHookManager());
|
||||
|
||||
SimpleHgWorkdirFactory workdirFactory = new SimpleHgWorkdirFactory(Providers.of(hgRepositoryEnvironmentBuilder), new WorkdirProvider()) {
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateBranch() {
|
||||
BranchRequest branchRequest = new BranchRequest();
|
||||
branchRequest.setNewBranch("new_branch");
|
||||
|
||||
new HgBranchCommand(cmdContext, repository, workdirFactory).branch(branchRequest);
|
||||
Branch newBranch = new HgBranchCommand(cmdContext, repository, workdirFactory).branch(branchRequest);
|
||||
|
||||
Assertions.assertThat(readBranches()).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty();
|
||||
assertThat(readBranches()).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty();
|
||||
assertThat(cmdContext.open().changeset(newBranch.getRevision()).getParent1().getBranch()).isEqualTo("default");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateBranchOnSpecificParent() {
|
||||
BranchRequest branchRequest = new BranchRequest();
|
||||
branchRequest.setParentBranch("test-branch");
|
||||
branchRequest.setNewBranch("new_branch");
|
||||
|
||||
Branch newBranch = new HgBranchCommand(cmdContext, repository, workdirFactory).branch(branchRequest);
|
||||
|
||||
assertThat(readBranches()).filteredOn(b -> b.getName().equals("new_branch")).isNotEmpty();
|
||||
assertThat(cmdContext.open().changeset(newBranch.getRevision()).getParent1().getBranch()).isEqualTo("test-branch");
|
||||
}
|
||||
|
||||
private List<Branch> readBranches() {
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import com.google.inject.util.Providers;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import sonia.scm.AlreadyExistsException;
|
||||
import sonia.scm.NoChangesMadeException;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.repository.HgHookManager;
|
||||
import sonia.scm.repository.HgTestUtil;
|
||||
import sonia.scm.repository.Person;
|
||||
import sonia.scm.repository.util.WorkdirProvider;
|
||||
import sonia.scm.web.HgRepositoryEnvironmentBuilder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class HgModifyCommandTest extends AbstractHgCommandTestBase {
|
||||
|
||||
private HgModifyCommand hgModifyCommand;
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@Before
|
||||
public void initHgModifyCommand() {
|
||||
HgHookManager hookManager = HgTestUtil.createHookManager();
|
||||
HgRepositoryEnvironmentBuilder environmentBuilder = new HgRepositoryEnvironmentBuilder(handler, hookManager);
|
||||
SimpleHgWorkdirFactory workdirFactory = new SimpleHgWorkdirFactory(Providers.of(environmentBuilder), new WorkdirProvider()) {
|
||||
@Override
|
||||
public void configure(com.aragost.javahg.commands.PullCommand pullCommand) {
|
||||
// we do not want to configure http hooks in this unit test
|
||||
}
|
||||
};
|
||||
hgModifyCommand = new HgModifyCommand(cmdContext, workdirFactory
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRemoveFiles() {
|
||||
ModifyCommandRequest request = new ModifyCommandRequest();
|
||||
request.addRequest(new ModifyCommandRequest.DeleteFileRequest("a.txt"));
|
||||
request.setCommitMessage("this is great");
|
||||
request.setAuthor(new Person("Arthur Dent", "dent@hitchhiker.com"));
|
||||
|
||||
String result = hgModifyCommand.execute(request);
|
||||
|
||||
assertThat(cmdContext.open().tip().getNode()).isEqualTo(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateFilesWithoutOverwrite() throws IOException {
|
||||
File testFile = temporaryFolder.newFile();
|
||||
|
||||
ModifyCommandRequest request = new ModifyCommandRequest();
|
||||
request.addRequest(new ModifyCommandRequest.CreateFileRequest("Answer.txt", testFile, false));
|
||||
request.setCommitMessage("I found the answer");
|
||||
request.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com"));
|
||||
|
||||
String changeSet = hgModifyCommand.execute(request);
|
||||
|
||||
assertThat(cmdContext.open().tip().getNode()).isEqualTo(changeSet);
|
||||
assertThat(cmdContext.open().tip().getAddedFiles().size()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldOverwriteExistingFiles() throws IOException {
|
||||
File testFile = temporaryFolder.newFile();
|
||||
|
||||
new FileOutputStream(testFile).write(42);
|
||||
ModifyCommandRequest request2 = new ModifyCommandRequest();
|
||||
request2.addRequest(new ModifyCommandRequest.CreateFileRequest("a.txt", testFile, true));
|
||||
request2.setCommitMessage(" Now i really found the answer");
|
||||
request2.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com"));
|
||||
|
||||
String changeSet2 = hgModifyCommand.execute(request2);
|
||||
|
||||
assertThat(cmdContext.open().tip().getNode()).isEqualTo(changeSet2);
|
||||
assertThat(cmdContext.open().tip().getModifiedFiles().size()).isEqualTo(1);
|
||||
assertThat(cmdContext.open().tip().getModifiedFiles().get(0)).isEqualTo("a.txt");
|
||||
}
|
||||
|
||||
@Test(expected = AlreadyExistsException.class)
|
||||
public void shouldThrowFileAlreadyExistsException() throws IOException {
|
||||
|
||||
File testFile = temporaryFolder.newFile();
|
||||
new FileOutputStream(testFile).write(21);
|
||||
|
||||
ModifyCommandRequest request = new ModifyCommandRequest();
|
||||
request.addRequest(new ModifyCommandRequest.CreateFileRequest("Answer.txt", testFile, false));
|
||||
request.setCommitMessage("I found the answer");
|
||||
request.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com"));
|
||||
|
||||
hgModifyCommand.execute(request);
|
||||
|
||||
new FileOutputStream(testFile).write(42);
|
||||
ModifyCommandRequest request2 = new ModifyCommandRequest();
|
||||
request2.addRequest(new ModifyCommandRequest.CreateFileRequest("Answer.txt", testFile, false));
|
||||
request2.setCommitMessage(" Now i really found the answer");
|
||||
request2.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com"));
|
||||
|
||||
hgModifyCommand.execute(request2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldModifyExistingFile() throws IOException {
|
||||
File testFile = temporaryFolder.newFile("a.txt");
|
||||
|
||||
new FileOutputStream(testFile).write(42);
|
||||
ModifyCommandRequest request2 = new ModifyCommandRequest();
|
||||
request2.addRequest(new ModifyCommandRequest.ModifyFileRequest("a.txt", testFile));
|
||||
request2.setCommitMessage(" Now i really found the answer");
|
||||
request2.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com"));
|
||||
|
||||
String changeSet2 = hgModifyCommand.execute(request2);
|
||||
|
||||
assertThat(cmdContext.open().tip().getNode()).isEqualTo(changeSet2);
|
||||
assertThat(cmdContext.open().tip().getModifiedFiles().size()).isEqualTo(1);
|
||||
assertThat(cmdContext.open().tip().getModifiedFiles().get(0)).isEqualTo(testFile.getName());
|
||||
}
|
||||
|
||||
@Test(expected = NotFoundException.class)
|
||||
public void shouldThrowNotFoundExceptionIfFileDoesNotExist() throws IOException {
|
||||
File testFile = temporaryFolder.newFile("Answer.txt");
|
||||
|
||||
new FileOutputStream(testFile).write(42);
|
||||
ModifyCommandRequest request2 = new ModifyCommandRequest();
|
||||
request2.addRequest(new ModifyCommandRequest.ModifyFileRequest("Answer.txt", testFile));
|
||||
request2.setCommitMessage(" Now i really found the answer");
|
||||
request2.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com"));
|
||||
|
||||
hgModifyCommand.execute(request2);
|
||||
}
|
||||
|
||||
@Test(expected = NullPointerException.class)
|
||||
public void shouldThrowNPEIfAuthorIsMissing() throws IOException {
|
||||
File testFile = temporaryFolder.newFile();
|
||||
|
||||
ModifyCommandRequest request = new ModifyCommandRequest();
|
||||
request.addRequest(new ModifyCommandRequest.CreateFileRequest("Answer.txt", testFile, false));
|
||||
request.setCommitMessage("I found the answer");
|
||||
hgModifyCommand.execute(request);
|
||||
}
|
||||
|
||||
@Test(expected = NullPointerException.class)
|
||||
public void shouldThrowNPEIfCommitMessageIsMissing() throws IOException {
|
||||
File testFile = temporaryFolder.newFile();
|
||||
|
||||
ModifyCommandRequest request = new ModifyCommandRequest();
|
||||
request.addRequest(new ModifyCommandRequest.CreateFileRequest("Answer.txt", testFile, false));
|
||||
request.setAuthor(new Person("Trillian Astra", "trillian@hitchhiker.com"));
|
||||
hgModifyCommand.execute(request);
|
||||
}
|
||||
|
||||
@Test(expected = NoChangesMadeException.class)
|
||||
public void shouldThrowNoChangesMadeExceptionIfEmptyLocalChangesetAfterRequest() {
|
||||
hgModifyCommand.execute(new ModifyCommandRequest());
|
||||
}
|
||||
}
|
||||
@@ -46,11 +46,10 @@ import org.tmatesoft.svn.core.wc.SVNRevision;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.SvnUtil;
|
||||
import sonia.scm.repository.api.DiffCommandBuilder;
|
||||
import sonia.scm.repository.api.DiffFormat;
|
||||
import sonia.scm.util.Util;
|
||||
|
||||
import java.io.OutputStream;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -70,33 +69,34 @@ public class SvnDiffCommand extends AbstractSvnCommand implements DiffCommand {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getDiffResult(DiffCommandRequest request, OutputStream output) {
|
||||
public DiffCommandBuilder.OutputStreamConsumer getDiffResult(DiffCommandRequest request) {
|
||||
logger.debug("create diff for {}", request);
|
||||
Preconditions.checkNotNull(request, "request is required");
|
||||
Preconditions.checkNotNull(output, "outputstream is required");
|
||||
|
||||
String path = request.getPath();
|
||||
SVNClientManager clientManager = null;
|
||||
try {
|
||||
SVNURL svnurl = context.createUrl();
|
||||
if (Util.isNotEmpty(path)) {
|
||||
svnurl = svnurl.appendPath(path, true);
|
||||
return output -> {
|
||||
SVNClientManager clientManager = null;
|
||||
try {
|
||||
SVNURL svnurl = context.createUrl();
|
||||
if (Util.isNotEmpty(path)) {
|
||||
svnurl = svnurl.appendPath(path, true);
|
||||
}
|
||||
clientManager = SVNClientManager.newInstance();
|
||||
SVNDiffClient diffClient = clientManager.getDiffClient();
|
||||
diffClient.setDiffGenerator(new SvnNewDiffGenerator(new SCMSvnDiffGenerator()));
|
||||
|
||||
long currentRev = SvnUtil.getRevisionNumber(request.getRevision(), repository);
|
||||
|
||||
diffClient.setGitDiffFormat(request.getFormat() == DiffFormat.GIT);
|
||||
|
||||
diffClient.doDiff(svnurl, SVNRevision.HEAD,
|
||||
SVNRevision.create(currentRev - 1), SVNRevision.create(currentRev),
|
||||
SVNDepth.INFINITY, false, output);
|
||||
} catch (SVNException ex) {
|
||||
throw new InternalRepositoryException(repository, "could not create diff", ex);
|
||||
} finally {
|
||||
SvnUtil.dispose(clientManager);
|
||||
}
|
||||
clientManager = SVNClientManager.newInstance();
|
||||
SVNDiffClient diffClient = clientManager.getDiffClient();
|
||||
diffClient.setDiffGenerator(new SvnNewDiffGenerator(new SCMSvnDiffGenerator()));
|
||||
|
||||
long currentRev = SvnUtil.getRevisionNumber(request.getRevision(), repository);
|
||||
|
||||
diffClient.setGitDiffFormat(request.getFormat() == DiffFormat.GIT);
|
||||
|
||||
diffClient.doDiff(svnurl, SVNRevision.HEAD,
|
||||
SVNRevision.create(currentRev - 1), SVNRevision.create(currentRev),
|
||||
SVNDepth.INFINITY, false, output);
|
||||
} catch (SVNException ex) {
|
||||
throw new InternalRepositoryException(repository, "could not create diff", ex);
|
||||
} finally {
|
||||
SvnUtil.dispose(clientManager);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
216
scm-ui/src/admin/plugins/components/PluginActionModal.js
Normal file
216
scm-ui/src/admin/plugins/components/PluginActionModal.js
Normal file
@@ -0,0 +1,216 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
ErrorNotification,
|
||||
Modal
|
||||
} from "@scm-manager/ui-components";
|
||||
import type { PendingPlugins, PluginCollection } from "@scm-manager/ui-types";
|
||||
import { translate } from "react-i18next";
|
||||
import SuccessNotification from "./SuccessNotification";
|
||||
|
||||
type Props = {
|
||||
onClose: () => void,
|
||||
actionType: string,
|
||||
pendingPlugins?: PendingPlugins,
|
||||
installedPlugins?: PluginCollection,
|
||||
refresh: () => void,
|
||||
execute: () => Promise<any>,
|
||||
description: string,
|
||||
label: string,
|
||||
|
||||
children?: React.Node,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
type State = {
|
||||
loading: boolean,
|
||||
success: boolean,
|
||||
error?: Error
|
||||
};
|
||||
|
||||
class PluginActionModal extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: false,
|
||||
success: false
|
||||
};
|
||||
}
|
||||
|
||||
renderNotifications = () => {
|
||||
const { children } = this.props;
|
||||
const { error, success } = this.state;
|
||||
if (error) {
|
||||
return <ErrorNotification error={error} />;
|
||||
} else if (success) {
|
||||
return <SuccessNotification />;
|
||||
} else {
|
||||
return children;
|
||||
}
|
||||
};
|
||||
|
||||
executeAction = () => {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
|
||||
this.props
|
||||
.execute()
|
||||
.then(() => {
|
||||
this.setState({
|
||||
success: true,
|
||||
loading: false
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({
|
||||
success: false,
|
||||
loading: false,
|
||||
error: error
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
renderModalContent = () => {
|
||||
return (
|
||||
<>
|
||||
{this.renderUpdatable()}
|
||||
{this.renderInstallQueue()}
|
||||
{this.renderUpdateQueue()}
|
||||
{this.renderUninstallQueue()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
renderUpdatable = () => {
|
||||
const { installedPlugins, t } = this.props;
|
||||
return (
|
||||
<>
|
||||
{installedPlugins &&
|
||||
installedPlugins._embedded &&
|
||||
installedPlugins._embedded.plugins && (
|
||||
<>
|
||||
<strong>{t("plugins.modal.updateQueue")}</strong>
|
||||
<ul>
|
||||
{installedPlugins._embedded.plugins
|
||||
.filter(plugin => plugin._links && plugin._links.update)
|
||||
.map(plugin => (
|
||||
<li key={plugin.name}>{plugin.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
renderInstallQueue = () => {
|
||||
const { pendingPlugins, t } = this.props;
|
||||
return (
|
||||
<>
|
||||
{pendingPlugins &&
|
||||
pendingPlugins._embedded &&
|
||||
pendingPlugins._embedded.new.length > 0 && (
|
||||
<>
|
||||
<strong>{t("plugins.modal.installQueue")}</strong>
|
||||
<ul>
|
||||
{pendingPlugins._embedded.new.map(plugin => (
|
||||
<li key={plugin.name}>{plugin.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
renderUpdateQueue = () => {
|
||||
const { pendingPlugins, t } = this.props;
|
||||
return (
|
||||
<>
|
||||
{pendingPlugins &&
|
||||
pendingPlugins._embedded &&
|
||||
pendingPlugins._embedded.update.length > 0 && (
|
||||
<>
|
||||
<strong>{t("plugins.modal.updateQueue")}</strong>
|
||||
<ul>
|
||||
{pendingPlugins._embedded.update.map(plugin => (
|
||||
<li key={plugin.name}>{plugin.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
renderUninstallQueue = () => {
|
||||
const { pendingPlugins, t } = this.props;
|
||||
return (
|
||||
<>
|
||||
{pendingPlugins &&
|
||||
pendingPlugins._embedded &&
|
||||
pendingPlugins._embedded.uninstall.length > 0 && (
|
||||
<>
|
||||
<strong>{t("plugins.modal.uninstallQueue")}</strong>
|
||||
<ul>
|
||||
{pendingPlugins._embedded.uninstall.map(plugin => (
|
||||
<li key={plugin.name}>{plugin.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
renderBody = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="media">
|
||||
<div className="content">
|
||||
<p>{this.props.description}</p>
|
||||
{this.renderModalContent()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="media">{this.renderNotifications()}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
renderFooter = () => {
|
||||
const { onClose, t } = this.props;
|
||||
const { loading, error, success } = this.state;
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
color="warning"
|
||||
label={this.props.label}
|
||||
loading={loading}
|
||||
action={this.executeAction}
|
||||
disabled={error || success}
|
||||
/>
|
||||
<Button label={t("plugins.modal.abort")} action={onClose} />
|
||||
</ButtonGroup>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { onClose } = this.props;
|
||||
return (
|
||||
<Modal
|
||||
title={this.props.label}
|
||||
closeFunction={onClose}
|
||||
body={this.renderBody()}
|
||||
footer={this.renderFooter()}
|
||||
active={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("admin")(PluginActionModal);
|
||||
@@ -46,7 +46,7 @@ export function createUrl(url: string) {
|
||||
|
||||
class ApiClient {
|
||||
get(url: string): Promise<Response> {
|
||||
return fetch(createUrl(url), applyFetchOptions).then(handleFailure);
|
||||
return fetch(createUrl(url), applyFetchOptions({})).then(handleFailure);
|
||||
}
|
||||
|
||||
post(url: string, payload: any, contentType: string = "application/json") {
|
||||
|
||||
@@ -14,7 +14,7 @@ class ButtonGroup extends React.Component<Props> {
|
||||
const childWrapper = [];
|
||||
React.Children.forEach(children, child => {
|
||||
if (child) {
|
||||
childWrapper.push(<p className="control" key={childWrapper.length}>{child}</p>);
|
||||
childWrapper.push(<div className="control" key={childWrapper.length}>{child}</div>);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -7,20 +7,22 @@ import TagGroup from "./TagGroup";
|
||||
type Props = {
|
||||
members: string[],
|
||||
memberListChanged: (string[]) => void,
|
||||
label?: string,
|
||||
helpText?: string,
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class MemberNameTagGroup extends React.Component<Props> {
|
||||
render() {
|
||||
const { members, t } = this.props;
|
||||
const { members, label, helpText, t } = this.props;
|
||||
const membersExtended = members.map(id => {
|
||||
return { id, displayName: id, mail: "" };
|
||||
});
|
||||
return (
|
||||
<TagGroup
|
||||
items={membersExtended}
|
||||
label={t("group.members")}
|
||||
helpText={t("groupForm.help.memberHelpText")}
|
||||
label={label ? label : t("group.members")}
|
||||
helpText={helpText ? helpText : t("groupForm.help.memberHelpText")}
|
||||
onRemove={this.removeEntry}
|
||||
/>
|
||||
);
|
||||
|
||||
21
scm-ui/ui-components/src/layout/Level.js
Normal file
21
scm-ui/ui-components/src/layout/Level.js
Normal file
@@ -0,0 +1,21 @@
|
||||
//@flow
|
||||
import * as React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
left?: React.Node,
|
||||
right?: React.Node
|
||||
};
|
||||
|
||||
export default class Level extends React.Component<Props> {
|
||||
render() {
|
||||
const { className, left, right } = this.props;
|
||||
return (
|
||||
<div className={classNames("level", className)}>
|
||||
<div className="level-left">{left}</div>
|
||||
<div className="level-right">{right}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
export { default as Footer } from "./Footer.js";
|
||||
export { default as Header } from "./Header.js";
|
||||
export { default as Level } from "./Level.js";
|
||||
export { default as Page } from "./Page.js";
|
||||
export { default as PageActions } from "./PageActions.js";
|
||||
export { default as Subtitle } from "./Subtitle.js";
|
||||
|
||||
@@ -4,7 +4,8 @@ import DiffFile from "./DiffFile";
|
||||
import type {DiffObjectProps, File} from "./DiffTypes";
|
||||
|
||||
type Props = DiffObjectProps & {
|
||||
diff: File[]
|
||||
diff: File[],
|
||||
defaultCollapse?: boolean
|
||||
};
|
||||
|
||||
class Diff extends React.Component<Props> {
|
||||
@@ -17,7 +18,7 @@ class Diff extends React.Component<Props> {
|
||||
return (
|
||||
<>
|
||||
{diff.map((file, index) => (
|
||||
<DiffFile key={index} file={file} {...fileProps} />
|
||||
<DiffFile key={index} file={file} {...fileProps} {...this.props} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,8 @@ import Diff from "./Diff";
|
||||
import type {DiffObjectProps, File} from "./DiffTypes";
|
||||
|
||||
type Props = DiffObjectProps & {
|
||||
url: string
|
||||
url: string,
|
||||
defaultCollapse?: boolean
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {translate} from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
changeset: Changeset,
|
||||
defaultCollapse?: boolean,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
@@ -23,12 +24,12 @@ class ChangesetDiff extends React.Component<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { changeset, t } = this.props;
|
||||
const { changeset, defaultCollapse, t } = this.props;
|
||||
if (!this.isDiffSupported(changeset)) {
|
||||
return <Notification type="danger">{t("changeset.diffNotSupported")}</Notification>;
|
||||
} else {
|
||||
const url = this.createUrl(changeset);
|
||||
return <LoadingDiff url={url} />;
|
||||
return <LoadingDiff url={url} defaultCollapse={defaultCollapse} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export type Plugin = {
|
||||
};
|
||||
|
||||
export type PluginCollection = Collection & {
|
||||
_links: Links,
|
||||
_embedded: {
|
||||
plugins: Plugin[] | string[]
|
||||
}
|
||||
|
||||
@@ -29,7 +29,11 @@
|
||||
"installedNavLink": "Installiert",
|
||||
"availableNavLink": "Verfügbar"
|
||||
},
|
||||
"executePending": "Ausstehende Plugin-Änderungen ausführen",
|
||||
"executePending": "Änderungen ausführen",
|
||||
"outdatedPlugins": "{{count}} veraltetes Plugin",
|
||||
"outdatedPlugins_plural": "{{count}} veraltete Plugins",
|
||||
"updateAll": "Alle Plugins aktualisieren",
|
||||
"cancelPending": "Änderungen abbrechen",
|
||||
"noPlugins": "Keine Plugins gefunden.",
|
||||
"modal": {
|
||||
"title": {
|
||||
@@ -48,6 +52,7 @@
|
||||
"updateAndRestart": "Aktualisieren und Neustarten",
|
||||
"uninstallAndRestart": "Deinstallieren and Neustarten",
|
||||
"executeAndRestart": "Ausführen und Neustarten",
|
||||
"updateAll": "Alle Plugins aktualisieren",
|
||||
"abort": "Abbrechen",
|
||||
"author": "Autor",
|
||||
"version": "Version",
|
||||
@@ -58,7 +63,9 @@
|
||||
"successNotification": "Das Plugin wurde erfolgreich installiert. Um Änderungen an der UI zu sehen, muss die Seite neu geladen werden:",
|
||||
"reload": "jetzt neu laden",
|
||||
"restartNotification": "Der SCM-Manager Kontext sollte nur neu gestartet werden, wenn aktuell niemand damit arbeitet.",
|
||||
"executePending": "Die folgenden Plugin-Änderungen werden ausgeführt. Anschließend wird der SCM-Manager Kontext neu gestartet."
|
||||
"executePending": "Die folgenden Plugin-Änderungen werden ausgeführt. Anschließend wird der SCM-Manager Kontext neu gestartet.",
|
||||
"cancelPending": "Die folgenden Plugin-Änderungen werden abgebrochen und zurückgesetzt.",
|
||||
"updateAllInfo": "Die folgenden Plugins werden aktualisiert. Die Änderungen werden nach dem nächsten Neustart wirksam."
|
||||
}
|
||||
},
|
||||
"repositoryRole": {
|
||||
|
||||
@@ -73,7 +73,8 @@
|
||||
"errorTitle": "Fehler",
|
||||
"errorSubtitle": "Changesets konnten nicht abgerufen werden",
|
||||
"noChangesets": "Keine Changesets in diesem Branch gefunden.",
|
||||
"branchSelectorLabel": "Branches"
|
||||
"branchSelectorLabel": "Branches",
|
||||
"collapseDiffs": "Auf-/Zuklappen"
|
||||
},
|
||||
"changeset": {
|
||||
"description": "Beschreibung",
|
||||
|
||||
@@ -29,7 +29,11 @@
|
||||
"installedNavLink": "Installed",
|
||||
"availableNavLink": "Available"
|
||||
},
|
||||
"executePending": "Execute pending plugin changes",
|
||||
"executePending": "Execute changes",
|
||||
"outdatedPlugins": "{{count}} outdated plugin",
|
||||
"outdatedPlugins_plural": "{{count}} outdated plugins",
|
||||
"updateAll": "Update all plugins",
|
||||
"cancelPending": "Cancel changes",
|
||||
"noPlugins": "No plugins found.",
|
||||
"modal": {
|
||||
"title": {
|
||||
@@ -48,6 +52,7 @@
|
||||
"updateAndRestart": "Update and Restart",
|
||||
"uninstallAndRestart": "Uninstall and Restart",
|
||||
"executeAndRestart": "Execute and Restart",
|
||||
"updateAll": "Update all plugins",
|
||||
"abort": "Abort",
|
||||
"author": "Author",
|
||||
"version": "Version",
|
||||
@@ -58,7 +63,9 @@
|
||||
"successNotification": "Successful installed plugin. You have to reload the page, to see ui changes:",
|
||||
"reload": "reload now",
|
||||
"restartNotification": "You should only restart the scm-manager context if no one else is currently working with it.",
|
||||
"executePending": "The following plugin changes will be executed and after that the scm-manager context will be restarted."
|
||||
"executePending": "The following plugin changes will be executed and after that the scm-manager context will be restarted.",
|
||||
"cancelPending": "The following plugin changes will be canceled.",
|
||||
"updateAllInfo": "The following plugin changes will be executed. You need to restart the scm-manager to make these changes effective."
|
||||
}
|
||||
},
|
||||
"repositoryRole": {
|
||||
|
||||
@@ -73,7 +73,8 @@
|
||||
"errorTitle": "Error",
|
||||
"errorSubtitle": "Could not fetch changesets",
|
||||
"noChangesets": "No changesets found for this branch.",
|
||||
"branchSelectorLabel": "Branches"
|
||||
"branchSelectorLabel": "Branches",
|
||||
"collapseDiffs": "Collapse"
|
||||
},
|
||||
"changeset": {
|
||||
"description": "Description",
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
"availableNavLink": "Disponibles"
|
||||
},
|
||||
"executePending": "Ejecutar los complementos pendientes",
|
||||
"updateAll": "Actualizar todos los complementos",
|
||||
"cancelPending": "Cancelar los complementos pendientes",
|
||||
"noPlugins": "No se han encontrado complementos.",
|
||||
"modal": {
|
||||
"title": {
|
||||
|
||||
@@ -73,7 +73,8 @@
|
||||
"errorTitle": "Error",
|
||||
"errorSubtitle": "No se han podido recuperar los changesets",
|
||||
"noChangesets": "No se han encontrado changesets para esta rama branch.",
|
||||
"branchSelectorLabel": "Ramas"
|
||||
"branchSelectorLabel": "Ramas",
|
||||
"collapseDiffs": "Colapso"
|
||||
},
|
||||
"changeset": {
|
||||
"description": "Descripción",
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import PluginActionModal from "./PluginActionModal";
|
||||
import type { PendingPlugins } from "@scm-manager/ui-types";
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
import { translate } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
onClose: () => void,
|
||||
refresh: () => void,
|
||||
pendingPlugins: PendingPlugins,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class CancelPendingActionModal extends React.Component<Props> {
|
||||
render() {
|
||||
const { onClose, pendingPlugins, t } = this.props;
|
||||
|
||||
return (
|
||||
<PluginActionModal
|
||||
description={t("plugins.modal.cancelPending")}
|
||||
label={t("plugins.cancelPending")}
|
||||
onClose={onClose}
|
||||
pendingPlugins={pendingPlugins}
|
||||
execute={this.cancelPending}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
cancelPending = () => {
|
||||
const { pendingPlugins, refresh, onClose } = this.props;
|
||||
return apiClient
|
||||
.post(pendingPlugins._links.cancel.href)
|
||||
.then(refresh)
|
||||
.then(onClose);
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("admin")(CancelPendingActionModal);
|
||||
@@ -0,0 +1,45 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import PluginActionModal from "./PluginActionModal";
|
||||
import type { PendingPlugins } from "@scm-manager/ui-types";
|
||||
import waitForRestart from "./waitForRestart";
|
||||
import { apiClient, Notification } from "@scm-manager/ui-components";
|
||||
import { translate } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
onClose: () => void,
|
||||
pendingPlugins: PendingPlugins,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class ExecutePendingActionModal extends React.Component<Props> {
|
||||
render() {
|
||||
const { onClose, pendingPlugins, t } = this.props;
|
||||
|
||||
return (
|
||||
<PluginActionModal
|
||||
description={t("plugins.modal.executePending")}
|
||||
label={t("plugins.modal.executeAndRestart")}
|
||||
onClose={onClose}
|
||||
pendingPlugins={pendingPlugins}
|
||||
execute={this.executeAndRestart}
|
||||
>
|
||||
<Notification type="warning">
|
||||
{t("plugins.modal.restartNotification")}
|
||||
</Notification>
|
||||
</PluginActionModal>
|
||||
);
|
||||
}
|
||||
|
||||
executeAndRestart = () => {
|
||||
const { pendingPlugins } = this.props;
|
||||
return apiClient
|
||||
.post(pendingPlugins._links.execute.href)
|
||||
.then(waitForRestart);
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("admin")(ExecutePendingActionModal);
|
||||
@@ -114,6 +114,7 @@ class PluginModal extends React.Component<Props, State> {
|
||||
.catch(error => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
success: false,
|
||||
error: error
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import PluginActionModal from "./PluginActionModal";
|
||||
import type { PluginCollection } from "@scm-manager/ui-types";
|
||||
import { apiClient } from "@scm-manager/ui-components";
|
||||
import { translate } from "react-i18next";
|
||||
|
||||
type Props = {
|
||||
onClose: () => void,
|
||||
refresh: () => void,
|
||||
installedPlugins: PluginCollection,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class UpdateAllActionModal extends React.Component<Props> {
|
||||
render() {
|
||||
const { onClose, installedPlugins, t } = this.props;
|
||||
|
||||
return (
|
||||
<PluginActionModal
|
||||
description={t("plugins.modal.updateAll")}
|
||||
label={t("plugins.updateAll")}
|
||||
onClose={onClose}
|
||||
installedPlugins={installedPlugins}
|
||||
execute={this.updateAll}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
updateAll = () => {
|
||||
const { installedPlugins, refresh, onClose } = this.props;
|
||||
return apiClient
|
||||
.post(installedPlugins._links.update.href)
|
||||
.then(refresh)
|
||||
.then(onClose);
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("admin")(UpdateAllActionModal);
|
||||
@@ -5,11 +5,13 @@ import { translate } from "react-i18next";
|
||||
import { compose } from "redux";
|
||||
import type { PendingPlugins, PluginCollection } from "@scm-manager/ui-types";
|
||||
import {
|
||||
ButtonGroup,
|
||||
ErrorNotification,
|
||||
Loading,
|
||||
Notification,
|
||||
Subtitle,
|
||||
Title
|
||||
Title,
|
||||
Button
|
||||
} from "@scm-manager/ui-components";
|
||||
import {
|
||||
fetchPendingPlugins,
|
||||
@@ -27,7 +29,9 @@ import {
|
||||
} from "../../../modules/indexResource";
|
||||
import PluginTopActions from "../components/PluginTopActions";
|
||||
import PluginBottomActions from "../components/PluginBottomActions";
|
||||
import ExecutePendingAction from "../components/ExecutePendingAction";
|
||||
import ExecutePendingActionModal from "../components/ExecutePendingActionModal";
|
||||
import CancelPendingActionModal from "../components/CancelPendingActionModal";
|
||||
import UpdateAllActionModal from "../components/UpdateAllActionModal";
|
||||
|
||||
type Props = {
|
||||
loading: boolean,
|
||||
@@ -41,14 +45,29 @@ type Props = {
|
||||
pendingPlugins: PendingPlugins,
|
||||
|
||||
// context objects
|
||||
t: string => string,
|
||||
t: (key: string, params?: Object) => string,
|
||||
|
||||
// dispatched functions
|
||||
fetchPluginsByLink: (link: string) => void,
|
||||
fetchPendingPlugins: (link: string) => void
|
||||
};
|
||||
|
||||
class PluginsOverview extends React.Component<Props> {
|
||||
type State = {
|
||||
showPendingModal: boolean,
|
||||
showUpdateAllModal: boolean,
|
||||
showCancelModal: boolean
|
||||
};
|
||||
|
||||
class PluginsOverview extends React.Component<Props, State> {
|
||||
constructor(props: Props, context: *) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
showPendingModal: false,
|
||||
showUpdateAllModal: false,
|
||||
showCancelModal: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchPlugins();
|
||||
}
|
||||
@@ -102,17 +121,72 @@ class PluginsOverview extends React.Component<Props> {
|
||||
};
|
||||
|
||||
createActions = () => {
|
||||
const { pendingPlugins } = this.props;
|
||||
const { pendingPlugins, collection, t } = this.props;
|
||||
const buttons = [];
|
||||
|
||||
if (
|
||||
pendingPlugins &&
|
||||
pendingPlugins._links &&
|
||||
pendingPlugins._links.execute
|
||||
) {
|
||||
return <ExecutePendingAction pendingPlugins={pendingPlugins} />;
|
||||
buttons.push(
|
||||
<Button
|
||||
color="primary"
|
||||
reducedMobile={true}
|
||||
key={"executePending"}
|
||||
icon={"arrow-circle-right"}
|
||||
label={t("plugins.executePending")}
|
||||
action={() => this.setState({ showPendingModal: true })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
pendingPlugins &&
|
||||
pendingPlugins._links &&
|
||||
pendingPlugins._links.cancel
|
||||
) {
|
||||
buttons.push(
|
||||
<Button
|
||||
color="primary"
|
||||
reducedMobile={true}
|
||||
key={"cancelPending"}
|
||||
icon={"times"}
|
||||
label={t("plugins.cancelPending")}
|
||||
action={() => this.setState({ showCancelModal: true })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (collection && collection._links && collection._links.update) {
|
||||
buttons.push(
|
||||
<Button
|
||||
color="primary"
|
||||
reducedMobile={true}
|
||||
key={"updateAll"}
|
||||
icon={"sync-alt"}
|
||||
label={this.computeUpdateAllSize()}
|
||||
action={() => this.setState({ showUpdateAllModal: true })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (buttons.length > 0) {
|
||||
return <ButtonGroup>{buttons}</ButtonGroup>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
computeUpdateAllSize = () => {
|
||||
const { collection, t } = this.props;
|
||||
const outdatedPlugins = collection._embedded.plugins.filter(
|
||||
p => p._links.update
|
||||
).length;
|
||||
return t("plugins.outdatedPlugins", {
|
||||
count: outdatedPlugins
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading, error, collection } = this.props;
|
||||
|
||||
@@ -131,10 +205,47 @@ class PluginsOverview extends React.Component<Props> {
|
||||
<hr className="header-with-actions" />
|
||||
{this.renderPluginsList()}
|
||||
{this.renderFooter(actions)}
|
||||
{this.renderModals()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderModals = () => {
|
||||
const { collection, pendingPlugins } = this.props;
|
||||
const {
|
||||
showPendingModal,
|
||||
showCancelModal,
|
||||
showUpdateAllModal
|
||||
} = this.state;
|
||||
|
||||
if (showPendingModal) {
|
||||
return (
|
||||
<ExecutePendingActionModal
|
||||
onClose={() => this.setState({ showPendingModal: false })}
|
||||
pendingPlugins={pendingPlugins}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (showCancelModal) {
|
||||
return (
|
||||
<CancelPendingActionModal
|
||||
onClose={() => this.setState({ showCancelModal: false })}
|
||||
refresh={this.fetchPlugins}
|
||||
pendingPlugins={pendingPlugins}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (showUpdateAllModal) {
|
||||
return (
|
||||
<UpdateAllActionModal
|
||||
onClose={() => this.setState({ showUpdateAllModal: false })}
|
||||
refresh={this.fetchPlugins}
|
||||
installedPlugins={collection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
renderPluginsList() {
|
||||
const { collection, t } = this.props;
|
||||
|
||||
|
||||
@@ -71,5 +71,5 @@ export default withRouter(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(translate("changesets")(ChangesetView))
|
||||
)(translate("repos")(ChangesetView))
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode;
|
||||
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
|
||||
import sonia.scm.NotFoundException;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.api.DiffCommandBuilder;
|
||||
import sonia.scm.repository.api.DiffFormat;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
@@ -20,6 +21,7 @@ import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.StreamingOutput;
|
||||
import java.io.IOException;
|
||||
|
||||
public class DiffRootResource {
|
||||
|
||||
@@ -55,20 +57,17 @@ public class DiffRootResource {
|
||||
@ResponseCode(code = 404, condition = "not found, no revision with the specified param for the repository available or repository not found"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision , @Pattern(regexp = DIFF_FORMAT_VALUES_REGEX) @DefaultValue("NATIVE") @QueryParam("format") String format ){
|
||||
public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision , @Pattern(regexp = DIFF_FORMAT_VALUES_REGEX) @DefaultValue("NATIVE") @QueryParam("format") String format ) throws IOException {
|
||||
HttpUtil.checkForCRLFInjection(revision);
|
||||
DiffFormat diffFormat = DiffFormat.valueOf(format);
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
StreamingOutput responseEntry = output -> {
|
||||
repositoryService.getDiffCommand()
|
||||
.setRevision(revision)
|
||||
.setFormat(diffFormat)
|
||||
.retrieveContent(output);
|
||||
};
|
||||
return Response.ok(responseEntry)
|
||||
DiffCommandBuilder.OutputStreamConsumer outputStreamConsumer = repositoryService.getDiffCommand()
|
||||
.setRevision(revision)
|
||||
.setFormat(diffFormat)
|
||||
.retrieveContent();
|
||||
return Response.ok((StreamingOutput) outputStreamConsumer::accept)
|
||||
.header(HEADER_CONTENT_DISPOSITION, HttpUtil.createContentDispositionAttachmentHeader(String.format("%s-%s.diff", name, revision)))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import sonia.scm.repository.ChangesetPagingResult;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryPermissions;
|
||||
import sonia.scm.repository.api.DiffCommandBuilder;
|
||||
import sonia.scm.repository.api.DiffFormat;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
@@ -138,14 +139,13 @@ public class IncomingRootResource {
|
||||
HttpUtil.checkForCRLFInjection(target);
|
||||
DiffFormat diffFormat = DiffFormat.valueOf(format);
|
||||
try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) {
|
||||
StreamingOutput responseEntry = output ->
|
||||
repositoryService.getDiffCommand()
|
||||
.setRevision(source)
|
||||
.setAncestorChangeset(target)
|
||||
.setFormat(diffFormat)
|
||||
.retrieveContent(output);
|
||||
DiffCommandBuilder.OutputStreamConsumer outputStreamConsumer = repositoryService.getDiffCommand()
|
||||
.setRevision(source)
|
||||
.setAncestorChangeset(target)
|
||||
.setFormat(diffFormat)
|
||||
.retrieveContent();
|
||||
|
||||
return Response.ok(responseEntry)
|
||||
return Response.ok((StreamingOutput) outputStreamConsumer::accept)
|
||||
.header(HEADER_CONTENT_DISPOSITION, HttpUtil.createContentDispositionAttachmentHeader(String.format("%s-%s.diff", name, source)))
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -56,6 +56,21 @@ public class InstalledPluginResource {
|
||||
return Response.ok(collectionMapper.mapInstalled(plugins, available)).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all installed plugins.
|
||||
*/
|
||||
@POST
|
||||
@Path("/update")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
@TypeHint(CollectionDto.class)
|
||||
public Response updateAll() {
|
||||
pluginManager.updateAll();
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the installed plugin with the given id.
|
||||
*
|
||||
|
||||
@@ -46,8 +46,6 @@ public class PendingPluginResource {
|
||||
})
|
||||
@Produces(VndMediaType.PLUGIN_COLLECTION)
|
||||
public Response getPending() {
|
||||
PluginPermissions.manage().check();
|
||||
|
||||
List<AvailablePlugin> pending = pluginManager
|
||||
.getAvailable()
|
||||
.stream()
|
||||
@@ -71,8 +69,12 @@ public class PendingPluginResource {
|
||||
List<PluginDto> updateDtos = updatePlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList());
|
||||
List<PluginDto> uninstallDtos = uninstallPlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList());
|
||||
|
||||
if (!installDtos.isEmpty() || !updateDtos.isEmpty() || !uninstallDtos.isEmpty()) {
|
||||
if (
|
||||
PluginPermissions.manage().isPermitted() &&
|
||||
(!installDtos.isEmpty() || !updateDtos.isEmpty() || !uninstallDtos.isEmpty())
|
||||
) {
|
||||
linksBuilder.single(link("execute", resourceLinks.pendingPluginCollection().executePending()));
|
||||
linksBuilder.single(link("cancel", resourceLinks.pendingPluginCollection().cancelPending()));
|
||||
}
|
||||
|
||||
Embedded.Builder embedded = Embedded.embeddedBuilder();
|
||||
@@ -106,8 +108,18 @@ public class PendingPluginResource {
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response executePending() {
|
||||
PluginPermissions.manage().check();
|
||||
pluginManager.executePendingAndRestart();
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/cancel")
|
||||
@StatusCodes({
|
||||
@ResponseCode(code = 200, condition = "success"),
|
||||
@ResponseCode(code = 500, condition = "internal server error")
|
||||
})
|
||||
public Response cancelPending() {
|
||||
pluginManager.cancelPending();
|
||||
return Response.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,15 @@ package sonia.scm.api.v2.resources;
|
||||
import com.google.inject.Inject;
|
||||
import de.otto.edison.hal.Embedded;
|
||||
import de.otto.edison.hal.HalRepresentation;
|
||||
import de.otto.edison.hal.Link;
|
||||
import de.otto.edison.hal.Links;
|
||||
import sonia.scm.plugin.AvailablePlugin;
|
||||
import sonia.scm.plugin.InstalledPlugin;
|
||||
import sonia.scm.plugin.PluginPermissions;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static de.otto.edison.hal.Embedded.embeddedBuilder;
|
||||
import static de.otto.edison.hal.Link.link;
|
||||
import static de.otto.edison.hal.Links.linkingTo;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
|
||||
@@ -19,11 +19,13 @@ public class PluginDtoCollectionMapper {
|
||||
|
||||
private final ResourceLinks resourceLinks;
|
||||
private final PluginDtoMapper mapper;
|
||||
private final PluginManager manager;
|
||||
|
||||
@Inject
|
||||
public PluginDtoCollectionMapper(ResourceLinks resourceLinks, PluginDtoMapper mapper) {
|
||||
public PluginDtoCollectionMapper(ResourceLinks resourceLinks, PluginDtoMapper mapper, PluginManager manager) {
|
||||
this.resourceLinks = resourceLinks;
|
||||
this.mapper = mapper;
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
public HalRepresentation mapInstalled(List<InstalledPlugin> plugins, List<AvailablePlugin> availablePlugins) {
|
||||
@@ -44,6 +46,11 @@ public class PluginDtoCollectionMapper {
|
||||
|
||||
Links.Builder linksBuilder = linkingTo()
|
||||
.with(Links.linkingTo().self(baseUrl).build());
|
||||
|
||||
if (!manager.getUpdatable().isEmpty()) {
|
||||
linksBuilder.single(link("update", resourceLinks.installedPluginCollection().update()));
|
||||
}
|
||||
|
||||
return linksBuilder.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -686,6 +686,10 @@ class ResourceLinks {
|
||||
String self() {
|
||||
return installedPluginCollectionLinkBuilder.method("installedPlugins").parameters().method("getInstalledPlugins").parameters().href();
|
||||
}
|
||||
|
||||
String update() {
|
||||
return installedPluginCollectionLinkBuilder.method("installedPlugins").parameters().method("updateAll").parameters().href();
|
||||
}
|
||||
}
|
||||
|
||||
public AvailablePluginLinks availablePlugin() {
|
||||
@@ -739,6 +743,10 @@ class ResourceLinks {
|
||||
return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("executePending").parameters().href();
|
||||
}
|
||||
|
||||
String cancelPending() {
|
||||
return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("cancelPending").parameters().href();
|
||||
}
|
||||
|
||||
String self() {
|
||||
return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("getPending").parameters().href();
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ import sonia.scm.lifecycle.RestartEvent;
|
||||
import sonia.scm.version.Version;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@@ -72,7 +72,8 @@ public class DefaultPluginManager implements PluginManager {
|
||||
private final PluginLoader loader;
|
||||
private final PluginCenter center;
|
||||
private final PluginInstaller installer;
|
||||
private final Collection<PendingPluginInstallation> pendingQueue = new ArrayList<>();
|
||||
private final Collection<PendingPluginInstallation> pendingInstallQueue = new ArrayList<>();
|
||||
private final Collection<PendingPluginUninstallation> pendingUninstallQueue = new ArrayList<>();
|
||||
private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker();
|
||||
|
||||
@Inject
|
||||
@@ -106,7 +107,7 @@ public class DefaultPluginManager implements PluginManager {
|
||||
}
|
||||
|
||||
private Optional<AvailablePlugin> getPending(String name) {
|
||||
return pendingQueue
|
||||
return pendingInstallQueue
|
||||
.stream()
|
||||
.map(PendingPluginInstallation::getPlugin)
|
||||
.filter(filterByName(name))
|
||||
@@ -138,6 +139,15 @@ public class DefaultPluginManager implements PluginManager {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<InstalledPlugin> getUpdatable() {
|
||||
return getInstalled()
|
||||
.stream()
|
||||
.filter(p -> isUpdatable(p.getDescriptor().getInformation().getName()))
|
||||
.filter(p -> !p.isMarkedForUninstall())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private <T extends Plugin> Predicate<T> filterByName(String name) {
|
||||
return plugin -> name.equals(plugin.getDescriptor().getInformation().getName());
|
||||
}
|
||||
@@ -179,7 +189,7 @@ public class DefaultPluginManager implements PluginManager {
|
||||
if (restartAfterInstallation) {
|
||||
restart("plugin installation");
|
||||
} else {
|
||||
pendingQueue.addAll(pendingInstallations);
|
||||
pendingInstallQueue.addAll(pendingInstallations);
|
||||
updateMayUninstallFlag();
|
||||
}
|
||||
}
|
||||
@@ -192,10 +202,7 @@ public class DefaultPluginManager implements PluginManager {
|
||||
.orElseThrow(() -> NotFoundException.notFound(entity(InstalledPlugin.class, name)));
|
||||
doThrow().violation("plugin is a core plugin and cannot be uninstalled").when(installed.isCore());
|
||||
|
||||
dependencyTracker.removeInstalled(installed.getDescriptor());
|
||||
installed.setMarkedForUninstall(true);
|
||||
|
||||
createMarkerFile(installed, InstalledPlugin.UNINSTALL_MARKER_FILENAME);
|
||||
markForUninstall(installed);
|
||||
|
||||
if (restartAfterInstallation) {
|
||||
restart("plugin installation");
|
||||
@@ -215,18 +222,22 @@ public class DefaultPluginManager implements PluginManager {
|
||||
&& dependencyTracker.mayUninstall(p.getDescriptor().getInformation().getName());
|
||||
}
|
||||
|
||||
private void createMarkerFile(InstalledPlugin plugin, String markerFile) {
|
||||
private void markForUninstall(InstalledPlugin plugin) {
|
||||
dependencyTracker.removeInstalled(plugin.getDescriptor());
|
||||
try {
|
||||
Files.createFile(plugin.getDirectory().resolve(markerFile));
|
||||
} catch (IOException e) {
|
||||
throw new PluginException("could not mark plugin " + plugin.getId() + " in path " + plugin.getDirectory() + "as " + markerFile, e);
|
||||
Path file = Files.createFile(plugin.getDirectory().resolve(InstalledPlugin.UNINSTALL_MARKER_FILENAME));
|
||||
pendingUninstallQueue.add(new PendingPluginUninstallation(plugin, file));
|
||||
plugin.setMarkedForUninstall(true);
|
||||
} catch (Exception e) {
|
||||
dependencyTracker.addInstalled(plugin.getDescriptor());
|
||||
throw new PluginException("could not mark plugin " + plugin.getId() + " in path " + plugin.getDirectory() + "as " + InstalledPlugin.UNINSTALL_MARKER_FILENAME, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executePendingAndRestart() {
|
||||
PluginPermissions.manage().check();
|
||||
if (!pendingQueue.isEmpty() || getInstalled().stream().anyMatch(InstalledPlugin::isMarkedForUninstall)) {
|
||||
if (!pendingInstallQueue.isEmpty() || getInstalled().stream().anyMatch(InstalledPlugin::isMarkedForUninstall)) {
|
||||
restart("execute pending plugin changes");
|
||||
}
|
||||
}
|
||||
@@ -269,4 +280,25 @@ public class DefaultPluginManager implements PluginManager {
|
||||
private boolean isUpdatable(String name) {
|
||||
return getAvailable(name).isPresent() && !getPending(name).isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelPending() {
|
||||
PluginPermissions.manage().check();
|
||||
pendingUninstallQueue.forEach(PendingPluginUninstallation::cancel);
|
||||
pendingInstallQueue.forEach(PendingPluginInstallation::cancel);
|
||||
pendingUninstallQueue.clear();
|
||||
pendingInstallQueue.clear();
|
||||
updateMayUninstallFlag();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAll() {
|
||||
PluginPermissions.manage().check();
|
||||
for (InstalledPlugin installedPlugin : getInstalled()) {
|
||||
String pluginName = installedPlugin.getDescriptor().getInformation().getName();
|
||||
if (isUpdatable(pluginName)) {
|
||||
install(pluginName, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class PendingPluginInstallation {
|
||||
try {
|
||||
Files.delete(file);
|
||||
} catch (IOException ex) {
|
||||
throw new PluginFailedToCancelInstallationException("failed to cancel installation of plugin " + name, ex);
|
||||
throw new PluginFailedToCancelInstallationException("failed to cancel plugin installation ", name, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package sonia.scm.plugin;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
class PendingPluginUninstallation {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PendingPluginUninstallation.class);
|
||||
|
||||
private final InstalledPlugin plugin;
|
||||
private final Path uninstallFile;
|
||||
|
||||
PendingPluginUninstallation(InstalledPlugin plugin, Path uninstallFile) {
|
||||
this.plugin = plugin;
|
||||
this.uninstallFile = uninstallFile;
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
String name = plugin.getDescriptor().getInformation().getName();
|
||||
LOG.info("cancel uninstallation of plugin {}", name);
|
||||
try {
|
||||
Files.delete(uninstallFile);
|
||||
plugin.setMarkedForUninstall(false);
|
||||
} catch (IOException ex) {
|
||||
throw new PluginFailedToCancelInstallationException("failed to cancel uninstallation", name, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,10 @@ class PluginDependencyTracker {
|
||||
}
|
||||
|
||||
private void removeDependency(String from, String to) {
|
||||
plugins.get(to).remove(from);
|
||||
Collection<String> dependencies = plugins.get(to);
|
||||
if (dependencies == null) {
|
||||
throw new NullPointerException("inverse dependencies not found for " + to);
|
||||
}
|
||||
dependencies.remove(from);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
package sonia.scm.plugin;
|
||||
|
||||
public class PluginFailedToCancelInstallationException extends RuntimeException {
|
||||
public PluginFailedToCancelInstallationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
import sonia.scm.ExceptionWithContext;
|
||||
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
|
||||
public class PluginFailedToCancelInstallationException extends ExceptionWithContext {
|
||||
public PluginFailedToCancelInstallationException(String message, String name, Exception cause) {
|
||||
super(entity("plugin", name).build(), message, cause);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCode() {
|
||||
return "65RdZ5atX1";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +175,14 @@
|
||||
"40RaYIeeR1": {
|
||||
"displayName": "Es wurden keine Änderungen durchgeführt",
|
||||
"description": "Das Repository wurde nicht verändert. Daher konnte kein neuer Commit erzeugt werden."
|
||||
},
|
||||
"4iRct4avG1": {
|
||||
"displayName": "Die Revisionen haben keinen gemeinsamen Ursprung",
|
||||
"description": "Die Historie der Revisionen hat keinen gemeinsamen Urspung und kann somit auch nicht gegen einen solchen verglichen werden."
|
||||
},
|
||||
"65RdZ5atX1": {
|
||||
"displayName": "Fehler beim Löschen von Plugin-Dateien",
|
||||
"description": "Einige Dateien für die Plugin-Deinstallation konnten nicht gelöscht werden. Dieses kann zu Inkonsistenzen führen, so dass der SCM-Manager nicht mehr korrekt starten kann. Bitte prüfen Sie die Logs und bereinigen Sie das Plugin-Verzeichnis des SCM-Managers manuell. Um die Installation eines Plugins abzubrechen, löschen Sie die zugehörige smp Datei aus dem Plugin-Verzeichnis. Um ein Entfernen eines Plugins zu verhindern, entfernen Sie die Datei namens 'uninstall' aus dem entsprechenden Verzeichnis des Plugins."
|
||||
}
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
|
||||
@@ -175,6 +175,14 @@
|
||||
"40RaYIeeR1": {
|
||||
"displayName": "No changes were made",
|
||||
"description": "No changes were made to the files of the repository. Therefor no new commit could be created."
|
||||
},
|
||||
"4iRct4avG1": {
|
||||
"displayName": "The revisions have unrelated histories",
|
||||
"description": "The revisions have unrelated histories. Therefor there is no common commit to compare with."
|
||||
},
|
||||
"65RdZ5atX1": {
|
||||
"displayName": "Error removing plugin files",
|
||||
"description": "Some files to cancel the plugin (un)installation could not be deleted. This can lead to inconsistencies so that the SCM-Manager cannot restart properly. Please check the logs and clean up the plugin folder manually. To cancel the installation of a plugin, remove the corresponding smp file. To cancel the uninstallation, remove the file named 'uninstall' inside the directory for this plugin."
|
||||
}
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
|
||||
@@ -91,7 +91,7 @@ public class DiffResourceTest extends RepositoryTestBase {
|
||||
public void shouldGetDiffs() throws Exception {
|
||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retrieveContent(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retrieveContent()).thenReturn(output -> {});
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get(DIFF_URL + "revision")
|
||||
.accept(VndMediaType.DIFF);
|
||||
@@ -123,7 +123,7 @@ public class DiffResourceTest extends RepositoryTestBase {
|
||||
public void shouldGet404OnMissingRevision() throws Exception {
|
||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retrieveContent(any())).thenThrow(new NotFoundException("Text", "x"));
|
||||
when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Text", "x"));
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get(DIFF_URL + "revision")
|
||||
@@ -139,7 +139,7 @@ public class DiffResourceTest extends RepositoryTestBase {
|
||||
public void shouldGet400OnCrlfInjection() throws Exception {
|
||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retrieveContent(any())).thenThrow(new NotFoundException("Text", "x"));
|
||||
when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Text", "x"));
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get(DIFF_URL + "ny%0D%0ASet-cookie:%20Tamper=3079675143472450634")
|
||||
@@ -153,7 +153,7 @@ public class DiffResourceTest extends RepositoryTestBase {
|
||||
public void shouldGet400OnUnknownFormat() throws Exception {
|
||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retrieveContent(any())).thenThrow(new NotFoundException("Test", "test"));
|
||||
when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Test", "test"));
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get(DIFF_URL + "revision?format=Unknown")
|
||||
@@ -167,7 +167,7 @@ public class DiffResourceTest extends RepositoryTestBase {
|
||||
public void shouldAcceptDiffFormats() throws Exception {
|
||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retrieveContent(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retrieveContent()).thenReturn(output -> {});
|
||||
|
||||
Arrays.stream(DiffFormat.values()).map(DiffFormat::name).forEach(
|
||||
this::assertRequestOk
|
||||
|
||||
@@ -171,7 +171,7 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
|
||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setAncestorChangeset(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retrieveContent(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retrieveContent()).thenReturn(output -> {});
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff")
|
||||
.accept(VndMediaType.DIFF);
|
||||
@@ -206,7 +206,7 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
|
||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setAncestorChangeset(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retrieveContent(any())).thenThrow(new NotFoundException("Text", "x"));
|
||||
when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Text", "x"));
|
||||
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff")
|
||||
@@ -223,7 +223,7 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
|
||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setAncestorChangeset(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retrieveContent(any())).thenThrow(new NotFoundException("Text", "x"));
|
||||
when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Text", "x"));
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get(INCOMING_DIFF_URL + "ny%0D%0ASet-cookie:%20Tamper=3079675143472450634/ny%0D%0ASet-cookie:%20Tamper=3079675143472450634/diff")
|
||||
.accept(VndMediaType.DIFF);
|
||||
@@ -240,7 +240,7 @@ public class IncomingRootResourceTest extends RepositoryTestBase {
|
||||
when(diffCommandBuilder.setRevision(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setAncestorChangeset(anyString())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.setFormat(any())).thenReturn(diffCommandBuilder);
|
||||
when(diffCommandBuilder.retrieveContent(any())).thenThrow(new NotFoundException("Test", "test"));
|
||||
when(diffCommandBuilder.retrieveContent()).thenThrow(new NotFoundException("Test", "test"));
|
||||
MockHttpRequest request = MockHttpRequest
|
||||
.get(INCOMING_DIFF_URL + "src_changeset_id/target_changeset_id/diff?format=Unknown")
|
||||
.accept(VndMediaType.DIFF);
|
||||
|
||||
@@ -3,6 +3,7 @@ package sonia.scm.api.v2.resources;
|
||||
import com.github.sdorra.shiro.SubjectAware;
|
||||
import com.google.common.io.Resources;
|
||||
import com.google.inject.util.Providers;
|
||||
import org.apache.shiro.subject.PrincipalCollection;
|
||||
import org.apache.shiro.subject.Subject;
|
||||
import org.apache.shiro.util.ThreadContext;
|
||||
import org.jboss.resteasy.core.Dispatcher;
|
||||
@@ -24,6 +25,7 @@ import sonia.scm.repository.api.MergeDryRunCommandResult;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
import sonia.scm.repository.spi.MergeCommand;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import java.net.URL;
|
||||
@@ -32,6 +34,7 @@ import static java.util.Arrays.asList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.repository.RepositoryTestData.createHeartOfGold;
|
||||
|
||||
@@ -105,6 +108,7 @@ public class MergeResourceTest extends RepositoryTestBase {
|
||||
@Test
|
||||
void shouldHandleSuccessfulMerge() throws Exception {
|
||||
when(mergeCommand.merge(any())).thenReturn(MergeCommandResult.success());
|
||||
mockUser();
|
||||
|
||||
URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json");
|
||||
byte[] mergeCommandJson = Resources.toByteArray(url);
|
||||
@@ -122,6 +126,7 @@ public class MergeResourceTest extends RepositoryTestBase {
|
||||
@Test
|
||||
void shouldHandleFailedMerge() throws Exception {
|
||||
when(mergeCommand.merge(any())).thenReturn(MergeCommandResult.failure(asList("file1", "file2")));
|
||||
mockUser();
|
||||
|
||||
URL url = Resources.getResource("sonia/scm/api/v2/mergeCommand.json");
|
||||
byte[] mergeCommandJson = Resources.toByteArray(url);
|
||||
@@ -189,5 +194,12 @@ public class MergeResourceTest extends RepositoryTestBase {
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(204);
|
||||
}
|
||||
|
||||
|
||||
private void mockUser() {
|
||||
PrincipalCollection collection = mock(PrincipalCollection.class);
|
||||
when(subject.getPrincipals()).thenReturn(collection);
|
||||
when(collection.oneByType(User.class)).thenReturn(new User("dummy"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,11 +34,8 @@ import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -74,12 +71,12 @@ class PendingPluginResourceTest {
|
||||
void mockMapper() {
|
||||
lenient().when(mapper.mapAvailable(any())).thenAnswer(invocation -> {
|
||||
PluginDto dto = new PluginDto();
|
||||
dto.setName(((AvailablePlugin)invocation.getArgument(0)).getDescriptor().getInformation().getName());
|
||||
dto.setName(((AvailablePlugin) invocation.getArgument(0)).getDescriptor().getInformation().getName());
|
||||
return dto;
|
||||
});
|
||||
lenient().when(mapper.mapInstalled(any(), any())).thenAnswer(invocation -> {
|
||||
PluginDto dto = new PluginDto();
|
||||
dto.setName(((InstalledPlugin)invocation.getArgument(0)).getDescriptor().getInformation().getName());
|
||||
dto.setName(((InstalledPlugin) invocation.getArgument(0)).getDescriptor().getInformation().getName());
|
||||
return dto;
|
||||
});
|
||||
}
|
||||
@@ -90,7 +87,7 @@ class PendingPluginResourceTest {
|
||||
@BeforeEach
|
||||
void bindSubject() {
|
||||
ThreadContext.bind(subject);
|
||||
doNothing().when(subject).checkPermission("plugin:manage");
|
||||
lenient().when(subject.isPermitted("plugin:manage")).thenReturn(true);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@@ -113,7 +110,7 @@ class PendingPluginResourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetPendingAvailablePluginListWithInstallLink() throws URISyntaxException, UnsupportedEncodingException {
|
||||
void shouldGetPendingAvailablePluginListWithInstallAndCancelLink() throws URISyntaxException, UnsupportedEncodingException {
|
||||
AvailablePlugin availablePlugin = createAvailablePlugin("pending-available-plugin");
|
||||
when(availablePlugin.isPending()).thenReturn(true);
|
||||
when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin));
|
||||
@@ -124,6 +121,7 @@ class PendingPluginResourceTest {
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
assertThat(response.getContentAsString()).contains("\"new\":[{\"name\":\"pending-available-plugin\"");
|
||||
assertThat(response.getContentAsString()).contains("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}");
|
||||
assertThat(response.getContentAsString()).contains("\"cancel\":{\"href\":\"/v2/plugins/pending/cancel\"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -166,6 +164,17 @@ class PendingPluginResourceTest {
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
verify(pluginManager).executePendingAndRestart();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCancelPendingPlugins() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.post("/v2/plugins/pending/cancel");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
verify(pluginManager).cancelPending();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
@@ -174,7 +183,7 @@ class PendingPluginResourceTest {
|
||||
@BeforeEach
|
||||
void bindSubject() {
|
||||
ThreadContext.bind(subject);
|
||||
doThrow(new ShiroException()).when(subject).checkPermission("plugin:manage");
|
||||
when(subject.isPermitted("plugin:manage")).thenReturn(false);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@@ -183,23 +192,18 @@ class PendingPluginResourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotListPendingPlugins() throws URISyntaxException {
|
||||
void shouldGetPendingAvailablePluginListWithoutInstallAndCancelLink() throws URISyntaxException, UnsupportedEncodingException {
|
||||
AvailablePlugin availablePlugin = createAvailablePlugin("pending-available-plugin");
|
||||
when(availablePlugin.isPending()).thenReturn(true);
|
||||
when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin));
|
||||
|
||||
MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
verify(pluginManager, never()).executePendingAndRestart();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotExecutePendingPlugins() throws URISyntaxException {
|
||||
MockHttpRequest request = MockHttpRequest.post("/v2/plugins/pending/execute");
|
||||
|
||||
dispatcher.invoke(request, response);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
verify(pluginManager, never()).executePendingAndRestart();
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
|
||||
assertThat(response.getContentAsString()).contains("\"new\":[{\"name\":\"pending-available-plugin\"");
|
||||
assertThat(response.getContentAsString()).doesNotContain("\"execute\":{\"href\":\"/v2/plugins/pending/execute\"}");
|
||||
assertThat(response.getContentAsString()).doesNotContain("\"cancel\":{\"href\":\"/v2/plugins/pending/cancel\"}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,15 +10,19 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.plugin.AvailablePlugin;
|
||||
import sonia.scm.plugin.AvailablePluginDescriptor;
|
||||
import sonia.scm.plugin.InstalledPlugin;
|
||||
import sonia.scm.plugin.InstalledPluginDescriptor;
|
||||
import sonia.scm.plugin.PluginInformation;
|
||||
import sonia.scm.plugin.PluginManager;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -34,6 +38,9 @@ class PluginDtoCollectionMapperTest {
|
||||
@InjectMocks
|
||||
PluginDtoMapperImpl pluginDtoMapper;
|
||||
|
||||
@Mock
|
||||
PluginManager manager;
|
||||
|
||||
Subject subject = mock(Subject.class);
|
||||
ThreadState subjectThreadState = new SubjectThreadState(subject);
|
||||
|
||||
@@ -43,6 +50,11 @@ class PluginDtoCollectionMapperTest {
|
||||
ThreadContext.bind(subject);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void mockPluginManager() {
|
||||
lenient().when(manager.getUpdatable()).thenReturn(new ArrayList<>());
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void unbindSubject() {
|
||||
ThreadContext.unbindSubject();
|
||||
@@ -51,7 +63,7 @@ class PluginDtoCollectionMapperTest {
|
||||
|
||||
@Test
|
||||
void shouldMapInstalledPluginsWithoutUpdateWhenNoNewerVersionIsAvailable() {
|
||||
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper);
|
||||
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager);
|
||||
|
||||
HalRepresentation result = mapper.mapInstalled(
|
||||
singletonList(createInstalledPlugin("scm-some-plugin", "1")),
|
||||
@@ -66,7 +78,7 @@ class PluginDtoCollectionMapperTest {
|
||||
|
||||
@Test
|
||||
void shouldSetNewVersionInInstalledPluginWhenAvailableVersionIsNewer() {
|
||||
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper);
|
||||
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper,manager);
|
||||
|
||||
HalRepresentation result = mapper.mapInstalled(
|
||||
singletonList(createInstalledPlugin("scm-some-plugin", "1")),
|
||||
@@ -80,7 +92,7 @@ class PluginDtoCollectionMapperTest {
|
||||
@Test
|
||||
void shouldNotAddInstallLinkForNewVersionWhenNotPermitted() {
|
||||
when(subject.isPermitted("plugin:manage")).thenReturn(false);
|
||||
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper);
|
||||
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager);
|
||||
|
||||
HalRepresentation result = mapper.mapInstalled(
|
||||
singletonList(createInstalledPlugin("scm-some-plugin", "1")),
|
||||
@@ -93,7 +105,7 @@ class PluginDtoCollectionMapperTest {
|
||||
@Test
|
||||
void shouldNotAddInstallLinkForNewVersionWhenInstallationIsPending() {
|
||||
when(subject.isPermitted("plugin:manage")).thenReturn(true);
|
||||
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper);
|
||||
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager);
|
||||
|
||||
AvailablePlugin availablePlugin = createAvailablePlugin("scm-some-plugin", "2");
|
||||
when(availablePlugin.isPending()).thenReturn(true);
|
||||
@@ -108,7 +120,7 @@ class PluginDtoCollectionMapperTest {
|
||||
@Test
|
||||
void shouldAddInstallLinkForNewVersionWhenPermitted() {
|
||||
when(subject.isPermitted("plugin:manage")).thenReturn(true);
|
||||
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper);
|
||||
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager);
|
||||
|
||||
HalRepresentation result = mapper.mapInstalled(
|
||||
singletonList(createInstalledPlugin("scm-some-plugin", "1")),
|
||||
@@ -121,7 +133,7 @@ class PluginDtoCollectionMapperTest {
|
||||
@Test
|
||||
void shouldSetInstalledPluginPendingWhenCorrespondingAvailablePluginIsPending() {
|
||||
when(subject.isPermitted("plugin:manage")).thenReturn(true);
|
||||
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper);
|
||||
PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager);
|
||||
|
||||
AvailablePlugin availablePlugin = createAvailablePlugin("scm-some-plugin", "2");
|
||||
when(availablePlugin.isPending()).thenReturn(true);
|
||||
|
||||
@@ -20,6 +20,8 @@ import sonia.scm.ScmConstraintViolationException;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.lifecycle.RestartEvent;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@@ -30,11 +32,13 @@ import static java.util.Collections.singletonList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.plugin.PluginTestHelper.createAvailable;
|
||||
@@ -358,6 +362,24 @@ class DefaultPluginManagerTest {
|
||||
verify(mailPlugin).setMarkedForUninstall(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotChangeStateWhenUninstallFileCouldNotBeCreated() {
|
||||
InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin");
|
||||
InstalledPlugin reviewPlugin = createInstalled("scm-review-plugin");
|
||||
when(reviewPlugin.getDescriptor().getDependencies()).thenReturn(singleton("scm-mail-plugin"));
|
||||
|
||||
when(reviewPlugin.getDirectory()).thenThrow(new PluginException("when the file could not be written an exception like this is thrown"));
|
||||
|
||||
when(loader.getInstalledPlugins()).thenReturn(asList(mailPlugin, reviewPlugin));
|
||||
|
||||
manager.computeInstallationDependencies();
|
||||
|
||||
assertThrows(PluginException.class, () -> manager.uninstall("scm-review-plugin", false));
|
||||
|
||||
verify(mailPlugin, never()).setMarkedForUninstall(true);
|
||||
assertThrows(ScmConstraintViolationException.class, () -> manager.uninstall("scm-mail-plugin", false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenUninstallingCorePlugin(@TempDirectory.TempDir Path temp) {
|
||||
InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin");
|
||||
@@ -427,6 +449,71 @@ class DefaultPluginManagerTest {
|
||||
|
||||
verify(eventBus).post(any(RestartEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUndoPendingInstallations(@TempDirectory.TempDir Path temp) throws IOException {
|
||||
InstalledPlugin mailPlugin = createInstalled("scm-ssh-plugin");
|
||||
Path mailPluginPath = temp.resolve("scm-mail-plugin");
|
||||
Files.createDirectories(mailPluginPath);
|
||||
when(mailPlugin.getDirectory()).thenReturn(mailPluginPath);
|
||||
when(loader.getInstalledPlugins()).thenReturn(singletonList(mailPlugin));
|
||||
ArgumentCaptor<Boolean> uninstallCaptor = ArgumentCaptor.forClass(Boolean.class);
|
||||
doNothing().when(mailPlugin).setMarkedForUninstall(uninstallCaptor.capture());
|
||||
|
||||
AvailablePlugin git = createAvailable("scm-git-plugin");
|
||||
when(center.getAvailable()).thenReturn(ImmutableSet.of(git));
|
||||
PendingPluginInstallation gitPendingPluginInformation = mock(PendingPluginInstallation.class);
|
||||
when(installer.install(git)).thenReturn(gitPendingPluginInformation);
|
||||
|
||||
manager.install("scm-git-plugin", false);
|
||||
manager.uninstall("scm-ssh-plugin", false);
|
||||
|
||||
manager.cancelPending();
|
||||
|
||||
assertThat(mailPluginPath.resolve("uninstall")).doesNotExist();
|
||||
verify(gitPendingPluginInformation).cancel();
|
||||
Boolean lasUninstallMarkerSet = uninstallCaptor.getAllValues().get(uninstallCaptor.getAllValues().size() - 1);
|
||||
assertThat(lasUninstallMarkerSet).isFalse();
|
||||
|
||||
Files.createFile(mailPluginPath.resolve("uninstall"));
|
||||
|
||||
manager.cancelPending();
|
||||
verify(gitPendingPluginInformation, times(1)).cancel();
|
||||
assertThat(mailPluginPath.resolve("uninstall")).exists();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUpdateAllPlugins() {
|
||||
InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin");
|
||||
InstalledPlugin reviewPlugin = createInstalled("scm-review-plugin");
|
||||
|
||||
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(mailPlugin, reviewPlugin));
|
||||
|
||||
AvailablePlugin newMailPlugin = createAvailable("scm-mail-plugin", "2.0.0");
|
||||
AvailablePlugin newReviewPlugin = createAvailable("scm-review-plugin", "2.0.0");
|
||||
|
||||
when(center.getAvailable()).thenReturn(ImmutableSet.of(newMailPlugin, newReviewPlugin));
|
||||
|
||||
manager.updateAll();
|
||||
|
||||
verify(installer).install(newMailPlugin);
|
||||
verify(installer).install(newReviewPlugin);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void shouldNotUpdateToOldPluginVersions() {
|
||||
InstalledPlugin scriptPlugin = createInstalled("scm-script-plugin");
|
||||
|
||||
when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(scriptPlugin));
|
||||
AvailablePlugin oldScriptPlugin = createAvailable("scm-script-plugin", "0.9");
|
||||
|
||||
when(center.getAvailable()).thenReturn(ImmutableSet.of(oldScriptPlugin));
|
||||
|
||||
manager.updateAll();
|
||||
|
||||
verify(installer, never()).install(oldScriptPlugin);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@@ -482,5 +569,14 @@ class DefaultPluginManagerTest {
|
||||
assertThrows(AuthorizationException.class, () -> manager.executePendingAndRestart());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowAuthorizationExceptionsForCancelPending() {
|
||||
assertThrows(AuthorizationException.class, () -> manager.cancelPending());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowAuthorizationExceptionsForUpdateAll() {
|
||||
assertThrows(AuthorizationException.class, () -> manager.updateAll());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,13 @@ public class PluginTestHelper {
|
||||
return createAvailable(information);
|
||||
}
|
||||
|
||||
public static AvailablePlugin createAvailable(String name, String version) {
|
||||
PluginInformation information = new PluginInformation();
|
||||
information.setName(name);
|
||||
information.setVersion(version);
|
||||
return createAvailable(information);
|
||||
}
|
||||
|
||||
public static InstalledPlugin createInstalled(String name) {
|
||||
PluginInformation information = new PluginInformation();
|
||||
information.setName(name);
|
||||
|
||||
Reference in New Issue
Block a user