From c3baf274d1be6bcf8c676324c305378ffaf30765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Fri, 30 Oct 2020 12:52:07 +0100 Subject: [PATCH] Set default branch for repository on init --- .../scm/repository/spi/GitModifyCommand.java | 22 ++- .../repository/spi/GitModifyCommandTest.java | 14 +- .../GitModifyCommand_InitialCommitTest.java | 126 ------------------ .../spi/GitModifyCommand_LFSTest.java | 7 +- ...ModifyCommand_withEmptyRepositoryTest.java | 73 +++++++++- ...RepositoryConfigStoreProviderTestUtil.java | 47 +++++++ .../spi/scm-git-spi-empty-repo-test.zip | Bin 13430 -> 0 bytes 7 files changed, 144 insertions(+), 145 deletions(-) delete mode 100644 scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_InitialCommitTest.java create mode 100644 scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitRepositoryConfigStoreProviderTestUtil.java delete mode 100644 scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-empty-repo-test.zip 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 80c8c196dc..05de362b90 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,15 +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; @@ -50,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 @@ -112,9 +114,15 @@ public class GitModifyCommand extends AbstractGitCommand implements ModifyComman if (StringUtils.isNotBlank(branch)) { try { getClone().checkout().setName(branch).setCreateBranch(true).call(); + ConfigurationStore store = gitRepositoryConfigStoreProvider + .get(repository); + GitRepositoryConfig gitRepositoryConfig = store + .getOptional() + .orElse(new GitRepositoryConfig()); + gitRepositoryConfig.setDefaultBranch(branch); + store.set(gitRepositoryConfig); } catch (GitAPIException e) { throw new InternalRepositoryException(repository, "could not create default branch for initial commit", e); - } } } 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..97abda18a8 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 @@ -27,23 +27,17 @@ 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; @@ -53,7 +47,6 @@ 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; @@ -62,6 +55,7 @@ import java.nio.file.Files; import static org.assertj.core.api.Assertions.assertThat; 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") public class GitModifyCommandTest extends AbstractGitCommandTestBase { @@ -381,7 +375,11 @@ public class GitModifyCommandTest extends AbstractGitCommandTestBase { } private GitModifyCommand createCommand() { - return new GitModifyCommand(createContext(), new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider())), lfsBlobStoreFactory); + return new GitModifyCommand( + createContext(), + new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider())), + lfsBlobStoreFactory, + createGitRepositoryConfigStoreProvider()); } @FunctionalInterface diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_InitialCommitTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_InitialCommitTest.java deleted file mode 100644 index f9b1a61a8c..0000000000 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModifyCommand_InitialCommitTest.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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.lib.GpgSigner; -import org.eclipse.jgit.lib.Ref; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -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.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_InitialCommitTest extends AbstractGitCommandTestBase { - - @Rule - public TemporaryFolder temporaryFolder = new TemporaryFolder(); - @Rule - public ShiroRule shiro = new ShiroRule(); - @Rule - public BindTransportProtocolRule transportProtocolRule = new BindTransportProtocolRule(); - - @BeforeClass - public static void setSigner() { - GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner()); - } - - @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"); - } - } - - private void executeModifyCommand() throws IOException { - createCommand().execute(createRequest()); - } - - private ModifyCommandRequest createRequest() throws IOException { - File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); - - 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; - } - - private GitModifyCommand createCommand() { - return new GitModifyCommand(createContext(), new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider())), mock(LfsBlobStoreFactory.class)); - } - - @Override - protected String getZippedRepositoryResource() { - return "sonia/scm/repository/spi/scm-git-spi-empty-repo-test.zip"; - } -} 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..ccb0e200c0 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 @@ -50,6 +50,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static sonia.scm.repository.spi.GitRepositoryConfigStoreProviderTestUtil.createGitRepositoryConfigStoreProvider; @SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret") public class GitModifyCommand_LFSTest extends AbstractGitCommandTestBase { @@ -131,7 +132,11 @@ public class GitModifyCommand_LFSTest extends AbstractGitCommandTestBase { } private GitModifyCommand createCommand() { - return new GitModifyCommand(createContext(), new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider())), lfsBlobStoreFactory); + return new GitModifyCommand( + createContext(), + new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider())), + lfsBlobStoreFactory, + createGitRepositoryConfigStoreProvider()); } @Override 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..c9307b9578 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 @@ -29,14 +29,18 @@ 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.lib.Ref; 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.Test; import org.junit.rules.TemporaryFolder; +import sonia.scm.repository.GitTestHelper; import sonia.scm.repository.Person; import sonia.scm.repository.work.NoneCachingWorkingCopyPool; import sonia.scm.repository.work.WorkdirProvider; @@ -45,9 +49,11 @@ 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; +import static sonia.scm.repository.spi.GitRepositoryConfigStoreProviderTestUtil.createGitRepositoryConfigStoreProvider; @SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini", username = "admin", password = "secret") public class GitModifyCommand_withEmptyRepositoryTest extends AbstractGitCommandTestBase { @@ -61,6 +67,11 @@ public class GitModifyCommand_withEmptyRepositoryTest extends AbstractGitCommand private final LfsBlobStoreFactory lfsBlobStoreFactory = mock(LfsBlobStoreFactory.class); + @BeforeClass + public static void setSigner() { + GpgSigner.setDefault(new GitTestHelper.SimpleGpgSigner()); + } + @Test public void shouldCreateNewFileInEmptyRepository() throws IOException, GitAPIException { File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); @@ -79,6 +90,44 @@ 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"; @@ -97,12 +146,30 @@ public class GitModifyCommand_withEmptyRepositoryTest extends AbstractGitCommand } } - private RevCommit getLastCommit(Git git) throws GitAPIException { - return git.log().setMaxCount(1).call().iterator().next(); + private RevCommit getLastCommit(Git git) throws GitAPIException, IOException { + return git.log().setMaxCount(1).all().call().iterator().next(); + } + + private void executeModifyCommand() throws IOException { + createCommand().execute(createRequest()); + } + + private ModifyCommandRequest createRequest() throws IOException { + File newFile = Files.write(temporaryFolder.newFile().toPath(), "new content".getBytes()).toFile(); + + 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; } private GitModifyCommand createCommand() { - return new GitModifyCommand(createContext(), new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider())), lfsBlobStoreFactory); + return new GitModifyCommand( + createContext(), + new SimpleGitWorkingCopyFactory(new NoneCachingWorkingCopyPool(new WorkdirProvider())), + lfsBlobStoreFactory, + createGitRepositoryConfigStoreProvider()); } @FunctionalInterface 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..1229c5f3cc --- /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; + +public 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-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-empty-repo-test.zip b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/repository/spi/scm-git-spi-empty-repo-test.zip deleted file mode 100644 index 80f2dbd18b6b2f26af992f60ddd9f721a09e5280..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13430 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