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

@@ -71,10 +71,10 @@ import static java.util.Optional.ofNullable;
public final class GitUtil {
private static final GitUserAgentProvider GIT_USER_AGENT_PROVIDER = new GitUserAgentProvider();
public static final String REF_HEAD = "HEAD";
public static final String REF_HEAD_PREFIX = "refs/heads/";
public static final String REF_MAIN = "main";
private static final GitUserAgentProvider GIT_USER_AGENT_PROVIDER = new GitUserAgentProvider();
private static final String DIRECTORY_DOTGIT = ".git";
private static final String DIRECTORY_OBJETCS = "objects";
private static final String DIRECTORY_REFS = "refs";
@@ -84,15 +84,13 @@ public final class GitUtil {
private static final String REMOTE_REF = "refs/remote/scm/%s/%s";
private static final int TIMEOUT = 5;
private static final Logger logger = LoggerFactory.getLogger(GitUtil.class);
private static final String REF_SPEC = "refs/heads/*:refs/heads/*";
private static final String GPG_HEADER = "-----BEGIN PGP SIGNATURE-----";
private GitUtil() {
}
public static void close(org.eclipse.jgit.lib.Repository repo) {
if (repo != null) {
repo.close();
@@ -181,7 +179,6 @@ public final class GitUtil {
}
}
public static String getBranch(Ref ref) {
String branch = null;
@@ -234,7 +231,6 @@ public final class GitUtil {
}
}
public static Ref getBranchId(org.eclipse.jgit.lib.Repository repo,
String branchName)
throws IOException {
@@ -291,22 +287,43 @@ public final class GitUtil {
/**
* Returns the commit for the given ref.
* If the given ref is for a tag, the commit that this tag belongs to is returned instead.
*
* @param repository jgit repository
* @param revWalk rev walk
* @param ref commit/tag ref
* @return {@link RevCommit}
* @throws IOException exception
*/
public static RevCommit getCommit(org.eclipse.jgit.lib.Repository repository,
RevWalk revWalk, Ref ref)
throws IOException {
RevCommit commit = null;
ObjectId id = ref.getPeeledObjectId();
if (id == null) {
id = ref.getObjectId();
}
return getCommit(repository, revWalk, id);
}
/**
* Returns the commit for the given object id. The id is expected to be a commit and not a tag.
*
* @param repository jgit repository
* @param revWalk rev walk
* @param id commit id
* @return {@link RevCommit}
* @throws IOException exception
* @since 3.8.0
*/
public static RevCommit getCommit(org.eclipse.jgit.lib.Repository repository,
RevWalk revWalk, ObjectId id) throws IOException {
RevCommit commit = null;
if (id != null) {
if (revWalk == null) {
revWalk = new RevWalk(repository);
}
commit = revWalk.parseCommit(id);
}
@@ -330,7 +347,6 @@ public final class GitUtil {
return tag;
}
public static long getCommitTime(RevCommit commit) {
long date = commit.getCommitTime();
@@ -339,7 +355,6 @@ public final class GitUtil {
return date;
}
public static String getId(AnyObjectId objectId) {
String id = Util.EMPTY_STRING;
@@ -350,7 +365,6 @@ public final class GitUtil {
return id;
}
public static Ref getRefForCommit(org.eclipse.jgit.lib.Repository repository,
ObjectId id)
throws IOException {
@@ -415,7 +429,6 @@ public final class GitUtil {
.findFirst();
}
public static ObjectId getRevisionId(org.eclipse.jgit.lib.Repository repo,
String revision)
throws IOException {
@@ -430,7 +443,6 @@ public final class GitUtil {
return revId;
}
public static String getScmRemoteRefName(Repository repository,
Ref localBranch) {
return getScmRemoteRefName(repository, localBranch.getName());
@@ -463,7 +475,6 @@ public final class GitUtil {
return tagName;
}
public static String getTagName(Ref ref) {
String name = ref.getName();
@@ -474,8 +485,6 @@ public final class GitUtil {
return name;
}
private static final String GPG_HEADER = "-----BEGIN PGP SIGNATURE-----";
public static Optional<Signature> getTagSignature(RevObject revObject, GPG gpg, RevWalk revWalk) throws IOException {
if (revObject instanceof RevTag) {
final byte[] messageBytes = revWalk.getObjectReader().open(revObject.getId()).getBytes();

View File

@@ -49,7 +49,8 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider {
Command.MIRROR,
Command.FILE_LOCK,
Command.BRANCH_DETAILS,
Command.CHANGESETS
Command.CHANGESETS,
Command.REVERT
);
protected static final Set<Feature> FEATURES = EnumSet.of(
@@ -184,6 +185,11 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider {
return injector.getInstance(GitChangesetsCommand.Factory.class).create(context);
}
@Override
public RevertCommand getRevertCommand() {
return injector.getInstance(GitRevertCommand.Factory.class).create(context);
}
@Override
public Set<Command> getSupportedCommands() {
return COMMANDS;

View File

@@ -0,0 +1,172 @@
/*
* 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 com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.merge.RecursiveMerger;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import sonia.scm.NoChangesMadeException;
import sonia.scm.NotFoundException;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.MultipleParentsNotAllowedException;
import sonia.scm.repository.NoParentException;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.api.RevertCommandResult;
import java.io.IOException;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
@Slf4j
public class GitRevertCommand extends AbstractGitCommand implements RevertCommand {
private final RepositoryManager repositoryManager;
private final GitRepositoryHookEventFactory eventFactory;
@Inject
GitRevertCommand(@Assisted GitContext context, RepositoryManager repositoryManager, GitRepositoryHookEventFactory eventFactory) {
super(context);
this.repositoryManager = repositoryManager;
this.eventFactory = eventFactory;
}
@Override
public RevertCommandResult revert(RevertCommandRequest request) {
log.debug("revert {} on {} in repository {}",
request.getRevision(),
request.getBranch().orElse("default branch"),
repository.getName());
try (Repository jRepository = context.open();
RevWalk revWalk = new RevWalk(jRepository)) {
ObjectId sourceRevision = getSourceRevision(request, jRepository, repository);
ObjectId targetRevision = getTargetRevision(request, jRepository, repository);
RevCommit parent = getParentRevision(revWalk, sourceRevision, jRepository);
RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(jRepository, true);
merger.setBase(sourceRevision);
boolean mergeSucceeded = merger.merge(targetRevision, parent);
if (!mergeSucceeded) {
log.info("revert merge fail: {} on {} in repository {}",
sourceRevision.getName(), targetRevision.getName(), repository.getName());
return RevertCommandResult.failure(MergeHelper.getFailingPaths(merger));
}
ObjectId oldTreeId = revWalk.parseCommit(targetRevision).getTree().toObjectId();
ObjectId newTreeId = merger.getResultTreeId();
if (oldTreeId.equals(newTreeId)) {
throw new NoChangesMadeException(repository);
}
log.debug("revert {} on {} in repository {} successful, preparing commit",
sourceRevision.getName(), targetRevision.getName(), repository.getName());
CommitHelper commitHelper = new CommitHelper(context, repositoryManager, eventFactory);
ObjectId commitId = commitHelper.createCommit(
newTreeId,
request.getAuthor(),
request.getAuthor(),
determineMessage(request, GitUtil.getCommit(jRepository, revWalk, sourceRevision)),
request.isSign(),
targetRevision
);
commitHelper.updateBranch(
request.getBranch().orElseGet(() -> context.getConfig().getDefaultBranch()), commitId, targetRevision
);
return RevertCommandResult.success(commitId.getName());
} catch (CanceledException | IOException | UnsupportedSigningFormatException e) {
throw new RuntimeException(e);
}
}
private ObjectId getSourceRevision(RevertCommandRequest request,
Repository jRepository,
sonia.scm.repository.Repository sRepository) throws IOException {
ObjectId sourceRevision = GitUtil.getRevisionId(jRepository, request.getRevision());
if (sourceRevision == null) {
log.error("source revision not found!");
throw NotFoundException.notFound(entity(ObjectId.class, request.getRevision()).in(sRepository));
}
log.debug("got source revision {} for repository {}", sourceRevision.getName(), jRepository.getIdentifier());
return sourceRevision;
}
private ObjectId getTargetRevision(RevertCommandRequest request,
Repository jRepository,
sonia.scm.repository.Repository sRepository) throws IOException {
if (request.getBranch().isEmpty() || request.getBranch().get().isEmpty()) {
ObjectId targetRevision = GitUtil.getRepositoryHead(jRepository);
log.debug("given target branch is empty, returning HEAD revision for repository {}", jRepository.getIdentifier());
return targetRevision;
}
ObjectId targetRevision = GitUtil.getRevisionId(jRepository, request.getBranch().get());
if (targetRevision == null) {
log.error("target revision not found!");
throw NotFoundException.notFound(entity(ObjectId.class, request.getBranch().get()).in(sRepository));
}
log.debug("got target revision {} for repository {}", targetRevision.getName(), jRepository.getIdentifier());
return targetRevision;
}
private RevCommit getParentRevision(RevWalk revWalk, ObjectId sourceRevision, Repository jRepository) throws IOException {
RevCommit source = revWalk.parseCommit(sourceRevision);
int sourceParents = source.getParentCount();
if (sourceParents == 0) {
throw new NoParentException(sourceRevision.getName());
} else if (sourceParents > 1) {
throw new MultipleParentsNotAllowedException(sourceRevision.getName());
}
RevCommit parent = source.getParent(0);
log.debug("got parent revision {} of revision {} for repository {}", parent.getName(), sourceRevision.getName(), jRepository.getIdentifier());
return parent;
}
private String determineMessage(RevertCommandRequest request, RevCommit revertedCommit) {
return request.getMessage().orElseGet(() -> {
log.debug("no custom message given, choose default message");
return String.format("""
Revert "%s"
This reverts commit %s.""", revertedCommit.getShortMessage(), revertedCommit.getId().getName());
});
}
public interface Factory {
RevertCommand create(GitContext context);
}
}

View File

@@ -22,6 +22,7 @@ import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.merge.RecursiveMerger;
import org.eclipse.jgit.merge.ResolveMerger;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -80,6 +81,15 @@ class MergeHelper {
this.message = request.getMessage();
}
static Collection<String> getFailingPaths(ResolveMerger merger) {
return merger.getMergeResults()
.entrySet()
.stream()
.filter(entry -> entry.getValue().containsConflicts())
.map(Map.Entry::getKey)
.toList();
}
ObjectId getTargetRevision() {
return targetRevision;
}
@@ -107,15 +117,6 @@ class MergeHelper {
}
}
Collection<String> getFailingPaths(ResolveMerger merger) {
return merger.getMergeResults()
.entrySet()
.stream()
.filter(entry -> entry.getValue().containsConflicts())
.map(Map.Entry::getKey)
.toList();
}
boolean isMergedInto(ObjectId baseRevision, ObjectId revisionToCheck) {
try (RevWalk revWalk = new RevWalk(context.open())) {
RevCommit baseCommit = revWalk.parseCommit(baseRevision);

View File

@@ -57,6 +57,7 @@ import sonia.scm.repository.spi.GitModifyCommand;
import sonia.scm.repository.spi.GitOutgoingCommand;
import sonia.scm.repository.spi.GitPullCommand;
import sonia.scm.repository.spi.GitPushCommand;
import sonia.scm.repository.spi.GitRevertCommand;
import sonia.scm.repository.spi.GitTagCommand;
import sonia.scm.repository.spi.GitTagsCommand;
import sonia.scm.repository.spi.GitUnbundleCommand;
@@ -70,6 +71,7 @@ import sonia.scm.repository.spi.OutgoingCommand;
import sonia.scm.repository.spi.PostReceiveRepositoryHookEventFactory;
import sonia.scm.repository.spi.PullCommand;
import sonia.scm.repository.spi.PushCommand;
import sonia.scm.repository.spi.RevertCommand;
import sonia.scm.repository.spi.SimpleGitWorkingCopyFactory;
import sonia.scm.repository.spi.TagCommand;
import sonia.scm.repository.spi.TagsCommand;
@@ -119,7 +121,6 @@ public class GitServletModule extends ServletModule {
install(new FactoryModuleBuilder().implement(FileLockCommand.class, GitFileLockCommand.class).build(GitFileLockCommand.Factory.class));
install(new FactoryModuleBuilder().implement(BranchDetailsCommand.class, GitBranchDetailsCommand.class).build(GitBranchDetailsCommand.Factory.class));
install(new FactoryModuleBuilder().implement(ChangesetsCommand.class, GitChangesetsCommand.class).build(GitChangesetsCommand.Factory.class));
install(new FactoryModuleBuilder().implement(RevertCommand.class, GitRevertCommand.class).build(GitRevertCommand.Factory.class));
}
}

View File

@@ -18,53 +18,49 @@ package sonia.scm.repository.spi;
import org.junit.After;
import org.junit.jupiter.api.AfterEach;
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
import sonia.scm.repository.GitConfig;
import sonia.scm.repository.GitRepositoryConfig;
import sonia.scm.store.InMemoryConfigurationStoreFactory;
import sonia.scm.store.InMemoryByteConfigurationStoreFactory;
public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase
{
public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase {
@After
public void close()
{
private GitContext context;
@After
@AfterEach
public void close() {
if (context != null) {
context.setConfig(new GitRepositoryConfig());
context.close();
}
}
protected GitContext createContext()
{
if (context == null)
{
protected GitContext createContext() {
return createContext("main");
}
protected GitContext createContext(String defaultBranch) {
if (context == null) {
GitConfig config = new GitConfig();
config.setDefaultBranch("master");
context = new GitContext(repositoryDirectory, repository, new GitRepositoryConfigStoreProvider(InMemoryConfigurationStoreFactory.create()), config);
config.setDefaultBranch(defaultBranch);
GitRepositoryConfigStoreProvider storeProvider = new GitRepositoryConfigStoreProvider(new InMemoryByteConfigurationStoreFactory());
storeProvider.setDefaultBranch(repository, defaultBranch);
context = new GitContext(repositoryDirectory, repository, storeProvider, config);
}
return context;
}
@Override
protected String getType()
{
protected String getType() {
return "git";
}
@Override
protected String getZippedRepositoryResource()
{
protected String getZippedRepositoryResource() {
return "sonia/scm/repository/spi/scm-git-spi-test.zip";
}
//~--- fields ---------------------------------------------------------------
private GitContext context;
}

View File

@@ -35,9 +35,9 @@ public class GitBranchesCommandTest extends AbstractGitCommandTestBase {
List<Branch> branches = branchesCommand.getBranches();
assertThat(findBranch(branches, "master")).isEqualTo(
assertThat(findBranch(branches, "main")).isEqualTo(
defaultBranch(
"master",
"main",
"fcd0ef1831e4002ac43ea539f4094334c79ea9ec",
1339428655000L,
new Person("Zaphod Beeblebrox", "zaphod.beeblebrox@hitchhiker.com")

View File

@@ -44,7 +44,7 @@ public class GitBrowseCommand_BrokenSubmoduleTest extends AbstractGitCommandTest
@Before
public void createCommand() {
command = new GitBrowseCommand(createContext(), lfsBlobStoreFactory, synchronousExecutor());
command = new GitBrowseCommand(createContext("master"), lfsBlobStoreFactory, synchronousExecutor());
}
@Test

View File

@@ -71,6 +71,6 @@ public class GitBrowseCommand_RecursiveDirectoryNameTest extends AbstractGitComm
}
private GitBrowseCommand createCommand() {
return new GitBrowseCommand(createContext(), lfsBlobStoreFactory, synchronousExecutor());
return new GitBrowseCommand(createContext("master"), lfsBlobStoreFactory, synchronousExecutor());
}
}

View File

@@ -68,7 +68,7 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
assertEquals("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1", result.getChangesets().get(1).getId());
assertEquals("592d797cd36432e591416e8b2b98154f4f163411", result.getChangesets().get(2).getId());
assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", result.getChangesets().get(3).getId());
assertEquals("master", result.getBranchName());
assertEquals("main", result.getBranchName());
assertTrue(result.getChangesets().stream().allMatch(r -> r.getBranches().isEmpty()));
// set default branch and fetch again
@@ -271,15 +271,6 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase
assertEquals("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4", c.getId());
}
@Test
public void shouldFindDefaultBranchFromHEAD() throws Exception {
setRepositoryHeadReference("ref: refs/heads/test-branch");
ChangesetPagingResult changesets = createCommand().getChangesets(new LogCommandRequest());
assertEquals("test-branch", changesets.getBranchName());
}
@Test
public void shouldFindMasterBranchWhenHEADisNoRef() throws Exception {
setRepositoryHeadReference("592d797cd36432e591416e8b2b98154f4f163411");

View File

@@ -67,7 +67,7 @@ class GitModifyCommandTestBase extends AbstractGitCommandTestBase {
RepositoryHookEvent postReceiveEvent = mockEvent(POST_RECEIVE);
when(eventFactory.createPostReceiveEvent(any(), any(), any(), any())).thenReturn(postReceiveEvent);
return new GitModifyCommand(
createContext(),
createContext("master"),
lfsBlobStoreFactory,
repositoryManager,
eventFactory

View File

@@ -0,0 +1,460 @@
/*
* 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 org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.eclipse.jgit.lib.GpgConfig;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.Signers;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.github.sdorra.jse.ShiroExtension;
import org.github.sdorra.jse.SubjectAware;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.NoChangesMadeException;
import sonia.scm.NotFoundException;
import sonia.scm.repository.GitTestHelper;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.MultipleParentsNotAllowedException;
import sonia.scm.repository.NoParentException;
import sonia.scm.repository.Person;
import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.api.RevertCommandResult;
import sonia.scm.user.User;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertThrows;
@ExtendWith({MockitoExtension.class, ShiroExtension.class})
class GitRevertCommandTest extends AbstractGitCommandTestBase {
static final String HEAD_REVISION = "18e22df410df66f027dc49bf0f229f4b9efb8ce5";
static final String HEAD_MINUS_0_REVISION = "9d39c9f59030fd4e3d37e1d3717bcca43a9a5eef";
static final String CONFLICTING_TARGET_BRANCH = "conflictingTargetBranch";
static final String CONFLICTING_SOURCE_REVISION = "0d5be1f22687d75916c82ce10eb592375ba0fb21";
static final String PARENTLESS_REVISION = "190bc4670197edeb724f0ee1e49d3a5307635228";
static final String DIVERGING_BRANCH = "divergingBranch";
static final String DIVERGING_MAIN_LATEST_ANCESTOR = "0d5be1f22687d75916c82ce10eb592375ba0fb21";
static final String DIVERGING_BRANCH_LATEST_COMMIT = "e77fd7c8cd45be992e19a6d22170ead4fcd5f9ce";
static final String MERGED_REVISION = "00da9cca94a507346c5b8284983f8a69840cc277";
@Mock
RepositoryManager repositoryManager;
@Mock
GitRepositoryHookEventFactory gitRepositoryHookEventFactory;
@Override
protected String getZippedRepositoryResource() {
return "sonia/scm/repository/spi/scm-git-spi-revert-test.zip";
}
@Nested
class Revert {
@BeforeAll
public static void setSigner() {
Signers.set(GpgConfig.GpgFormat.OPENPGP, new GitTestHelper.SimpleGpgSigner());
}
/**
* We expect the newly created revision to be merged into the given branch.
*/
@Test
void shouldBeTipOfHeadBranchAfterRevert() throws IOException {
GitRevertCommand command = createCommand();
RevertCommandRequest request = new RevertCommandRequest();
request.setRevision(HEAD_REVISION);
request.setAuthor(new Person("Luke Skywalker", "luke@je.di"));
RevertCommandResult result = command.revert(request);
try (
GitContext context = createContext();
Repository jRepository = context.open()) {
assertThat(GitUtil.getBranchId(jRepository, "main").getObjectId().getName()).isEqualTo(result.getRevision());
}
}
@Test
void shouldBeTipOfDifferentBranchAfterRevert() throws IOException {
GitRevertCommand command = createCommand();
RevertCommandRequest request = new RevertCommandRequest();
request.setRevision(DIVERGING_MAIN_LATEST_ANCESTOR);
request.setAuthor(new Person("Luke Skywalker", "luke@je.di"));
request.setBranch(DIVERGING_BRANCH);
RevertCommandResult result = command.revert(request);
try (
GitContext context = createContext();
Repository jRepository = context.open()) {
assertThat(GitUtil.getBranchId(jRepository, DIVERGING_BRANCH).getObjectId().getName()).isEqualTo(result.getRevision());
}
}
@Test
void shouldNotRevertWithoutChange() {
GitRevertCommand command = createCommand();
RevertCommandRequest request = new RevertCommandRequest();
request.setRevision(HEAD_REVISION);
request.setAuthor(new Person("Luke Skywalker", "luke@je.di"));
command.revert(request);
assertThrows(NoChangesMadeException.class, () -> command.revert(request));
}
/**
* Reverting this very commit.
*/
@Test
void shouldRevertHeadCommit() throws IOException {
GitRevertCommand command = createCommand();
RevertCommandRequest request = new RevertCommandRequest();
request.setRevision(HEAD_REVISION);
request.setAuthor(new Person("Luke Skywalker", "luke@je.di"));
request.setBranch("main");
RevertCommandResult result = command.revert(request);
assertThat(result.isSuccessful()).isTrue();
try (GitContext context = createContext()) {
GitDiffCommand diffCommand = new GitDiffCommand(context);
DiffCommandRequest diffRequest = new DiffCommandRequest();
diffRequest.setRevision(result.getRevision());
diffRequest.setPath("hitchhiker");
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
diffCommand.getDiffResult(diffRequest).accept(baos);
assertThat(baos.toString()).contains("George Lucas\n-Darth Vader");
}
}
}
/**
* Reverting this very commit.
* The branch is not explicitly set, so we expect the default branch.
*/
@Test
void shouldRevertHeadCommitImplicitly() throws IOException {
GitRevertCommand command = createCommand();
RevertCommandRequest request = new RevertCommandRequest();
request.setRevision(HEAD_REVISION);
request.setAuthor(new Person("Luke Skywalker", "luke@je.di"));
RevertCommandResult result = command.revert(request);
assertThat(result.isSuccessful()).isTrue();
try (GitContext context = createContext()) {
GitDiffCommand diffCommand = new GitDiffCommand(context);
DiffCommandRequest diffRequest = new DiffCommandRequest();
diffRequest.setRevision(result.getRevision());
diffRequest.setPath("hitchhiker");
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
diffCommand.getDiffResult(diffRequest).accept(baos);
assertThat(baos.toString()).contains("George Lucas\n-Darth Vader");
}
}
}
/**
* Reverting a change from one commit ago.
*/
@Test
void shouldRevertPreviousHistoryCommit() throws IOException {
GitRevertCommand command = createCommand();
RevertCommandRequest request = new RevertCommandRequest();
request.setRevision(HEAD_MINUS_0_REVISION);
request.setAuthor(new Person("Luke Skywalker", "luke@je.di"));
request.setBranch("main");
RevertCommandResult result = command.revert(request);
assertThat(result.isSuccessful()).isTrue();
try (GitContext context = createContext()) {
GitDiffCommand diffCommand = new GitDiffCommand(context);
DiffCommandRequest diffRequest = new DiffCommandRequest();
diffRequest.setRevision(result.getRevision());
diffRequest.setPath("kerbal");
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
diffCommand.getDiffResult(diffRequest).accept(baos);
assertThat(baos.toString()).contains("-deathstar\n+kerbin");
}
}
}
@Test
void shouldRevertCommitOnDifferentBranch() throws IOException {
GitRevertCommand command = createCommand();
RevertCommandRequest request = new RevertCommandRequest();
request.setRevision(DIVERGING_MAIN_LATEST_ANCESTOR);
request.setAuthor(new Person("Luke Skywalker", "luke@je.di"));
request.setBranch(DIVERGING_BRANCH);
RevertCommandResult result = command.revert(request);
assertThat(result.isSuccessful()).isTrue();
try (
GitContext context = createContext();
Repository jRepository = context.open();
RevWalk revWalk = new RevWalk(jRepository)) {
ObjectId objectId = GitUtil.getRevisionId(jRepository, result.getRevision());
RevCommit commit = GitUtil.getCommit(jRepository, revWalk, objectId);
assertThat(commit.getParent(0).getName()).isEqualTo(DIVERGING_BRANCH_LATEST_COMMIT);
GitDiffCommand diffCommand = new GitDiffCommand(context);
DiffCommandRequest diffRequest = new DiffCommandRequest();
diffRequest.setRevision(result.getRevision());
diffRequest.setAncestorChangeset(DIVERGING_BRANCH_LATEST_COMMIT);
diffRequest.setPath("hitchhiker");
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
diffCommand.getDiffResult(diffRequest).accept(baos);
assertThat(baos.toString()).contains("""
-George Lucas
+Douglas Adams"""
);
}
}
}
@Test
void shouldRevertTwiceOnDiffHeads() throws IOException {
GitRevertCommand command = createCommand();
RevertCommandRequest request = new RevertCommandRequest();
request.setRevision(HEAD_MINUS_0_REVISION);
request.setAuthor(new Person("Luke Skywalker", "luke@je.di"));
request.setBranch("main");
RevertCommandResult result1 = command.revert(request);
assertThat(result1.isSuccessful()).isTrue();
request.setRevision(result1.getRevision());
RevertCommandResult result2 = command.revert(request);
assertThat(result2.isSuccessful()).isTrue();
try (GitContext context = createContext()) {
GitDiffCommand diffCommand = new GitDiffCommand(context);
DiffCommandRequest diffRequest = new DiffCommandRequest();
// Check against original head; should be the same
diffRequest.setRevision(HEAD_REVISION);
diffRequest.setPath("kerbal");
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
diffCommand.getDiffResult(diffRequest).accept(baos);
// no difference, thus empty
assertThat(baos.toString()).isEmpty();
}
}
}
@Test
void shouldReportCorrectFilesAfterMergeConflict() {
GitRevertCommand command = createCommand();
RevertCommandRequest request = new RevertCommandRequest();
request.setRevision(CONFLICTING_SOURCE_REVISION);
request.setAuthor(new Person("Luke Skywalker", "luke@je.di"));
request.setBranch(CONFLICTING_TARGET_BRANCH);
RevertCommandResult result = command.revert(request);
assertThat(result.isSuccessful()).isFalse();
assertThat(result.getFilesWithConflict()).containsExactly("hitchhiker");
}
@Test
void shouldSetCustomMessageIfGiven() throws IOException {
GitRevertCommand command = createCommand();
RevertCommandRequest request = new RevertCommandRequest();
request.setRevision(HEAD_REVISION);
request.setAuthor(new Person("Luke Skywalker", "luke@je.di"));
request.setBranch("main");
request.setMessage("I will never join you!");
RevertCommandResult result = command.revert(request);
assertThat(result.isSuccessful()).isTrue();
try (
GitContext context = createContext();
Repository jRepository = context.open();
RevWalk revWalk = new RevWalk(jRepository)) {
ObjectId objectId = GitUtil.getRevisionId(jRepository, result.getRevision());
RevCommit commit = GitUtil.getCommit(jRepository, revWalk, objectId);
assertThat(commit.getShortMessage()).isEqualTo("I will never join you!");
}
}
@Test
void shouldSetDefaultMessageIfNoCustomMessageGiven() throws IOException {
GitRevertCommand command = createCommand();
RevertCommandRequest request = new RevertCommandRequest();
request.setRevision(HEAD_REVISION);
request.setAuthor(new Person("Luke Skywalker", "luke@je.di"));
request.setBranch("main");
RevertCommandResult result = command.revert(request);
try (
GitContext context = createContext();
Repository jRepository = context.open();
RevWalk revWalk = new RevWalk(jRepository)) {
ObjectId revertedCommitId = GitUtil.getRevisionId(jRepository, request.getRevision());
RevCommit revertedCommit = GitUtil.getCommit(jRepository, revWalk, revertedCommitId);
ObjectId newCommitId = GitUtil.getRevisionId(jRepository, result.getRevision());
RevCommit newCommit = GitUtil.getCommit(jRepository, revWalk, newCommitId);
String expectedFullMessage = String.format("""
Revert "%s"
This reverts commit %s.""",
revertedCommit.getShortMessage(), revertedCommit.getName());
assertThat(newCommit.getShortMessage()).isEqualTo(
"Revert \"" + revertedCommit.getShortMessage() + "\"");
assertThat(newCommit.getFullMessage()).isEqualTo(expectedFullMessage);
}
}
@Test
void shouldSignRevertCommit() throws IOException {
GitRevertCommand command = createCommand();
RevertCommandRequest request = new RevertCommandRequest();
request.setRevision(HEAD_REVISION);
request.setAuthor(new Person("Luke Skywalker", "luke@je.di"));
RevertCommandResult result = command.revert(request);
try (
GitContext context = createContext();
Repository jRepository = context.open();
RevWalk revWalk = new RevWalk(jRepository)) {
ObjectId newCommitId = GitUtil.getRevisionId(jRepository, result.getRevision());
RevCommit newCommit = GitUtil.getCommit(jRepository, revWalk, newCommitId);
assertThat(newCommit.getRawGpgSignature()).isNotEmpty();
assertThat(newCommit.getRawGpgSignature()).isEqualTo(GitTestHelper.SimpleGpgSigner.getSignature());
}
}
@Test
void shouldSignNoRevertCommitIfSigningIsDisabled() throws IOException {
GitRevertCommand command = createCommand();
RevertCommandRequest request = new RevertCommandRequest();
request.setRevision(HEAD_REVISION);
request.setAuthor(new Person("Luke Skywalker", "luke@je.di"));
request.setSign(false);
RevertCommandResult result = command.revert(request);
try (
GitContext context = createContext();
Repository jRepository = context.open();
RevWalk revWalk = new RevWalk(jRepository)) {
ObjectId newCommitId = GitUtil.getRevisionId(jRepository, result.getRevision());
RevCommit newCommit = GitUtil.getCommit(jRepository, revWalk, newCommitId);
assertThat(newCommit.getRawGpgSignature()).isNullOrEmpty();
}
}
@Test
@SubjectAware(value = "admin", permissions = "*:*:*")
void shouldTakeAuthorFromSubjectIfNotSet() throws IOException {
SimplePrincipalCollection principals = new SimplePrincipalCollection();
principals.add("admin", "AdminRealm");
principals.add(new User("hitchhiker", "Douglas Adams", "ga@la.xy"), "AdminRealm");
setSubject(new Subject.Builder()
.principals(principals)
.authenticated(true)
.buildSubject());
GitRevertCommand command = createCommand();
RevertCommandRequest request = new RevertCommandRequest();
request.setRevision(HEAD_REVISION);
RevertCommandResult result = command.revert(request);
assertThat(result.isSuccessful()).isTrue();
try (
GitContext context = createContext();
Repository jRepository = context.open();
RevWalk revWalk = new RevWalk(jRepository)) {
ObjectId newCommitId = GitUtil.getRevisionId(jRepository, result.getRevision());
RevCommit newCommit = GitUtil.getCommit(jRepository, revWalk, newCommitId);
PersonIdent author = newCommit.getAuthorIdent();
assertThat(author.getName()).isEqualTo("Douglas Adams");
assertThat(author.getEmailAddress()).isEqualTo("ga@la.xy");
}
}
@Test
void shouldThrowNotFoundExceptionWhenBranchNotExist() {
GitRevertCommand command = createCommand();
RevertCommandRequest request = new RevertCommandRequest();
request.setRevision(HEAD_REVISION);
request.setAuthor(new Person("Luke Skywalker", "luke@je.di"));
request.setBranch("BogusBranch");
assertThatThrownBy(() -> command.revert(request))
.isInstanceOf(NotFoundException.class)
.hasMessage("could not find objectid with id BogusBranch in repository with id hitchhiker/HeartOfGold");
}
@Test
void shouldThrowNotFoundExceptionWhenRevisionNotExist() {
GitRevertCommand command = createCommand();
RevertCommandRequest request = new RevertCommandRequest();
request.setRevision("BogusRevision");
request.setAuthor(new Person("Luke Skywalker", "luke@je.di"));
assertThatThrownBy(() -> command.revert(request))
.isInstanceOf(NotFoundException.class)
.hasMessage("could not find objectid with id BogusRevision in repository with id hitchhiker/HeartOfGold");
}
@Test
void shouldThrowNoParentExceptionWhenParentNotExist() {
GitRevertCommand command = createCommand();
RevertCommandRequest request = new RevertCommandRequest();
request.setRevision(PARENTLESS_REVISION);
request.setAuthor(new Person("Luke Skywalker", "luke@je.di"));
assertThatThrownBy(() -> command.revert(request))
.isInstanceOf(NoParentException.class)
.hasMessage(PARENTLESS_REVISION + " has no parent.");
}
@Test
void shouldThrowMultipleParentsExceptionWhenPickingMergedCommit() {
GitRevertCommand command = createCommand();
RevertCommandRequest request = new RevertCommandRequest();
request.setRevision(MERGED_REVISION);
request.setAuthor(new Person("Luke Skywalker", "luke@je.di"));
assertThatThrownBy(() -> command.revert(request))
.isInstanceOf(MultipleParentsNotAllowedException.class)
.hasMessage(MERGED_REVISION + " has more than one parent changeset, which is not allowed with this request.");
}
private GitRevertCommand createCommand() {
return new GitRevertCommand(createContext("main"), repositoryManager, gitRepositoryHookEventFactory);
}
}
}

View File

@@ -162,10 +162,10 @@ public class SimpleGitWorkingCopyFactoryTest extends AbstractGitCommandTestBase
File workdir = createExistingClone(factory);
GitContext context = createContext();
context.getGlobalConfig().setDefaultBranch("master");
context.getGlobalConfig().setDefaultBranch("main");
factory.reclaim(context, workdir, null);
assertBranchCheckedOutAndClean(workdir, "master");
assertBranchCheckedOutAndClean(workdir, "main");
}
@Test

View File

@@ -0,0 +1,6 @@
You can properly zip a new repository with:
```
ZIP_NAME=your name
(cd scm-git-${ZIP_NAME}-test && zip -r ../scm-git-${ZIP_NAME}-test.zip .)
```