mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-12 08:25:44 +01:00
@@ -66,6 +66,10 @@ public enum Command
|
|||||||
/**
|
/**
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
*/
|
*/
|
||||||
MODIFICATIONS
|
MODIFICATIONS,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 2.0
|
||||||
|
*/
|
||||||
|
MERGE
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package sonia.scm.repository.api;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import sonia.scm.repository.Person;
|
||||||
|
import sonia.scm.repository.spi.MergeCommand;
|
||||||
|
import sonia.scm.repository.spi.MergeCommandRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this {@link MergeCommandBuilder} to merge two branches of a repository ({@link #executeMerge()}) or to check if
|
||||||
|
* the branches could be merged without conflicts ({@link #dryRun()}). To do so, you have to specify the name of
|
||||||
|
* the target branch ({@link #setTargetBranch(String)}) and the name of the branch that should be merged
|
||||||
|
* ({@link #setBranchToMerge(String)}). Additionally you can specify an author that should be used for the commit
|
||||||
|
* ({@link #setAuthor(Person)}) and a message template ({@link #setMessageTemplate(String)}) if you are not doing a dry
|
||||||
|
* run only. If no author is specified, the logged in user and a default message will be used instead.
|
||||||
|
*
|
||||||
|
* To actually merge <code>feature_branch</code> into <code>integration_branch</code> do this:
|
||||||
|
* <pre><code>
|
||||||
|
* repositoryService.gerMergeCommand()
|
||||||
|
* .setBranchToMerge("feature_branch")
|
||||||
|
* .setTargetBranch("integration_branch")
|
||||||
|
* .executeMerge();
|
||||||
|
* </code></pre>
|
||||||
|
*
|
||||||
|
* If the merge is successful, the result will look like this:
|
||||||
|
* <pre><code>
|
||||||
|
* O <- Merge result (new head of integration_branch)
|
||||||
|
* |\
|
||||||
|
* | \
|
||||||
|
* old integration_branch -> O O <- feature_branch
|
||||||
|
* | |
|
||||||
|
* O O
|
||||||
|
* </code></pre>
|
||||||
|
*
|
||||||
|
* To check whether they can be merged without conflicts beforehand do this:
|
||||||
|
* <pre><code>
|
||||||
|
* repositoryService.gerMergeCommand()
|
||||||
|
* .setBranchToMerge("feature_branch")
|
||||||
|
* .setTargetBranch("integration_branch")
|
||||||
|
* .dryRun()
|
||||||
|
* .isMergeable();
|
||||||
|
* </code></pre>
|
||||||
|
*
|
||||||
|
* Keep in mind that you should <em>always</em> check the result of a merge even though you may have done a dry run
|
||||||
|
* beforehand, because the branches can change between the dry run and the actual merge.
|
||||||
|
*
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public class MergeCommandBuilder {
|
||||||
|
|
||||||
|
private final MergeCommand mergeCommand;
|
||||||
|
private final MergeCommandRequest request = new MergeCommandRequest();
|
||||||
|
|
||||||
|
MergeCommandBuilder(MergeCommand mergeCommand) {
|
||||||
|
this.mergeCommand = mergeCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this to set the branch that should be merged into the target branch.
|
||||||
|
*
|
||||||
|
* <b>This is mandatory.</b>
|
||||||
|
*
|
||||||
|
* @return This builder instance.
|
||||||
|
*/
|
||||||
|
public MergeCommandBuilder setBranchToMerge(String branchToMerge) {
|
||||||
|
request.setBranchToMerge(branchToMerge);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this to set the target branch the other branch should be merged into.
|
||||||
|
*
|
||||||
|
* <b>This is mandatory.</b>
|
||||||
|
*
|
||||||
|
* @return This builder instance.
|
||||||
|
*/
|
||||||
|
public MergeCommandBuilder setTargetBranch(String targetBranch) {
|
||||||
|
request.setTargetBranch(targetBranch);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this to set the author of the merge commit manually. If this is omitted, the currently logged in user will be
|
||||||
|
* used instead.
|
||||||
|
*
|
||||||
|
* This is optional and for {@link #executeMerge()} only.
|
||||||
|
*
|
||||||
|
* @return This builder instance.
|
||||||
|
*/
|
||||||
|
public MergeCommandBuilder setAuthor(Person author) {
|
||||||
|
request.setAuthor(author);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this to set a template for the commit message. If no message is set, a default message will be used.
|
||||||
|
*
|
||||||
|
* You can use the placeholder <code>{0}</code> for the branch to be merged and <code>{1}</code> for the target
|
||||||
|
* branch, eg.:
|
||||||
|
*
|
||||||
|
* <pre><code>
|
||||||
|
* ...setMessageTemplate("Merge of {0} into {1}")...
|
||||||
|
* </code></pre>
|
||||||
|
*
|
||||||
|
* This is optional and for {@link #executeMerge()} only.
|
||||||
|
*
|
||||||
|
* @return This builder instance.
|
||||||
|
*/
|
||||||
|
public MergeCommandBuilder setMessageTemplate(String messageTemplate) {
|
||||||
|
request.setMessageTemplate(messageTemplate);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this to reset the command.
|
||||||
|
* @return This builder instance.
|
||||||
|
*/
|
||||||
|
public MergeCommandBuilder reset() {
|
||||||
|
request.reset();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this to actually do the merge. If an automatic merge is not possible, {@link MergeCommandResult#isSuccess()}
|
||||||
|
* will return <code>false</code>.
|
||||||
|
*
|
||||||
|
* @return The result of the merge.
|
||||||
|
*/
|
||||||
|
public MergeCommandResult executeMerge() {
|
||||||
|
Preconditions.checkArgument(request.isValid(), "revision to merge and target revision is required");
|
||||||
|
return mergeCommand.merge(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this to check whether the given branches can be merged autmatically. If this is possible,
|
||||||
|
* {@link MergeDryRunCommandResult#isMergeable()} will return <code>true</code>.
|
||||||
|
*
|
||||||
|
* @return The result whether the given branches can be merged automatically.
|
||||||
|
*/
|
||||||
|
public MergeDryRunCommandResult dryRun() {
|
||||||
|
Preconditions.checkArgument(request.isValid(), "revision to merge and target revision is required");
|
||||||
|
return mergeCommand.dryRun(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package sonia.scm.repository.api;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
|
||||||
|
import static java.util.Collections.emptyList;
|
||||||
|
import static java.util.Collections.unmodifiableCollection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class keeps the result of a merge of branches. Use {@link #isSuccess()} to check whether the merge was
|
||||||
|
* sucessfully executed. If the result is <code>false</code> the merge could not be done without conflicts. In this
|
||||||
|
* case you can use {@link #getFilesWithConflict()} to get a list of files with merge conflicts.
|
||||||
|
*/
|
||||||
|
public class MergeCommandResult {
|
||||||
|
private final Collection<String> filesWithConflict;
|
||||||
|
|
||||||
|
private MergeCommandResult(Collection<String> filesWithConflict) {
|
||||||
|
this.filesWithConflict = filesWithConflict;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MergeCommandResult success() {
|
||||||
|
return new MergeCommandResult(emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MergeCommandResult failure(Collection<String> filesWithConflict) {
|
||||||
|
return new MergeCommandResult(new HashSet<>(filesWithConflict));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this returns <code>true</code>, the merge was successfull. If this returns <code>false</code> there were
|
||||||
|
* merge conflicts. In this case you can use {@link #getFilesWithConflict()} to check what files could not be merged.
|
||||||
|
*/
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return filesWithConflict.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the merge was not successful ({@link #isSuccess()} returns <code>false</code>) this will give you a list of
|
||||||
|
* file paths that could not be merged automatically.
|
||||||
|
*/
|
||||||
|
public Collection<String> getFilesWithConflict() {
|
||||||
|
return unmodifiableCollection(filesWithConflict);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package sonia.scm.repository.api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class keeps the result of a merge dry run. Use {@link #isMergeable()} to check whether an automatic merge is
|
||||||
|
* possible or not.
|
||||||
|
*/
|
||||||
|
public class MergeDryRunCommandResult {
|
||||||
|
|
||||||
|
private final boolean mergeable;
|
||||||
|
|
||||||
|
public MergeDryRunCommandResult(boolean mergeable) {
|
||||||
|
this.mergeable = mergeable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will return <code>true</code>, when an automatic merge is possible <em>at the moment</em>; <code>false</code>
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isMergeable() {
|
||||||
|
return mergeable;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -79,6 +79,7 @@ import java.util.stream.Stream;
|
|||||||
* @apiviz.uses sonia.scm.repository.api.PushCommandBuilder
|
* @apiviz.uses sonia.scm.repository.api.PushCommandBuilder
|
||||||
* @apiviz.uses sonia.scm.repository.api.BundleCommandBuilder
|
* @apiviz.uses sonia.scm.repository.api.BundleCommandBuilder
|
||||||
* @apiviz.uses sonia.scm.repository.api.UnbundleCommandBuilder
|
* @apiviz.uses sonia.scm.repository.api.UnbundleCommandBuilder
|
||||||
|
* @apiviz.uses sonia.scm.repository.api.MergeCommandBuilder
|
||||||
* @since 1.17
|
* @since 1.17
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -353,6 +354,22 @@ public final class RepositoryService implements Closeable {
|
|||||||
repository);
|
repository);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The merge command executes a merge of two branches. It is possible to do a dry run to check, whether the given
|
||||||
|
* branches can be merged without conflicts.
|
||||||
|
*
|
||||||
|
* @return instance of {@link MergeCommandBuilder}
|
||||||
|
* @throws CommandNotSupportedException if the command is not supported
|
||||||
|
* by the implementation of the repository service provider.
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
public MergeCommandBuilder gerMergeCommand() {
|
||||||
|
logger.debug("create unbundle command for repository {}",
|
||||||
|
repository.getNamespaceAndName());
|
||||||
|
|
||||||
|
return new MergeCommandBuilder(provider.getMergeCommand());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the command is supported by the repository service.
|
* Returns true if the command is supported by the repository service.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import sonia.scm.repository.api.MergeCommandResult;
|
||||||
|
import sonia.scm.repository.api.MergeDryRunCommandResult;
|
||||||
|
|
||||||
|
public interface MergeCommand {
|
||||||
|
MergeCommandResult merge(MergeCommandRequest request);
|
||||||
|
|
||||||
|
MergeDryRunCommandResult dryRun(MergeCommandRequest request);
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import com.google.common.base.MoreObjects;
|
||||||
|
import com.google.common.base.Objects;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import sonia.scm.Validateable;
|
||||||
|
import sonia.scm.repository.Person;
|
||||||
|
import sonia.scm.util.Util;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public class MergeCommandRequest implements Validateable, Resetable, Serializable, Cloneable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -2650236557922431528L;
|
||||||
|
|
||||||
|
private String branchToMerge;
|
||||||
|
private String targetBranch;
|
||||||
|
private Person author;
|
||||||
|
private String messageTemplate;
|
||||||
|
|
||||||
|
public String getBranchToMerge() {
|
||||||
|
return branchToMerge;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBranchToMerge(String branchToMerge) {
|
||||||
|
this.branchToMerge = branchToMerge;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTargetBranch() {
|
||||||
|
return targetBranch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTargetBranch(String targetBranch) {
|
||||||
|
this.targetBranch = targetBranch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Person getAuthor() {
|
||||||
|
return author;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuthor(Person author) {
|
||||||
|
this.author = author;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessageTemplate() {
|
||||||
|
return messageTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessageTemplate(String messageTemplate) {
|
||||||
|
this.messageTemplate = messageTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValid() {
|
||||||
|
return !Strings.isNullOrEmpty(getBranchToMerge())
|
||||||
|
&& !Strings.isNullOrEmpty(getTargetBranch());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
this.setBranchToMerge(null);
|
||||||
|
this.setTargetBranch(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (obj == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getClass() != obj.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final MergeCommandRequest other = (MergeCommandRequest) obj;
|
||||||
|
|
||||||
|
return Objects.equal(branchToMerge, other.branchToMerge)
|
||||||
|
&& Objects.equal(targetBranch, other.targetBranch)
|
||||||
|
&& Objects.equal(author, other.author);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hashCode(branchToMerge, targetBranch, author);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return MoreObjects.toStringHelper(this)
|
||||||
|
.add("branchToMerge", branchToMerge)
|
||||||
|
.add("targetBranch", targetBranch)
|
||||||
|
.add("author", author)
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -251,4 +251,12 @@ public abstract class RepositoryServiceProvider implements Closeable
|
|||||||
{
|
{
|
||||||
throw new CommandNotSupportedException(Command.UNBUNDLE);
|
throw new CommandNotSupportedException(Command.UNBUNDLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since 2.0
|
||||||
|
*/
|
||||||
|
public MergeCommand getMergeCommand()
|
||||||
|
{
|
||||||
|
throw new CommandNotSupportedException(Command.MERGE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package sonia.scm.repository;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class CloseableWrapper<C> implements AutoCloseable {
|
||||||
|
|
||||||
|
private final C wrapped;
|
||||||
|
private final Consumer<C> cleanup;
|
||||||
|
|
||||||
|
public CloseableWrapper(C wrapped, Consumer<C> cleanup) {
|
||||||
|
this.wrapped = wrapped;
|
||||||
|
this.cleanup = cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
public C get() { return wrapped; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
try {
|
||||||
|
cleanup.accept(wrapped);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
throw new RuntimeException(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -91,6 +91,8 @@ public class GitRepositoryHandler
|
|||||||
|
|
||||||
private final Scheduler scheduler;
|
private final Scheduler scheduler;
|
||||||
|
|
||||||
|
private final GitWorkdirFactory workdirFactory;
|
||||||
|
|
||||||
private Task task;
|
private Task task;
|
||||||
|
|
||||||
//~--- constructors ---------------------------------------------------------
|
//~--- constructors ---------------------------------------------------------
|
||||||
@@ -104,10 +106,11 @@ public class GitRepositoryHandler
|
|||||||
* @param scheduler
|
* @param scheduler
|
||||||
*/
|
*/
|
||||||
@Inject
|
@Inject
|
||||||
public GitRepositoryHandler(ConfigurationStoreFactory storeFactory, FileSystem fileSystem, Scheduler scheduler)
|
public GitRepositoryHandler(ConfigurationStoreFactory storeFactory, FileSystem fileSystem, Scheduler scheduler, GitWorkdirFactory workdirFactory)
|
||||||
{
|
{
|
||||||
super(storeFactory, fileSystem);
|
super(storeFactory, fileSystem);
|
||||||
this.scheduler = scheduler;
|
this.scheduler = scheduler;
|
||||||
|
this.workdirFactory = workdirFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
//~--- get methods ----------------------------------------------------------
|
//~--- get methods ----------------------------------------------------------
|
||||||
@@ -234,4 +237,8 @@ public class GitRepositoryHandler
|
|||||||
{
|
{
|
||||||
return new File(directory, DIRECTORY_REFS).exists();
|
return new File(directory, DIRECTORY_REFS).exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GitWorkdirFactory getWorkdirFactory() {
|
||||||
|
return workdirFactory;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package sonia.scm.repository;
|
||||||
|
|
||||||
|
import sonia.scm.repository.spi.GitContext;
|
||||||
|
import sonia.scm.repository.spi.WorkingCopy;
|
||||||
|
|
||||||
|
public interface GitWorkdirFactory {
|
||||||
|
WorkingCopy createWorkingCopy(GitContext gitContext);
|
||||||
|
}
|
||||||
@@ -106,6 +106,10 @@ public class GitContext implements Closeable
|
|||||||
return repository;
|
return repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File getDirectory() {
|
||||||
|
return directory;
|
||||||
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
//~--- fields ---------------------------------------------------------------
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import org.apache.shiro.SecurityUtils;
|
||||||
|
import org.apache.shiro.subject.Subject;
|
||||||
|
import org.eclipse.jgit.api.Git;
|
||||||
|
import org.eclipse.jgit.api.MergeResult;
|
||||||
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
|
import org.eclipse.jgit.lib.ObjectId;
|
||||||
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
import org.eclipse.jgit.merge.MergeStrategy;
|
||||||
|
import org.eclipse.jgit.merge.ResolveMerger;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.repository.GitWorkdirFactory;
|
||||||
|
import sonia.scm.repository.InternalRepositoryException;
|
||||||
|
import sonia.scm.repository.Person;
|
||||||
|
import sonia.scm.repository.api.MergeCommandResult;
|
||||||
|
import sonia.scm.repository.api.MergeDryRunCommandResult;
|
||||||
|
import sonia.scm.user.User;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
|
||||||
|
public class GitMergeCommand extends AbstractGitCommand implements MergeCommand {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(GitMergeCommand.class);
|
||||||
|
|
||||||
|
private static final String MERGE_COMMIT_MESSAGE_TEMPLATE = String.join("\n",
|
||||||
|
"Merge of branch {0} into {1}",
|
||||||
|
"",
|
||||||
|
"Automatic merge by SCM-Manager.");
|
||||||
|
|
||||||
|
private final GitWorkdirFactory workdirFactory;
|
||||||
|
|
||||||
|
GitMergeCommand(GitContext context, sonia.scm.repository.Repository repository, GitWorkdirFactory workdirFactory) {
|
||||||
|
super(context, repository);
|
||||||
|
this.workdirFactory = workdirFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MergeCommandResult merge(MergeCommandRequest request) {
|
||||||
|
try (WorkingCopy workingCopy = workdirFactory.createWorkingCopy(context)) {
|
||||||
|
Repository repository = workingCopy.get();
|
||||||
|
logger.debug("cloned repository to folder {}", repository.getWorkTree());
|
||||||
|
return new MergeWorker(repository, request).merge();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new InternalRepositoryException("could not clone repository for merge", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MergeDryRunCommandResult dryRun(MergeCommandRequest request) {
|
||||||
|
try {
|
||||||
|
Repository repository = context.open();
|
||||||
|
ResolveMerger merger = (ResolveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
|
||||||
|
return new MergeDryRunCommandResult(merger.merge(repository.resolve(request.getBranchToMerge()), repository.resolve(request.getTargetBranch())));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new InternalRepositoryException("could not clone repository for merge", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class MergeWorker {
|
||||||
|
|
||||||
|
private final String target;
|
||||||
|
private final String toMerge;
|
||||||
|
private final Person author;
|
||||||
|
private final Git clone;
|
||||||
|
private final String messageTemplate;
|
||||||
|
|
||||||
|
private MergeWorker(Repository clone, MergeCommandRequest request) {
|
||||||
|
this.target = request.getTargetBranch();
|
||||||
|
this.toMerge = request.getBranchToMerge();
|
||||||
|
this.author = request.getAuthor();
|
||||||
|
this.messageTemplate = request.getMessageTemplate();
|
||||||
|
this.clone = new Git(clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MergeCommandResult merge() throws IOException {
|
||||||
|
checkOutTargetBranch();
|
||||||
|
MergeResult result = doMergeInClone();
|
||||||
|
if (result.getMergeStatus().isSuccessful()) {
|
||||||
|
doCommit();
|
||||||
|
push();
|
||||||
|
return MergeCommandResult.success();
|
||||||
|
} else {
|
||||||
|
return analyseFailure(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkOutTargetBranch() {
|
||||||
|
try {
|
||||||
|
clone.checkout().setName(target).call();
|
||||||
|
} catch (GitAPIException e) {
|
||||||
|
throw new InternalRepositoryException("could not checkout target branch for merge: " + target, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MergeResult doMergeInClone() throws IOException {
|
||||||
|
MergeResult result;
|
||||||
|
try {
|
||||||
|
result = clone.merge()
|
||||||
|
.setCommit(false) // we want to set the author manually
|
||||||
|
.include(toMerge, resolveRevision(toMerge))
|
||||||
|
.call();
|
||||||
|
} catch (GitAPIException e) {
|
||||||
|
throw new InternalRepositoryException("could not merge branch " + toMerge + " into " + target, e);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doCommit() {
|
||||||
|
logger.debug("merged branch {} into {}", toMerge, target);
|
||||||
|
Person authorToUse = determineAuthor();
|
||||||
|
try {
|
||||||
|
clone.commit()
|
||||||
|
.setAuthor(authorToUse.getName(), authorToUse.getMail())
|
||||||
|
.setMessage(MessageFormat.format(determineMessageTemplate(), toMerge, target))
|
||||||
|
.call();
|
||||||
|
} catch (GitAPIException e) {
|
||||||
|
throw new InternalRepositoryException("could not commit merge between branch " + toMerge + " and " + target, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String determineMessageTemplate() {
|
||||||
|
if (Strings.isNullOrEmpty(messageTemplate)) {
|
||||||
|
return MERGE_COMMIT_MESSAGE_TEMPLATE;
|
||||||
|
} else {
|
||||||
|
return messageTemplate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Person determineAuthor() {
|
||||||
|
if (author == null) {
|
||||||
|
Subject subject = SecurityUtils.getSubject();
|
||||||
|
User user = subject.getPrincipals().oneByType(User.class);
|
||||||
|
String name = user.getDisplayName();
|
||||||
|
String email = user.getMail();
|
||||||
|
logger.debug("no author set; using logged in user: {} <{}>", name, email);
|
||||||
|
return new Person(name, email);
|
||||||
|
} else {
|
||||||
|
return author;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void push() {
|
||||||
|
try {
|
||||||
|
clone.push().call();
|
||||||
|
} catch (GitAPIException e) {
|
||||||
|
throw new InternalRepositoryException("could not push merged branch " + toMerge + " to origin", e);
|
||||||
|
}
|
||||||
|
logger.debug("pushed merged branch {}", target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MergeCommandResult analyseFailure(MergeResult result) {
|
||||||
|
logger.info("could not merged branch {} into {} due to conflict in paths {}", toMerge, target, result.getConflicts().keySet());
|
||||||
|
return MergeCommandResult.failure(result.getConflicts().keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectId resolveRevision(String branchToMerge) throws IOException {
|
||||||
|
ObjectId resolved = clone.getRepository().resolve(branchToMerge);
|
||||||
|
if (resolved == null) {
|
||||||
|
return clone.getRepository().resolve("origin/" + branchToMerge);
|
||||||
|
} else {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,7 +63,8 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
|
|||||||
Command.INCOMING,
|
Command.INCOMING,
|
||||||
Command.OUTGOING,
|
Command.OUTGOING,
|
||||||
Command.PUSH,
|
Command.PUSH,
|
||||||
Command.PULL
|
Command.PULL,
|
||||||
|
Command.MERGE
|
||||||
);
|
);
|
||||||
//J+
|
//J+
|
||||||
|
|
||||||
@@ -240,6 +241,11 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider
|
|||||||
return new GitTagsCommand(context, repository);
|
return new GitTagsCommand(context, repository);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MergeCommand getMergeCommand() {
|
||||||
|
return new GitMergeCommand(context, repository, handler.getWorkdirFactory());
|
||||||
|
}
|
||||||
|
|
||||||
//~--- fields ---------------------------------------------------------------
|
//~--- fields ---------------------------------------------------------------
|
||||||
|
|
||||||
/** Field description */
|
/** Field description */
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.api.Git;
|
||||||
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
import org.eclipse.jgit.util.FileUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import sonia.scm.repository.GitWorkdirFactory;
|
||||||
|
import sonia.scm.repository.InternalRepositoryException;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
|
||||||
|
public class SimpleGitWorkdirFactory implements GitWorkdirFactory {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(SimpleGitWorkdirFactory.class);
|
||||||
|
|
||||||
|
private final File poolDirectory;
|
||||||
|
|
||||||
|
public SimpleGitWorkdirFactory() {
|
||||||
|
this(new File(System.getProperty("java.io.tmpdir"), "scmm-git-pool"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimpleGitWorkdirFactory(File poolDirectory) {
|
||||||
|
this.poolDirectory = poolDirectory;
|
||||||
|
poolDirectory.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
public WorkingCopy createWorkingCopy(GitContext gitContext) {
|
||||||
|
try {
|
||||||
|
Repository clone = cloneRepository(gitContext.getDirectory(), createNewWorkdir());
|
||||||
|
return new WorkingCopy(clone, this::close);
|
||||||
|
} catch (GitAPIException e) {
|
||||||
|
throw new InternalRepositoryException("could not clone working copy of repository", e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new InternalRepositoryException("could not create temporary directory for clone of repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private File createNewWorkdir() throws IOException {
|
||||||
|
return Files.createTempDirectory(poolDirectory.toPath(),"workdir").toFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Repository cloneRepository(File bareRepository, File target) throws GitAPIException {
|
||||||
|
return Git.cloneRepository()
|
||||||
|
.setURI(bareRepository.getAbsolutePath())
|
||||||
|
.setDirectory(target)
|
||||||
|
.call()
|
||||||
|
.getRepository();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void close(Repository repository) {
|
||||||
|
repository.close();
|
||||||
|
try {
|
||||||
|
FileUtils.delete(repository.getWorkTree(), FileUtils.RECURSIVE);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.warn("could not delete temporary git workdir '{}'", repository.getWorkTree(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
import sonia.scm.repository.CloseableWrapper;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class WorkingCopy extends CloseableWrapper<Repository> {
|
||||||
|
WorkingCopy(Repository wrapped, Consumer<Repository> cleanup) {
|
||||||
|
super(wrapped, cleanup);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,8 @@ import org.mapstruct.factory.Mappers;
|
|||||||
import sonia.scm.api.v2.resources.GitConfigDtoToGitConfigMapper;
|
import sonia.scm.api.v2.resources.GitConfigDtoToGitConfigMapper;
|
||||||
import sonia.scm.api.v2.resources.GitConfigToGitConfigDtoMapper;
|
import sonia.scm.api.v2.resources.GitConfigToGitConfigDtoMapper;
|
||||||
import sonia.scm.plugin.Extension;
|
import sonia.scm.plugin.Extension;
|
||||||
|
import sonia.scm.repository.GitWorkdirFactory;
|
||||||
|
import sonia.scm.repository.spi.SimpleGitWorkdirFactory;
|
||||||
import sonia.scm.web.lfs.LfsBlobStoreFactory;
|
import sonia.scm.web.lfs.LfsBlobStoreFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,5 +65,7 @@ public class GitServletModule extends ServletModule
|
|||||||
|
|
||||||
bind(GitConfigDtoToGitConfigMapper.class).to(Mappers.getMapper(GitConfigDtoToGitConfigMapper.class).getClass());
|
bind(GitConfigDtoToGitConfigMapper.class).to(Mappers.getMapper(GitConfigDtoToGitConfigMapper.class).getClass());
|
||||||
bind(GitConfigToGitConfigDtoMapper.class).to(Mappers.getMapper(GitConfigToGitConfigDtoMapper.class).getClass());
|
bind(GitConfigToGitConfigDtoMapper.class).to(Mappers.getMapper(GitConfigToGitConfigDtoMapper.class).getClass());
|
||||||
|
|
||||||
|
bind(GitWorkdirFactory.class).to(SimpleGitWorkdirFactory.class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package sonia.scm.repository;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.spy;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
public class CloseableWrapperTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldExecuteGivenMethodAtClose() {
|
||||||
|
Consumer<String> wrapped = new Consumer<String>() {
|
||||||
|
// no this cannot be replaced with a lambda because otherwise we could not use Mockito#spy
|
||||||
|
@Override
|
||||||
|
public void accept(String s) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Consumer<String> closer = spy(wrapped);
|
||||||
|
|
||||||
|
try (CloseableWrapper<String> wrapper = new CloseableWrapper<>("test", closer)) {
|
||||||
|
// nothing to do here
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(closer).accept("test");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,6 +61,9 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
|
|||||||
@Mock
|
@Mock
|
||||||
private ConfigurationStoreFactory factory;
|
private ConfigurationStoreFactory factory;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private GitWorkdirFactory gitWorkdirFactory;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void checkDirectory(File directory) {
|
protected void checkDirectory(File directory) {
|
||||||
File head = new File(directory, "HEAD");
|
File head = new File(directory, "HEAD");
|
||||||
@@ -84,7 +87,7 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
|
|||||||
protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory,
|
protected RepositoryHandler createRepositoryHandler(ConfigurationStoreFactory factory,
|
||||||
File directory) {
|
File directory) {
|
||||||
GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory,
|
GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory,
|
||||||
new DefaultFileSystem(), scheduler);
|
new DefaultFileSystem(), scheduler, gitWorkdirFactory);
|
||||||
|
|
||||||
repositoryHandler.init(contextProvider);
|
repositoryHandler.init(contextProvider);
|
||||||
|
|
||||||
@@ -100,7 +103,7 @@ public class GitRepositoryHandlerTest extends SimpleRepositoryHandlerTestBase {
|
|||||||
@Test
|
@Test
|
||||||
public void getDirectory() {
|
public void getDirectory() {
|
||||||
GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory,
|
GitRepositoryHandler repositoryHandler = new GitRepositoryHandler(factory,
|
||||||
new DefaultFileSystem(), scheduler);
|
new DefaultFileSystem(), scheduler, gitWorkdirFactory);
|
||||||
|
|
||||||
GitConfig gitConfig = new GitConfig();
|
GitConfig gitConfig = new GitConfig();
|
||||||
gitConfig.setRepositoryDirectory(new File("/path"));
|
gitConfig.setRepositoryDirectory(new File("/path"));
|
||||||
|
|||||||
@@ -50,8 +50,10 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase
|
|||||||
@After
|
@After
|
||||||
public void close()
|
public void close()
|
||||||
{
|
{
|
||||||
|
if (context != null) {
|
||||||
context.close();
|
context.close();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Method description
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import com.github.sdorra.shiro.ShiroRule;
|
||||||
|
import com.github.sdorra.shiro.SubjectAware;
|
||||||
|
import org.apache.shiro.subject.SimplePrincipalCollection;
|
||||||
|
import org.apache.shiro.subject.Subject;
|
||||||
|
import org.eclipse.jgit.api.Git;
|
||||||
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
|
import org.eclipse.jgit.lib.PersonIdent;
|
||||||
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
import org.eclipse.jgit.revwalk.RevCommit;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import sonia.scm.repository.Person;
|
||||||
|
import sonia.scm.repository.api.MergeCommandResult;
|
||||||
|
import sonia.scm.user.User;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini")
|
||||||
|
public class GitMergeCommandTest extends AbstractGitCommandTestBase {
|
||||||
|
|
||||||
|
private static final String REALM = "AdminRealm";
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public ShiroRule shiro = new ShiroRule();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldDetectMergeableBranches() {
|
||||||
|
GitMergeCommand command = createCommand();
|
||||||
|
MergeCommandRequest request = new MergeCommandRequest();
|
||||||
|
request.setBranchToMerge("mergeable");
|
||||||
|
request.setTargetBranch("master");
|
||||||
|
|
||||||
|
boolean mergeable = command.dryRun(request).isMergeable();
|
||||||
|
|
||||||
|
assertThat(mergeable).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldDetectNotMergeableBranches() {
|
||||||
|
GitMergeCommand command = createCommand();
|
||||||
|
MergeCommandRequest request = new MergeCommandRequest();
|
||||||
|
request.setBranchToMerge("test-branch");
|
||||||
|
request.setTargetBranch("master");
|
||||||
|
|
||||||
|
boolean mergeable = command.dryRun(request).isMergeable();
|
||||||
|
|
||||||
|
assertThat(mergeable).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldMergeMergeableBranches() throws IOException, GitAPIException {
|
||||||
|
GitMergeCommand command = createCommand();
|
||||||
|
MergeCommandRequest request = new MergeCommandRequest();
|
||||||
|
request.setTargetBranch("master");
|
||||||
|
request.setBranchToMerge("mergeable");
|
||||||
|
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
|
||||||
|
|
||||||
|
MergeCommandResult mergeCommandResult = command.merge(request);
|
||||||
|
|
||||||
|
assertThat(mergeCommandResult.isSuccess()).isTrue();
|
||||||
|
|
||||||
|
Repository repository = createContext().open();
|
||||||
|
Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
|
||||||
|
RevCommit mergeCommit = commits.iterator().next();
|
||||||
|
PersonIdent mergeAuthor = mergeCommit.getAuthorIdent();
|
||||||
|
String message = mergeCommit.getFullMessage();
|
||||||
|
assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently");
|
||||||
|
assertThat(mergeAuthor.getEmailAddress()).isEqualTo("dirk@holistic.det");
|
||||||
|
assertThat(message).contains("master", "mergeable");
|
||||||
|
// We expect the merge result of file b.txt here by looking up the sha hash of its content.
|
||||||
|
// If the file is missing (aka not merged correctly) this will throw a MissingObjectException:
|
||||||
|
byte[] contentOfFileB = repository.open(repository.resolve("9513e9c76e73f3e562fd8e4c909d0607113c77c6")).getBytes();
|
||||||
|
assertThat(new String(contentOfFileB)).isEqualTo("b\ncontent from branch\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldUseConfiguredCommitMessageTemplate() throws IOException, GitAPIException {
|
||||||
|
GitMergeCommand command = createCommand();
|
||||||
|
MergeCommandRequest request = new MergeCommandRequest();
|
||||||
|
request.setTargetBranch("master");
|
||||||
|
request.setBranchToMerge("mergeable");
|
||||||
|
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
|
||||||
|
request.setMessageTemplate("simple");
|
||||||
|
|
||||||
|
MergeCommandResult mergeCommandResult = command.merge(request);
|
||||||
|
|
||||||
|
assertThat(mergeCommandResult.isSuccess()).isTrue();
|
||||||
|
|
||||||
|
Repository repository = createContext().open();
|
||||||
|
Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
|
||||||
|
RevCommit mergeCommit = commits.iterator().next();
|
||||||
|
String message = mergeCommit.getFullMessage();
|
||||||
|
assertThat(message).isEqualTo("simple");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotMergeConflictingBranches() {
|
||||||
|
GitMergeCommand command = createCommand();
|
||||||
|
MergeCommandRequest request = new MergeCommandRequest();
|
||||||
|
request.setBranchToMerge("test-branch");
|
||||||
|
request.setTargetBranch("master");
|
||||||
|
|
||||||
|
MergeCommandResult mergeCommandResult = command.merge(request);
|
||||||
|
|
||||||
|
assertThat(mergeCommandResult.isSuccess()).isFalse();
|
||||||
|
assertThat(mergeCommandResult.getFilesWithConflict()).containsExactly("a.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SubjectAware(username = "admin", password = "secret")
|
||||||
|
public void shouldTakeAuthorFromSubjectIfNotSet() throws IOException, GitAPIException {
|
||||||
|
shiro.setSubject(
|
||||||
|
new Subject.Builder()
|
||||||
|
.principals(new SimplePrincipalCollection(new User("dirk", "Dirk Gently", "dirk@holistic.det"), REALM))
|
||||||
|
.buildSubject());
|
||||||
|
GitMergeCommand command = createCommand();
|
||||||
|
MergeCommandRequest request = new MergeCommandRequest();
|
||||||
|
request.setTargetBranch("master");
|
||||||
|
request.setBranchToMerge("mergeable");
|
||||||
|
|
||||||
|
MergeCommandResult mergeCommandResult = command.merge(request);
|
||||||
|
|
||||||
|
assertThat(mergeCommandResult.isSuccess()).isTrue();
|
||||||
|
|
||||||
|
Repository repository = createContext().open();
|
||||||
|
Iterable<RevCommit> mergeCommit = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
|
||||||
|
PersonIdent mergeAuthor = mergeCommit.iterator().next().getAuthorIdent();
|
||||||
|
assertThat(mergeAuthor.getName()).isEqualTo("Dirk Gently");
|
||||||
|
assertThat(mergeAuthor.getEmailAddress()).isEqualTo("dirk@holistic.det");
|
||||||
|
}
|
||||||
|
|
||||||
|
private GitMergeCommand createCommand() {
|
||||||
|
return new GitMergeCommand(createContext(), repository, new SimpleGitWorkdirFactory());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||||
|
import org.eclipse.jgit.lib.Repository;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.spy;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
public class SimpleGitWorkdirFactoryTest extends AbstractGitCommandTestBase {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void emptyPoolShouldCreateNewWorkdir() throws IOException {
|
||||||
|
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
|
||||||
|
File masterRepo = createRepositoryDirectory();
|
||||||
|
|
||||||
|
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
|
||||||
|
|
||||||
|
assertThat(workingCopy.get().getDirectory())
|
||||||
|
.exists()
|
||||||
|
.isNotEqualTo(masterRepo)
|
||||||
|
.isDirectory();
|
||||||
|
assertThat(new File(workingCopy.get().getWorkTree(), "a.txt"))
|
||||||
|
.exists()
|
||||||
|
.isFile()
|
||||||
|
.hasContent("a\nline for blame");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cloneFromPoolShouldBeClosed() throws IOException {
|
||||||
|
PoolWithSpy factory = new PoolWithSpy(temporaryFolder.newFolder());
|
||||||
|
|
||||||
|
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
|
||||||
|
assertThat(workingCopy).isNotNull();
|
||||||
|
}
|
||||||
|
verify(factory.createdClone).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cloneFromPoolShouldNotBeReused() throws IOException {
|
||||||
|
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
|
||||||
|
|
||||||
|
File firstDirectory;
|
||||||
|
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
|
||||||
|
firstDirectory = workingCopy.get().getDirectory();
|
||||||
|
}
|
||||||
|
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
|
||||||
|
File secondDirectory = workingCopy.get().getDirectory();
|
||||||
|
assertThat(secondDirectory).isNotEqualTo(firstDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cloneFromPoolShouldBeDeletedOnClose() throws IOException {
|
||||||
|
SimpleGitWorkdirFactory factory = new SimpleGitWorkdirFactory(temporaryFolder.newFolder());
|
||||||
|
|
||||||
|
File directory;
|
||||||
|
try (WorkingCopy workingCopy = factory.createWorkingCopy(createContext())) {
|
||||||
|
directory = workingCopy.get().getWorkTree();
|
||||||
|
}
|
||||||
|
assertThat(directory).doesNotExist();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PoolWithSpy extends SimpleGitWorkdirFactory {
|
||||||
|
PoolWithSpy(File poolDirectory) {
|
||||||
|
super(poolDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
Repository createdClone;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Repository cloneRepository(File bareRepository, File destination) throws GitAPIException {
|
||||||
|
createdClone = spy(super.cloneRepository(bareRepository, destination));
|
||||||
|
return createdClone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Reference in New Issue
Block a user