Introduce Git Revert functionality to SCM-Manager

This commit is contained in:
Till-André Diegeler
2025-02-27 11:11:57 +01:00
parent 99f6422577
commit f0f7e922bf
72 changed files with 2612 additions and 649 deletions

View File

@@ -0,0 +1,46 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm;
import sonia.scm.repository.NamespaceAndName;
import java.util.Collection;
import java.util.List;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
public class ConflictException extends ExceptionWithContext {
private static final String CODE = "7XUd94Iwo1";
public ConflictException(NamespaceAndName namespaceAndName, Collection<String> conflictingFiles) {
super(
createContext(namespaceAndName, conflictingFiles),
"conflict"
);
}
private static List<ContextEntry> createContext(NamespaceAndName namespaceAndName, Collection<String> conflictingFiles) {
return entity("files", String.join(", ", conflictingFiles))
.in(namespaceAndName)
.build();
}
@Override
public String getCode() {
return CODE;
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.repository;
import sonia.scm.BadRequestException;
import java.util.Collections;
@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here
public class MultipleParentsNotAllowedException extends BadRequestException {
public MultipleParentsNotAllowedException(String changeset) {
super(
Collections.emptyList(),
String.format("%s has more than one parent changeset, which is not allowed with this request.", changeset));
}
@Override
public String getCode() {
return "3a47Hzu1e3";
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.repository;
import lombok.Getter;
import sonia.scm.BadRequestException;
import static java.util.Collections.emptyList;
/**
* Thrown when a changeset has no parent.
* @since 3.8
*/
@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here
@Getter
public class NoParentException extends BadRequestException {
public NoParentException(String changeset) {
super(emptyList(), String.format("%s has no parent.", changeset));
this.revision = changeset;
}
private final String revision;
@Override
public String getCode() {
return "a37jI66dup";
}
}

View File

@@ -82,5 +82,10 @@ public enum Command
/**
* @since 2.39.0
*/
CHANGESETS
CHANGESETS,
/**
* @since 3.8
*/
REVERT
}

View File

@@ -17,6 +17,7 @@
package sonia.scm.repository.api;
import jakarta.annotation.Nullable;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.cache.CacheManager;
@@ -55,22 +56,6 @@ import java.util.stream.Stream;
* after work is finished. For closing the connection to the repository use the
* {@link #close()} method.
*
* @apiviz.uses sonia.scm.repository.Feature
* @apiviz.uses sonia.scm.repository.api.Command
* @apiviz.uses sonia.scm.repository.api.BlameCommandBuilder
* @apiviz.uses sonia.scm.repository.api.BrowseCommandBuilder
* @apiviz.uses sonia.scm.repository.api.CatCommandBuilder
* @apiviz.uses sonia.scm.repository.api.DiffCommandBuilder
* @apiviz.uses sonia.scm.repository.api.LogCommandBuilder
* @apiviz.uses sonia.scm.repository.api.TagsCommandBuilder
* @apiviz.uses sonia.scm.repository.api.BranchesCommandBuilder
* @apiviz.uses sonia.scm.repository.api.IncomingCommandBuilder
* @apiviz.uses sonia.scm.repository.api.OutgoingCommandBuilder
* @apiviz.uses sonia.scm.repository.api.PullCommandBuilder
* @apiviz.uses sonia.scm.repository.api.PushCommandBuilder
* @apiviz.uses sonia.scm.repository.api.BundleCommandBuilder
* @apiviz.uses sonia.scm.repository.api.UnbundleCommandBuilder
* @apiviz.uses sonia.scm.repository.api.MergeCommandBuilder
* @since 1.17
*/
public final class RepositoryService implements Closeable {
@@ -80,7 +65,10 @@ public final class RepositoryService implements Closeable {
private final CacheManager cacheManager;
private final PreProcessorUtil preProcessorUtil;
private final RepositoryServiceProvider provider;
@Getter
private final Repository repository;
@SuppressWarnings({"rawtypes", "java:S3740"})
private final Set<ScmProtocolProvider> protocolProviders;
private final WorkdirProvider workdirProvider;
@@ -119,7 +107,7 @@ public final class RepositoryService implements Closeable {
/**
* Closes the connection to the repository and releases all locks
* and resources. This method should be called in a finally block e.g.:
* and resources. This method should be called in a finally block; e.g.:
*
* <pre><code>
* RepositoryService service = null;
@@ -143,7 +131,29 @@ public final class RepositoryService implements Closeable {
}
/**
* The blame command shows changeset information by line for a given file.
* Returns true if the command is supported by the repository service.
*
* @param command command
* @return true if the command is supported
*/
public boolean isSupported(Command command) {
return provider.getSupportedCommands().contains(command);
}
/**
* Returns true if the feature is supported by the repository service.
*
* @param feature feature
* @return true if the feature is supported
* @since 1.25
*/
public boolean isSupported(Feature feature) {
return provider.getSupportedFeatures().contains(feature);
}
/**
* Creates a {@link BlameCommandBuilder}. It can take the respective parameters and be executed to show
* changeset information by line for a given file.
*
* @return instance of {@link BlameCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
@@ -157,21 +167,8 @@ public final class RepositoryService implements Closeable {
}
/**
* The branches command list all repository branches.
*
* @return instance of {@link BranchesCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
* by the implementation of the repository service provider.
*/
public BranchesCommandBuilder getBranchesCommand() {
LOG.debug("create branches command for repository {}", repository);
return new BranchesCommandBuilder(cacheManager,
provider.getBranchesCommand(), repository);
}
/**
* The branch command creates new branches.
* Creates a {@link BranchCommandBuilder}. It can take the respective parameters and be executed to
* create new branches, if supported by the particular SCM system.
*
* @return instance of {@link BranchCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
@@ -186,7 +183,37 @@ public final class RepositoryService implements Closeable {
}
/**
* The browse command allows browsing of a repository.
* Creates a {@link BranchDetailsCommandBuilder}. It can take the respective parameters and be executed to
* get details for a branch.
*
* @return instance of {@link BranchDetailsCommand}
* @throws CommandNotSupportedException if the command is not supported
* by the implementation of the repository service provider.
* @since 2.28.0
*/
public BranchDetailsCommandBuilder getBranchDetailsCommand() {
LOG.debug("create branch details command for repository {}", repository);
return new BranchDetailsCommandBuilder(repository, provider.getBranchDetailsCommand(), cacheManager);
}
/**
* Creates a {@link BranchesCommandBuilder}. It can take the respective parameters and be executed to list
* all repository branches.
*
* @return instance of {@link BranchesCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
* by the implementation of the repository service provider.
*/
public BranchesCommandBuilder getBranchesCommand() {
LOG.debug("create branches command for repository {}", repository);
return new BranchesCommandBuilder(cacheManager,
provider.getBranchesCommand(), repository);
}
/**
* Creates a {@link BrowseCommandBuilder}. It can take the respective parameters and be executed to
* browse for content within a repository.
*
* @return instance of {@link BrowseCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
@@ -200,7 +227,8 @@ public final class RepositoryService implements Closeable {
}
/**
* The bundle command creates an archive from the repository.
* Creates a {@link BundleCommandBuilder}. It can take the respective parameters and be executed to
* create an archive from the repository.
*
* @return instance of {@link BundleCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
@@ -214,7 +242,8 @@ public final class RepositoryService implements Closeable {
}
/**
* The cat command show the content of a given file.
* Creates a {@link CatCommandBuilder}. It can take the respective parameters and be executed to
* show the content of a given file.
*
* @return instance of {@link CatCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
@@ -227,8 +256,21 @@ public final class RepositoryService implements Closeable {
}
/**
* The diff command shows differences between revisions for a specified file
* or the entire revision.
* Creates a {@link ChangesetsCommandBuilder}. It can take the respective parameters and be executed to
* retrieve a set of at least one changeset.
*
* @return Instance of {@link ChangesetsCommandBuilder}.
* @throws CommandNotSupportedException if the command is not supported by
* the implementation of the {@link RepositoryServiceProvider}.
*/
public ChangesetsCommandBuilder getChangesetsCommand() {
LOG.debug("create changesets command for repository {}", repository);
return new ChangesetsCommandBuilder(repository, provider.getChangesetsCommand());
}
/**
* Creates a {@link DiffCommandBuilder}. It can take the respective parameters and be executed to
* show differences between revisions for a specified file or the entire revision.
*
* @return instance of {@link DiffCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
@@ -241,8 +283,8 @@ public final class RepositoryService implements Closeable {
}
/**
* The diff command shows differences between revisions for a specified file
* or the entire revision.
* Creates a {@link DiffResultCommandBuilder}. It can take the respective parameters and be executed to
* show differences between revisions for a specified file or the entire revision.
*
* @return instance of {@link DiffResultCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
@@ -255,8 +297,36 @@ public final class RepositoryService implements Closeable {
}
/**
* The incoming command shows new {@link Changeset}s found in a different
* repository location.
* Creates a {@link FullHealthCheckCommandBuilder}. It can take the respective parameters and be executed to
* inspect a repository profoundly. This might take a while in contrast to the lighter checks executed at startup.
*
* @return instance of {@link FullHealthCheckCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
* by the implementation of the repository service provider.
* @since 2.17.0
*/
public FullHealthCheckCommandBuilder getFullCheckCommand() {
LOG.debug("create full check command for repository {}", repository);
return new FullHealthCheckCommandBuilder(provider.getFullHealthCheckCommand());
}
/**
* Creates a {@link FileLockCommandBuilder}. It can take the respective parameters and be executed to
* lock and unlock files.
*
* @return instance of {@link FileLockCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
* by the implementation of the repository service provider.
* @since 2.26.0
*/
public FileLockCommandBuilder getLockCommand() {
LOG.debug("create lock command for repository {}", repository);
return new FileLockCommandBuilder(provider.getFileLockCommand(), repository);
}
/**
* Creates a {@link IncomingCommandBuilder}. It can take the respective parameters and be executed to
* show new {@link Changeset}s found in a different repository location.
*
* @return instance of {@link IncomingCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
@@ -271,7 +341,8 @@ public final class RepositoryService implements Closeable {
}
/**
* The log command shows revision history of entire repository or files.
* Creates a {@link LogCommandBuilder}. It can take the respective parameters and be executed to
* show revision history of entire repository or files.
*
* @return instance of {@link LogCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
@@ -285,7 +356,54 @@ public final class RepositoryService implements Closeable {
}
/**
* The modification command shows file modifications in a revision.
* Creates a {@link LookupCommandBuilder}. It can take the respective parameters and be executed to
* conduct a lookup which returns additional information for the repository.
*
* @return instance of {@link LookupCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
* by the implementation of the repository service provider.
* @since 2.10.0
*/
public LookupCommandBuilder getLookupCommand() {
LOG.debug("create lookup command for repository {}", repository);
return new LookupCommandBuilder(provider.getLookupCommand());
}
/**
* Creates a {@link MergeCommandBuilder}. It can take the respective parameters and be executed to
* conduct a merge of two branches.
*
* @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 getMergeCommand() {
RepositoryReadOnlyChecker.checkReadOnly(getRepository());
LOG.debug("create merge command for repository {}", repository);
return new MergeCommandBuilder(provider.getMergeCommand(), eMail);
}
/**
* Creates a {@link MirrorCommandBuilder}. It can take the respective parameters and be executed to
* create a 'mirror' of an existing repository (specified by a URL) by copying all data
* to the repository of this service. Therefore, this repository has to be empty (otherwise the behaviour is
* not specified).
*
* @return instance of {@link MirrorCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
* by the implementation of the repository service provider.
* @since 2.19.0
*/
public MirrorCommandBuilder getMirrorCommand() {
LOG.debug("create mirror command for repository {}", repository);
return new MirrorCommandBuilder(provider.getMirrorCommand(), repository);
}
/**
* Creates a {@link ModificationsCommandBuilder}. It can take the respective parameters and be executed to
* show file modifications in a revision.
*
* @return instance of {@link ModificationsCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
@@ -297,7 +415,25 @@ public final class RepositoryService implements Closeable {
}
/**
* The outgoing command show {@link Changeset}s not found in a remote repository.
* Creates a {@link ModifyCommandBuilder}. It can take the respective parameters and be executed to
* makes changes to the files within a changeset.
*
* @return instance of {@link ModifyCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
* by the implementation of the repository service provider.
* @see ModifyCommandBuilder
* @since 2.0.0
*/
public ModifyCommandBuilder getModifyCommand() {
RepositoryReadOnlyChecker.checkReadOnly(getRepository());
LOG.debug("create modify command for repository {}", repository);
return new ModifyCommandBuilder(provider.getModifyCommand(), workdirProvider, repository.getId(), eMail);
}
/**
* Creates an {@link OutgoingCommandBuilder}. It can take the respective parameters and be executed to
* show {@link Changeset}s not found in a remote repository.
*
* @return instance of {@link OutgoingCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
@@ -312,7 +448,8 @@ public final class RepositoryService implements Closeable {
}
/**
* The pull command pull changes from a other repository.
* Creates a {@link PullCommandBuilder}. It can take the respective parameters and be executed to
* pull changes from another repository.
*
* @return instance of {@link PullCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
@@ -327,7 +464,8 @@ public final class RepositoryService implements Closeable {
}
/**
* The push command pushes changes to a other repository.
* Creates a {@link PushCommandBuilder}. It can take the respective parameters and be executed to
* push changes to another repository.
*
* @return instance of {@link PushCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
@@ -341,12 +479,18 @@ public final class RepositoryService implements Closeable {
}
/**
* Returns the repository of this service.
* Creates a {@link RevertCommandBuilder}. It can take the respective parameters and be executed to
* apply a revert of a chosen changeset onto the given repository/branch combination.
*
* @return repository of this service
* @return Instance of {@link RevertCommandBuilder}.
* @throws CommandNotSupportedException if the command is not supported by
* the implementation of the {@link RepositoryServiceProvider}.
* @since 3.8
* @see RevertCommandBuilder
*/
public Repository getRepository() {
return repository;
public RevertCommandBuilder getRevertCommand() {
LOG.debug("create revert command for repository {}", repository);
return new RevertCommandBuilder(provider.getRevertCommand(), eMail);
}
/**
@@ -376,7 +520,8 @@ public final class RepositoryService implements Closeable {
}
/**
* The unbundle command restores a repository from the given bundle.
* Creates an {@link UnbundleCommandBuilder}. It can take the respective parameters and be executed to
* restore a repository from the given bundle.
*
* @return instance of {@link UnbundleCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
@@ -390,137 +535,6 @@ public final class RepositoryService implements Closeable {
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 getMergeCommand() {
RepositoryReadOnlyChecker.checkReadOnly(getRepository());
LOG.debug("create merge command for repository {}", repository);
return new MergeCommandBuilder(provider.getMergeCommand(), eMail);
}
/**
* The modify command makes changes to the head of a branch. It is possible to
* <ul>
* <li>create new files</li>
* <li>delete existing files</li>
* <li>modify/replace files</li>
* <li>move files</li>
* </ul>
*
* @return instance of {@link ModifyCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
* by the implementation of the repository service provider.
* @since 2.0.0
*/
public ModifyCommandBuilder getModifyCommand() {
RepositoryReadOnlyChecker.checkReadOnly(getRepository());
LOG.debug("create modify command for repository {}", repository);
return new ModifyCommandBuilder(provider.getModifyCommand(), workdirProvider, repository.getId(), eMail);
}
/**
* The lookup command executes a lookup which returns additional information for the repository.
*
* @return instance of {@link LookupCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
* by the implementation of the repository service provider.
* @since 2.10.0
*/
public LookupCommandBuilder getLookupCommand() {
LOG.debug("create lookup command for repository {}", repository);
return new LookupCommandBuilder(provider.getLookupCommand());
}
/**
* The full health check command inspects a repository in a way, that might take a while in contrast to the
* light checks executed at startup.
*
* @return instance of {@link FullHealthCheckCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
* by the implementation of the repository service provider.
* @since 2.17.0
*/
public FullHealthCheckCommandBuilder getFullCheckCommand() {
LOG.debug("create full check command for repository {}", repository);
return new FullHealthCheckCommandBuilder(provider.getFullHealthCheckCommand());
}
/**
* The mirror command creates a 'mirror' of an existing repository (specified by a URL) by copying all data
* to the repository of this service. Therefore this repository has to be empty (otherwise the behaviour is
* not specified).
*
* @return instance of {@link MirrorCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
* by the implementation of the repository service provider.
* @since 2.19.0
*/
public MirrorCommandBuilder getMirrorCommand() {
LOG.debug("create mirror command for repository {}", repository);
return new MirrorCommandBuilder(provider.getMirrorCommand(), repository);
}
/**
* Lock and unlock files.
*
* @return instance of {@link FileLockCommandBuilder}
* @throws CommandNotSupportedException if the command is not supported
* by the implementation of the repository service provider.
* @since 2.26.0
*/
public FileLockCommandBuilder getLockCommand() {
LOG.debug("create lock command for repository {}", repository);
return new FileLockCommandBuilder(provider.getFileLockCommand(), repository);
}
/**
* Get details for a branch.
*
* @return instance of {@link BranchDetailsCommand}
* @throws CommandNotSupportedException if the command is not supported
* by the implementation of the repository service provider.
* @since 2.28.0
*/
public BranchDetailsCommandBuilder getBranchDetailsCommand() {
LOG.debug("create branch details command for repository {}", repository);
return new BranchDetailsCommandBuilder(repository, provider.getBranchDetailsCommand(), cacheManager);
}
public ChangesetsCommandBuilder getChangesetsCommand() {
LOG.debug("create changesets command for repository {}", repository);
return new ChangesetsCommandBuilder(repository, provider.getChangesetsCommand());
}
/**
* Returns true if the command is supported by the repository service.
*
* @param command command
* @return true if the command is supported
*/
public boolean isSupported(Command command) {
return provider.getSupportedCommands().contains(command);
}
/**
* Returns true if the feature is supported by the repository service.
*
* @param feature feature
* @return true if the feature is supported
* @since 1.25
*/
public boolean isSupported(Feature feature) {
return provider.getSupportedFeatures().contains(feature);
}
public Stream<ScmProtocol> getSupportedProtocols() {
return protocolProviders.stream()
.filter(protocolProvider -> protocolProvider.getType().equals(getRepository().getType()))

View File

@@ -0,0 +1,110 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.repository.api;
import com.google.common.base.Preconditions;
import jakarta.annotation.Nullable;
import sonia.scm.repository.spi.RevertCommand;
import sonia.scm.repository.spi.RevertCommandRequest;
import sonia.scm.repository.util.AuthorUtil;
import sonia.scm.user.DisplayUser;
import sonia.scm.user.EMail;
/**
* Applies a revert of a chosen changeset onto the given repository/branch combination.
*
* @since 3.8
*/
public final class RevertCommandBuilder {
private final RevertCommand command;
private final RevertCommandRequest request;
@Nullable
private final EMail email;
/**
* @param command A {@link RevertCommand} implementation provided by some source.
*/
public RevertCommandBuilder(RevertCommand command, @Nullable EMail email) {
this.command = command;
this.email = email;
this.request = new RevertCommandRequest();
}
/**
* Use this to set the author of the revert commit manually. If this is omitted, the currently logged-in user will be
* used instead. If the given user object does not have an email address, we will use {@link EMail} to compute a
* fallback address.
*
* @param author Author entity.
* @return This instance.
*/
public RevertCommandBuilder setAuthor(DisplayUser author) {
request.setAuthor(AuthorUtil.createAuthorWithMailFallback(author, email));
return this;
}
/**
* Obligatory value.
*
* @param revision Identifier of the revision.
* @return This instance.
*/
public RevertCommandBuilder setRevision(String revision) {
request.setRevision(revision);
return this;
}
/**
* This is an optional parameter. Not every SCM system supports branches.
* If null or empty and supported by the SCM, the default branch of the repository shall be used.
*
* @param branch Name of the branch.
* @return This instance.
*/
public RevertCommandBuilder setBranch(String branch) {
request.setBranch(branch);
return this;
}
/**
* This is an optional parameter. If null or empty, a default message will be set.
*
* @param message Particular message.
* @return This instance.
*/
public RevertCommandBuilder setMessage(String message) {
request.setMessage(message);
return this;
}
/**
* Executes the revert with the given builder parameters.
*
* @return {@link RevertCommandResult} with information about the executed revert.
*/
public RevertCommandResult execute() {
AuthorUtil.setAuthorIfNotAvailable(request, email);
Preconditions.checkArgument(request.isValid(), "Revert request is invalid, request was: %s", request);
return command.revert(request);
}
protected RevertCommandRequest getRequest() {
return this.request;
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.repository.api;
import lombok.Getter;
import java.util.Collection;
import java.util.HashSet;
import static java.util.Collections.emptyList;
/**
* Contains the result of an executed revert command.
*
* @since 3.8
*/
@Getter
public class RevertCommandResult {
/**
* The identifier of the revision after the applied revert.
*/
private final String revision;
/**
* A collection of files where conflicts occur.
*/
private final Collection<String> filesWithConflict;
/**
* Creates a {@link RevertCommandResult}.
*
* @param revision revision identifier
* @param filesWithConflict a collection of files where conflicts occur
*/
public RevertCommandResult(String revision, Collection<String> filesWithConflict) {
this.revision = revision;
this.filesWithConflict = filesWithConflict;
}
/**
* Used to indicate a successful revert.
*
* @param newHeadRevision id of the newly created revert
* @return {@link RevertCommandResult}
*/
public static RevertCommandResult success(String newHeadRevision) {
return new RevertCommandResult(newHeadRevision, emptyList());
}
/**
* Used to indicate a failed revert.
*
* @param filesWithConflict collection of conflicting files
* @return {@link RevertCommandResult}
*/
public static RevertCommandResult failure(Collection<String> filesWithConflict) {
return new RevertCommandResult(null, new HashSet<>(filesWithConflict));
}
/**
* If this returns <code>true</code>, the revert was successful. If this returns <code>false</code>, there may have
* been problems like a merge conflict after the revert.
*/
public boolean isSuccessful() {
return filesWithConflict.isEmpty() && revision != null;
}
}

View File

@@ -16,6 +16,7 @@
package sonia.scm.repository.spi;
import lombok.extern.slf4j.Slf4j;
import sonia.scm.repository.Feature;
import sonia.scm.repository.api.Command;
import sonia.scm.repository.api.CommandNotSupportedException;
@@ -26,13 +27,17 @@ import java.util.Collections;
import java.util.Set;
/**
* This class is an extension base for SCM system providers to implement command functionalitites.
* If unimplemented, the methods within this class throw {@link CommandNotSupportedException}. These are not supposed
* to be called if unimplemented for an SCM system.
*
* @see sonia.scm.repository.api.RepositoryService
* @since 1.17
*/
public abstract class RepositoryServiceProvider implements Closeable
{
@Slf4j
public abstract class RepositoryServiceProvider implements Closeable {
public abstract Set<Command> getSupportedCommands();
@@ -41,195 +46,118 @@ public abstract class RepositoryServiceProvider implements Closeable
* free resources, close connections or release locks than you have to
* override this method.
*
*
* @throws IOException
*/
@Override
public void close() throws IOException
{
// should be implmentented from a service provider
public void close() throws IOException {
log.warn("warning: close() has been called without implementation from a service provider.");
}
public BlameCommand getBlameCommand()
{
throw new CommandNotSupportedException(Command.BLAME);
}
public BranchesCommand getBranchesCommand()
{
throw new CommandNotSupportedException(Command.BRANCHES);
}
public BranchCommand getBranchCommand()
{
throw new CommandNotSupportedException(Command.BRANCH);
}
public BrowseCommand getBrowseCommand()
{
throw new CommandNotSupportedException(Command.BROWSE);
}
/**
* @since 1.43
*/
public BundleCommand getBundleCommand()
{
throw new CommandNotSupportedException(Command.BUNDLE);
}
public CatCommand getCatCommand()
{
throw new CommandNotSupportedException(Command.CAT);
}
public DiffCommand getDiffCommand()
{
throw new CommandNotSupportedException(Command.DIFF);
}
public DiffResultCommand getDiffResultCommand()
{
throw new CommandNotSupportedException(Command.DIFF_RESULT);
}
/**
* @since 1.31
*/
public IncomingCommand getIncomingCommand()
{
throw new CommandNotSupportedException(Command.INCOMING);
}
public LogCommand getLogCommand()
{
throw new CommandNotSupportedException(Command.LOG);
}
/**
* Get the corresponding {@link ModificationsCommand} implemented from the Plugins
*
* @return the corresponding {@link ModificationsCommand} implemented from the Plugins
* @throws CommandNotSupportedException if there is no Implementation
*/
public ModificationsCommand getModificationsCommand() {
throw new CommandNotSupportedException(Command.MODIFICATIONS);
}
/**
* @since 1.31
*/
public OutgoingCommand getOutgoingCommand()
{
throw new CommandNotSupportedException(Command.OUTGOING);
}
/**
* @since 1.31
*/
public PullCommand getPullCommand()
{
throw new CommandNotSupportedException(Command.PULL);
}
/**
* @since 1.31
*/
public PushCommand getPushCommand()
{
throw new CommandNotSupportedException(Command.PUSH);
}
public Set<Feature> getSupportedFeatures()
{
public Set<Feature> getSupportedFeatures() {
return Collections.emptySet();
}
public TagsCommand getTagsCommand()
{
throw new CommandNotSupportedException(Command.TAGS);
public BlameCommand getBlameCommand() {
throw new CommandNotSupportedException(Command.BLAME);
}
/**
* @since 2.11.0
*/
public TagCommand getTagCommand()
{
throw new CommandNotSupportedException(Command.TAG);
public BranchesCommand getBranchesCommand() {
throw new CommandNotSupportedException(Command.BRANCHES);
}
/**
* @since 1.43
*/
public UnbundleCommand getUnbundleCommand()
{
throw new CommandNotSupportedException(Command.UNBUNDLE);
public BranchCommand getBranchCommand() {
throw new CommandNotSupportedException(Command.BRANCH);
}
/**
* @since 2.0
*/
public MergeCommand getMergeCommand()
{
throw new CommandNotSupportedException(Command.MERGE);
}
/**
* @since 2.0
*/
public ModifyCommand getModifyCommand()
{
throw new CommandNotSupportedException(Command.MODIFY);
}
/**
* @since 2.10.0
*/
public LookupCommand getLookupCommand()
{
throw new CommandNotSupportedException(Command.LOOKUP);
}
/**
* @since 2.17.0
*/
public FullHealthCheckCommand getFullHealthCheckCommand() {
throw new CommandNotSupportedException(Command.FULL_HEALTH_CHECK);
}
/**
* @since 2.19.0
*/
public MirrorCommand getMirrorCommand() {
throw new CommandNotSupportedException(Command.MIRROR);
}
/**
* @since 2.26.0
*/
public FileLockCommand getFileLockCommand() {
throw new CommandNotSupportedException(Command.FILE_LOCK);
}
/**
* @since 2.28.0
*/
public BranchDetailsCommand getBranchDetailsCommand() {
throw new CommandNotSupportedException(Command.BRANCH_DETAILS);
}
public BrowseCommand getBrowseCommand() {
throw new CommandNotSupportedException(Command.BROWSE);
}
public BundleCommand getBundleCommand() {
throw new CommandNotSupportedException(Command.BUNDLE);
}
public CatCommand getCatCommand() {
throw new CommandNotSupportedException(Command.CAT);
}
public ChangesetsCommand getChangesetsCommand() {
throw new CommandNotSupportedException(Command.CHANGESETS);
}
public DiffCommand getDiffCommand() {
throw new CommandNotSupportedException(Command.DIFF);
}
public DiffResultCommand getDiffResultCommand() {
throw new CommandNotSupportedException(Command.DIFF_RESULT);
}
public FileLockCommand getFileLockCommand() {
throw new CommandNotSupportedException(Command.FILE_LOCK);
}
public FullHealthCheckCommand getFullHealthCheckCommand() {
throw new CommandNotSupportedException(Command.FULL_HEALTH_CHECK);
}
public IncomingCommand getIncomingCommand() {
throw new CommandNotSupportedException(Command.INCOMING);
}
public LogCommand getLogCommand() {
throw new CommandNotSupportedException(Command.LOG);
}
public LookupCommand getLookupCommand() {
throw new CommandNotSupportedException(Command.LOOKUP);
}
public MergeCommand getMergeCommand() {
throw new CommandNotSupportedException(Command.MERGE);
}
public MirrorCommand getMirrorCommand() {
throw new CommandNotSupportedException(Command.MIRROR);
}
public ModificationsCommand getModificationsCommand() {
throw new CommandNotSupportedException(Command.MODIFICATIONS);
}
public ModifyCommand getModifyCommand() {
throw new CommandNotSupportedException(Command.MODIFY);
}
public OutgoingCommand getOutgoingCommand() {
throw new CommandNotSupportedException(Command.OUTGOING);
}
public PullCommand getPullCommand() {
throw new CommandNotSupportedException(Command.PULL);
}
public PushCommand getPushCommand() {
throw new CommandNotSupportedException(Command.PUSH);
}
public RevertCommand getRevertCommand() {
throw new CommandNotSupportedException(Command.REVERT);
}
public TagsCommand getTagsCommand() {
throw new CommandNotSupportedException(Command.TAGS);
}
public TagCommand getTagCommand() {
throw new CommandNotSupportedException(Command.TAG);
}
public UnbundleCommand getUnbundleCommand() {
throw new CommandNotSupportedException(Command.UNBUNDLE);
}
}

View File

@@ -17,10 +17,12 @@
package sonia.scm.repository.spi;
/**
* @deprecated This interface may get removed at some point in the future.
* @since 1.17
*/
@Deprecated(since = "3.8")
public interface Resetable
{
public void reset();
void reset();
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.repository.spi;
import sonia.scm.repository.api.RevertCommandResult;
/**
* Removes the changes from a particular changeset as a revert. This, in turn, will result a new changeset.
*
* @since 3.8
*/
public interface RevertCommand {
/**
* Executes a revert.
* @param request parameter set for this command.
* @see RevertCommand
* @return result set of the executed command (see {@link RevertCommandResult}).
*/
RevertCommandResult revert(RevertCommandRequest request);
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.repository.spi;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import sonia.scm.Validateable;
import sonia.scm.repository.Person;
import sonia.scm.repository.util.AuthorUtil.CommandWithAuthor;
import java.util.Optional;
/**
* This class contains the information to run {@link RevertCommand#revert(RevertCommandRequest)}.
* @since 3.8
*/
@Setter
@ToString
public class RevertCommandRequest implements Validateable, CommandWithAuthor {
@Getter
private Person author;
@Getter
private String revision;
/**
* Reverts can be signed with a GPG key. This is set as <tt>true</tt> by default.
*/
@Getter
private boolean sign = true;
private String branch;
private String message;
public Optional<String> getBranch() {
return Optional.ofNullable(branch);
}
public Optional<String> getMessage() {
return Optional.ofNullable(message);
}
@Override
public boolean isValid() {
boolean validBranch = branch == null || !branch.isEmpty();
boolean validMessage = message == null || !message.isEmpty();
return revision != null && author != null && validBranch && validMessage;
}
}

View File

@@ -20,29 +20,61 @@ import jakarta.annotation.Nullable;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import sonia.scm.repository.Person;
import sonia.scm.user.DisplayUser;
import sonia.scm.user.EMail;
import sonia.scm.user.User;
/**
* Contains convenience methods to manage {@link CommandWithAuthor} classes.
*/
public class AuthorUtil {
/**
* @see AuthorUtil#createAuthorFromSubject(EMail)
* @param request {@link CommandWithAuthor}
*/
public static void setAuthorIfNotAvailable(CommandWithAuthor request) {
setAuthorIfNotAvailable(request, null);
}
/**
* @see AuthorUtil#createAuthorFromSubject(EMail)
* @param request {@link CommandWithAuthor}
* @param eMail {@link EMail}
*/
public static void setAuthorIfNotAvailable(CommandWithAuthor request, @Nullable EMail eMail) {
if (request.getAuthor() == null) {
request.setAuthor(createAuthorFromSubject(eMail));
}
}
private static Person createAuthorFromSubject(@Nullable EMail eMail) {
Subject subject = SecurityUtils.getSubject();
User user = subject.getPrincipals().oneByType(User.class);
/**
* Depending on the mail input, the {@link Person} is either created by the given nullable {@link EMail}
* or the information from {@link DisplayUser} if the mail remains null.
* @param user {@link DisplayUser}
* @param eMail (nullable) {@link EMail}
* @return {@link Person}
*/
public static Person createAuthorWithMailFallback(DisplayUser user, @Nullable EMail eMail) {
String name = user.getDisplayName();
String mailAddress = eMail != null ? eMail.getMailOrFallback(user) : user.getMail();
return new Person(name, mailAddress);
}
/**
* Creates an author from the Apache Shiro {@link Subject} given by the {@link SecurityUtils}.
* @param eMail {@link EMail}
* @return {@link Person}
*/
private static Person createAuthorFromSubject(@Nullable EMail eMail) {
Subject subject = SecurityUtils.getSubject();
User user = subject.getPrincipals().oneByType(User.class);
return createAuthorWithMailFallback(DisplayUser.from(user), eMail);
}
/**
* Command whose execution includes an author as a {@link Person}.
*/
public interface CommandWithAuthor {
Person getAuthor();

View File

@@ -16,8 +16,10 @@
package sonia.scm.user;
import lombok.EqualsAndHashCode;
import sonia.scm.ReducedModelObject;
@EqualsAndHashCode
public class DisplayUser implements ReducedModelObject {
private final String id;

View File

@@ -40,6 +40,7 @@ public class VndMediaType {
public static final String REPOSITORY_PERMISSION = PREFIX + "repositoryPermission" + SUFFIX;
public static final String CHANGESET = PREFIX + "changeset" + SUFFIX;
public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + SUFFIX;
public static final String REVERT = PREFIX + "revert" + SUFFIX;
public static final String MODIFICATIONS = PREFIX + "modifications" + SUFFIX;
public static final String TAG = PREFIX + "tag" + SUFFIX;
public static final String TAG_COLLECTION = PREFIX + "tagCollection" + SUFFIX;

View File

@@ -0,0 +1,123 @@
/*
* Copyright (c) 2020 - present Cloudogu GmbH
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
package sonia.scm.repository.api;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
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.repository.spi.RevertCommand;
import sonia.scm.repository.spi.RevertCommandRequest;
import sonia.scm.user.DisplayUser;
import sonia.scm.user.EMail;
import sonia.scm.user.User;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class RevertCommandBuilderTest {
@Mock
private RevertCommand revertCommand;
@Mock
private EMail eMail;
@InjectMocks
private RevertCommandBuilder revertCommandBuilder;
@BeforeEach
void prepareCommandBuilder() {
revertCommandBuilder.setRevision("irrelevant");
}
@Test
void shouldUseMailAddressFromEMailFallback() {
User user = new User("dent", "Arthur Dent", null);
DisplayUser author = DisplayUser.from(user);
when(eMail.getMailOrFallback(author)).thenReturn("dent@hitchhiker.com");
revertCommandBuilder.setAuthor(author);
revertCommandBuilder.execute();
verify(revertCommand).revert(argThat(revertCommandRequest -> {
assertThat(revertCommandRequest.getAuthor().getMail()).isEqualTo("dent@hitchhiker.com");
return true;
}));
}
@Test
void shouldSetAuthorFromShiroSubjectIfNotSet() {
User user = new User("dent", "Arthur Dent", null);DisplayUser author = DisplayUser.from(user);
when(eMail.getMailOrFallback(author)).thenReturn("dent@hitchhiker.com");
mockLoggedInUser(user);
revertCommandBuilder.execute();
RevertCommandRequest request = revertCommandBuilder.getRequest();
assertThat(request.getAuthor().getName()).isEqualTo("Arthur Dent");
assertThat(request.getAuthor().getMail()).isEqualTo("dent@hitchhiker.com");
mockLogout();
}
@Test
void shouldSetAllFieldsInRequest() {
User user = new User("dent", "Arthur Dent", null);
DisplayUser author = DisplayUser.from(user);
when(eMail.getMailOrFallback(author)).thenReturn("dent@hitchhiker.com");
revertCommandBuilder.setAuthor(author);
revertCommandBuilder.setBranch("someBranch");
revertCommandBuilder.setMessage("someMessage");
RevertCommandRequest request = revertCommandBuilder.getRequest();
assertThat(request.getAuthor().getName()).isEqualTo(author.getDisplayName());
assertThat(request.getBranch()).contains("someBranch");
assertThat(request.getMessage()).contains("someMessage");
assertThat(request.getRevision()).isEqualTo("irrelevant");
}
@Test
void shouldNotExecuteInvalidRequestDueToEmptyBranch() {
User user = new User("dent", "Arthur Dent", "dent@hitchhiker.com");
revertCommandBuilder.setAuthor(DisplayUser.from(user));
revertCommandBuilder.setBranch("");
assertThatThrownBy(() -> revertCommandBuilder.execute())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Revert request is invalid, request was: RevertCommandRequest(author=Arthur Dent, revision=irrelevant, sign=true, branch=Optional[], message=Optional.empty)");
}
private void mockLoggedInUser(User loggedInUser) {
Subject subject = mock(Subject.class);
ThreadContext.bind(subject);
PrincipalCollection principals = mock(PrincipalCollection.class);
when(subject.getPrincipals()).thenReturn(principals);
when(principals.oneByType(User.class)).thenReturn(loggedInUser);
}
private void mockLogout() {
ThreadContext.unbindSubject();
}
}

View File

@@ -26,6 +26,7 @@ import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.Person;
import sonia.scm.user.DisplayUser;
import sonia.scm.user.EMail;
import sonia.scm.user.User;
@@ -54,9 +55,9 @@ class AuthorUtilTest {
@Test
void shouldCreateMailAddressFromEmail() {
User trillian = new User("trillian");
when(subject.getPrincipals().oneByType(User.class)).thenReturn(trillian);
when(eMail.getMailOrFallback(trillian)).thenReturn("tricia@hitchhicker.com");
when(subject.getPrincipals().oneByType(User.class)).thenReturn(trillian);
when(eMail.getMailOrFallback(DisplayUser.from(trillian))).thenReturn("tricia@hitchhicker.com");
Command command = new Command(null);
AuthorUtil.setAuthorIfNotAvailable(command, eMail);