diff --git a/plugins/git4idea/resources/intellij.vcs.git.xml b/plugins/git4idea/resources/intellij.vcs.git.xml index c5702f9d8fe3..f057f973a0a7 100644 --- a/plugins/git4idea/resources/intellij.vcs.git.xml +++ b/plugins/git4idea/resources/intellij.vcs.git.xml @@ -514,6 +514,8 @@ + + @@ -728,6 +730,7 @@ + diff --git a/plugins/git4idea/resources/messages/GitBundle.properties b/plugins/git4idea/resources/messages/GitBundle.properties index 7029f0023e18..20a3ac02d3ca 100644 --- a/plugins/git4idea/resources/messages/GitBundle.properties +++ b/plugins/git4idea/resources/messages/GitBundle.properties @@ -484,6 +484,9 @@ unstash.clear.confirmation.title=Remove All Stashes? unstash.drop.confirmation.message=Do you want to remove {0}?
"{1}" unstash.drop.confirmation.title=Remove Stash {0}? +unshallow.repository.notification.message=Shallow repository detected +unshallow.repository.notification.title=This repository can be converted to a complete one + notification.title.unstash.failed=Unstash failed notification.title.unstash.failed.index.conflict=Unstash failed due to an index conflict exception.message.could.not.stash.root.error=Cannot stash {0}: {1} @@ -689,6 +692,9 @@ action.main.toolbar.git.Branches.text=VCS Widget action.main.toolbar.git.MergeRebase.text=VCS Merge/Rebase Widget action.not.possible.in.fresh.repo.generic=Action action.main.toolbar.git.project.not.trusted.separator.text=Safe mode, Version Control is not available +action.Git.Unshallow.text=Unshallow Repository +action.Git.Unshallow.progress.title=Unshallowing repository\u2026 +action.Git.Unshallow.failure.title=Failed to unshallow repository settings.git.option.group=Git settings.commit.group.title=Commit diff --git a/plugins/git4idea/src/git4idea/actions/GitUnshallowRepositoryAction.kt b/plugins/git4idea/src/git4idea/actions/GitUnshallowRepositoryAction.kt new file mode 100644 index 000000000000..2b63736a5cbf --- /dev/null +++ b/plugins/git4idea/src/git4idea/actions/GitUnshallowRepositoryAction.kt @@ -0,0 +1,47 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package git4idea.actions + +import com.intellij.dvcs.DvcsUtil +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.DumbAwareAction +import git4idea.GitVcs +import git4idea.fetch.GitFetchSupport +import git4idea.i18n.GitBundle +import git4idea.repo.GitRepository +import git4idea.repo.GitRepositoryManager + +internal class GitUnshallowRepositoryAction : DumbAwareAction() { + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT + + override fun update(e: AnActionEvent) { + val project = e.project + val guessedRepo: GitRepository? = project?.let { + DvcsUtil.guessRepositoryForOperation(it, GitRepositoryManager.getInstance(it), e.dataContext) + } + + e.presentation.isEnabledAndVisible = guessedRepo != null && guessedRepo.info.isShallow + } + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val repository = + DvcsUtil.guessRepositoryForOperation(project, GitRepositoryManager.getInstance(project), e.dataContext) + ?: return + + val fetcher = GitFetchSupport.fetchSupport(project) + val remote = fetcher.getDefaultRemoteToFetch(repository) ?: return + + GitVcs.runInBackground(object : Task.Backgroundable(project, GitBundle.message("action.Git.Unshallow.progress.title")) { + override fun run(indicator: ProgressIndicator) { + fetcher.fetchUnshallow(repository, remote).showNotificationIfFailed(GitBundle.message("action.Git.Unshallow.failure.title")) + } + }) + } + + internal companion object { + const val ACTION_ID = "Git.Unshallow" + } +} \ No newline at end of file diff --git a/plugins/git4idea/src/git4idea/fetch/GitFetchSupport.java b/plugins/git4idea/src/git4idea/fetch/GitFetchSupport.java index 90b340432094..5103146ab34d 100644 --- a/plugins/git4idea/src/git4idea/fetch/GitFetchSupport.java +++ b/plugins/git4idea/src/git4idea/fetch/GitFetchSupport.java @@ -35,6 +35,9 @@ public interface GitFetchSupport { @NotNull GitFetchResult fetch(@NotNull GitRepository repository, @NotNull GitRemote remote); + @NotNull + GitFetchResult fetchUnshallow(@NotNull GitRepository repository, @NotNull GitRemote remote); + /** * Fetches the given remotes. */ diff --git a/plugins/git4idea/src/git4idea/fetch/GitFetchSupportImpl.kt b/plugins/git4idea/src/git4idea/fetch/GitFetchSupportImpl.kt index 54c654e6bae9..0d9ca975264d 100644 --- a/plugins/git4idea/src/git4idea/fetch/GitFetchSupportImpl.kt +++ b/plugins/git4idea/src/git4idea/fetch/GitFetchSupportImpl.kt @@ -97,6 +97,10 @@ internal class GitFetchSupportImpl(private val project: Project) : GitFetchSuppo return fetch(listOf(RemoteRefCoordinates(repository, remote))) } + override fun fetchUnshallow(repository: GitRepository, remote: GitRemote): GitFetchResult { + return fetch(listOf(RemoteRefCoordinates(repository, remote, unshallow = true))) + } + override fun fetch(repository: GitRepository, remote: GitRemote, refspec: @NonNls String): GitFetchResult { return fetch(listOf(RemoteRefCoordinates(repository, remote, refspec))) } @@ -152,7 +156,10 @@ internal class GitFetchSupportImpl(private val project: Project) : GitFetchSuppo val executor = AppExecutorUtil.createBoundedApplicationPoolExecutor("GitFetch pool", maxThreads) val commonIndicator = progressManager.progressIndicator ?: EmptyProgressIndicator() val authenticationGate = RestrictingAuthenticationGate() - for ((repository, remote, refspec) in remotes) { + for (fetchTarget in remotes) { + val repository = fetchTarget.repository + val remote = fetchTarget.remote + LOG.debug("Fetching $remote in $repository") val future: Future = executor.submit { commonIndicator.checkCanceled() @@ -161,7 +168,7 @@ internal class GitFetchSupportImpl(private val project: Project) : GitFetchSuppo ProgressManager.getInstance().executeProcessUnderProgress({ commonIndicator.checkCanceled() result = fetchQueue.executeForRemote(repository, remote) { - doFetch(repository, remote, refspec, authenticationGate) + doFetch(fetchTarget, authenticationGate) } }, commonIndicator) result @@ -229,9 +236,7 @@ internal class GitFetchSupportImpl(private val project: Project) : GitFetchSuppo } } - private fun doFetch(repository: GitRepository, remote: GitRemote, refspec: String?, authenticationGate: AuthenticationGate? = null) - : SingleRemoteResult { - + private fun doFetch(fetchTarget: RemoteRefCoordinates, authenticationGate: AuthenticationGate?): SingleRemoteResult { val indicator = progressManager.progressIndicator val progressListener = GitLineHandlerListener { line, outputType -> if (indicator != null && outputType == ProcessOutputTypes.STDERR) { @@ -241,8 +246,15 @@ internal class GitFetchSupportImpl(private val project: Project) : GitFetchSuppo } val recurseSubmodules = "--recurse-submodules=no" - val params = if (refspec == null) arrayOf(recurseSubmodules) else arrayOf(refspec, recurseSubmodules) - val result = git.fetch(repository, remote, listOf(progressListener), authenticationGate, *params) + val params = buildList { + if (fetchTarget.refspec != null) add(fetchTarget.refspec) + add(recurseSubmodules) + if (fetchTarget.unshallow) add("--unshallow") + }.toTypedArray() + + val repository = fetchTarget.repository + val remote = fetchTarget.remote + val result = git.fetch(fetchTarget.repository, remote, listOf(progressListener), authenticationGate, *params) val pruned = result.output.mapNotNull { getPrunedRef(it) } if (result.success()) { BackgroundTaskUtil.syncPublisher(repository.project, GIT_AUTHENTICATION_SUCCESS).authenticationSucceeded(repository, remote) @@ -257,7 +269,12 @@ internal class GitFetchSupportImpl(private val project: Project) : GitFetchSuppo return if (matcher.matches()) matcher.group(1) else null } - private data class RemoteRefCoordinates(val repository: GitRepository, val remote: GitRemote, val refspec: String? = null) + private data class RemoteRefCoordinates( + val repository: GitRepository, + val remote: GitRemote, + val refspec: String? = null, + val unshallow: Boolean = false, + ) private class FetchTask(val repository: GitRepository, val remote: GitRemote, val future: Future) diff --git a/plugins/git4idea/src/git4idea/repo/GitShallowRepositoryCheck.kt b/plugins/git4idea/src/git4idea/repo/GitShallowRepositoryCheck.kt new file mode 100644 index 000000000000..9d792f4aee8d --- /dev/null +++ b/plugins/git4idea/src/git4idea/repo/GitShallowRepositoryCheck.kt @@ -0,0 +1,50 @@ +// 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 com.intellij.ide.util.RunOnceUtil +import com.intellij.notification.NotificationAction +import com.intellij.notification.NotificationType +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity +import com.intellij.openapi.vcs.VcsNotifier +import git4idea.actions.GitUnshallowRepositoryAction +import git4idea.fetch.GitFetchSupport +import git4idea.i18n.GitBundle + +internal class GitShallowRepositoryCheck() : ProjectActivity { + override suspend fun execute(project: Project) { + val repository = GitRepositoryManager.getInstance(project).repositories.singleOrNull() ?: return + + RunOnceUtil.runOnceForProject(project, ID) { + suggestToUnshallow(repository, project) + } + } + + private fun suggestToUnshallow(repository: GitRepository, project: Project) { + if (repository.info.isShallow) { + val fetcher = GitFetchSupport.fetchSupport(project) + val remote = fetcher.getDefaultRemoteToFetch(repository) + if (remote == null) { + LOG.debug("Couldn't detect remote for shallow repository") + return + } + + VcsNotifier.getInstance(project).notify( + VcsNotifier.importantNotification().createNotification( + GitBundle.message("unshallow.repository.notification.message"), + GitBundle.message("unshallow.repository.notification.title"), + NotificationType.INFORMATION, + ).addAction( + NotificationAction.createExpiring(GitBundle.message("action.Git.Unshallow.text")) { e, _ -> + GitUnshallowRepositoryAction().actionPerformed(e) + } + ) + ) + } + } +} + +private val LOG = Logger.getInstance(GitShallowRepositoryCheck::class.java) + +private const val ID = "git.unshallow" \ No newline at end of file diff --git a/plugins/git4idea/tests/git4idea/repo/GitShallowRepoTest.kt b/plugins/git4idea/tests/git4idea/repo/GitShallowRepoTest.kt index 470681e6a906..a144e889223d 100644 --- a/plugins/git4idea/tests/git4idea/repo/GitShallowRepoTest.kt +++ b/plugins/git4idea/tests/git4idea/repo/GitShallowRepoTest.kt @@ -1,8 +1,11 @@ // 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 com.intellij.openapi.vcs.VcsException +import git4idea.GitVcs import git4idea.commands.Git import git4idea.commands.GitShallowCloneOptions +import git4idea.fetch.GitFetchSupport import git4idea.test.GitSingleRepoTest import git4idea.test.makeCommit import git4idea.test.registerRepo @@ -10,18 +13,52 @@ import kotlin.io.path.name class GitShallowRepoTest: GitSingleRepoTest() { fun `test shallow repo detection`() { + val copyRepo = setupShallowCLone() + + assertFalse(repo.info.isShallow) + assertTrue(copyRepo.info.isShallow) + } + + fun `test fetch unshallow repository`() { + val copyRepo = setupShallowCLone() + val fetchSupport = GitFetchSupport.fetchSupport(project) + val remote = fetchSupport.getDefaultRemoteToFetch(copyRepo) + + assertNotNull(remote) + val fetchResult = fetchSupport.fetchUnshallow(copyRepo, remote!!) + fetchResult.throwExceptionIfFailed() + + GitVcs.getInstance(project).vfsListener.waitForExternalFilesEventsProcessedInTestMode() + + assertFalse(copyRepo.info.isShallow) + } + + fun `test normal repository can't be unshallowed`() { + val cloneResult = createClone(null) + val fetchSupport = GitFetchSupport.fetchSupport(project) + val remote = fetchSupport.getDefaultRemoteToFetch(cloneResult) + assertNotNull(remote) + val fetchResult = fetchSupport.fetchUnshallow(cloneResult, remote!!) + assertThrows(VcsException::class.java) { + fetchResult.throwExceptionIfFailed() + } + } + + private fun setupShallowCLone(): GitRepository { makeCommit("1.txt") makeCommit("2.txt") makeCommit("3.txt") - assertFalse(repo.info.isShallow) + return createClone(GitShallowCloneOptions(1)) + } + private fun createClone(shallowCloneOptions: GitShallowCloneOptions? = null): GitRepository { val copy = projectNioRoot.resolve("copy") val cloneResult = Git.getInstance().clone(project, copy.parent.toFile(), - "file://${repo.root.path}", copy.name, GitShallowCloneOptions(1)) + "file://${repo.root.path}", copy.name, shallowCloneOptions) assertTrue(cloneResult.success()) - val copyRepo = registerRepo(project, copy) - assertTrue(copyRepo.info.isShallow) + + return registerRepo(this.project, copy) } } \ No newline at end of file