Merge branch 'develop' into feature/admin_info

This commit is contained in:
Eduard Heimbuch
2020-09-24 10:39:02 +02:00
20 changed files with 212 additions and 35 deletions

View File

@@ -1,3 +1,4 @@
# Changelog
All notable changes to this project will be documented in this file.
@@ -6,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
- Add support for pr merge with prior rebase ([#1332](https://github.com/scm-manager/scm-manager/pull/1332))
- Tags overview for repository [#1331](https://github.com/scm-manager/scm-manager/pull/1331)
- Permissions can be specified for namespaces ([#1335](https://github.com/scm-manager/scm-manager/pull/1335))
- Show update info on admin information page ([#1342](https://github.com/scm-manager/scm-manager/pull/1342))
@@ -17,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Overflow for too long branch names ([#1339](https://github.com/scm-manager/scm-manager/pull/1339))
- Set default branch in branch selector if nothing is selected ([#1338](https://github.com/scm-manager/scm-manager/pull/1338))
- Handling of branch with slashes in source view ([#1340](https://github.com/scm-manager/scm-manager/pull/1340))
- Detect not existing paths correctly in Mercurial ([#1343](https://github.com/scm-manager/scm-manager/pull/1343))
## [2.5.0] - 2020-09-10
### Added

View File

@@ -464,7 +464,7 @@
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.17.0</version>
<version>3.17.1</version>
<scope>test</scope>
</dependency>
@@ -903,7 +903,7 @@
<properties>
<!-- test libraries -->
<mockito.version>3.5.7</mockito.version>
<mockito.version>3.5.9</mockito.version>
<hamcrest.version>2.1</hamcrest.version>
<junit.version>5.6.2</junit.version>

View File

@@ -21,11 +21,22 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.api;
public enum MergeStrategy {
MERGE_COMMIT,
FAST_FORWARD_IF_POSSIBLE,
SQUASH
MERGE_COMMIT(true),
FAST_FORWARD_IF_POSSIBLE(true),
SQUASH(true),
REBASE(false);
private final boolean commitMessageAllowed;
MergeStrategy(boolean commitMessageAllowed) {
this.commitMessageAllowed = commitMessageAllowed;
}
public boolean isCommitMessageAllowed() {
return commitMessageAllowed;
}
}

View File

@@ -60,7 +60,8 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
private static final Set<MergeStrategy> STRATEGIES = ImmutableSet.of(
MergeStrategy.MERGE_COMMIT,
MergeStrategy.FAST_FORWARD_IF_POSSIBLE,
MergeStrategy.SQUASH
MergeStrategy.SQUASH,
MergeStrategy.REBASE
);
@Inject
@@ -94,6 +95,9 @@ public class GitMergeCommand extends AbstractGitCommand implements MergeCommand
case MERGE_COMMIT:
return inClone(clone -> new GitMergeCommit(clone, request, context, repository), workingCopyFactory, request.getTargetBranch());
case REBASE:
return inClone(clone -> new GitMergeRebase(clone, request, context, repository), workingCopyFactory, request.getTargetBranch());
default:
throw new MergeStrategyNotSupportedException(repository, request.getMergeStrategy());
}

View File

@@ -0,0 +1,96 @@
/*
* 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.MergeCommand;
import org.eclipse.jgit.api.RebaseResult;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository;
import sonia.scm.repository.api.MergeCommandResult;
import java.io.IOException;
import java.util.Collections;
import java.util.Optional;
public class GitMergeRebase extends GitMergeStrategy {
private static final Logger logger = LoggerFactory.getLogger(GitMergeRebase.class);
private final MergeCommandRequest request;
GitMergeRebase(Git clone, MergeCommandRequest request, GitContext context, Repository repository) {
super(clone, request, context, repository);
this.request = request;
}
@Override
MergeCommandResult run() throws IOException {
RebaseResult result;
String branchToMerge = request.getBranchToMerge();
String targetBranch = request.getTargetBranch();
try {
checkOutBranch(branchToMerge);
result =
getClone()
.rebase()
.setUpstream(targetBranch)
.call();
} catch (GitAPIException e) {
throw new InternalRepositoryException(getContext().getRepository(), "could not rebase branch " + branchToMerge + " onto " + targetBranch, e);
}
if (result.getStatus().isSuccessful()) {
return fastForwardTargetBranch(branchToMerge, targetBranch, result);
} else {
logger.info("could not rebase branch {} into {} with rebase status '{}' due to ...", branchToMerge, targetBranch, result.getStatus());
logger.info("... conflicts: {}", result.getConflicts());
logger.info("... failing paths: {}", result.getFailingPaths());
logger.info("... message: {}", result);
return MergeCommandResult.failure(branchToMerge, targetBranch, Optional.ofNullable(result.getConflicts()).orElse(Collections.singletonList("UNKNOWN")));
}
}
private MergeCommandResult fastForwardTargetBranch(String branchToMerge, String targetBranch, RebaseResult result) throws IOException {
try {
getClone().checkout().setName(targetBranch).call();
ObjectId sourceRevision = resolveRevision(branchToMerge);
getClone()
.merge()
.setFastForward(MergeCommand.FastForwardMode.FF_ONLY)
.include(branchToMerge, sourceRevision)
.call();
push();
return MergeCommandResult.success(getTargetRevision().name(), branchToMerge, sourceRevision.name());
} catch (GitAPIException e) {
return MergeCommandResult.failure(branchToMerge, targetBranch, result.getConflicts());
}
}
}

View File

@@ -473,6 +473,47 @@ public class GitMergeCommandTest extends AbstractGitCommandTestBase {
}
@Test
public void shouldAllowMergeWithRebase() throws IOException, GitAPIException {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("master");
request.setBranchToMerge("mergeable");
request.setMergeStrategy(MergeStrategy.REBASE);
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
MergeCommandResult mergeCommandResult = command.merge(request);
assertThat(mergeCommandResult.isSuccess()).isTrue();
Repository repository = createContext().open();
Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
RevCommit mergeCommit = commits.iterator().next();
assertThat(mergeCommit.getParentCount()).isEqualTo(1);
assertThat(mergeCommit.getParent(0).name()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec");
assertThat(mergeCommit.getName()).isEqualTo(mergeCommandResult.getNewHeadRevision());
assertThat(mergeCommit.getName()).doesNotStartWith("91b99de908fcd04772798a31c308a64aea1a5523");
}
@Test
public void shouldRejectRebaseMergeIfBranchCannotBeRebased() throws IOException, GitAPIException {
GitMergeCommand command = createCommand();
MergeCommandRequest request = new MergeCommandRequest();
request.setTargetBranch("master");
request.setBranchToMerge("not-rebasable");
request.setMergeStrategy(MergeStrategy.REBASE);
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
MergeCommandResult mergeCommandResult = command.merge(request);
assertThat(mergeCommandResult.isSuccess()).isFalse();
Repository repository = createContext().open();
Iterable<RevCommit> commits = new Git(repository).log().add(repository.resolve("master")).setMaxCount(1).call();
RevCommit headCommit = commits.iterator().next();
assertThat(headCommit.getName()).isEqualTo("fcd0ef1831e4002ac43ea539f4094334c79ea9ec");
}
private GitMergeCommand createCommand() {
return createCommand(git -> {
});

View File

@@ -36,6 +36,9 @@ import sonia.scm.repository.spi.javahg.HgFileviewCommand;
import java.io.IOException;
import static sonia.scm.ContextEntry.ContextBuilder.entity;
import static sonia.scm.NotFoundException.notFound;
//~--- JDK imports ------------------------------------------------------------
/**
@@ -93,7 +96,8 @@ public class HgBrowseCommand extends AbstractCommand implements BrowseCommand
cmd.setLimit(request.getLimit());
cmd.setOffset(request.getOffset());
FileObject file = cmd.execute();
FileObject file = cmd.execute()
.orElseThrow(() -> notFound(entity("File", request.getPath()).in("Revision", revision).in(getRepository())));
return new BrowserResult(c == null? "tip": c.getNode(), revision, file);
}
}

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi.javahg;
//~--- non-JDK imports --------------------------------------------------------
@@ -29,12 +29,12 @@ package sonia.scm.repository.spi.javahg;
import com.aragost.javahg.Repository;
import com.aragost.javahg.internals.AbstractCommand;
import com.aragost.javahg.internals.HgInputStream;
import sonia.scm.repository.FileObject;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import java.util.Optional;
/**
* Mercurial command to list files of a repository.
@@ -161,7 +161,7 @@ public class HgFileviewCommand extends AbstractCommand
*
* @throws IOException
*/
public FileObject execute() throws IOException
public Optional<FileObject> execute() throws IOException
{
cmdAppend("-t");

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi.javahg;
import com.aragost.javahg.DateTime;
@@ -35,6 +35,10 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Optional;
import static java.util.Optional.empty;
import static java.util.Optional.of;
class HgFileviewCommandResultReader {
@@ -48,7 +52,7 @@ class HgFileviewCommandResultReader {
this.disableLastCommit = disableLastCommit;
}
FileObject parseResult() throws IOException {
Optional<FileObject> parseResult() throws IOException {
Deque<FileObject> stack = new LinkedList<>();
FileObject last = null;
@@ -82,13 +86,17 @@ class HgFileviewCommandResultReader {
if (stack.isEmpty()) {
// if the stack is empty, the requested path is probably a file
return last;
return of(last);
} else if (stack.size() == 1 && stack.getFirst().isDirectory() && stack.getFirst().getChildren().isEmpty()) {
// There are no empty directories in hg. When we get this,
// we just get the requested path as a directory, but it does not exist.
return empty();
} else {
// if the stack is not empty, the requested path is a directory
if (stream.read() == TRUNCATED_MARK) {
stack.getLast().setTruncated(true);
}
return stack.getLast();
return of(stack.getLast());
}
}

View File

@@ -21,11 +21,10 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.repository.spi.javahg;
import com.aragost.javahg.internals.HgInputStream;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import sonia.scm.repository.FileObject;
@@ -34,6 +33,7 @@ import java.io.IOException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Iterator;
import java.util.Optional;
import java.util.OptionalLong;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -55,7 +55,7 @@ class HgFileviewCommandResultReaderTest {
.file("b.txt", 100, time2.toEpochMilli(), "file b\nwith some\nmore text")
.build();
FileObject fileObject = reader.parseResult();
FileObject fileObject = reader.parseResult().get();
assertThat(fileObject.isDirectory()).isTrue();
assertThat(fileObject.getChildren())
@@ -84,7 +84,7 @@ class HgFileviewCommandResultReaderTest {
.file("a.txt")
.truncated();
FileObject fileObject = reader.parseResult();
FileObject fileObject = reader.parseResult().get();
assertThat(fileObject.isTruncated()).isTrue();
}
@@ -96,7 +96,7 @@ class HgFileviewCommandResultReaderTest {
.file("dir/a.txt")
.build();
FileObject fileObject = reader.parseResult();
FileObject fileObject = reader.parseResult().get();
assertThat(fileObject.isDirectory()).isTrue();
assertThat(fileObject.getName()).isEqualTo("dir");
@@ -117,7 +117,7 @@ class HgFileviewCommandResultReaderTest {
.file("d.txt")
.build();
FileObject fileObject = reader.parseResult();
FileObject fileObject = reader.parseResult().get();
assertThat(fileObject.getChildren())
.extracting("name")
@@ -152,7 +152,7 @@ class HgFileviewCommandResultReaderTest {
.file("d.txt")
.build();
FileObject fileObject = reader.parseResult();
FileObject fileObject = reader.parseResult().get();
assertThat(fileObject.getChildren())
.extracting("name")
@@ -179,7 +179,7 @@ class HgFileviewCommandResultReaderTest {
.file("d.txt")
.build();
FileObject fileObject = reader.parseResult();
FileObject fileObject = reader.parseResult().get();
assertThat(fileObject.getChildren())
.extracting("name")
@@ -213,7 +213,7 @@ class HgFileviewCommandResultReaderTest {
.file("directory/b.txt")
.build();
FileObject fileObject = reader.parseResult();
FileObject fileObject = reader.parseResult().get();
assertThat(fileObject.getChildren())
.extracting("name")
@@ -248,7 +248,7 @@ class HgFileviewCommandResultReaderTest {
.file("a.txt")
.build();
FileObject fileObject = reader.parseResult();
FileObject fileObject = reader.parseResult().get();
assertThat(fileObject.getChildren())
.extracting("description")
@@ -258,6 +258,17 @@ class HgFileviewCommandResultReaderTest {
.containsOnly(OptionalLong.empty());
}
@Test
void shouldIgnoreSingleEmptyDir() throws IOException {
HgFileviewCommandResultReader reader = new MockInput()
.dir("empty")
.build();
Optional<FileObject> fileObject = reader.parseResult();
assertThat(fileObject).isEmpty();
}
private HgInputStream createInputStream(String input) {
return new HgInputStream(new ByteArrayInputStream(input.getBytes(UTF_8)), UTF_8.newDecoder());
}

View File

@@ -69,7 +69,7 @@
</dependencies>
<properties>
<commons.daemon.version>1.2.2</commons.daemon.version>
<commons.daemon.version>1.2.3</commons.daemon.version>
</properties>
</project>

View File

@@ -61,7 +61,7 @@
"realmDescriptionHelpText": "Beschreibung des Authentication Realm.",
"dateFormatHelpText": "Moments Datumsformat. Zulässige Formate sind in der MomentJS Dokumentation beschrieben.",
"pluginUrlHelpText": "Die URL der Plugin Center API. Beschreibung der Platzhalter: version = SCM-Manager Version; os = Betriebssystem; arch = Architektur",
"releaseFeedUrlHelpText": "Die URL des RSS Release Feed des SCM-Manager. Darüber wird über die neue SCM-Manager Versionen informiert. Um diese Funktion zu deaktivieren lassen Sie dieses Feld leer.",
"releaseFeedUrlHelpText": "Die URL des RSS Release Feed des SCM-Manager. Darüber wird über die neue SCM-Manager Version informiert. Um diese Funktion zu deaktivieren lassen Sie dieses Feld leer.",
"enableForwardingHelpText": "mod_proxy Port Weiterleitung aktivieren.",
"disableGroupingGridHelpText": "Repository Gruppen deaktivieren. Nach einer Änderung an dieser Einstellung muss die Seite komplett neu geladen werden.",
"allowAnonymousAccessHelpText": "Anonyme Benutzer haben Zugriff auf freigegebene Repositories.",

View File

@@ -47,7 +47,7 @@ class NamespaceToNamespaceDtoMapper {
.self(links.namespace().self(namespace))
.single(link("repositories", links.repositoryCollection().forNamespace(namespace)));
if (NamespacePermissions.permissionRead().isPermitted() || NamespacePermissions.permissionWrite().isPermitted()) {
if (NamespacePermissions.permissionRead().isPermitted()) {
linkingTo
.single(link("permissions", links.namespacePermission().all(namespace)));
}

View File

@@ -78,9 +78,7 @@ public class RepositoryPermissionCollectionToDtoMapper {
}
private Links createLinks(Namespace namespace) {
if (!NamespacePermissions.permissionWrite().isPermitted()) {
NamespacePermissions.permissionRead().check();
}
NamespacePermissions.permissionRead().check();
Links.Builder linksBuilder = linkingTo()
.with(Links.linkingTo().self(resourceLinks.namespacePermission().all(namespace.getNamespace())).build());
if (NamespacePermissions.permissionWrite().isPermitted()) {

View File

@@ -104,7 +104,7 @@ public class DefaultNamespaceManager implements NamespaceManager {
}
private Namespace createNamespaceForName(String namespace) {
if (NamespacePermissions.permissionRead().isPermitted() || NamespacePermissions.permissionWrite().isPermitted()) {
if (NamespacePermissions.permissionRead().isPermitted()) {
return dao.get(namespace)
.map(Namespace::clone)
.orElse(new Namespace(namespace));

View File

@@ -48,7 +48,7 @@
<value>namespace:permissionRead</value>
</permission>
<permission>
<value>namespace:permissionWrite</value>
<value>namespace:permissionRead,permissionWrite</value>
</permission>
<permission>
<value>user:*</value>

View File

@@ -98,7 +98,7 @@
"displayName": "Berechtigungen auf Namespaces lesen",
"description": "Darf die Berechtigungen auf Namespace-Ebene sehen"
},
"permissionWrite": {
"permissionRead,permissionWrite": {
"displayName": "Berechtigungen auf Namespaces modifizieren",
"description": "Darf die Berechtigungen auf Namespace-Ebene lesen und bearbeiten"
}

View File

@@ -98,7 +98,7 @@
"displayName": "Read permissions on namespaces",
"description": "May see the permissions set for namespaces"
},
"permissionWrite": {
"permissionRead,permissionWrite": {
"displayName": "Modify permissions on namespaces",
"description": "May read and modify the permissions set for namespaces"
}

View File

@@ -132,6 +132,7 @@ class IndexDtoGeneratorTest {
when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(scmPathInfo));
when(resourceLinks.repositoryRoleCollection()).thenReturn(new ResourceLinks.RepositoryRoleCollectionLinks(scmPathInfo));
when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(scmPathInfo));
when(resourceLinks.namespaceCollection()).thenReturn(new ResourceLinks.NamespaceCollectionLinks(scmPathInfo));
when(resourceLinks.me()).thenReturn(new ResourceLinks.MeLinks(scmPathInfo, new ResourceLinks.UserLinks(scmPathInfo)));
}
}