diff --git a/CHANGELOG.md b/CHANGELOG.md index e7f4c97d97..3bb0f05a73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/docs/de/user/admin/assets/administration-settings-git.png b/docs/de/user/admin/assets/administration-settings-git.png new file mode 100644 index 0000000000..71ae9a6feb Binary files /dev/null and b/docs/de/user/admin/assets/administration-settings-git.png differ diff --git a/docs/de/user/admin/git.md b/docs/de/user/admin/git.md new file mode 100644 index 0000000000..e80864fcd3 --- /dev/null +++ b/docs/de/user/admin/git.md @@ -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) diff --git a/docs/de/user/admin/index.md b/docs/de/user/admin/index.md index 3f88a4c402..fd4e75c3f4 100644 --- a/docs/de/user/admin/index.md +++ b/docs/de/user/admin/index.md @@ -8,6 +8,7 @@ Im Bereich Administration kann die SCM-Manager Instanz administriert werden. Von * [Plugins](plugins/) * [Berechtigungsrollen](roles/) * [Einstellungen](settings/) +* [Git](git/) ### Information diff --git a/docs/en/user/admin/assets/administration-settings-git.png b/docs/en/user/admin/assets/administration-settings-git.png new file mode 100644 index 0000000000..31ed35138f Binary files /dev/null and b/docs/en/user/admin/assets/administration-settings-git.png differ diff --git a/docs/en/user/admin/git.md b/docs/en/user/admin/git.md new file mode 100644 index 0000000000..625bca93b8 --- /dev/null +++ b/docs/en/user/admin/git.md @@ -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) diff --git a/docs/en/user/admin/index.md b/docs/en/user/admin/index.md index d2e77554d1..af1b9fd116 100644 --- a/docs/en/user/admin/index.md +++ b/docs/en/user/admin/index.md @@ -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. diff --git a/scm-core/src/main/java/sonia/scm/repository/Branch.java b/scm-core/src/main/java/sonia/scm/repository/Branch.java index a1d500078c..bdcbc66a82 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Branch.java +++ b/scm-core/src/main/java/sonia/scm/repository/Branch.java @@ -21,18 +21,20 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- 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} * diff --git a/scm-it/src/test/java/sonia/scm/it/GitNonFastForwardITCase.java b/scm-it/src/test/java/sonia/scm/it/GitNonFastForwardITCase.java index 8a115a2392..c6f263fed9 100644 --- a/scm-it/src/test/java/sonia/scm/it/GitNonFastForwardITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/GitNonFastForwardITCase.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.it; import com.google.common.base.Charsets; @@ -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) diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDto.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDto.java index 640fadc8e8..893de773da 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDto.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigDto.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; @@ -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) { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java index 17f76913c5..71d97f27be 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigResource.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import io.swagger.v3.oas.annotations.OpenAPIDefinition; @@ -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); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java index facdcccbbf..f699093f73 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/GitConfig.java @@ -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; //~--- JDK imports ------------------------------------------------------------ @@ -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() { diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java index c7f0327312..4246dd4ff3 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContext.java @@ -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; diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContextFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContextFactory.java index 04bb37bed0..214eb090b8 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContextFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitContextFactory.java @@ -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()); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java index 01271d0fc3..ba6df0c1e8 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModifyCommand.java @@ -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 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 = 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 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); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitWorkingCopyInitializer.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitWorkingCopyInitializer.java index 11019b288c..346a2776b1 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitWorkingCopyInitializer.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitWorkingCopyInitializer.java @@ -67,7 +67,11 @@ class GitWorkingCopyInitializer { Ref head = clone.exactRef(Constants.HEAD); if (head == null || !head.isSymbolic() || (initialBranch != null && !head.getTarget().getName().endsWith(initialBranch))) { - throw notFound(entity("Branch", initialBranch).in(context.getRepository())); + 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); diff --git a/scm-plugins/scm-git-plugin/src/main/js/GitConfigurationForm.tsx b/scm-plugins/scm-git-plugin/src/main/js/GitConfigurationForm.tsx index 036c7f3f40..cec3f1b85f 100644 --- a/scm-plugins/scm-git-plugin/src/main/js/GitConfigurationForm.tsx +++ b/scm-plugins/scm-git-plugin/src/main/js/GitConfigurationForm.tsx @@ -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 { ); }; + 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 { onChange={this.onNonFastForwardDisallowed} disabled={readOnly} /> + ); } diff --git a/scm-plugins/scm-git-plugin/src/main/resources/locales/de/plugins.json b/scm-plugins/scm-git-plugin/src/main/resources/locales/de/plugins.json index 7553856e57..ad452fd559 100644 --- a/scm-plugins/scm-git-plugin/src/main/resources/locales/de/plugins.json +++ b/scm-plugins/scm-git-plugin/src/main/resources/locales/de/plugins.json @@ -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" diff --git a/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json b/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json index 23ab1ffce0..f894d1dbf6 100644 --- a/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json +++ b/scm-plugins/scm-git-plugin/src/main/resources/locales/en/plugins.json @@ -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" diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java index ab4c71b7fc..e3041dd447 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import com.github.sdorra.shiro.ShiroRule; @@ -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); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java index d593a58678..2c2ee26f80 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractGitCommandTestBase.java @@ -21,24 +21,17 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + 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; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java index 5cfab8de24..270c548120 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java @@ -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() ); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java index 1657191d21..28ddd0a234 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java @@ -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); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java index 473356d8bf..e4e09c5bd5 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTest.java @@ -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; - } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTestBase.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTestBase.java new file mode 100644 index 0000000000..a890a83255 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommandTestBase.java @@ -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; + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_LFSTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_LFSTest.java index 6709505c90..955d16965b 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_LFSTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_LFSTest.java @@ -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"; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_withEmptyRepositoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_withEmptyRepositoryTest.java index 5898845a74..7877f1a686 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_withEmptyRepositoryTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_withEmptyRepositoryTest.java @@ -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 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 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 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; } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java index 22ccb94e1c..5609a222f9 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java @@ -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() ); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPushCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPushCommandTest.java index 1c5796a44f..6deb3afe53 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPushCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPushCommandTest.java @@ -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())); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryConfigStoreProviderTestUtil.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryConfigStoreProviderTestUtil.java new file mode 100644 index 0000000000..196b5258de --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryConfigStoreProviderTestUtil.java @@ -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> storeMap = new HashMap<>(); + when(gitRepositoryConfigStoreProvider.get(any())).thenAnswer(invocation -> storeMap.computeIfAbsent(invocation.getArgument(0, Repository.class).getId(), id -> new InMemoryConfigurationStore<>())); + return gitRepositoryConfigStoreProvider; + } +} diff --git a/scm-ui/ui-components/src/validation.ts b/scm-ui/ui-components/src/validation.ts index fcdb1a6dfd..9e2b372dcb 100644 --- a/scm-ui/ui-components/src/validation.ts +++ b/scm-ui/ui-components/src/validation.ts @@ -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) => { diff --git a/scm-ui/ui-webapp/src/repos/branches/components/BranchForm.tsx b/scm-ui/ui-webapp/src/repos/branches/components/BranchForm.tsx index 35d2a42bd9..e4d9864c1f 100644 --- a/scm-ui/ui-webapp/src/repos/branches/components/BranchForm.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchForm.tsx @@ -124,15 +124,13 @@ class BranchForm extends React.Component { handleSourceChange = (source: string) => { this.setState({ - ...this.state, source }); }; handleNameChange = (name: string) => { this.setState({ - nameValidationError: !validator.isNameValid(name), - ...this.state, + nameValidationError: !validator.isBranchValid(name), name }); }; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java index db144c18b2..4825291c1b 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchDto.java @@ -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) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRequestDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRequestDto.java index c81aaf18c7..f56de8c38b 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRequestDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRequestDto.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import lombok.Getter; @@ -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 diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchDtoTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchDtoTest.java index 57e9b3b031..7f11520f33 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchDtoTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchDtoTest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import org.junit.jupiter.params.ParameterizedTest; @@ -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)); } }