[git] IJPL-84816 Support shallow clone at "Get from Version Control" screen

GitOrigin-RevId: 27e27e722ce57d5807e7081bedbaf4f487077962
This commit is contained in:
Ilia.Shulgin
2024-10-03 09:26:33 +02:00
committed by intellij-monorepo-bot
parent 13a6cb55f8
commit ca7bfcfe06
11 changed files with 201 additions and 33 deletions

View File

@@ -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

View File

@@ -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<ValidationInfo> {
val list = ArrayList<ValidationInfo>()
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)
}
}

View File

@@ -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."/>
<registryKey key="git.read.branches.from.disk" defaultValue="false"
description="When enabled, read the '.git/refs' directory contents. When disabled, delegate to 'git branch' call."/>
<registryKey key="git.clone.shallow" defaultValue="false"
description="When enabled, allows shallow cloning of the git repository"/>
<search.projectOptionsTopHitProvider implementation="git4idea.config.GitOptionsTopHitProvider"/>
<vcs name="Git" vcsClass="git4idea.GitVcs" displayName="Git" administrativeAreaName=".git"/>

View File

@@ -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

View File

@@ -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;
}

View File

@@ -74,7 +74,20 @@ public interface Git {
@Nullable List<String> 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

View File

@@ -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);

View File

@@ -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?)

View File

@@ -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"
}
}

View File

@@ -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

View File

@@ -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)
}
}