Merge pull request #1399 from scm-manager/feature/default_branch

Make default branch for git configurable
This commit is contained in:
Sebastian Sdorra
2020-11-03 08:23:03 +01:00
committed by GitHub
35 changed files with 411 additions and 197 deletions

View File

@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
- Automatic user converter for external users ([#1380](https://github.com/scm-manager/scm-manager/pull/1380))
- The name of the initial git branch can be configured and is set to `main` by default ([#1399](https://github.com/scm-manager/scm-manager/pull/1399))
### Fixed
- Internal server error for git sub modules without tree object ([#1397](https://github.com/scm-manager/scm-manager/pull/1397))

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

23
docs/de/user/admin/git.md Normal file
View File

@@ -0,0 +1,23 @@
---
title: Administration
subtitle: Git
---
Unter dem Eintrag Git können die folgenden Git-spezifischen Einstellungen vorgenommen werden:
- GC Cron Ausdruck
Wenn hier ein Wert gesetzt wird, führt der SCM-Manager zu den
[entsprechenden Zeiten](https://de.wikipedia.org/wiki/Cron)
eine "Git Garbage Collection" aus.
- Deaktiviere "Non Fast-Forward"
Wenn dieses aktiviert ist, werden "forcierte" Pushs abgelehnt, wenn diese keine "fast forwards" sind.
- Default Branch
Der hier gesetzte Branch Name wird bei der Initialisierung von neuen Repositories genutzt.
Bitte beachten Sie, dass dieser Name aufgrund von Git-Spezifika nicht bei leeren Repositories genutzt
werden kann (hier wird immer der Git-interne Default Name genutzt, derzeit also `master`).
![Administration-Plugins-Installed](assets/administration-settings-git.png)

View File

@@ -8,6 +8,7 @@ Im Bereich Administration kann die SCM-Manager Instanz administriert werden. Von
* [Plugins](plugins/)
* [Berechtigungsrollen](roles/)
* [Einstellungen](settings/)
* [Git](git/)
<!--- AppendLinkContentEnd -->
### Information

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

22
docs/en/user/admin/git.md Normal file
View File

@@ -0,0 +1,22 @@
---
title: Administration
subtitle: Git
---
In the git section there are the following git specific settings:
- GC Cron Expression
If this is set, SCM-Manager will execute a git garbage collection matching the given
[cron expression](https://en.wikipedia.org/wiki/Cron#CRON_expression).
- Disable Non Fast-Forward
Activate this to reject forced pushes that are not fast forwards.
- Default Branch
The branch name configured here will be used for the initialization of new repositories.
Please mind, that due to git internals this cannot work for empty repositories (here git
will always use its internal default branch, so at the time being `master`).
![Administration-Plugins-Installed](assets/administration-settings-git.png)

View File

@@ -7,6 +7,7 @@ The SCM-Manager instance can be administered in the Administration area. From he
* [Plugins](plugins/)
* [Permission Roles](roles/)
* [Settings](settings/)
* [Git](git/)
### Information
On the information page in the administration area you can find the version of your SCM-Manager instance and helpful links to get in touch with the SCM-Manager support team. If there is a newer version for SCM-Manager, it will be shown with the link to the download section on the official SCM-Manager homepage.

View File

@@ -28,11 +28,13 @@ package sonia.scm.repository;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import sonia.scm.Validateable;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import java.io.Serializable;
import java.util.regex.Pattern;
//~--- JDK imports ------------------------------------------------------------
@@ -44,9 +46,14 @@ import java.io.Serializable;
*/
@XmlRootElement(name = "branch")
@XmlAccessorType(XmlAccessType.FIELD)
public final class Branch implements Serializable
public final class Branch implements Serializable, Validateable
{
private static final String VALID_CHARACTERS_AT_START_AND_END = "\\w-,;\\]{}@&+=$#`|<>";
private static final String VALID_CHARACTERS = VALID_CHARACTERS_AT_START_AND_END + "/.";
public static final String VALID_BRANCH_NAMES = "[" + VALID_CHARACTERS_AT_START_AND_END + "]([" + VALID_CHARACTERS + "]*[" + VALID_CHARACTERS_AT_START_AND_END + "])?";
public static final Pattern VALID_BRANCH_NAME_PATTERN = Pattern.compile(VALID_BRANCH_NAMES);
/** Field description */
private static final long serialVersionUID = -4602244691711222413L;
@@ -83,6 +90,11 @@ public final class Branch implements Serializable
//~--- methods --------------------------------------------------------------
@Override
public boolean isValid() {
return VALID_BRANCH_NAME_PATTERN.matcher(name).matches();
}
/**
* {@inheritDoc}
*

View File

@@ -185,7 +185,7 @@ public class GitNonFastForwardITCase {
}
private static void setNonFastForwardDisallowed(boolean nonFastForwardDisallowed) {
String config = String.format("{'disabled': false, 'gcExpression': null, 'nonFastForwardDisallowed': %s}", nonFastForwardDisallowed)
String config = String.format("{'disabled': false, 'gcExpression': null, 'defaultBranch': 'main', 'nonFastForwardDisallowed': %s}", nonFastForwardDisallowed)
.replace('\'', '"');
given(VndMediaType.PREFIX + "gitConfig" + VndMediaType.SUFFIX)

View File

@@ -29,6 +29,12 @@ import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import static sonia.scm.repository.Branch.VALID_BRANCH_NAMES;
@NoArgsConstructor
@Getter
@@ -41,6 +47,11 @@ public class GitConfigDto extends HalRepresentation {
private boolean nonFastForwardDisallowed;
@NotEmpty
@Length(min = 1, max = 100)
@Pattern(regexp = VALID_BRANCH_NAMES)
private String defaultBranch;
@Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package
protected HalRepresentation add(Links links) {

View File

@@ -38,6 +38,7 @@ import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
@@ -126,7 +127,7 @@ public class GitConfigResource {
mediaType = VndMediaType.ERROR_TYPE,
schema = @Schema(implementation = ErrorDto.class)
))
public Response update(GitConfigDto configDto) {
public Response update(@Valid GitConfigDto configDto) {
GitConfig config = dtoToConfigMapper.map(configDto);

View File

@@ -33,7 +33,6 @@ import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
/**
*
* @author Sebastian Sdorra
*/
@XmlRootElement(name = "config")
@@ -49,6 +48,9 @@ public class GitConfig extends RepositoryConfig {
@XmlElement(name = "disallow-non-fast-forward")
private boolean nonFastForwardDisallowed;
@XmlElement(name = "default-branch")
private String defaultBranch = "main";
public String getGcExpression() {
return gcExpression;
}
@@ -65,6 +67,14 @@ public class GitConfig extends RepositoryConfig {
this.nonFastForwardDisallowed = nonFastForwardDisallowed;
}
public String getDefaultBranch() {
return defaultBranch;
}
public void setDefaultBranch(String defaultBranch) {
this.defaultBranch = defaultBranch;
}
@Override
@XmlTransient // Only for permission checks, don't serialize to XML
public String getId() {

View File

@@ -27,6 +27,7 @@ package sonia.scm.repository.spi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
import sonia.scm.repository.GitConfig;
import sonia.scm.repository.GitRepositoryConfig;
import sonia.scm.repository.GitUtil;
import sonia.scm.repository.Repository;
@@ -49,20 +50,12 @@ public class GitContext implements Closeable, RepositoryProvider
private static final Logger logger =
LoggerFactory.getLogger(GitContext.class);
//~--- constructors ---------------------------------------------------------
/**
* Constructs ...
*
*
* @param directory
* @param repository
*/
public GitContext(File directory, Repository repository, GitRepositoryConfigStoreProvider storeProvider)
public GitContext(File directory, Repository repository, GitRepositoryConfigStoreProvider storeProvider, GitConfig config)
{
this.directory = directory;
this.repository = repository;
this.storeProvider = storeProvider;
this.config = config;
}
//~--- methods --------------------------------------------------------------
@@ -126,12 +119,17 @@ public class GitContext implements Closeable, RepositoryProvider
storeProvider.get(repository).set(newConfig);
}
GitConfig getGlobalConfig() {
return config;
}
//~--- fields ---------------------------------------------------------------
/** Field description */
private final File directory;
private final Repository repository;
private final GitRepositoryConfigStoreProvider storeProvider;
private final GitConfig config;
/** Field description */
private org.eclipse.jgit.lib.Repository gitRepository;

View File

@@ -42,7 +42,7 @@ class GitContextFactory {
}
GitContext create(Repository repository) {
return new GitContext(handler.getDirectory(repository.getId()), repository, storeProvider);
return new GitContext(handler.getDirectory(repository.getId()), repository, storeProvider, handler.getConfig());
}
}

View File

@@ -30,14 +30,16 @@ import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.attributes.FilterCommandRegistry;
import org.eclipse.jgit.revwalk.RevCommit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.ConcurrentModificationException;
import sonia.scm.ContextEntry;
import sonia.scm.NoChangesMadeException;
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
import sonia.scm.repository.GitRepositoryConfig;
import sonia.scm.repository.GitRepositoryHandler;
import sonia.scm.repository.GitWorkingCopyFactory;
import sonia.scm.repository.InternalRepositoryException;
import sonia.scm.repository.Repository;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
import javax.inject.Inject;
@@ -49,21 +51,22 @@ import java.util.concurrent.locks.Lock;
public class GitModifyCommand extends AbstractGitCommand implements ModifyCommand {
private static final Logger LOG = LoggerFactory.getLogger(GitModifyCommand.class);
private static final Striped<Lock> REGISTER_LOCKS = Striped.lock(5);
private final GitWorkingCopyFactory workingCopyFactory;
private final LfsBlobStoreFactory lfsBlobStoreFactory;
private final GitRepositoryConfigStoreProvider gitRepositoryConfigStoreProvider;
@Inject
GitModifyCommand(GitContext context, GitRepositoryHandler repositoryHandler, LfsBlobStoreFactory lfsBlobStoreFactory) {
this(context, repositoryHandler.getWorkingCopyFactory(), lfsBlobStoreFactory);
GitModifyCommand(GitContext context, GitRepositoryHandler repositoryHandler, LfsBlobStoreFactory lfsBlobStoreFactory, GitRepositoryConfigStoreProvider gitRepositoryConfigStoreProvider) {
this(context, repositoryHandler.getWorkingCopyFactory(), lfsBlobStoreFactory, gitRepositoryConfigStoreProvider);
}
GitModifyCommand(GitContext context, GitWorkingCopyFactory workingCopyFactory, LfsBlobStoreFactory lfsBlobStoreFactory) {
GitModifyCommand(GitContext context, GitWorkingCopyFactory workingCopyFactory, LfsBlobStoreFactory lfsBlobStoreFactory, GitRepositoryConfigStoreProvider gitRepositoryConfigStoreProvider) {
super(context);
this.workingCopyFactory = workingCopyFactory;
this.lfsBlobStoreFactory = lfsBlobStoreFactory;
this.gitRepositoryConfigStoreProvider = gitRepositoryConfigStoreProvider;
}
@Override
@@ -85,19 +88,49 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman
@Override
String run() throws IOException {
getClone().getRepository().getFullBranch();
boolean initialCommit = getClone().getRepository().getRefDatabase().getRefs().isEmpty();
if (!StringUtils.isEmpty(request.getExpectedRevision())
&& !request.getExpectedRevision().equals(getCurrentRevision().getName())) {
throw new ConcurrentModificationException("branch", request.getBranch() == null ? "default" : request.getBranch());
throw new ConcurrentModificationException(ContextEntry.ContextBuilder.entity("Branch", request.getBranch() == null ? "default" : request.getBranch()).in(repository).build());
}
for (ModifyCommandRequest.PartialRequest r : request.getRequests()) {
r.execute(this);
}
failIfNotChanged(() -> new NoChangesMadeException(repository, ModifyWorker.this.request.getBranch()));
Optional<RevCommit> revCommit = doCommit(request.getCommitMessage(), request.getAuthor(), request.isSign());
if (initialCommit) {
handleBranchForInitialCommit();
}
push();
return revCommit.orElseThrow(() -> new NoChangesMadeException(repository, ModifyWorker.this.request.getBranch())).name();
}
private void handleBranchForInitialCommit() {
String branch = StringUtils.isNotBlank(request.getBranch()) ? request.getBranch() : context.getGlobalConfig().getDefaultBranch();
if (StringUtils.isNotBlank(branch)) {
try {
getClone().checkout().setName(branch).setCreateBranch(true).call();
setBranchInConfig(branch);
} catch (GitAPIException e) {
throw new InternalRepositoryException(repository, "could not create default branch for initial commit", e);
}
}
}
private void setBranchInConfig(String branch) {
ConfigurationStore<GitRepositoryConfig> store = gitRepositoryConfigStoreProvider
.get(repository);
GitRepositoryConfig gitRepositoryConfig = store
.getOptional()
.orElse(new GitRepositoryConfig());
gitRepositoryConfig.setDefaultBranch(branch);
store.set(gitRepositoryConfig);
}
@Override
public void addFileToScm(String name, Path file) {
addToGitWithLfsSupport(name, file);

View File

@@ -67,8 +67,12 @@ class GitWorkingCopyInitializer {
Ref head = clone.exactRef(Constants.HEAD);
if (head == null || !head.isSymbolic() || (initialBranch != null && !head.getTarget().getName().endsWith(initialBranch))) {
if (clone.getRefDatabase().getRefs().isEmpty()) {
LOG.warn("could not initialize empty clone with given branch {}; this has to be handled later on", initialBranch);
} else {
throw notFound(entity("Branch", initialBranch).in(context.getRepository()));
}
}
return new ParentAndClone<>(null, clone, target);
} catch (GitAPIException | IOException e) {

View File

@@ -24,12 +24,13 @@
import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import { Links } from "@scm-manager/ui-types";
import { InputField, Checkbox } from "@scm-manager/ui-components";
import { InputField, Checkbox, validation as validator } from "@scm-manager/ui-components";
type Configuration = {
repositoryDirectory?: string;
gcExpression?: string;
nonFastForwardDisallowed: boolean;
defaultBranch: string;
_links: Links;
};
@@ -68,8 +69,21 @@ class GitConfigurationForm extends React.Component<Props, State> {
);
};
onDefaultBranchChange = (value: string) => {
this.setState(
{
defaultBranch: value
},
() => this.props.onConfigurationChange(this.state, this.isValidDefaultBranch())
);
};
isValidDefaultBranch = () => {
return validator.isBranchValid(this.state.defaultBranch);
};
render() {
const { gcExpression, nonFastForwardDisallowed } = this.state;
const { gcExpression, nonFastForwardDisallowed, defaultBranch } = this.state;
const { readOnly, t } = this.props;
return (
@@ -90,6 +104,16 @@ class GitConfigurationForm extends React.Component<Props, State> {
onChange={this.onNonFastForwardDisallowed}
disabled={readOnly}
/>
<InputField
name="defaultBranch"
label={t("scm-git-plugin.config.defaultBranch")}
helpText={t("scm-git-plugin.config.defaultBranchHelpText")}
value={defaultBranch}
onChange={this.onDefaultBranchChange}
disabled={readOnly}
validationError={!this.isValidDefaultBranch()}
errorMessage={t("scm-git-plugin.config.defaultBranchValidationError")}
/>
</>
);
}

View File

@@ -24,6 +24,9 @@
"gcExpressionHelpText": "Benutze Quartz Cron Ausdrücke (SECOND MINUTE HOUR DAYOFMONTH MONTH DAYOFWEEK), um git GC regelmäßig auszuführen.",
"nonFastForwardDisallowed": "Deaktiviere \"Non Fast-Forward\"",
"nonFastForwardDisallowedHelpText": "Git Pushes ablehnen, die nicht \"fast-forward\" sind, wie \"--force\".",
"defaultBranch": "Default Branch",
"defaultBranchHelpText": "Dieser Name wird bei der Initialisierung neuer Git Repositories genutzt. Er hat keine weiteren Auswirkungen (insbesondere hat er keinen Einfluss auf den Branchnamen bei leeren Repositories).",
"defaultBranchValidationError": "Dies ist kein valider Branchname",
"disabled": "Deaktiviert",
"disabledHelpText": "Aktiviere oder deaktiviere das Git Plugin",
"submit": "Speichern"

View File

@@ -24,6 +24,9 @@
"gcExpressionHelpText": "Use Quartz Cron Expressions (SECOND MINUTE HOUR DAYOFMONTH MONTH DAYOFWEEK) to run git gc in intervals.",
"nonFastForwardDisallowed": "Disallow Non Fast-Forward",
"nonFastForwardDisallowedHelpText": "Reject git pushes which are non fast-forward such as --force.",
"defaultBranch": "Default Branch",
"defaultBranchHelpText": "This name will be used for the initialization of new git repositories. It has no effect otherwise (especially this cannot change the initial branch name for empty repositories).",
"defaultBranchValidationError": "This is not a valid branch name",
"disabled": "Disabled",
"disabledHelpText": "Enable or disable the Git plugin",
"submit": "Submit"

View File

@@ -261,7 +261,7 @@ public class GitConfigResourceTest {
private MockHttpResponse put() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.put("/" + GitConfigResource.GIT_CONFIG_PATH_V2)
.contentType(GitVndMediaType.GIT_CONFIG)
.content("{\"disabled\":true}".getBytes());
.content("{\"disabled\":true, \"defaultBranch\":\"main\"}".getBytes());
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);

View File

@@ -26,19 +26,12 @@ package sonia.scm.repository.spi;
//~--- non-JDK imports --------------------------------------------------------
import org.eclipse.jgit.transport.ScmTransportProtocol;
import org.eclipse.jgit.transport.Transport;
import org.junit.After;
import org.junit.Before;
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
import sonia.scm.repository.GitConfig;
import sonia.scm.repository.GitRepositoryConfig;
import sonia.scm.repository.PreProcessorUtil;
import sonia.scm.repository.api.HookContextFactory;
import sonia.scm.store.InMemoryConfigurationStoreFactory;
import static com.google.inject.util.Providers.of;
import static org.mockito.Mockito.mock;
/**
*
* @author Sebastian Sdorra
@@ -69,7 +62,7 @@ public class AbstractGitCommandTestBase extends ZippedRepositoryTestBase
{
if (context == null)
{
context = new GitContext(repositoryDirectory, repository, new GitRepositoryConfigStoreProvider(InMemoryConfigurationStoreFactory.create()));
context = new GitContext(repositoryDirectory, repository, new GitRepositoryConfigStoreProvider(InMemoryConfigurationStoreFactory.create()), new GitConfig());
}
return context;

View File

@@ -32,7 +32,7 @@ import org.junit.Ignore;
import org.junit.Test;
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitConfig;
import sonia.scm.repository.GitTestHelper;
import sonia.scm.store.InMemoryConfigurationStoreFactory;
@@ -99,7 +99,7 @@ public class GitIncomingCommandTest
commit(outgoing, "added a");
GitPullCommand pull = new GitPullCommand(handler, new GitContext(incomingDirectory, incomingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory())));
GitPullCommand pull = new GitPullCommand(handler, new GitContext(incomingDirectory, incomingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory()), new GitConfig()));
PullCommandRequest req = new PullCommandRequest();
req.setRemoteRepository(outgoingRepository);
pull.pull(req);
@@ -177,7 +177,7 @@ public class GitIncomingCommandTest
private GitIncomingCommand createCommand() {
return new GitIncomingCommand(
new GitContext(incomingDirectory, incomingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory())),
new GitContext(incomingDirectory, incomingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory()), new GitConfig()),
handler,
GitTestHelper.createConverterFactory()
);

View File

@@ -27,6 +27,7 @@ package sonia.scm.repository.spi;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Before;
import org.junit.Test;
import sonia.scm.repository.GitConfig;
import sonia.scm.repository.Modifications;
import java.io.File;
@@ -42,8 +43,8 @@ public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase {
@Before
public void init() {
incomingModificationsCommand = new GitModificationsCommand(new GitContext(incomingDirectory, incomingRepository, null));
outgoingModificationsCommand = new GitModificationsCommand(new GitContext(outgoingDirectory, outgoingRepository, null));
incomingModificationsCommand = new GitModificationsCommand(new GitContext(incomingDirectory, incomingRepository, null, new GitConfig()));
outgoingModificationsCommand = new GitModificationsCommand(new GitContext(outgoingDirectory, outgoingRepository, null, new GitConfig()));
}
@Test
@@ -106,11 +107,11 @@ public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase {
}
void pushOutgoingAndPullIncoming() throws IOException {
GitPushCommand cmd = new GitPushCommand(handler, new GitContext(outgoingDirectory, outgoingRepository, null));
GitPushCommand cmd = new GitPushCommand(handler, new GitContext(outgoingDirectory, outgoingRepository, null, new GitConfig()));
PushCommandRequest request = new PushCommandRequest();
request.setRemoteRepository(incomingRepository);
cmd.push(request);
GitPullCommand pullCommand = new GitPullCommand(handler, new GitContext(incomingDirectory, incomingRepository, null));
GitPullCommand pullCommand = new GitPullCommand(handler, new GitContext(incomingDirectory, incomingRepository, null, new GitConfig()));
PullCommandRequest pullRequest = new PullCommandRequest();
pullRequest.setRemoteRepository(incomingRepository);
pullCommand.pull(pullRequest);

View File

@@ -24,61 +24,26 @@
package sonia.scm.repository.spi;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.GpgSignature;
import org.eclipse.jgit.lib.GpgSigner;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.rules.TemporaryFolder;
import sonia.scm.AlreadyExistsException;
import sonia.scm.BadRequestException;
import sonia.scm.ConcurrentModificationException;
import sonia.scm.NotFoundException;
import sonia.scm.repository.GitTestHelper;
import sonia.scm.repository.Person;
import sonia.scm.repository.work.NoneCachingWorkingCopyPool;
import sonia.scm.repository.work.WorkdirProvider;
import sonia.scm.security.PublicKey;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret")
public class GitModifyCommandTest extends AbstractGitCommandTestBase {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Rule
public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule();
@Rule
public ShiroRule shiro = new ShiroRule();
private final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class);
@BeforeClass
public static void setSigner() {
GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner());
}
public class GitModifyCommandTest extends GitModifyCommandTestBase {
@Test
public void shouldCreateCommit() throws IOException, GitAPIException {
@@ -362,30 +327,4 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase {
assertThat(lastCommit.getRawGpgSignature()).isNullOrEmpty();
}
}
private void assertInTree(TreeAssertions assertions) throws IOException, GitAPIException {
try (Git git = new Git(createContext().open())) {
RevCommit lastCommit = getLastCommit(git);
try (RevWalk walk = new RevWalk(git.getRepository())) {
RevCommit commit = walk.parseCommit(lastCommit);
ObjectId treeId = commit.getTree().getId();
try (ObjectReader reader = git.getRepository().newObjectReader()) {
assertions.checkAssertions(new CanonicalTreeParser(null, reader, treeId));
}
}
}
}
private RevCommit getLastCommit(Git git) throws GitAPIException {
return git.log().setMaxCount(1).call().iterator().next();
}
private GitModifyCommand createCommand() {
return new GitModifyCommand(createContext(), new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider())), lfsBlobStoreFactory);
}
@FunctionalInterface
private interface TreeAssertions {
void checkAssertions(CanonicalTreeParser treeParser) throws CorruptObjectException;
}
}

View File

@@ -0,0 +1,97 @@
/*
* 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 com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.lib.GpgSigner;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import sonia.scm.repository.GitTestHelper;
import sonia.scm.repository.work.NoneCachingWorkingCopyPool;
import sonia.scm.repository.work.WorkdirProvider;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
import java.io.IOException;
import static org.mockito.Mockito.mock;
import static sonia.scm.repository.spi.GitRepositoryConfigStoreProviderTestUtil.createGitRepositoryConfigStoreProvider;
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret")
class GitModifyCommandTestBase extends AbstractGitCommandTestBase {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Rule
public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule();
@Rule
public ShiroRule shiro = new ShiroRule();
final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class);
@BeforeClass
public static void setSigner() {
GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner());
}
RevCommit getLastCommit(Git git) throws GitAPIException, IOException {
return git.log().setMaxCount(1).call().iterator().next();
}
GitModifyCommand createCommand() {
return new GitModifyCommand(
createContext(),
new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider())),
lfsBlobStoreFactory,
createGitRepositoryConfigStoreProvider());
}
void assertInTree(TreeAssertions assertions) throws IOException, GitAPIException {
try (Git git = new Git(createContext().open())) {
RevCommit lastCommit = getLastCommit(git);
try (RevWalk walk = new RevWalk(git.getRepository())) {
RevCommit commit = walk.parseCommit(lastCommit);
ObjectId treeId = commit.getTree().getId();
try (ObjectReader reader = git.getRepository().newObjectReader()) {
assertions.checkAssertions(new CanonicalTreeParser(null, reader, treeId));
}
}
}
}
@FunctionalInterface
interface TreeAssertions {
void checkAssertions(CanonicalTreeParser treeParser) throws CorruptObjectException;
}
}

View File

@@ -24,22 +24,15 @@
package sonia.scm.repository.spi;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import sonia.scm.repository.Person;
import sonia.scm.repository.work.NoneCachingWorkingCopyPool;
import sonia.scm.repository.work.WorkdirProvider;
import sonia.scm.store.Blob;
import sonia.scm.store.BlobStore;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -51,17 +44,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret")
public class GitModifyCommand_LFSTest extends AbstractGitCommandTestBase {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Rule
public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule();
@Rule
public ShiroRule shiro = new ShiroRule();
private final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class);
public class GitModifyCommand_LFSTest extends GitModifyCommandTestBase {
@BeforeClass
public static void registerFilter() {
@@ -126,14 +109,6 @@ public class GitModifyCommand_LFSTest extends AbstractGitCommandTestBase {
return command.execute(request);
}
private RevCommit getLastCommit(Git git) throws GitAPIException {
return git.log().setMaxCount(1).call().iterator().next();
}
private GitModifyCommand createCommand() {
return new GitModifyCommand(createContext(), new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider())), lfsBlobStoreFactory);
}
@Override
protected String getZippedRepositoryResource() {
return "sonia/scm/repository/spi/scm-git-spi-lfs-test.zip";

View File

@@ -24,42 +24,21 @@
package sonia.scm.repository.spi;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import sonia.scm.repository.Person;
import sonia.scm.repository.work.NoneCachingWorkingCopyPool;
import sonia.scm.repository.work.WorkdirProvider;
import sonia.scm.web.lfs.LfsBlobStoreFactory;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret")
public class GitModifyCommand_withEmptyRepositoryTest extends AbstractGitCommandTestBase {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Rule
public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule();
@Rule
public ShiroRule shiro = new ShiroRule();
private final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class);
public class GitModifyCommand_withEmptyRepositoryTest extends GitModifyCommandTestBase {
@Test
public void shouldCreateNewFileInEmptyRepository() throws IOException, GitAPIException {
@@ -79,34 +58,65 @@ public class GitModifyCommand_withEmptyRepositoryTest extends AbstractGitCommand
assertInTree(assertions);
}
@Test
public void shouldCreateCommitOnMasterByDefault() throws IOException, GitAPIException {
createContext().getGlobalConfig().setDefaultBranch("");
executeModifyCommand();
try (Git git = new Git(createContext().open())) {
List<Ref> branches = git.branchList().call();
assertThat(branches).extracting("name").containsExactly("refs/heads/master");
}
}
@Test
public void shouldCreateCommitWithConfiguredDefaultBranch() throws IOException, GitAPIException {
createContext().getGlobalConfig().setDefaultBranch("main");
executeModifyCommand();
try (Git git = new Git(createContext().open())) {
List<Ref> branches = git.branchList().call();
assertThat(branches).extracting("name").containsExactly("refs/heads/main");
}
}
@Test
public void shouldCreateCommitWithBranchFromRequestIfPresent() throws IOException, GitAPIException {
createContext().getGlobalConfig().setDefaultBranch("main");
ModifyCommandRequest request = createRequest();
request.setBranch("different");
createCommand().execute(request);
try (Git git = new Git(createContext().open())) {
List<Ref> branches = git.branchList().call();
assertThat(branches).extracting("name").containsExactly("refs/heads/different");
}
}
@Override
protected String getZippedRepositoryResource() {
return "sonia/scm/repository/spi/scm-git-empty-repo.zip";
}
private void assertInTree(TreeAssertions assertions) throws IOException, GitAPIException {
try (Git git = new Git(createContext().open())) {
RevCommit lastCommit = getLastCommit(git);
try (RevWalk walk = new RevWalk(git.getRepository())) {
RevCommit commit = walk.parseCommit(lastCommit);
ObjectId treeId = commit.getTree().getId();
try (ObjectReader reader = git.getRepository().newObjectReader()) {
assertions.checkAssertions(new CanonicalTreeParser(null, reader, treeId));
}
}
}
@Override
RevCommit getLastCommit(Git git) throws GitAPIException, IOException {
return git.log().setMaxCount(1).all().call().iterator().next();
}
private RevCommit getLastCommit(Git git) throws GitAPIException {
return git.log().setMaxCount(1).call().iterator().next();
private void executeModifyCommand() throws IOException {
createCommand().execute(createRequest());
}
private GitModifyCommand createCommand() {
return new GitModifyCommand(createContext(), new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider())), lfsBlobStoreFactory);
}
private ModifyCommandRequest createRequest() throws IOException {
File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile();
@FunctionalInterface
private interface TreeAssertions {
void checkAssertions(CanonicalTreeParser treeParser) throws CorruptObjectException;
ModifyCommandRequest request = new ModifyCommandRequest();
request.setCommitMessage("initial commit");
request.addRequest(new ModifyCommandRequest.CreateFileRequest("new_file", newFile, false));
request.setAuthor(new Person("Dirk Gently", "dirk@holistic.det"));
return request;
}
}

View File

@@ -31,7 +31,7 @@ import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Test;
import sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
import sonia.scm.repository.ChangesetPagingResult;
import sonia.scm.repository.GitChangesetConverterFactory;
import sonia.scm.repository.GitConfig;
import sonia.scm.repository.GitTestHelper;
import sonia.scm.store.InMemoryConfigurationStoreFactory;
@@ -99,7 +99,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
commit(outgoing, "added a");
GitPushCommand push = new GitPushCommand(handler,
new GitContext(outgoingDirectory, outgoingRepository, null)
new GitContext(outgoingDirectory, outgoingRepository, null, new GitConfig())
);
PushCommandRequest req = new PushCommandRequest();
@@ -154,7 +154,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase
private GitOutgoingCommand createCommand()
{
return new GitOutgoingCommand(
new GitContext(outgoingDirectory, outgoingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory())),
new GitContext(outgoingDirectory, outgoingRepository, new GitRepositoryConfigStoreProvider(new InMemoryConfigurationStoreFactory()), new GitConfig()),
handler,
GitTestHelper.createConverterFactory()
);

View File

@@ -29,6 +29,7 @@ package sonia.scm.repository.spi;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Test;
import sonia.scm.repository.GitConfig;
import sonia.scm.repository.api.PushResponse;
import java.io.IOException;
@@ -89,6 +90,6 @@ public class GitPushCommandTest extends AbstractRemoteCommandTestBase
*/
private GitPushCommand createCommand()
{
return new GitPushCommand(handler, new GitContext(outgoingDirectory, outgoingRepository, null));
return new GitPushCommand(handler, new GitContext(outgoingDirectory, outgoingRepository, null, new GitConfig()));
}
}

View File

@@ -0,0 +1,47 @@
/*
* 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 sonia.scm.api.v2.resources.GitRepositoryConfigStoreProvider;
import sonia.scm.repository.GitRepositoryConfig;
import sonia.scm.repository.Repository;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.InMemoryConfigurationStore;
import java.util.HashMap;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class GitRepositoryConfigStoreProviderTestUtil {
static GitRepositoryConfigStoreProvider createGitRepositoryConfigStoreProvider() {
GitRepositoryConfigStoreProvider gitRepositoryConfigStoreProvider = mock(GitRepositoryConfigStoreProvider.class);
HashMap<String, ConfigurationStore<GitRepositoryConfig>> storeMap = new HashMap<>();
when(gitRepositoryConfigStoreProvider.get(any())).thenAnswer(invocation -> storeMap.computeIfAbsent(invocation.getArgument(0, Repository.class).getId(), id -> new InMemoryConfigurationStore<>()));
return gitRepositoryConfigStoreProvider;
}
}

View File

@@ -28,6 +28,12 @@ export const isNameValid = (name: string) => {
return nameRegex.test(name);
};
export const branchRegex = /^[\w-,;\]{}@&+=$#`|<>]([\w-,;\]{}@&+=$#`|<>/.]*[\w-,;\]{}@&+=$#`|<>])?$/;
export const isBranchValid = (name: string) => {
return branchRegex.test(name);
};
const mailRegex = /^[ -~]+@[A-Za-z0-9][\w\-.]*\.[A-Za-z0-9][A-Za-z0-9-]+$/;
export const isMailValid = (mail: string) => {

View File

@@ -124,15 +124,13 @@ class BranchForm extends React.Component<Props, State> {
handleSourceChange = (source: string) => {
this.setState({
...this.state,
source
});
};
handleNameChange = (name: string) => {
this.setState({
nameValidationError: !validator.isNameValid(name),
...this.state,
nameValidationError: !validator.isBranchValid(name),
name
});
};

View File

@@ -35,16 +35,14 @@ import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import static sonia.scm.repository.Branch.VALID_BRANCH_NAMES;
@Getter
@Setter
@NoArgsConstructor
@SuppressWarnings("java:S2160") // we do not need this for dto
public class BranchDto extends HalRepresentation {
private static final String VALID_CHARACTERS_AT_START_AND_END = "\\w-,;\\]{}@&+=$#`|<>";
private static final String VALID_CHARACTERS = VALID_CHARACTERS_AT_START_AND_END + "/.";
static final String VALID_BRANCH_NAMES = "[" + VALID_CHARACTERS_AT_START_AND_END + "]([" + VALID_CHARACTERS + "]*[" + VALID_CHARACTERS_AT_START_AND_END + "])?";
@NotEmpty
@Length(min = 1, max = 100)
@Pattern(regexp = VALID_BRANCH_NAMES)

View File

@@ -31,7 +31,7 @@ import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import static sonia.scm.api.v2.resources.BranchDto.VALID_BRANCH_NAMES;
import static sonia.scm.repository.Branch.VALID_BRANCH_NAMES;
@Getter
@Setter

View File

@@ -29,6 +29,7 @@ import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static sonia.scm.repository.Branch.VALID_BRANCH_NAMES;
class BranchDtoTest {
@@ -54,10 +55,11 @@ class BranchDtoTest {
"val{d",
"val{}d",
"val|kill",
"val}"
"val}",
"va/li/d"
})
void shouldAcceptValidBranchName(String branchName) {
assertTrue(branchName.matches(BranchDto.VALID_BRANCH_NAMES));
assertTrue(branchName.matches(VALID_BRANCH_NAMES));
}
@ParameterizedTest
@@ -70,6 +72,6 @@ class BranchDtoTest {
"val id"
})
void shouldRejectInvalidBranchName(String branchName) {
assertFalse(branchName.matches(BranchDto.VALID_BRANCH_NAMES));
assertFalse(branchName.matches(VALID_BRANCH_NAMES));
}
}