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