mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[vcs] don't execute action manually just to update the project
Extract the functionality to a separate file and mark actions impl internal GitOrigin-RevId: e41f7a45168c316144bd04912905e94a6bd9ddd1
This commit is contained in:
committed by
intellij-monorepo-bot
parent
a2889d758c
commit
bfee77d859
@@ -1,14 +1,12 @@
|
||||
package com.intellij.smartUpdate
|
||||
|
||||
import com.intellij.openapi.actionSystem.ActionPlaces
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.CommonDataKeys
|
||||
import com.intellij.openapi.actionSystem.DataContext
|
||||
import com.intellij.openapi.actionSystem.impl.SimpleDataContext
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.vcs.update.ActionInfo
|
||||
import com.intellij.openapi.vcs.update.CommonUpdateProjectAction
|
||||
import com.intellij.openapi.vcs.update.ScopeInfo
|
||||
import com.intellij.openapi.vcs.update.VcsUpdateProcess
|
||||
import com.intellij.ui.dsl.builder.panel
|
||||
import com.intellij.ui.layout.ComponentPredicate
|
||||
import javax.swing.JComponent
|
||||
@@ -16,30 +14,24 @@ import javax.swing.JComponent
|
||||
private const val VCS_UPDATE = "vcs.update"
|
||||
|
||||
internal class VcsUpdateStep : SmartUpdateStep {
|
||||
private val actionInfo = ActionInfo.UPDATE
|
||||
private val scopeInfo = ScopeInfo.PROJECT
|
||||
|
||||
private lateinit var showOptionsListener: (Boolean) -> Unit
|
||||
override val id: String = VCS_UPDATE
|
||||
override val stepName = SmartUpdateBundle.message("checkbox.update.project")
|
||||
|
||||
override fun performUpdateStep(project: Project, e: AnActionEvent?, onSuccess: () -> Unit) {
|
||||
val start = System.currentTimeMillis()
|
||||
val action = object : CommonUpdateProjectAction() {
|
||||
override fun isShowOptions(project: Project?) = false
|
||||
|
||||
override fun onSuccess() {
|
||||
SmartUpdateUsagesCollector.logUpdate(System.currentTimeMillis() - start)
|
||||
onSuccess.invoke()
|
||||
}
|
||||
val dataContext = SimpleDataContext.getProjectContext(project)
|
||||
VcsUpdateProcess.launchUpdate(project, actionInfo, scopeInfo, dataContext, false, SmartUpdateBundle.message("action.update.project.text")) {
|
||||
SmartUpdateUsagesCollector.logUpdate(System.currentTimeMillis() - start)
|
||||
onSuccess.invoke()
|
||||
}
|
||||
action.templatePresentation.text = SmartUpdateBundle.message("action.update.project.text")
|
||||
val dataContext = SimpleDataContext.builder()
|
||||
.add(CommonDataKeys.PROJECT, project)
|
||||
.build()
|
||||
val actionEvent = AnActionEvent.createFromAnAction(action, null, ActionPlaces.UNKNOWN, dataContext)
|
||||
action.actionPerformed(actionEvent)
|
||||
}
|
||||
|
||||
override fun getDetailsComponent(project: Project): JComponent? {
|
||||
if (!ActionInfo.UPDATE.showOptions(project)) return super.getDetailsComponent(project)
|
||||
if (!actionInfo.showOptions(project)) return super.getDetailsComponent(project)
|
||||
return panel {
|
||||
row { label(SmartUpdateBundle.message("warning.default.update.options.will.be.applied")) }
|
||||
row { link(SmartUpdateBundle.message("label.change.options")) { showOptionsDialog(project) } }
|
||||
@@ -50,9 +42,9 @@ internal class VcsUpdateStep : SmartUpdateStep {
|
||||
val showOptions = ActionInfo.UPDATE.showOptions(project)
|
||||
showOptionsListener.invoke(showOptions)
|
||||
if (showOptions) {
|
||||
val map = CommonUpdateProjectAction().getConfigurableToEnvMap(project)
|
||||
val scopeName = ScopeInfo.PROJECT.getScopeName(DataContext.EMPTY_CONTEXT, ActionInfo.UPDATE)
|
||||
ActionInfo.UPDATE.createOptionsDialog(project, map, scopeName).show()
|
||||
val roots = VcsUpdateProcess.getRoots(project, actionInfo, scopeInfo, SimpleDataContext.getProjectContext(project))
|
||||
val spec = VcsUpdateProcess.createUpdateSpec(project, roots, actionInfo)
|
||||
VcsUpdateProcess.showOptionsDialog(project, actionInfo, scopeInfo, spec, DataContext.EMPTY_CONTEXT)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +54,7 @@ internal class VcsUpdateStep : SmartUpdateStep {
|
||||
showOptionsListener = listener
|
||||
}
|
||||
|
||||
override fun invoke() = ActionInfo.UPDATE.showOptions(project)
|
||||
override fun invoke() = actionInfo.showOptions(project)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
c:com.intellij.openapi.vcs.changes.ui.TreeModelBuilder
|
||||
- com.intellij.openapi.vcs.changes.ui.ChangesViewModelBuilder
|
||||
- *:build(Z):javax.swing.tree.DefaultTreeModel
|
||||
*f:com.intellij.openapi.vcs.update.VcsUpdateProcess
|
||||
- sf:INSTANCE:com.intellij.openapi.vcs.update.VcsUpdateProcess
|
||||
- sf:checkUpdateHasCustomNotification(java.util.Collection):Z
|
||||
- f:launchUpdate(com.intellij.openapi.project.Project,com.intellij.openapi.vcs.update.ActionInfo,com.intellij.openapi.vcs.update.ScopeInfo,com.intellij.openapi.actionSystem.DataContext,java.lang.String,Z):V
|
||||
- bs:launchUpdate$default(com.intellij.openapi.vcs.update.VcsUpdateProcess,com.intellij.openapi.project.Project,com.intellij.openapi.vcs.update.ActionInfo,com.intellij.openapi.vcs.update.ScopeInfo,com.intellij.openapi.actionSystem.DataContext,java.lang.String,Z,I,java.lang.Object):V
|
||||
*:com.intellij.vcs.CacheableAnnotationProvider
|
||||
- a:getFromCache(com.intellij.openapi.vfs.VirtualFile):com.intellij.openapi.vcs.annotate.FileAnnotation
|
||||
- a:populateCache(com.intellij.openapi.vfs.VirtualFile):V
|
||||
|
||||
@@ -4068,20 +4068,6 @@ c:com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier
|
||||
- run():V
|
||||
- s:showOverChangesView(com.intellij.openapi.project.Project,java.lang.String,com.intellij.openapi.ui.MessageType,com.intellij.openapi.util.NamedRunnable[]):V
|
||||
- s:showOverVersionControlView(com.intellij.openapi.project.Project,java.lang.String,com.intellij.openapi.ui.MessageType):V
|
||||
a:com.intellij.openapi.vcs.update.AbstractCommonUpdateAction
|
||||
- com.intellij.openapi.project.DumbAwareAction
|
||||
- sf:Companion:com.intellij.openapi.vcs.update.AbstractCommonUpdateAction$Companion
|
||||
- p:<init>(com.intellij.openapi.vcs.update.ActionInfo,com.intellij.openapi.vcs.update.ScopeInfo,Z):V
|
||||
- actionPerformed(com.intellij.openapi.actionSystem.AnActionEvent):V
|
||||
- pa:filterRootsBeforeAction():Z
|
||||
- getActionUpdateThread():com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
- f:getConfigurableToEnvMap(com.intellij.openapi.project.Project):java.util.LinkedHashMap
|
||||
- p:isShowOptions(com.intellij.openapi.project.Project):Z
|
||||
- p:onSuccess():V
|
||||
- sf:showsCustomNotification(java.util.Collection):Z
|
||||
- update(com.intellij.openapi.actionSystem.AnActionEvent):V
|
||||
f:com.intellij.openapi.vcs.update.AbstractCommonUpdateAction$Companion
|
||||
- f:showsCustomNotification(java.util.Collection):Z
|
||||
com.intellij.openapi.vcs.update.ActionInfo
|
||||
- sf:INTEGRATE:com.intellij.openapi.vcs.update.ActionInfo
|
||||
- sf:STATUS:com.intellij.openapi.vcs.update.ActionInfo
|
||||
@@ -4094,10 +4080,6 @@ com.intellij.openapi.vcs.update.ActionInfo
|
||||
- a:getEnvironment(com.intellij.openapi.vcs.AbstractVcs):com.intellij.openapi.vcs.update.UpdateEnvironment
|
||||
- a:getGroupName(com.intellij.openapi.vcs.update.FileGroup):java.lang.String
|
||||
- a:showOptions(com.intellij.openapi.project.Project):Z
|
||||
c:com.intellij.openapi.vcs.update.CommonUpdateProjectAction
|
||||
- com.intellij.openapi.vcs.update.AbstractCommonUpdateAction
|
||||
- <init>():V
|
||||
- p:filterRootsBeforeAction():Z
|
||||
com.intellij.openapi.vcs.update.FilePathChange
|
||||
- a:getAfterPath():com.intellij.openapi.vcs.FilePath
|
||||
- a:getBeforePath():com.intellij.openapi.vcs.FilePath
|
||||
|
||||
+33
-194
@@ -4,7 +4,6 @@
|
||||
package com.intellij.openapi.vcs.update
|
||||
|
||||
import com.intellij.configurationStore.StoreReloadManager
|
||||
import com.intellij.configurationStore.forPoorJavaClientOnlySaveProjectIndEdtDoNotUseThisMethod
|
||||
import com.intellij.history.Label
|
||||
import com.intellij.history.LocalHistory
|
||||
import com.intellij.history.LocalHistoryAction
|
||||
@@ -13,14 +12,10 @@ import com.intellij.notification.Notification
|
||||
import com.intellij.notification.NotificationType
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.DataContext
|
||||
import com.intellij.openapi.actionSystem.impl.SimpleDataContext
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.diagnostic.debug
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.openapi.options.Configurable
|
||||
import com.intellij.openapi.progress.*
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.progress.Task
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.NlsContexts
|
||||
@@ -28,7 +23,6 @@ import com.intellij.openapi.util.Ref
|
||||
import com.intellij.openapi.util.text.HtmlBuilder
|
||||
import com.intellij.openapi.util.text.HtmlChunk
|
||||
import com.intellij.openapi.vcs.*
|
||||
import com.intellij.openapi.vcs.actions.DescindingFilesFilter
|
||||
import com.intellij.openapi.vcs.changes.RemoteRevisionsCache
|
||||
import com.intellij.openapi.vcs.changes.VcsAnnotationRefresher
|
||||
import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager
|
||||
@@ -38,178 +32,40 @@ import com.intellij.openapi.vcs.ex.ProjectLevelVcsManagerEx
|
||||
import com.intellij.openapi.vfs.VfsUtilCore
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.openapi.vfs.VirtualFileManager
|
||||
import com.intellij.util.containers.MultiMap
|
||||
import com.intellij.util.ui.OptionsDialog
|
||||
import com.intellij.vcs.VcsActivity
|
||||
import com.intellij.vcs.ViewUpdateInfoNotification
|
||||
import com.intellij.vcsUtil.VcsUtil
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.Nls
|
||||
import org.jetbrains.annotations.NonNls
|
||||
|
||||
private val LOG = Logger.getInstance(AbstractCommonUpdateAction::class.java)
|
||||
|
||||
@ApiStatus.Internal
|
||||
abstract class AbstractCommonUpdateAction protected constructor(
|
||||
private val actionInfo: ActionInfo,
|
||||
private val scopeInfo: ScopeInfo,
|
||||
private val alwaysVisible: Boolean
|
||||
private val alwaysVisible: Boolean,
|
||||
) : DumbAwareAction() {
|
||||
companion object {
|
||||
@Deprecated("Use VcsUpdateProcess.checkUpdateHasCustomNotification",
|
||||
ReplaceWith("VcsUpdateProcess.checkUpdateHasCustomNotification(vcss)", "com.intellij.openapi.vcs.update.VcsUpdateProcess"))
|
||||
@JvmStatic
|
||||
fun showsCustomNotification(vcss: Collection<AbstractVcs>): Boolean {
|
||||
return vcss.all { vcs ->
|
||||
val environment = vcs.updateEnvironment
|
||||
environment != null && environment.hasCustomNotification()
|
||||
}
|
||||
}
|
||||
fun showsCustomNotification(vcss: Collection<AbstractVcs>): Boolean = VcsUpdateProcess.checkUpdateHasCustomNotification(vcss)
|
||||
}
|
||||
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
|
||||
|
||||
@Suppress("IncorrectCancellationExceptionHandling")
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val project = e.project
|
||||
val showUpdateOptions = isShowOptions(project)
|
||||
|
||||
LOG.debug { "project: $project, show update options: $showUpdateOptions" }
|
||||
|
||||
if (project == null) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val roots = getRoots(project, e.dataContext)
|
||||
if (roots.isEmpty()) {
|
||||
LOG.debug("No roots found.")
|
||||
return
|
||||
}
|
||||
|
||||
val vcsToVirtualFiles = createVcsToFilesMap(roots, project)
|
||||
for (vcs in vcsToVirtualFiles.keys) {
|
||||
val updateEnvironment = actionInfo.getEnvironment(vcs)
|
||||
if (updateEnvironment != null && !updateEnvironment.validateOptions(vcsToVirtualFiles.get(vcs))) {
|
||||
// messages already shown
|
||||
LOG.debug { "Options not valid for files: $vcsToVirtualFiles" }
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (showUpdateOptions || OptionsDialog.shiftIsPressed(e.modifiers)) {
|
||||
val scopeName = scopeInfo.getScopeName(e.dataContext, actionInfo)
|
||||
showOptionsDialog(vcsToVirtualFiles, project, scopeName)
|
||||
}
|
||||
|
||||
if (ApplicationManager.getApplication().isDispatchThread()) {
|
||||
// Not only documents, but also project settings should be saved,
|
||||
// to ensure that if as a result of Update some project settings will be changed,
|
||||
// all local changes are saved in prior and do not overwrite remote changes.
|
||||
// Also, there is a chance that save during update can break it -
|
||||
// we do disable auto saving during update, but still, there is a chance that save will occur.
|
||||
FileDocumentManager.getInstance().saveAllDocuments()
|
||||
forPoorJavaClientOnlySaveProjectIndEdtDoNotUseThisMethod(project, false)
|
||||
}
|
||||
|
||||
val task = object : Updater(project, roots, vcsToVirtualFiles, actionInfo, getTemplatePresentation().text) {
|
||||
override fun onSuccess() {
|
||||
super.onSuccess()
|
||||
this@AbstractCommonUpdateAction.onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
if (ApplicationManager.getApplication().isUnitTestMode()) {
|
||||
task.run(EmptyProgressIndicator())
|
||||
}
|
||||
else {
|
||||
ProgressManager.getInstance().run(task)
|
||||
}
|
||||
}
|
||||
catch (_: ProcessCanceledException) {
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun isShowOptions(project: Project?): Boolean = actionInfo.showOptions(project)
|
||||
|
||||
protected open fun onSuccess() {
|
||||
}
|
||||
|
||||
private fun showOptionsDialog(
|
||||
updateEnvToVirtualFiles: Map<AbstractVcs, Collection<FilePath>>, project: Project?,
|
||||
scopeName: String?
|
||||
) {
|
||||
val envToConfMap = createConfigurableToEnvMap(updateEnvToVirtualFiles)
|
||||
LOG.debug { "configurables map: $envToConfMap" }
|
||||
if (!envToConfMap.isEmpty()) {
|
||||
val dialogOrStatus = actionInfo.createOptionsDialog(project, envToConfMap, scopeName)
|
||||
if (!dialogOrStatus.showAndGet()) {
|
||||
throw ProcessCanceledException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRoots(project: Project, context: DataContext): Array<FilePath> {
|
||||
val filePaths = scopeInfo.getRoots(context, actionInfo)
|
||||
return DescindingFilesFilter.filterDescindingFiles(filterRoots(project, filePaths), project)
|
||||
}
|
||||
|
||||
private fun createConfigurableToEnvMap(updateEnvToVirtualFiles: Map<AbstractVcs, Collection<FilePath>>): LinkedHashMap<Configurable, AbstractVcs> {
|
||||
val envToConfMap = LinkedHashMap<Configurable, AbstractVcs>()
|
||||
for (vcs in updateEnvToVirtualFiles.keys) {
|
||||
val configurable = actionInfo.getEnvironment(vcs).createConfigurable(updateEnvToVirtualFiles.get(vcs))
|
||||
if (configurable != null) {
|
||||
envToConfMap.put(configurable, vcs)
|
||||
}
|
||||
}
|
||||
return envToConfMap
|
||||
}
|
||||
|
||||
fun getConfigurableToEnvMap(project: Project): LinkedHashMap<Configurable, AbstractVcs> {
|
||||
val roots = getRoots(project, SimpleDataContext.getProjectContext(project))
|
||||
val vcsToFilesMap = createVcsToFilesMap(roots, project)
|
||||
return createConfigurableToEnvMap(vcsToFilesMap)
|
||||
}
|
||||
|
||||
private fun createVcsToFilesMap(roots: Array<FilePath>, project: Project): Map<AbstractVcs, Collection<FilePath>> {
|
||||
val resultPrep = MultiMap.createSet<AbstractVcs, FilePath>()
|
||||
for (file in roots) {
|
||||
val vcs = VcsUtil.getVcsFor(project, file) ?: continue
|
||||
val updateEnvironment = actionInfo.getEnvironment(vcs)
|
||||
if (updateEnvironment != null) {
|
||||
resultPrep.putValue(vcs, file)
|
||||
}
|
||||
}
|
||||
|
||||
val result = HashMap<AbstractVcs, MutableCollection<FilePath>>()
|
||||
for (entry in resultPrep.entrySet()) {
|
||||
val vcs = entry.key
|
||||
@Suppress("DEPRECATION")
|
||||
result.put(vcs, vcs.filterUniqueRoots(ArrayList(entry.value)) { it.getVirtualFile() })
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun filterRoots(project: Project, roots: MutableList<FilePath>): Array<FilePath> {
|
||||
val result = ArrayList<FilePath>()
|
||||
for (file in roots) {
|
||||
val vcs = VcsUtil.getVcsFor(project, file) ?: continue
|
||||
if (!scopeInfo.filterExistsInVcs() || AbstractVcs.fileInVcsByFileStatus(project, file)) {
|
||||
val updateEnvironment = actionInfo.getEnvironment(vcs)
|
||||
if (updateEnvironment != null) {
|
||||
result.add(file)
|
||||
}
|
||||
}
|
||||
else {
|
||||
val virtualFile = file.getVirtualFile()
|
||||
if (virtualFile != null && virtualFile.isDirectory()) {
|
||||
val vcsRoots = ProjectLevelVcsManager.getInstance(project).getAllVersionedRoots()
|
||||
for (vcsRoot in vcsRoots) {
|
||||
if (VfsUtilCore.isAncestor(virtualFile, vcsRoot, false)) {
|
||||
result.add(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toTypedArray()
|
||||
val project = e.project ?: return
|
||||
VcsUpdateProcess.launchUpdate(
|
||||
project,
|
||||
actionInfo,
|
||||
scopeInfo,
|
||||
e.dataContext,
|
||||
actionName = getTemplatePresentation().text,
|
||||
forceShowOptions = OptionsDialog.shiftIsPressed(e.modifiers),
|
||||
)
|
||||
}
|
||||
|
||||
protected abstract fun filterRootsBeforeAction(): Boolean
|
||||
@@ -243,8 +99,7 @@ abstract class AbstractCommonUpdateAction protected constructor(
|
||||
}
|
||||
|
||||
if (filterRootsBeforeAction()) {
|
||||
val filePaths = scopeInfo.getRoots(e.dataContext, actionInfo)
|
||||
val roots = filterRoots(project, filePaths)
|
||||
val roots = VcsUpdateProcess.getRoots(project, actionInfo, scopeInfo, e.dataContext, false)
|
||||
if (roots.isEmpty()) {
|
||||
presentation.setVisible(alwaysVisible)
|
||||
presentation.setEnabled(false)
|
||||
@@ -257,13 +112,12 @@ abstract class AbstractCommonUpdateAction protected constructor(
|
||||
presentation.setEnabled(!vcsManager.isBackgroundVcsOperationRunning() && (singleVcs == null || !singleVcs.isUpdateActionDisabled))
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
open class Updater(
|
||||
project: Project,
|
||||
internal open class Updater(
|
||||
private val project: Project,
|
||||
private val roots: Array<FilePath>,
|
||||
private val vcsToVirtualFiles: Map<AbstractVcs, Collection<FilePath>>,
|
||||
private val spec: List<VcsUpdateSpecification>,
|
||||
private val actionInfo: ActionInfo,
|
||||
private val actionName: @Nls @NlsContexts.ProgressTitle String
|
||||
private val actionName: @Nls @NlsContexts.ProgressTitle String,
|
||||
) : Task.Backgroundable(project, actionName, true) {
|
||||
private val projectLevelVcsManager = ProjectLevelVcsManagerEx.getInstanceEx(project)
|
||||
protected var updatedFiles: UpdatedFiles = UpdatedFiles.create()
|
||||
@@ -292,31 +146,25 @@ abstract class AbstractCommonUpdateAction protected constructor(
|
||||
}
|
||||
|
||||
private fun runImpl() {
|
||||
val project = myProject
|
||||
if (project != null) {
|
||||
StoreReloadManager.getInstance(project).blockReloadingProjectOnExternalChanges()
|
||||
}
|
||||
StoreReloadManager.getInstance(project).blockReloadingProjectOnExternalChanges()
|
||||
projectLevelVcsManager.startBackgroundVcsOperation()
|
||||
|
||||
val progressIndicator = ProgressManager.getInstance().getProgressIndicator()
|
||||
|
||||
before = LocalHistory.getInstance().putSystemLabel(project!!, VcsBundle.message("update.label.before.update"))
|
||||
before = LocalHistory.getInstance().putSystemLabel(project, VcsBundle.message("update.label.before.update"))
|
||||
localHistoryAction = LocalHistory.getInstance().startAction(VcsBundle.message("activity.name.update"), VcsActivity.Update)
|
||||
progressIndicator?.setIndeterminate(false)
|
||||
val activity = VcsStatisticsCollector.UPDATE_ACTIVITY.started(project)
|
||||
try {
|
||||
val toBeProcessed = vcsToVirtualFiles.size
|
||||
val toBeProcessed = spec.size
|
||||
var processed = 0
|
||||
for (vcs in vcsToVirtualFiles.keys) {
|
||||
val updateEnvironment = actionInfo.getEnvironment(vcs)
|
||||
for ((vcs, updateEnvironment, files) in spec) {
|
||||
updateEnvironment.fillGroups(updatedFiles)
|
||||
val files = vcsToVirtualFiles.get(vcs)!!
|
||||
|
||||
val context = contextInfo.get(vcs)
|
||||
val refContext = Ref<SequentialUpdatesContext>(context)
|
||||
|
||||
// actual update
|
||||
val updateSession = performUpdate(progressIndicator, updateEnvironment, files, refContext)
|
||||
val updateSession = updateEnvironment.updateDirectories(files.toTypedArray(), updatedFiles, progressIndicator, refContext)
|
||||
|
||||
contextInfo.put(vcs, refContext.get())
|
||||
processed++
|
||||
@@ -343,15 +191,6 @@ abstract class AbstractCommonUpdateAction protected constructor(
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun performUpdate(
|
||||
progressIndicator: ProgressIndicator?,
|
||||
updateEnvironment: UpdateEnvironment,
|
||||
files: Collection<FilePath>,
|
||||
refContext: Ref<SequentialUpdatesContext>
|
||||
): UpdateSession {
|
||||
return updateEnvironment.updateDirectories(files.toTypedArray(), updatedFiles, progressIndicator, refContext)
|
||||
}
|
||||
|
||||
private fun gatherExceptions(vcs: AbstractVcs, exceptionList: MutableList<VcsException>) {
|
||||
val fixer = vcs.vcsExceptionsHotFixer
|
||||
if (fixer == null) {
|
||||
@@ -389,7 +228,7 @@ abstract class AbstractCommonUpdateAction protected constructor(
|
||||
private fun prepareNotification(
|
||||
tree: UpdateInfoTree,
|
||||
someSessionWasCancelled: Boolean,
|
||||
updateSessions: List<UpdateSession>
|
||||
updateSessions: List<UpdateSession>,
|
||||
): Notification {
|
||||
val allFilesCount = updatedFiles.topLevelGroups.sumOf { getFilesCount(it) }
|
||||
|
||||
@@ -507,7 +346,7 @@ abstract class AbstractCommonUpdateAction protected constructor(
|
||||
.setDisplayId(VcsNotificationIdsHolder.PROJECT_UPDATE_FINISHED))
|
||||
}
|
||||
else if (!updatedFiles.isEmpty) {
|
||||
if (updateSessions.size == 1 && showsCustomNotification(vcsToVirtualFiles.keys)) {
|
||||
if (updateSessions.size == 1 && VcsUpdateProcess.checkUpdateHasCustomNotification(spec.map { it.vcs })) {
|
||||
// multi-vcs projects behave as before: only a compound notification & file tree is shown for them, for the sake of simplicity
|
||||
updateSessions.get(0).showNotification()
|
||||
}
|
||||
@@ -563,13 +402,13 @@ abstract class AbstractCommonUpdateAction protected constructor(
|
||||
val updateInfoTree = projectLevelVcsManager.showUpdateProjectInfo(updatedFiles, text, actionInfo, wasCanceled)!!
|
||||
updateInfoTree.setBefore(before)
|
||||
updateInfoTree.setAfter(after)
|
||||
updateInfoTree.setCanGroupByChangeList(canGroupByChangelist(vcsToVirtualFiles.keys))
|
||||
updateInfoTree.setCanGroupByChangeList(canGroupByChangelist(spec.map { it.vcs }))
|
||||
return updateInfoTree
|
||||
}
|
||||
|
||||
private fun canGroupByChangelist(abstractVcses: Set<AbstractVcs>): Boolean {
|
||||
private fun canGroupByChangelist(vcses: Collection<AbstractVcs>): Boolean {
|
||||
if (actionInfo.canGroupByChangelist()) {
|
||||
for (vcs in abstractVcses) {
|
||||
for (vcs in vcses) {
|
||||
if (vcs.getCachingCommittedChangesProvider() != null) {
|
||||
return true
|
||||
}
|
||||
@@ -598,7 +437,7 @@ private fun prepareScopeUpdatedText(tree: UpdateInfoTree): @Nls HtmlChunk {
|
||||
}
|
||||
}
|
||||
|
||||
private fun someSessionWasCanceled(updateSessions: List<UpdateSession>): Boolean = updateSessions.any { it.isCanceled() }
|
||||
private fun someSessionWasCanceled(updateSessions: List<UpdateSession>): Boolean = updateSessions.any { it.isCanceled() }
|
||||
|
||||
private fun getAllFilesAreUpToDateMessage(roots: Array<FilePath>): @NlsContexts.NotificationContent String {
|
||||
if (roots.size == 1 && !roots[0].isDirectory()) {
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
package com.intellij.openapi.vcs.update;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
@ApiStatus.Internal
|
||||
public class CommonUpdateProjectAction extends AbstractCommonUpdateAction {
|
||||
public CommonUpdateProjectAction() {
|
||||
super(ActionInfo.UPDATE, ScopeInfo.PROJECT, true);
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.openapi.vcs.update
|
||||
|
||||
import com.intellij.configurationStore.forPoorJavaClientOnlySaveProjectIndEdtDoNotUseThisMethod
|
||||
import com.intellij.openapi.actionSystem.DataContext
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.diagnostic.debug
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.openapi.options.Configurable
|
||||
import com.intellij.openapi.progress.EmptyProgressIndicator
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.vcs.AbstractVcs
|
||||
import com.intellij.openapi.vcs.FilePath
|
||||
import com.intellij.openapi.vcs.ProjectLevelVcsManager
|
||||
import com.intellij.openapi.vcs.actions.DescindingFilesFilter
|
||||
import com.intellij.openapi.vcs.update.AbstractCommonUpdateAction.Updater
|
||||
import com.intellij.openapi.vfs.VfsUtilCore
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.containers.SmartHashSet
|
||||
import com.intellij.vcsUtil.VcsUtil
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.Nls
|
||||
|
||||
@ApiStatus.Experimental
|
||||
object VcsUpdateProcess {
|
||||
@RequiresEdt
|
||||
fun launchUpdate(
|
||||
project: Project,
|
||||
actionInfo: ActionInfo,
|
||||
scopeInfo: ScopeInfo,
|
||||
context: DataContext,
|
||||
@Nls actionName: String,
|
||||
forceShowOptions: Boolean = false,
|
||||
) {
|
||||
val showUpdateOptions = actionInfo.showOptions(project) || forceShowOptions
|
||||
launchUpdate(project, actionInfo, scopeInfo, context, showUpdateOptions, actionName)
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
@RequiresEdt
|
||||
fun launchUpdate(
|
||||
project: Project,
|
||||
actionInfo: ActionInfo,
|
||||
scopeInfo: ScopeInfo,
|
||||
context: DataContext,
|
||||
showUpdateOptions: Boolean,
|
||||
@Nls actionName: String,
|
||||
@RequiresEdt onSuccess: () -> Unit = {},
|
||||
) {
|
||||
LOG.debug { "project: $project, show update options: $showUpdateOptions" }
|
||||
|
||||
val roots = getRoots(project, actionInfo, scopeInfo, context)
|
||||
if (roots.isEmpty()) {
|
||||
LOG.debug { "No roots found." }
|
||||
return
|
||||
}
|
||||
|
||||
val updateSpec = createUpdateSpec(project, roots, actionInfo)
|
||||
if (!isUpdateSpecValid(updateSpec)) {
|
||||
LOG.debug { "Options not valid for update spec: $updateSpec" }
|
||||
return
|
||||
}
|
||||
|
||||
if (showUpdateOptions) {
|
||||
val dialogOk = showOptionsDialog(project, actionInfo, scopeInfo, updateSpec, context)
|
||||
if (!dialogOk) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// could be called by external plugin via com.intellij.openapi.vcs.update.AbstractCommonUpdateAction.actionPerformed
|
||||
// and we have no guarantees about threading there
|
||||
if (ApplicationManager.getApplication().isDispatchThread()) {
|
||||
// Not only documents, but also project settings should be saved,
|
||||
// to ensure that if as a result of Update some project settings will be changed,
|
||||
// all local changes are saved in prior and do not overwrite remote changes.
|
||||
// Also, there is a chance that save during update can break it -
|
||||
// we do disable auto saving during update, but still, there is a chance that save will occur.
|
||||
FileDocumentManager.getInstance().saveAllDocuments()
|
||||
forPoorJavaClientOnlySaveProjectIndEdtDoNotUseThisMethod(project, false)
|
||||
}
|
||||
|
||||
launchUpdate(project, roots, updateSpec, actionInfo, actionName, onSuccess)
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
fun launchUpdate(
|
||||
project: Project,
|
||||
roots: Array<FilePath>,
|
||||
updateSpec: List<VcsUpdateSpecification>,
|
||||
actionInfo: ActionInfo,
|
||||
@Nls actionName: String,
|
||||
@RequiresEdt onSuccess: () -> Unit = {},
|
||||
) {
|
||||
object : Updater(project, roots, updateSpec, actionInfo, actionName) {
|
||||
override fun onSuccess() {
|
||||
super.onSuccess()
|
||||
onSuccess()
|
||||
}
|
||||
}.queue()
|
||||
}
|
||||
|
||||
@ApiStatus.Obsolete
|
||||
@ApiStatus.Internal
|
||||
@JvmStatic
|
||||
fun runUpdateBlocking(
|
||||
project: Project,
|
||||
roots: Array<FilePath>,
|
||||
updateSpec: List<VcsUpdateSpecification>,
|
||||
actionInfo: ActionInfo,
|
||||
@Nls actionName: String,
|
||||
) {
|
||||
Updater(project, roots, updateSpec, actionInfo, actionName).run(EmptyProgressIndicator())
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
@JvmStatic
|
||||
fun getRoots(
|
||||
project: Project,
|
||||
actionInfo: ActionInfo,
|
||||
scopeInfo: ScopeInfo,
|
||||
context: DataContext,
|
||||
filterDescending: Boolean = true,
|
||||
): Array<FilePath> {
|
||||
val filePaths = scopeInfo.getRoots(context, actionInfo)
|
||||
val filterExistsInVcs = scopeInfo.filterExistsInVcs()
|
||||
val roots = filterRoots(project, filePaths, filterExistsInVcs, actionInfo::getEnvironment)
|
||||
return if (filterDescending) {
|
||||
DescindingFilesFilter.filterDescindingFiles(roots, project)
|
||||
}
|
||||
else {
|
||||
roots
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterRoots(
|
||||
project: Project,
|
||||
roots: Collection<FilePath>,
|
||||
filterExistsInVcs: Boolean,
|
||||
updateEnvironmentSupplier: (AbstractVcs) -> UpdateEnvironment?,
|
||||
): Array<FilePath> {
|
||||
val result = ArrayList<FilePath>()
|
||||
for (file in roots) {
|
||||
val vcs = VcsUtil.getVcsFor(project, file) ?: continue
|
||||
if (!filterExistsInVcs || AbstractVcs.fileInVcsByFileStatus(project, file)) {
|
||||
val updateEnvironment = updateEnvironmentSupplier(vcs)
|
||||
if (updateEnvironment != null) {
|
||||
result.add(file)
|
||||
}
|
||||
}
|
||||
else {
|
||||
val virtualFile = file.getVirtualFile()
|
||||
if (virtualFile != null && virtualFile.isDirectory()) {
|
||||
val vcsRoots = ProjectLevelVcsManager.getInstance(project).getAllVersionedRoots()
|
||||
for (vcsRoot in vcsRoots) {
|
||||
if (VfsUtilCore.isAncestor(virtualFile, vcsRoot, false)) {
|
||||
result.add(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toTypedArray()
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
@JvmStatic
|
||||
fun createUpdateSpec(
|
||||
project: Project,
|
||||
roots: Array<FilePath>,
|
||||
actionInfo: ActionInfo,
|
||||
): List<VcsUpdateSpecification> {
|
||||
val resultPrep = mutableMapOf<AbstractVcs, MutableCollection<FilePath>>()
|
||||
for (file in roots) {
|
||||
val vcs = VcsUtil.getVcsFor(project, file) ?: continue
|
||||
resultPrep.getOrPut(vcs) { SmartHashSet() }.add(file)
|
||||
}
|
||||
return buildList {
|
||||
for ((vcs, roots) in resultPrep.entries) {
|
||||
val environment = actionInfo.getEnvironment(vcs) ?: continue
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val uniqueRoots = vcs.filterUniqueRoots(roots.toList(), FilePath::getVirtualFile)
|
||||
add(VcsUpdateSpecification(vcs, environment, uniqueRoots))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isUpdateSpecValid(spec: List<VcsUpdateSpecification>): Boolean {
|
||||
for ((_, updateEnvironment, roots) in spec) {
|
||||
if (!updateEnvironment.validateOptions(roots)) {
|
||||
// messages already shown
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
fun showOptionsDialog(
|
||||
project: Project,
|
||||
actionInfo: ActionInfo,
|
||||
scopeInfo: ScopeInfo,
|
||||
updateSpec: List<VcsUpdateSpecification>,
|
||||
dataContext: DataContext,
|
||||
): Boolean {
|
||||
val envToConfMap = createConfigurableToEnvMap(updateSpec)
|
||||
val scopeName = scopeInfo.getScopeName(dataContext, actionInfo)
|
||||
if (!envToConfMap.isEmpty()) {
|
||||
val dialogOrStatus = actionInfo.createOptionsDialog(project, envToConfMap, scopeName)
|
||||
return dialogOrStatus.showAndGet()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun createConfigurableToEnvMap(updateSpec: List<VcsUpdateSpecification>): LinkedHashMap<Configurable, AbstractVcs> {
|
||||
val envToConfMap = LinkedHashMap<Configurable, AbstractVcs>()
|
||||
for ((vcs, environment, roots) in updateSpec) {
|
||||
val configurable = environment.createConfigurable(roots)
|
||||
if (configurable != null) {
|
||||
envToConfMap[configurable] = vcs
|
||||
}
|
||||
}
|
||||
return envToConfMap
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun checkUpdateHasCustomNotification(vcss: Collection<AbstractVcs>): Boolean {
|
||||
return vcss.all { vcs ->
|
||||
val environment = vcs.updateEnvironment
|
||||
environment != null && environment.hasCustomNotification()
|
||||
}
|
||||
}
|
||||
|
||||
private val LOG = logger<VcsUpdateProcess>()
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
data class VcsUpdateSpecification(
|
||||
val vcs: AbstractVcs,
|
||||
val environment: UpdateEnvironment,
|
||||
val roots: Collection<FilePath>,
|
||||
)
|
||||
@@ -1,18 +1,17 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea.update
|
||||
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.progress.ProgressIndicator
|
||||
import com.intellij.openapi.progress.ProgressManager
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Ref
|
||||
import com.intellij.openapi.vcs.AbstractVcs
|
||||
import com.intellij.openapi.vcs.FilePath
|
||||
import com.intellij.openapi.vcs.VcsNotifier
|
||||
import com.intellij.openapi.vcs.update.*
|
||||
import com.intellij.util.containers.toArray
|
||||
import com.intellij.vcsUtil.VcsUtil
|
||||
import com.intellij.vcsUtil.VcsUtil.getFilePath
|
||||
import git4idea.GitNotificationIdsHolder.Companion.BRANCH_SET_UPSTREAM_ERROR
|
||||
import git4idea.GitNotificationIdsHolder.Companion.UPDATE_NOTHING_TO_UPDATE
|
||||
import git4idea.GitVcs
|
||||
import git4idea.branch.GitBranchPair
|
||||
import git4idea.commands.Git
|
||||
import git4idea.commands.GitCommand
|
||||
@@ -20,7 +19,6 @@ import git4idea.commands.GitLineHandler
|
||||
import git4idea.config.UpdateMethod
|
||||
import git4idea.i18n.GitBundle
|
||||
import git4idea.repo.GitRepository
|
||||
import git4idea.update.GitUpdateEnvironment.performUpdate
|
||||
|
||||
internal class GitUpdateExecutionProcess(
|
||||
private val project: Project,
|
||||
@@ -37,47 +35,26 @@ internal class GitUpdateExecutionProcess(
|
||||
return
|
||||
}
|
||||
|
||||
val vcsToRoots = getVcsRoots(repositories)
|
||||
val roots = vcsToRoots.values.flatten().toArray(emptyArray())
|
||||
val roots = repositories.map { getFilePath(it.root) }
|
||||
val gitUpdateEnvironment = project.service<GitUpdateEnvironment>()
|
||||
val updateEnvironment = object : UpdateEnvironment by gitUpdateEnvironment {
|
||||
override fun updateDirectories(contentRoots: Array<out FilePath>, updatedFiles: UpdatedFiles, progressIndicator: ProgressIndicator, context: Ref<SequentialUpdatesContext?>): UpdateSession {
|
||||
if (shouldSetAsUpstream) {
|
||||
updateConfig.forEach { (repository, branchPair) -> setBranchUpstream(repository, branchPair) }
|
||||
}
|
||||
|
||||
ProgressManager.getInstance()
|
||||
.run(UpdateExecution(
|
||||
project = project,
|
||||
vcsToRoots = vcsToRoots,
|
||||
roots = roots,
|
||||
updateConfig = updateConfig,
|
||||
updateMethod = updateMethod,
|
||||
shouldSetAsUpstream = shouldSetAsUpstream,
|
||||
))
|
||||
}
|
||||
return GitUpdateEnvironment.performUpdate(project, contentRoots, updatedFiles, progressIndicator, updateMethod, updateConfig)
|
||||
}
|
||||
|
||||
private fun getVcsRoots(repositories: Collection<GitRepository>): Map<AbstractVcs, Collection<FilePath>> {
|
||||
return repositories.associate { repo ->
|
||||
repo.vcs to listOf(VcsUtil.getFilePath(repo.root))
|
||||
override fun hasCustomNotification(): Boolean = gitUpdateEnvironment.hasCustomNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
val spec = VcsUpdateSpecification(GitVcs.getInstance(project), updateEnvironment, roots)
|
||||
|
||||
private class UpdateExecution(
|
||||
project: Project,
|
||||
vcsToRoots: Map<AbstractVcs, Collection<FilePath>>,
|
||||
private val roots: Array<FilePath>,
|
||||
private val updateConfig: Map<GitRepository, GitBranchPair>,
|
||||
private val updateMethod: UpdateMethod,
|
||||
private val shouldSetAsUpstream: Boolean = false,
|
||||
)
|
||||
: AbstractCommonUpdateAction.Updater(project, roots, vcsToRoots, ActionInfo.UPDATE, GitBundle.message("progress.title.update")) {
|
||||
override fun performUpdate(
|
||||
progressIndicator: ProgressIndicator?,
|
||||
updateEnvironment: UpdateEnvironment,
|
||||
files: Collection<FilePath>,
|
||||
refContext: Ref<SequentialUpdatesContext>,
|
||||
): UpdateSession {
|
||||
if (shouldSetAsUpstream) {
|
||||
updateConfig.forEach { (repository, branchPair) -> setBranchUpstream(repository, branchPair) }
|
||||
}
|
||||
|
||||
return performUpdate(project, roots, updatedFiles, progressIndicator, updateMethod, updateConfig)
|
||||
VcsUpdateProcess.launchUpdate(project,
|
||||
roots.toTypedArray(),
|
||||
listOf(spec),
|
||||
ActionInfo.UPDATE,
|
||||
GitBundle.message("progress.title.update"))
|
||||
}
|
||||
|
||||
private fun setBranchUpstream(repository: GitRepository, branchConfig: GitBranchPair) {
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
package org.jetbrains.idea.svn;
|
||||
|
||||
import com.intellij.execution.process.ProcessOutput;
|
||||
import com.intellij.openapi.actionSystem.ActionManager;
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent;
|
||||
import com.intellij.openapi.actionSystem.Presentation;
|
||||
import com.intellij.openapi.actionSystem.impl.SimpleDataContext;
|
||||
import com.intellij.openapi.application.PathManager;
|
||||
import com.intellij.openapi.command.undo.UndoManager;
|
||||
@@ -16,7 +13,9 @@ import com.intellij.openapi.vcs.*;
|
||||
import com.intellij.openapi.vcs.changes.*;
|
||||
import com.intellij.openapi.vcs.impl.ProjectLevelVcsManagerImpl;
|
||||
import com.intellij.openapi.vcs.rollback.RollbackProgressListener;
|
||||
import com.intellij.openapi.vcs.update.CommonUpdateProjectAction;
|
||||
import com.intellij.openapi.vcs.update.ActionInfo;
|
||||
import com.intellij.openapi.vcs.update.ScopeInfo;
|
||||
import com.intellij.openapi.vcs.update.VcsUpdateProcess;
|
||||
import com.intellij.openapi.vfs.VfsUtil;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.testFramework.ApplicationRule;
|
||||
@@ -440,11 +439,13 @@ public abstract class SvnTestCase extends AbstractJunitVcsTestCase {
|
||||
|
||||
protected void imitUpdate() {
|
||||
vcsManager.getOptions(VcsConfiguration.StandardOption.UPDATE).setValue(false);
|
||||
final CommonUpdateProjectAction action = new CommonUpdateProjectAction();
|
||||
action.getTemplatePresentation().setText("1");
|
||||
action
|
||||
.actionPerformed(new AnActionEvent(null, SimpleDataContext.getProjectContext(myProject), "test", new Presentation(), ActionManager.getInstance(), 0));
|
||||
var actionInfo = ActionInfo.UPDATE;
|
||||
var scopeInfo = ScopeInfo.PROJECT;
|
||||
var context = SimpleDataContext.getProjectContext(myProject);
|
||||
|
||||
var roots = VcsUpdateProcess.getRoots(myProject, actionInfo, scopeInfo, context, true);
|
||||
var updateSpec = VcsUpdateProcess.createUpdateSpec(myProject, roots, actionInfo);
|
||||
VcsUpdateProcess.runUpdateBlocking(myProject, roots, updateSpec, actionInfo, "1");
|
||||
waitChangesAndAnnotations();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user