Implement commit search features for git (#2111)

Implements the required features for the commit search plugin for git.
This commit is contained in:
René Pfeuffer
2022-09-01 15:31:33 +02:00
committed by GitHub
parent d7bf14518a
commit e0a56a1369
15 changed files with 925 additions and 356 deletions

View File

@@ -24,6 +24,8 @@
package sonia.scm.repository.spi;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -32,9 +34,14 @@ import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.Branch;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.GitChangesetConverter;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.PreReceiveRepositoryHookEvent;
import sonia.scm.repository.api.BranchRequest;
import sonia.scm.repository.api.HookChangesetBuilder;
import sonia.scm.repository.api.HookContext;
import sonia.scm.repository.api.HookContextFactory;
@@ -48,14 +55,30 @@ import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class GitBranchCommandTest extends AbstractGitCommandTestBase {
@Mock
private PreProcessorUtil preProcessorUtil;
private HookContextFactory hookContextFactory;
@Mock
private ScmEventBus eventBus;
@Mock
private GitChangesetConverterFactory converterFactory;
@Before
public void mockConverterFactory() {
GitChangesetConverter gitChangesetConverter = mock(GitChangesetConverter.class);
when(converterFactory.create(any(), any()))
.thenReturn(gitChangesetConverter);
when(gitChangesetConverter.createChangeset(any(), (String[]) any()))
.thenAnswer(invocation -> {
RevCommit revCommit = invocation.getArgument(0, RevCommit.class);
Changeset changeset = new Changeset(revCommit.name(), null, null);
return changeset;
});
hookContextFactory = new HookContextFactory(preProcessorUtil);
}
@Test
public void shouldCreateBranchWithDefinedSourceBranch() throws IOException {
@@ -108,7 +131,7 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase {
}
private GitBranchCommand createCommand() {
return new GitBranchCommand(createContext(), hookContextFactory, eventBus);
return new GitBranchCommand(createContext(), hookContextFactory, eventBus, converterFactory);
}
private List<Branch> readBranches(GitContext context) throws IOException {
@@ -119,7 +142,6 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase {
public void shouldPostCreateEvents() {
ArgumentCaptor<Object> captor = ArgumentCaptor.forClass(Object.class);
doNothing().when(eventBus).post(captor.capture());
when(hookContextFactory.createContext(any(), any())).thenAnswer(this::createMockedContext);
BranchRequest branchRequest = new BranchRequest();
branchRequest.setParentBranch("mergeable");
@@ -140,7 +162,6 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase {
public void shouldPostDeleteEvents() {
ArgumentCaptor<Object> captor = ArgumentCaptor.forClass(Object.class);
doNothing().when(eventBus).post(captor.capture());
when(hookContextFactory.createContext(any(), any())).thenAnswer(this::createMockedContext);
createCommand().deleteOrClose("squash");
@@ -151,11 +172,15 @@ public class GitBranchCommandTest extends AbstractGitCommandTestBase {
PreReceiveRepositoryHookEvent event = (PreReceiveRepositoryHookEvent) events.get(0);
assertThat(event.getContext().getBranchProvider().getDeletedOrClosed()).containsExactly("squash");
assertThat(event.getContext().getBranchProvider().getCreatedOrModified()).isEmpty();
}
private HookContext createMockedContext(InvocationOnMock invocation) {
HookContext mock = mock(HookContext.class);
when(mock.getBranchProvider()).thenReturn(((HookContextProvider) invocation.getArgument(0)).getBranchProvider());
return mock;
HookChangesetBuilder changesetProvider = event.getContext().getChangesetProvider();
assertThat(changesetProvider.getChangesets()).isEmpty();
assertThat(changesetProvider.getRemovedChangesets())
.extracting("id")
.containsExactly(
"35597e9e98fe53167266583848bfef985c2adb27",
"f360a8738e4a29333786c5817f97a2c912814536",
"d1dfecbfd5b4a2f77fe40e1bde29e640f7f944be"
);
}
}

View File

@@ -0,0 +1,76 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi;
import org.junit.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.GitTestHelper;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockitoExtension.class)
public class GitChangesetsCommandTest extends AbstractGitCommandTestBase {
@Test
public void shouldFindLatestCommit() {
GitChangesetsCommand command = new GitChangesetsCommand(createContext(), GitTestHelper.createConverterFactory());
Optional<Changeset> changeset = command.getLatestChangeset();
assertThat(changeset).get().extracting("id").isEqualTo("a8495c0335a13e6e432df90b3727fa91943189a7");
}
@Test
public void shouldListAllRevisions() {
GitChangesetsCommand command = new GitChangesetsCommand(createContext(), GitTestHelper.createConverterFactory());
Iterable<Changeset> changesets = command.getChangesets(new ChangesetsCommandRequest());
assertThat(changesets)
.extracting("id")
.hasSize(19)
.contains(
"a8495c0335a13e6e432df90b3727fa91943189a7",
"03ca33468c2094249973d0ca11b80243a20de368",
"9e93d8631675a89615fac56b09209686146ff3c0",
"383b954b27e052db6880d57f1c860dc208795247",
"1fcebf45a215a43f0713a57b807d55e8387a6d70",
"9f28cf5eb3a4df05d284c6f2d276c20c0f0e5b6c",
"674ca9a2208df60224b0f33beeea5259b374d2d0",
"35597e9e98fe53167266583848bfef985c2adb27",
"f360a8738e4a29333786c5817f97a2c912814536",
"d1dfecbfd5b4a2f77fe40e1bde29e640f7f944be",
"d81ad6c63d7e2162308d69637b339dedd1d9201c",
"6a2abf9dca5cff5d76720d1276e1112d9c75ea60",
"2f95f02d9c568594d31e78464bd11a96c62e3f91",
"fcd0ef1831e4002ac43ea539f4094334c79ea9ec",
"86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1",
"91b99de908fcd04772798a31c308a64aea1a5523",
"3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4",
"592d797cd36432e591416e8b2b98154f4f163411",
"435df2f061add3589cb326cc64be9b9c3897ceca"
);
}
}

View File

@@ -0,0 +1,211 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
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.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.ReceivePack;
import org.junit.Before;
import org.junit.Test;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.GitChangesetConverter;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.web.CollectingPackParserListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.util.Arrays.asList;
import static org.eclipse.jgit.lib.ObjectId.fromString;
import static org.eclipse.jgit.lib.ObjectId.zeroId;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class GitHookChangesetCollectorTest extends AbstractGitCommandTestBase {
private final ReceivePack rpack = mock(ReceivePack.class);
private final Collection<ReceiveCommand> receiveCommands = new ArrayList<>();
private final CollectingPackParserListener listener = mock(CollectingPackParserListener.class);
private final GitChangesetConverterFactory converterFactory = mock(GitChangesetConverterFactory.class);
private final GitChangesetConverter converter = mock(GitChangesetConverter.class);
private GitHookChangesetCollector collector;
@Before
public void init() throws IOException {
GitContext context = createContext();
Repository repository = context.open();
RevWalk revWalk = new RevWalk(repository);
when(rpack.getRepository()).thenReturn(repository);
when(rpack.getRevWalk()).thenReturn(revWalk);
when(rpack.getPackParserListener()).thenReturn(listener);
when(converterFactory.create(repository, revWalk)).thenReturn(converter);
when(converter.createChangeset(any(), (String[]) any()))
.thenAnswer(invocation -> new Changeset(invocation.getArgument(0, RevCommit.class).name(), null, null));
}
@Test
public void shouldCreateEmptyCollectionsWithoutChanges() {
collector = GitHookChangesetCollector.collectChangesets(converterFactory, receiveCommands, rpack);
assertThat(collector.getAddedChangesets()).isEmpty();
assertThat(collector.getRemovedChangesets()).isEmpty();
}
@Test
public void shouldFindAddedChangesetsFromNewBranch() {
receiveCommands.add(
new ReceiveCommand(
zeroId(),
fromString("91b99de908fcd04772798a31c308a64aea1a5523"),
"refs/heads/mergeable")
);
mockNewCommits(
"91b99de908fcd04772798a31c308a64aea1a5523",
"3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4",
"592d797cd36432e591416e8b2b98154f4f163411");
collector = GitHookChangesetCollector.collectChangesets(converterFactory, receiveCommands, rpack);
assertThat(collector.getAddedChangesets())
.extracting("id")
.contains(
"91b99de908fcd04772798a31c308a64aea1a5523",
"592d797cd36432e591416e8b2b98154f4f163411");
assertThat(collector.getRemovedChangesets()).isEmpty();
}
@Test
public void shouldFindAddedChangesetsFromNewBranchesOnce() throws IOException, GitAPIException {
new Git(createContext().open()).branchCreate().setStartPoint("mergeable").setName("second").call();
receiveCommands.add(
new ReceiveCommand(
zeroId(),
fromString("91b99de908fcd04772798a31c308a64aea1a5523"),
"refs/heads/mergeable")
);
receiveCommands.add(
new ReceiveCommand(
zeroId(),
fromString("91b99de908fcd04772798a31c308a64aea1a5523"),
"refs/heads/second")
);
mockNewCommits(
"91b99de908fcd04772798a31c308a64aea1a5523",
"3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4",
"592d797cd36432e591416e8b2b98154f4f163411");
collector = GitHookChangesetCollector.collectChangesets(converterFactory, receiveCommands, rpack);
assertThat(collector.getAddedChangesets())
.extracting("id")
.hasSize(2)
.contains(
"91b99de908fcd04772798a31c308a64aea1a5523",
"592d797cd36432e591416e8b2b98154f4f163411");
assertThat(collector.getRemovedChangesets()).isEmpty();
}
@Test
public void shouldFindAddedChangesetsFromChangedBranchWithoutIteratingOldCommits() {
receiveCommands.add(
new ReceiveCommand(
fromString("592d797cd36432e591416e8b2b98154f4f163411"),
fromString("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"),
"refs/heads/test-branch")
);
mockNewCommits("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
collector = GitHookChangesetCollector.collectChangesets(converterFactory, receiveCommands, rpack);
assertThat(collector.getAddedChangesets())
.extracting("id")
.contains("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
assertThat(collector.getRemovedChangesets()).isEmpty();
verify(listener, never()).isNew(argThat(argument -> argument.name().equals("592d797cd36432e591416e8b2b98154f4f163411")));
}
@Test
public void shouldFindRemovedChangesetsFromDeletedBranch() throws IOException, GitAPIException {
new Git(createContext().open()).branchDelete().setBranchNames("test-branch").setForce(true).call();
receiveCommands.add(
new ReceiveCommand(
fromString("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"),
zeroId(),
"refs/heads/test-branch",
ReceiveCommand.Type.DELETE)
);
mockNewCommits("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
collector = GitHookChangesetCollector.collectChangesets(converterFactory, receiveCommands, rpack);
assertThat(collector.getAddedChangesets()).isEmpty();
assertThat(collector.getRemovedChangesets())
.extracting("id")
.contains("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
verify(listener, never()).isNew(argThat(argument -> argument.name().equals("592d797cd36432e591416e8b2b98154f4f163411")));
}
@Test
public void shouldFindRemovedAndAddedChangesetsFromNonFastForwardChanged() throws IOException, GitAPIException {
new Git(createContext().open()).branchDelete().setBranchNames("test-branch").setForce(true).call();
receiveCommands.add(
new ReceiveCommand(
fromString("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4"),
fromString("91b99de908fcd04772798a31c308a64aea1a5523"),
"refs/heads/test-branch",
ReceiveCommand.Type.UPDATE_NONFASTFORWARD)
);
mockNewCommits("91b99de908fcd04772798a31c308a64aea1a5523");
collector = GitHookChangesetCollector.collectChangesets(converterFactory, receiveCommands, rpack);
assertThat(collector.getAddedChangesets())
.extracting("id")
.contains("91b99de908fcd04772798a31c308a64aea1a5523");
assertThat(collector.getRemovedChangesets())
.extracting("id")
.contains("3f76a12f08a6ba0dc988c68b7f0b2cd190efc3c4");
}
private void mockNewCommits(String... objectIds) {
when(listener.isNew(any()))
.thenAnswer(invocation -> asList(objectIds).contains(invocation.getArgument(0, RevCommit.class).name()));
}
}

View File

@@ -28,24 +28,33 @@ import org.apache.shiro.SecurityUtils;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.GpgSigner;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.event.ScmEventBus;
import sonia.scm.repository.Changeset;
import sonia.scm.repository.GitChangesetConverter;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitTestHelper;
import sonia.scm.repository.PostReceiveRepositoryHookEvent;
import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.PreReceiveRepositoryHookEvent;
import sonia.scm.repository.Tag;
import sonia.scm.repository.api.HookContext;
import sonia.scm.repository.api.HookChangesetBuilder;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.repository.api.TagDeleteRequest;
import sonia.scm.repository.api.HookTagProvider;
import sonia.scm.repository.api.TagCreateRequest;
import sonia.scm.repository.api.TagDeleteRequest;
import sonia.scm.security.GPG;
import sonia.scm.util.MockUtil;
@@ -66,12 +75,15 @@ public class GitTagCommandTest extends AbstractGitCommandTestBase {
private GPG gpg;
@Mock
private PreProcessorUtil preProcessorUtil;
private HookContextFactory hookContextFactory;
@Mock
private ScmEventBus eventBus;
private Subject subject;
@Mock
private GitChangesetConverterFactory converterFactory;
@Before
public void setSigner() {
@@ -81,10 +93,24 @@ public class GitTagCommandTest extends AbstractGitCommandTestBase {
@Before
public void bindThreadContext() {
SecurityUtils.setSecurityManager(new DefaultSecurityManager());
subject = MockUtil.createUserSubject(SecurityUtils.getSecurityManager());
Subject subject = MockUtil.createUserSubject(SecurityUtils.getSecurityManager());
ThreadContext.bind(subject);
}
@Before
public void mockConverterFactory() {
GitChangesetConverter gitChangesetConverter = mock(GitChangesetConverter.class);
when(converterFactory.create(any(), any()))
.thenReturn(gitChangesetConverter);
when(gitChangesetConverter.createChangeset(any(), (String[]) any()))
.thenAnswer(invocation -> {
RevCommit revCommit = invocation.getArgument(0, RevCommit.class);
Changeset changeset = new Changeset(revCommit.name(), null, null);
return changeset;
});
hookContextFactory = new HookContextFactory(preProcessorUtil);
}
@After
public void unbindThreadContext() {
ThreadContext.unbindSubject();
@@ -105,7 +131,6 @@ public class GitTagCommandTest extends AbstractGitCommandTestBase {
public void shouldPostCreateEvent() {
ArgumentCaptor<Object> captor = ArgumentCaptor.forClass(Object.class);
doNothing().when(eventBus).post(captor.capture());
when(hookContextFactory.createContext(any(), any())).thenAnswer(this::createMockedContext);
createCommand().create(new TagCreateRequest("592d797cd36432e591416e8b2b98154f4f163411", "newtag"));
@@ -131,26 +156,36 @@ public class GitTagCommandTest extends AbstractGitCommandTestBase {
}
@Test
public void shouldPostDeleteEvent() {
public void shouldPostDeleteEvent() throws IOException, GitAPIException {
Git git = new Git(createContext().open());
git.tag().setName("to-be-deleted").setObjectId(getCommit("383b954b27e052db6880d57f1c860dc208795247")).call();
git.branchDelete().setBranchNames("rename").setForce(true).call();
ArgumentCaptor<Object> captor = ArgumentCaptor.forClass(Object.class);
doNothing().when(eventBus).post(captor.capture());
when(hookContextFactory.createContext(any(), any())).thenAnswer(this::createMockedContext);
createCommand().delete(new TagDeleteRequest("test-tag"));
createCommand().delete(new TagDeleteRequest("to-be-deleted"));
List<Object> events = captor.getAllValues();
assertThat(events.get(0)).isInstanceOf(PreReceiveRepositoryHookEvent.class);
assertThat(events.get(1)).isInstanceOf(PostReceiveRepositoryHookEvent.class);
PreReceiveRepositoryHookEvent event = (PreReceiveRepositoryHookEvent) events.get(0);
assertThat(event.getContext().getTagProvider().getCreatedTags()).isEmpty();
final Tag deletedTag = event.getContext().getTagProvider().getDeletedTags().get(0);
assertThat(deletedTag.getName()).isEqualTo("test-tag");
assertThat(deletedTag.getRevision()).isEqualTo("86a6645eceefe8b9a247db5eb16e3d89a7e6e6d1");
HookTagProvider tagProvider = event.getContext().getTagProvider();
assertThat(tagProvider.getCreatedTags()).isEmpty();
Tag deletedTag = tagProvider.getDeletedTags().get(0);
assertThat(deletedTag.getName()).isEqualTo("to-be-deleted");
assertThat(deletedTag.getRevision()).isEqualTo("383b954b27e052db6880d57f1c860dc208795247");
HookChangesetBuilder changesetProvider = event.getContext().getChangesetProvider();
assertThat(changesetProvider.getChangesets()).isEmpty();
assertThat(changesetProvider.getRemovedChangesets())
.extracting("id")
.containsExactly("383b954b27e052db6880d57f1c860dc208795247");
}
private GitTagCommand createCommand() {
return new GitTagCommand(createContext(), hookContextFactory, eventBus);
return new GitTagCommand(createContext(), hookContextFactory, eventBus, converterFactory);
}
private List<Tag> readTags(GitContext context) throws IOException {
@@ -162,9 +197,10 @@ public class GitTagCommandTest extends AbstractGitCommandTestBase {
return tags.stream().filter(t -> name.equals(t.getName())).findFirst();
}
private HookContext createMockedContext(InvocationOnMock invocation) {
HookContext mock = mock(HookContext.class);
when(mock.getTagProvider()).thenReturn(((HookContextProvider) invocation.getArgument(0)).getTagProvider());
return mock;
private RevCommit getCommit(String revision) throws IOException {
ObjectId commitId = ObjectId.fromString(revision);
try (RevWalk revWalk = new RevWalk(createContext().open())) {
return revWalk.parseCommit(commitId);
}
}
}