diff --git a/platform/dvcs-impl/api-dump.txt b/platform/dvcs-impl/api-dump.txt index 49bb23fa59e2..caae15b1e38e 100644 --- a/platform/dvcs-impl/api-dump.txt +++ b/platform/dvcs-impl/api-dump.txt @@ -609,7 +609,7 @@ a:com.intellij.dvcs.ui.DvcsCloneDialogComponent - doValidateAll():java.util.List - f:getDirectory():java.lang.String - pf:getErrorComponent():com.intellij.util.ui.components.BorderLayoutPanel -- pf:getMainPanel():javax.swing.JPanel +- pf:getMainPanel():com.intellij.openapi.ui.DialogPanel - getPreferredFocusedComponent():javax.swing.JComponent - f:getProject():com.intellij.openapi.project.Project - pf:getRememberedInputs():com.intellij.dvcs.DvcsRememberedInputs diff --git a/platform/dvcs-impl/src/com/intellij/dvcs/ui/DvcsCloneDialogComponent.kt b/platform/dvcs-impl/src/com/intellij/dvcs/ui/DvcsCloneDialogComponent.kt index 6ab6c43c0f50..9caa476b76b0 100644 --- a/platform/dvcs-impl/src/com/intellij/dvcs/ui/DvcsCloneDialogComponent.kt +++ b/platform/dvcs-impl/src/com/intellij/dvcs/ui/DvcsCloneDialogComponent.kt @@ -7,6 +7,7 @@ import com.intellij.dvcs.ui.CloneDvcsValidationUtils.sanitizeCloneUrl import com.intellij.dvcs.ui.DvcsBundle.message import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogPanel import com.intellij.openapi.ui.TextFieldWithBrowseButton import com.intellij.openapi.ui.ValidationInfo import com.intellij.openapi.util.text.StringUtil @@ -18,22 +19,26 @@ import com.intellij.ui.DocumentAdapter import com.intellij.ui.TextFieldWithHistory import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.panel import com.intellij.util.concurrency.annotations.RequiresEdt -import com.intellij.util.containers.ContainerUtil import com.intellij.util.ui.JBEmptyBorder import com.intellij.util.ui.UIUtil import com.intellij.util.ui.components.BorderLayoutPanel +import org.jetbrains.annotations.ApiStatus import javax.swing.JComponent import javax.swing.JPanel import javax.swing.event.DocumentEvent -abstract class DvcsCloneDialogComponent(var project: Project, - private var vcsDirectoryName: String, - protected val rememberedInputs: DvcsRememberedInputs, - private val dialogStateListener: VcsCloneDialogComponentStateListener) - : VcsCloneComponent, VcsCloneComponent.WithSettableUrl { - protected val mainPanel: JPanel +abstract class DvcsCloneDialogComponent @ApiStatus.Internal constructor( + var project: Project, + private var vcsDirectoryName: String, + protected val rememberedInputs: DvcsRememberedInputs, + private val dialogStateListener: VcsCloneDialogComponentStateListener, + @ApiStatus.Internal + protected val mainPanelCustomizer: MainPanelCustomizer?, +) : VcsCloneComponent, VcsCloneComponent.WithSettableUrl { + protected val mainPanel: DialogPanel private val urlEditor = TextFieldWithHistory() private val directoryField = TextFieldWithBrowseButton() private val cloneDirectoryChildHandle = FilePathDocumentChildPathHandle @@ -41,6 +46,13 @@ abstract class DvcsCloneDialogComponent(var project: Project, protected lateinit var errorComponent: BorderLayoutPanel + constructor( + project: Project, + vcsDirectoryName: String, + rememberedInputs: DvcsRememberedInputs, + dialogStateListener: VcsCloneDialogComponentStateListener, + ): this(project, vcsDirectoryName, rememberedInputs, dialogStateListener, null) + init { directoryField.addBrowseFolderListener(project, FileChooserDescriptorFactory.createSingleFolderDescriptor() .withTitle(message("clone.destination.directory.browser.title")) @@ -48,14 +60,23 @@ abstract class DvcsCloneDialogComponent(var project: Project, .withShowFileSystemRoots(true) .withHideIgnored(false)) mainPanel = panel { - row(VcsBundle.message("vcs.common.labels.url")) { cell(urlEditor).align(AlignX.FILL) } - row(VcsBundle.message("vcs.common.labels.directory")) { cell(directoryField).align(AlignX.FILL) } - .bottomGap(BottomGap.SMALL) + row(VcsBundle.message("vcs.common.labels.url")) { + cell(urlEditor).align(AlignX.FILL).validationOnApply { + CloneDvcsValidationUtils.checkRepositoryURL(it, it.text.trim()) + } + } + row(VcsBundle.message("vcs.common.labels.directory")) { + cell(directoryField).align(AlignX.FILL).validationOnApply { + CloneDvcsValidationUtils.checkDirectory(it.text, it.textField) + } + }.bottomGap(BottomGap.SMALL) + mainPanelCustomizer?.configure(this) row { errorComponent = BorderLayoutPanel(UIUtil.DEFAULT_HGAP, 0) cell(errorComponent).align(AlignX.FILL) } } + mainPanel.registerValidators(this) val insets = UIUtil.PANEL_REGULAR_INSETS mainPanel.border = JBEmptyBorder(insets.top / 2, insets.left, insets.bottom, insets.right) @@ -75,17 +96,14 @@ abstract class DvcsCloneDialogComponent(var project: Project, return StringUtil.trimEnd(ClonePathProvider.relativeDirectoryPathForVcsUrl(project, url), vcsDirectoryName) } - override fun getView() = mainPanel + override fun getView(): JPanel = mainPanel override fun isOkEnabled(): Boolean { return false } override fun doValidateAll(): List { - val list = ArrayList() - ContainerUtil.addIfNotNull(list, CloneDvcsValidationUtils.checkDirectory(directoryField.text, directoryField.textField)) - ContainerUtil.addIfNotNull(list, CloneDvcsValidationUtils.checkRepositoryURL(urlEditor, urlEditor.text.trim())) - return list + return mainPanel.validateAll() } abstract override fun doClone(listener: CheckoutProvider.Listener) @@ -107,4 +125,9 @@ abstract class DvcsCloneDialogComponent(var project: Project, protected fun updateOkActionState(dialogStateListener: VcsCloneDialogComponentStateListener) { dialogStateListener.onOkActionEnabled(isOkActionEnabled()) } + + @ApiStatus.Internal + abstract class MainPanelCustomizer { + abstract fun configure(panel: Panel) + } } diff --git a/plugins/git4idea/resources/intellij.vcs.git.xml b/plugins/git4idea/resources/intellij.vcs.git.xml index 8808970f8110..c5702f9d8fe3 100644 --- a/plugins/git4idea/resources/intellij.vcs.git.xml +++ b/plugins/git4idea/resources/intellij.vcs.git.xml @@ -620,6 +620,8 @@ description="Enable embedded pinentry application for unlock GPG private key while Git perform commit signing. For remote dev (unix backend) and WSL."/> + diff --git a/plugins/git4idea/resources/messages/GitBundle.properties b/plugins/git4idea/resources/messages/GitBundle.properties index 4027dfc822a7..7029f0023e18 100644 --- a/plugins/git4idea/resources/messages/GitBundle.properties +++ b/plugins/git4idea/resources/messages/GitBundle.properties @@ -756,6 +756,9 @@ gpg.pinentry.title=Unlock GPG Private Key gpg.pinentry.default.description=Please enter the passphrase to unlock the GPG private key: clone.dialog.checking.git.version=Checking Git version\u2026 +clone.dialog.shallow.clone=Shallow clone with a history truncated to +clone.dialog.shallow.clone.depth=commits + push.dialog.push.tags=Push &tags push.dialog.push.tags.combo.current.branch=Current Branch push.dialog.push.tags.combo.all=All diff --git a/plugins/git4idea/src/git4idea/checkout/GitCheckoutProvider.java b/plugins/git4idea/src/git4idea/checkout/GitCheckoutProvider.java index b0ff60717ba5..4d0b0a454cae 100644 --- a/plugins/git4idea/src/git4idea/checkout/GitCheckoutProvider.java +++ b/plugins/git4idea/src/git4idea/checkout/GitCheckoutProvider.java @@ -27,10 +27,7 @@ import com.intellij.openapi.wm.impl.welcomeScreen.cloneableProjects.CloneablePro import com.intellij.util.containers.ContainerUtil; import git4idea.GitUtil; import git4idea.GitVcs; -import git4idea.commands.Git; -import git4idea.commands.GitCommandResult; -import git4idea.commands.GitLineHandlerListener; -import git4idea.commands.GitStandardProgressAnalyzer; +import git4idea.commands.*; import git4idea.i18n.GitBundle; import git4idea.ui.GitCloneDialogComponent; import org.jetbrains.annotations.NonNls; @@ -82,9 +79,22 @@ public final class GitCheckoutProvider extends CheckoutProviderEx { directoryName, parentDirectory); } + public static void clone(final @NotNull Project project, + final @NotNull Git git, + final Listener listener, + final VirtualFile destinationParent, + final String sourceRepositoryURL, + final String directoryName, + final String parentDirectory) { + clone(project, git, listener, destinationParent, sourceRepositoryURL, directoryName, parentDirectory, null); + } + public static void clone(final @NotNull Project project, final @NotNull Git git, final Listener listener, - final VirtualFile destinationParent, final String sourceRepositoryURL, - final String directoryName, final String parentDirectory) { + final VirtualFile destinationParent, + final String sourceRepositoryURL, + final String directoryName, + final String parentDirectory, + final GitShallowCloneOptions shallowCloneOptions) { String projectAbsolutePath = Paths.get(parentDirectory, directoryName).toAbsolutePath().toString(); String projectPath = FileUtilRt.toSystemIndependentName(projectAbsolutePath); @@ -109,7 +119,7 @@ public final class GitCheckoutProvider extends CheckoutProviderEx { GitCommandResult result; try { - result = git.clone(project, new File(parentDirectory), sourceRepositoryURL, directoryName, progressListener); + result = git.clone(project, new File(parentDirectory), sourceRepositoryURL, directoryName, shallowCloneOptions, progressListener); } catch (Exception e) { if (listener instanceof GitCheckoutListener) { @@ -143,13 +153,25 @@ public final class GitCheckoutProvider extends CheckoutProviderEx { CloneableProjectsService.getInstance().runCloneTask(projectPath, cloneTask); } - public static boolean doClone(@NotNull Project project, @NotNull Git git, - @NotNull String directoryName, @NotNull String parentDirectory, @NotNull String sourceRepositoryURL) { + public static boolean doClone(@NotNull Project project, + @NotNull Git git, + @NotNull String directoryName, + @NotNull String parentDirectory, + @NotNull String sourceRepositoryURL) { + return doClone(project, git, directoryName, parentDirectory, sourceRepositoryURL, null); + } + + public static boolean doClone(@NotNull Project project, + @NotNull Git git, + @NotNull String directoryName, + @NotNull String parentDirectory, + @NotNull String sourceRepositoryURL, + @Nullable GitShallowCloneOptions shallowCloneOptions) { ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator(); indicator.setIndeterminate(false); GitLineHandlerListener progressListener = GitStandardProgressAnalyzer.createListener(indicator); - GitCommandResult result = git.clone(project, new File(parentDirectory), sourceRepositoryURL, directoryName, progressListener); + GitCommandResult result = git.clone(project, new File(parentDirectory), sourceRepositoryURL, directoryName, shallowCloneOptions, progressListener); if (result.success()) { return true; } diff --git a/plugins/git4idea/src/git4idea/commands/Git.java b/plugins/git4idea/src/git4idea/commands/Git.java index 820df0ccc389..d82d453a0c8a 100644 --- a/plugins/git4idea/src/git4idea/commands/Git.java +++ b/plugins/git4idea/src/git4idea/commands/Git.java @@ -74,7 +74,20 @@ public interface Git { @Nullable List relativePaths) throws VcsException; @NotNull - GitCommandResult clone(@Nullable Project project, @NotNull File parentDirectory, @NotNull String url, @NotNull String clonedDirectoryName, + default GitCommandResult clone(@Nullable Project project, + @NotNull File parentDirectory, + @NotNull String url, + @NotNull String clonedDirectoryName, + GitLineHandlerListener @NotNull ... progressListeners) { + return clone(project, parentDirectory, url, clonedDirectoryName, null, progressListeners); + } + + @NotNull + GitCommandResult clone(@Nullable Project project, + @NotNull File parentDirectory, + @NotNull String url, + @NotNull String clonedDirectoryName, + @Nullable GitShallowCloneOptions shallowCloneOptions, GitLineHandlerListener @NotNull ... progressListeners); @NotNull diff --git a/plugins/git4idea/src/git4idea/commands/GitImpl.java b/plugins/git4idea/src/git4idea/commands/GitImpl.java index 86b069ee3ab9..13390f0a9892 100644 --- a/plugins/git4idea/src/git4idea/commands/GitImpl.java +++ b/plugins/git4idea/src/git4idea/commands/GitImpl.java @@ -180,8 +180,12 @@ public class GitImpl extends GitImplBase { } @Override - public @NotNull GitCommandResult clone(final @Nullable Project project, final @NotNull File parentDirectory, final @NotNull String url, - final @NotNull String clonedDirectoryName, final GitLineHandlerListener @NotNull ... listeners) { + public @NotNull GitCommandResult clone(final @Nullable Project project, + final @NotNull File parentDirectory, + final @NotNull String url, + final @NotNull String clonedDirectoryName, + final @Nullable GitShallowCloneOptions shallowCloneOptions, + final GitLineHandlerListener @NotNull ... listeners) { return runCommand(() -> { // do not use per-project executable for 'clone' command Project defaultProject = ProjectManager.getInstance().getDefaultProject(); @@ -195,6 +199,12 @@ public class GitImpl extends GitImplBase { AdvancedSettings.getBoolean("git.clone.recurse.submodules")) { handler.addParameters("--recurse-submodules"); } + if (shallowCloneOptions != null) { + Integer depth = shallowCloneOptions.getDepth(); + if (depth != null) { + handler.addParameters("--depth=" + depth); + } + } handler.addParameters(url); handler.endOptions(); handler.addParameters(clonedDirectoryName); diff --git a/plugins/git4idea/src/git4idea/commands/GitShallowCloneOptions.kt b/plugins/git4idea/src/git4idea/commands/GitShallowCloneOptions.kt new file mode 100644 index 000000000000..10469225563f --- /dev/null +++ b/plugins/git4idea/src/git4idea/commands/GitShallowCloneOptions.kt @@ -0,0 +1,4 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package git4idea.commands + +class GitShallowCloneOptions(val depth: Int?) \ No newline at end of file diff --git a/plugins/git4idea/src/git4idea/ui/GitCloneDialogComponent.kt b/plugins/git4idea/src/git4idea/ui/GitCloneDialogComponent.kt index 0bd26b625a7d..32b707960037 100644 --- a/plugins/git4idea/src/git4idea/ui/GitCloneDialogComponent.kt +++ b/plugins/git4idea/src/git4idea/ui/GitCloneDialogComponent.kt @@ -4,23 +4,28 @@ package git4idea.ui import com.intellij.application.subscribe import com.intellij.dvcs.ui.CloneDvcsValidationUtils import com.intellij.dvcs.ui.DvcsCloneDialogComponent +import com.intellij.ide.IdeBundle.message import com.intellij.openapi.application.ApplicationActivationListener import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.invokeAndWaitIfNeeded import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project +import com.intellij.openapi.util.NlsSafe +import com.intellij.openapi.util.registry.Registry import com.intellij.openapi.vcs.CheckoutProvider import com.intellij.openapi.vcs.VcsBundle import com.intellij.openapi.vcs.VcsNotifier import com.intellij.openapi.vcs.ui.cloneDialog.VcsCloneDialogComponentStateListener import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.wm.IdeFrame +import com.intellij.ui.dsl.builder.* import com.intellij.util.Alarm import com.intellij.util.concurrency.annotations.RequiresEdt import git4idea.GitNotificationIdsHolder.Companion.CLONE_ERROR_UNABLE_TO_CREATE_DESTINATION_DIR import git4idea.GitUtil import git4idea.checkout.GitCheckoutProvider import git4idea.commands.Git +import git4idea.commands.GitShallowCloneOptions import git4idea.config.* import git4idea.i18n.GitBundle import git4idea.remote.GitRememberedInputs @@ -32,7 +37,8 @@ class GitCloneDialogComponent(project: Project, DvcsCloneDialogComponent(project, GitUtil.DOT_GIT, GitRememberedInputs.getInstance(), - dialogStateListener) { + dialogStateListener, + GitCloneDialogMainPanelCustomizer()) { private val LOG = Logger.getInstance(GitCloneDialogComponent::class.java) private val executableManager get() = GitExecutableManager.getInstance() @@ -66,7 +72,16 @@ class GitCloneDialogComponent(project: Project, val directoryName = Paths.get(getDirectory()).fileName.toString() val parentDirectory = parent.toAbsolutePath().toString() - GitCheckoutProvider.clone(project, Git.getInstance(), listener, destinationParent, sourceRepositoryURL, directoryName, parentDirectory) + GitCheckoutProvider.clone( + project, + Git.getInstance(), + listener, + destinationParent, + sourceRepositoryURL, + directoryName, + parentDirectory, + (mainPanelCustomizer as GitCloneDialogMainPanelCustomizer).getShallowCloneOptions(), + ) val rememberedInputs = GitRememberedInputs.getInstance() rememberedInputs.addUrl(sourceRepositoryURL) rememberedInputs.cloneParentDir = parentDirectory @@ -144,3 +159,35 @@ class GitCloneDialogComponent(project: Project, FAILED } } + +private class GitCloneDialogMainPanelCustomizer : DvcsCloneDialogComponent.MainPanelCustomizer() { + private var shallowClone = false + private var depth = 1 + + override fun configure(panel: Panel) { + if (Registry.`is`("git.clone.shallow")) { + with(panel) { + row { + var shallowCloneCheckbox = checkBox(GitBundle.message("clone.dialog.shallow.clone")) + .gap(RightGap.SMALL) + .bindSelected(::shallowClone) + + val depthTextField = intTextField(1..Int.MAX_VALUE, 1) + .bindIntText(::depth) + .enabledIf(shallowCloneCheckbox.selected) + .gap(RightGap.SMALL) + depthTextField.component.toolTipText = GIT_CLONE_DEPTH_ARG + + @Suppress("DialogTitleCapitalization") + label(GitBundle.message("clone.dialog.shallow.clone.depth")) + }.bottomGap(BottomGap.SMALL) + } + } + } + + fun getShallowCloneOptions() = if (shallowClone) GitShallowCloneOptions(depth) else null + + private companion object { + const val GIT_CLONE_DEPTH_ARG: @NlsSafe String = "--depth" + } +} \ No newline at end of file diff --git a/plugins/git4idea/tests/git4idea/remote/GitRemoteTest.kt b/plugins/git4idea/tests/git4idea/remote/GitRemoteTest.kt index 2e9cf0dfc908..39d45d120c35 100644 --- a/plugins/git4idea/tests/git4idea/remote/GitRemoteTest.kt +++ b/plugins/git4idea/tests/git4idea/remote/GitRemoteTest.kt @@ -6,11 +6,16 @@ import com.intellij.openapi.util.text.StringUtil import com.intellij.util.UriUtil import com.intellij.util.io.URLUtil import git4idea.checkout.GitCheckoutProvider +import git4idea.commands.Git +import git4idea.commands.GitCommand import git4idea.commands.GitHttpAuthService import git4idea.commands.GitHttpAuthenticator +import git4idea.commands.GitLineHandler +import git4idea.commands.GitShallowCloneOptions import git4idea.config.GitVersion import git4idea.test.GitHttpAuthTestService import git4idea.test.GitPlatformTest +import git4idea.test.registerRepo import org.assertj.core.api.Assertions.assertThat import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @@ -80,6 +85,18 @@ class GitRemoteTest : GitPlatformTest() { assertCloneSuccessful(cloneWaiter) } + fun `test shallow clone`() { + val cloneWaiter = cloneOnPooledThread(makeUrlWithUsername(), GitShallowCloneOptions(depth = 1)) + + assertPasswordAsked() + authenticator.supplyPassword(token) + + assertCloneSuccessful(cloneWaiter) + + val repo = registerRepo(project, testNioRoot) + assertTrue(repo.info.isShallow) + } + fun `test clone fails if incorrect password`() { val url = makeUrlWithUsername() @@ -106,11 +123,11 @@ class GitRemoteTest : GitPlatformTest() { assertErrorNotification("Clone failed", expectedAuthFailureMessage) } - private fun cloneOnPooledThread(url: String): CountDownLatch { + private fun cloneOnPooledThread(url: String, shallowCloneOptions: GitShallowCloneOptions? = null): CountDownLatch { val cloneWaiter = CountDownLatch(1) executeOnPooledThread { val projectName = url.substring(url.lastIndexOf('/') + 1).replace(".git", "") - GitCheckoutProvider.doClone(project, git, projectName, testNioRoot.toString(), url) + GitCheckoutProvider.doClone(project, git, projectName, testNioRoot.toString(), url, shallowCloneOptions) cloneWaiter.countDown() } return cloneWaiter diff --git a/plugins/git4idea/tests/git4idea/repo/GitShallowRepoTest.kt b/plugins/git4idea/tests/git4idea/repo/GitShallowRepoTest.kt new file mode 100644 index 000000000000..470681e6a906 --- /dev/null +++ b/plugins/git4idea/tests/git4idea/repo/GitShallowRepoTest.kt @@ -0,0 +1,27 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package git4idea.repo + +import git4idea.commands.Git +import git4idea.commands.GitShallowCloneOptions +import git4idea.test.GitSingleRepoTest +import git4idea.test.makeCommit +import git4idea.test.registerRepo +import kotlin.io.path.name + +class GitShallowRepoTest: GitSingleRepoTest() { + fun `test shallow repo detection`() { + makeCommit("1.txt") + makeCommit("2.txt") + makeCommit("3.txt") + + assertFalse(repo.info.isShallow) + + val copy = projectNioRoot.resolve("copy") + val cloneResult = Git.getInstance().clone(project, + copy.parent.toFile(), + "file://${repo.root.path}", copy.name, GitShallowCloneOptions(1)) + assertTrue(cloneResult.success()) + val copyRepo = registerRepo(project, copy) + assertTrue(copyRepo.info.isShallow) + } +} \ No newline at end of file