From 5251541ffc8634f6b7fd758ee25c8c341580ed62 Mon Sep 17 00:00:00 2001 From: UNV Date: Tue, 18 Nov 2025 17:23:06 +0300 Subject: [PATCH 1/2] Localizing and refactoring (part 1). --- .../main/java/consulo/git/GitVcsFactory.java | 9 +- .../java/consulo/git/config/GitVcsPanel.java | 14 +- plugin/src/main/java/git4idea/GitTag.java | 122 +- plugin/src/main/java/git4idea/GitVcs.java | 86 +- .../java/git4idea/actions/BasicAction.java | 15 +- .../main/java/git4idea/actions/GitInit.java | 2 +- .../main/java/git4idea/actions/GitPull.java | 7 +- .../java/git4idea/branch/DeepComparator.java | 452 ++--- .../git4idea/branch/GitBranchOperation.java | 101 +- .../branch/GitBranchUiHandlerImpl.java | 183 +- .../java/git4idea/branch/GitBrancher.java | 2 +- .../branch/GitCheckoutNewBranchOperation.java | 237 +-- .../git4idea/branch/GitCheckoutOperation.java | 528 +++--- .../branch/GitDeleteBranchOperation.java | 124 +- .../GitDeleteRemoteBranchOperation.java | 379 +++-- .../git4idea/branch/GitMergeOperation.java | 794 +++++---- .../branch/GitRenameBranchOperation.java | 166 +- .../checkout/GitCheckoutProvider.java | 26 +- .../git4idea/checkout/GitCloneDialog.java | 337 +++- .../git4idea/cherrypick/GitCherryPicker.java | 1257 +++++++------- .../src/main/java/git4idea/commands/Git.java | 408 +++-- .../git4idea/commands/GitBinaryHandler.java | 302 ++-- .../java/git4idea/commands/GitCommand.java | 198 +-- .../git4idea/commands/GitCommandResult.java | 275 +-- .../git4idea/commands/GitCompoundResult.java | 103 +- .../java/git4idea/commands/GitHandler.java | 1501 ++++++++--------- .../git4idea/commands/GitHandlerListener.java | 3 +- .../git4idea/commands/GitHandlerUtil.java | 36 +- .../main/java/git4idea/commands/GitTask.java | 28 +- .../GitTaskResultNotificationHandler.java | 3 +- .../history/GitDiffFromHistoryHandler.java | 126 +- .../git4idea/history/GitHistoryUtils.java | 2 +- .../git4idea/merge/GitConflictResolver.java | 520 +++--- .../push/GitPushResultNotification.java | 89 +- .../rebase/GitAbortRebaseProcess.java | 336 ++-- .../git4idea/rebase/GitRebaseProcess.java | 1191 +++++++------ .../rebase/GitRebaseUnstructuredEditor.form | 56 - .../rebase/GitRebaseUnstructuredEditor.java | 137 ++ .../java/git4idea/rebase/GitRebaseUtils.java | 43 +- .../main/java/git4idea/rebase/GitRebaser.java | 50 +- .../git4idea/reset/GitResetOperation.java | 278 +-- .../git4idea/roots/GitIntegrationEnabler.java | 70 +- .../git4idea/stash/GitStashChangesSaver.java | 415 +++-- .../status/GitNewChangesCollector.java | 2 +- .../java/git4idea/update/GitFetchResult.java | 157 +- .../main/java/git4idea/update/GitFetcher.java | 47 +- .../java/git4idea/update/GitMergeUpdater.java | 90 +- .../git4idea/update/GitRebaseUpdater.java | 186 +- .../update/GitUpdateConfigurable.java | 2 + .../git4idea/update/GitUpdateEnvironment.java | 90 +- .../GitUpdateLocallyModifiedDialog.form | 71 - .../GitUpdateLocallyModifiedDialog.java | 235 ++- .../update/GitUpdateOptionsPanel.java | 57 +- .../git4idea/update/GitUpdateProcess.java | 745 ++++---- .../java/git4idea/update/GitUpdateResult.java | 55 +- .../git4idea/update/GitUpdateSession.java | 29 +- .../main/java/git4idea/update/GitUpdater.java | 388 ++--- .../git4idea/update/UpdatePolicyUtils.java | 89 +- .../main/java/git4idea/util/GitUIUtil.java | 91 +- .../util/GitUntrackedFilesHelper.java | 81 +- .../LocalChangesWouldBeOverwrittenHelper.java | 59 +- .../java/git4idea/util/StringScanner.java | 434 ++--- 62 files changed, 7401 insertions(+), 6518 deletions(-) delete mode 100644 plugin/src/main/java/git4idea/rebase/GitRebaseUnstructuredEditor.form delete mode 100644 plugin/src/main/java/git4idea/update/GitUpdateLocallyModifiedDialog.form diff --git a/plugin/src/main/java/consulo/git/GitVcsFactory.java b/plugin/src/main/java/consulo/git/GitVcsFactory.java index d7d79ed..3ffb611 100644 --- a/plugin/src/main/java/consulo/git/GitVcsFactory.java +++ b/plugin/src/main/java/consulo/git/GitVcsFactory.java @@ -4,6 +4,7 @@ import consulo.git.localize.GitLocalize; import consulo.localize.LocalizeValue; import consulo.project.Project; +import consulo.project.ui.notification.NotificationService; import consulo.versionControlSystem.AbstractVcs; import consulo.versionControlSystem.VcsFactory; import git4idea.GitVcs; @@ -35,6 +36,7 @@ public class GitVcsFactory implements VcsFactory { private final Provider myGitVcsApplicationSettings; private final Provider myGitVcsSettings; private final Provider myGitExecutableManager; + private final Provider myNotificationService; @Inject public GitVcsFactory( @@ -46,7 +48,8 @@ public GitVcsFactory( @Nonnull Provider gitRollbackEnvironment, @Nonnull Provider gitSettings, @Nonnull Provider gitProjectSettings, - @Nonnull Provider gitExecutableManager + @Nonnull Provider gitExecutableManager, + @Nonnull Provider notificationService ) { myProject = project; myGit = git; @@ -57,6 +60,7 @@ public GitVcsFactory( myGitVcsApplicationSettings = gitSettings; myGitVcsSettings = gitProjectSettings; myGitExecutableManager = gitExecutableManager; + myNotificationService = notificationService; } @Nonnull @@ -90,7 +94,8 @@ public AbstractVcs createVcs() { myGitRollbackEnvironment.get(), myGitVcsApplicationSettings.get(), myGitVcsSettings.get(), - myGitExecutableManager.get() + myGitExecutableManager.get(), + myNotificationService.get() ); } } diff --git a/plugin/src/main/java/consulo/git/config/GitVcsPanel.java b/plugin/src/main/java/consulo/git/config/GitVcsPanel.java index f8164c5..fa6e5d1 100644 --- a/plugin/src/main/java/consulo/git/config/GitVcsPanel.java +++ b/plugin/src/main/java/consulo/git/config/GitVcsPanel.java @@ -15,13 +15,13 @@ */ package consulo.git.config; -import consulo.application.AllIcons; import consulo.application.progress.Task; import consulo.disposer.Disposable; import consulo.fileChooser.FileChooserDescriptorFactory; import consulo.fileChooser.FileChooserTextBoxBuilder; import consulo.git.localize.GitLocalize; import consulo.localize.LocalizeValue; +import consulo.platform.base.icon.PlatformIconGroup; import consulo.process.cmd.ParametersListUtil; import consulo.project.Project; import consulo.ui.*; @@ -33,8 +33,8 @@ import consulo.ui.layout.VerticalLayout; import consulo.ui.util.LabeledBuilder; import consulo.util.collection.ContainerUtil; -import consulo.versionControlSystem.distributed.DvcsBundle; import consulo.versionControlSystem.distributed.branch.DvcsSyncSettings; +import consulo.versionControlSystem.distributed.localize.DistributedVcsLocalize; import git4idea.GitVcs; import git4idea.config.*; import git4idea.repo.GitRepositoryManager; @@ -80,7 +80,7 @@ public GitVcsPanel(@Nonnull Project project, @Nonnull Disposable uiDisposable) { myAutoUpdateIfPushRejected = CheckBox.create(GitLocalize.settingsAutoUpdateOnPushRejected()); myEnableForcePush = CheckBox.create(LocalizeValue.localizeTODO("Allow &force push")); - mySyncControl = CheckBox.create(DvcsBundle.message("sync.setting")); + mySyncControl = CheckBox.create(DistributedVcsLocalize.syncSetting()); myAutoCommitOnCherryPick = CheckBox.create(GitLocalize.settingsCommitAutomaticallyOnCherryPick()); myWarnAboutCrlf = CheckBox.create(GitLocalize.settingsCrlf()); myWarnAboutDetachedHead = CheckBox.create(GitLocalize.settingsDetachedHead()); @@ -101,12 +101,12 @@ public GitVcsPanel(@Nonnull Project project, @Nonnull Disposable uiDisposable) { Button testButton = Button.create(GitLocalize.cloneTest()); testButton.addClickListener(e -> testConnection()); - final GitRepositoryManager repositoryManager = GitRepositoryManager.getInstance(project); + GitRepositoryManager repositoryManager = GitRepositoryManager.getInstance(project); mySyncControl.setVisible(repositoryManager.moreThanOneRoot()); myProtectedBranchesLabel = Label.create(GitLocalize.settingsProtectedBranched()); myProtectedBranchesButton = TextBoxWithExpandAction.create( - AllIcons.Actions.ShowViewer, + PlatformIconGroup.actionsShow(), "Protected Branches", ParametersListUtil.COLON_LINE_PARSER, ParametersListUtil.COLON_LINE_JOINER @@ -137,7 +137,7 @@ public GitVcsPanel(@Nonnull Project project, @Nonnull Disposable uiDisposable) { */ @RequiredUIAccess private void testConnection() { - final String executable = getCurrentExecutablePath(); + String executable = getCurrentExecutablePath(); if (myAppSettings != null) { myAppSettings.setPathToGit(executable); } @@ -149,7 +149,7 @@ private void testConnection() { GitLocalize.cloneDialogCheckingGitVersion(), true, indicator -> { - final GitVersion version; + GitVersion version; try { version = GitVersion.identifyVersion(executable); } diff --git a/plugin/src/main/java/git4idea/GitTag.java b/plugin/src/main/java/git4idea/GitTag.java index b017a4f..1fd6c98 100644 --- a/plugin/src/main/java/git4idea/GitTag.java +++ b/plugin/src/main/java/git4idea/GitTag.java @@ -21,77 +21,83 @@ import git4idea.commands.GitCommand; import git4idea.commands.GitSimpleHandler; import jakarta.annotation.Nonnull; -import org.jetbrains.annotations.NonNls; import jakarta.annotation.Nullable; + import java.util.ArrayList; import java.util.Collection; +import java.util.List; /** * The tag reference object */ public class GitTag extends GitReference { - /** - * Prefix for tags ({@value}) - */ - @NonNls public static final String REFS_TAGS_PREFIX = "refs/tags/"; - - /** - * The constructor - * - * @param name the used name - */ - public GitTag(@Nonnull String name) { - super(name); - } + /** + * Prefix for tags ({@value}) + */ + public static final String REFS_TAGS_PREFIX = "refs/tags/"; - /** - * {@inheritDoc} - */ - @Nonnull - public String getFullName() { - return REFS_TAGS_PREFIX + myName; - } + /** + * The constructor + * + * @param name the used name + */ + public GitTag(@Nonnull String name) { + super(name); + } - /** - * List tags for the git root - * - * @param project the context - * @param root the git root - * @param tags the tag list - * @param containingCommit - * @throws VcsException if there is a problem with running git - */ - public static void listAsStrings(final Project project, final VirtualFile root, final Collection tags, - @Nullable final String containingCommit) throws VcsException { - GitSimpleHandler handler = new GitSimpleHandler(project, root, GitCommand.TAG); - handler.setSilent(true); - handler.addParameters("-l"); - if (containingCommit != null) { - handler.addParameters("--contains"); - handler.addParameters(containingCommit); + /** + * {@inheritDoc} + */ + @Nonnull + @Override + public String getFullName() { + return REFS_TAGS_PREFIX + myName; } - for (String line : handler.run().split("\n")) { - if (line.length() == 0) { - continue; - } - tags.add(new String(line)); + + /** + * List tags for the git root + * + * @param project the context + * @param root the git root + * @param tags the tag list + * @param containingCommit + * @throws VcsException if there is a problem with running git + */ + public static void listAsStrings( + Project project, + VirtualFile root, + Collection tags, + @Nullable String containingCommit + ) throws VcsException { + GitSimpleHandler handler = new GitSimpleHandler(project, root, GitCommand.TAG); + handler.setSilent(true); + handler.addParameters("-l"); + if (containingCommit != null) { + handler.addParameters("--contains"); + handler.addParameters(containingCommit); + } + for (String line : handler.run().split("\n")) { + if (line.length() == 0) { + continue; + } + tags.add(new String(line)); + } } - } - /** - * List tags for the git root - * - * @param project the context - * @param root the git root - * @param tags the tag list - * @throws VcsException if there is a problem with running git - */ - public static void list(final Project project, final VirtualFile root, final Collection tags) throws VcsException { - ArrayList temp = new ArrayList(); - listAsStrings(project, root, temp, null); - for (String t : temp) { - tags.add(new GitTag(t)); + /** + * List tags for the git root + * + * @param project the context + * @param root the git root + * @param tags the tag list + * @throws VcsException if there is a problem with running git + */ + public static void list(Project project, VirtualFile root, Collection tags) throws VcsException { + List temp = new ArrayList<>(); + listAsStrings(project, root, temp, null); + for (String t : temp) { + tags.add(new GitTag(t)); + } } - } } diff --git a/plugin/src/main/java/git4idea/GitVcs.java b/plugin/src/main/java/git4idea/GitVcs.java index 6beae96..62e3d13 100644 --- a/plugin/src/main/java/git4idea/GitVcs.java +++ b/plugin/src/main/java/git4idea/GitVcs.java @@ -18,20 +18,19 @@ import consulo.annotation.component.ComponentScope; import consulo.annotation.component.ServiceAPI; import consulo.annotation.component.ServiceImpl; -import consulo.application.Application; import consulo.application.progress.Task; import consulo.configurable.Configurable; import consulo.disposer.Disposer; import consulo.execution.ui.console.ConsoleViewContentType; import consulo.git.icon.GitIconGroup; import consulo.git.localize.GitLocalize; -import consulo.ide.ServiceManager; import consulo.ide.setting.ShowSettingsUtil; import consulo.localize.LocalizeValue; import consulo.logging.Logger; import consulo.platform.Platform; import consulo.project.Project; import consulo.project.ui.notification.Notification; +import consulo.project.ui.notification.NotificationService; import consulo.project.ui.notification.NotificationType; import consulo.project.ui.notification.event.NotificationListener; import consulo.ui.annotation.RequiredUIAccess; @@ -100,6 +99,7 @@ public class GitVcs extends AbstractVcs { private static final Logger log = Logger.getInstance(GitVcs.class); private static final VcsKey ourKey = createKey(NAME); + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("HH:mm:ss.SSS"); @Nullable private final ChangeProvider myChangeProvider; @@ -110,6 +110,7 @@ public class GitVcs extends AbstractVcs { private final GitAnnotationProvider myAnnotationProvider; private final DiffProvider myDiffProvider; private final VcsHistoryProvider myHistoryProvider; + private final NotificationService myNotificationService; @Nonnull private final Git myGit; private final GitVcsApplicationSettings myAppSettings; @@ -133,20 +134,21 @@ public static GitVcs getInstance(Project project) { if (project == null || project.isDisposed()) { return null; } - return (GitVcs)ProjectLevelVcsManager.getInstance(project).findVcsByName(NAME); + return (GitVcs) ProjectLevelVcsManager.getInstance(project).findVcsByName(NAME); } @Inject public GitVcs( @Nonnull Project project, @Nonnull Git git, - @Nonnull final GitAnnotationProvider gitAnnotationProvider, - @Nonnull final GitDiffProvider gitDiffProvider, - @Nonnull final GitHistoryProvider gitHistoryProvider, - @Nonnull final GitRollbackEnvironment gitRollbackEnvironment, - @Nonnull final GitVcsApplicationSettings gitSettings, - @Nonnull final GitVcsSettings gitProjectSettings, - @Nonnull GitExecutableManager gitExecutableManager + @Nonnull GitAnnotationProvider gitAnnotationProvider, + @Nonnull GitDiffProvider gitDiffProvider, + @Nonnull GitHistoryProvider gitHistoryProvider, + @Nonnull GitRollbackEnvironment gitRollbackEnvironment, + @Nonnull GitVcsApplicationSettings gitSettings, + @Nonnull GitVcsSettings gitProjectSettings, + @Nonnull GitExecutableManager gitExecutableManager, + @Nonnull NotificationService notificationService ) { super(project, NAME); myGit = git; @@ -165,6 +167,7 @@ public GitVcs( myTreeDiffProvider = new GitTreeDiffProvider(myProject); myCommitAndPushExecutor = myCheckinEnvironment != null ? new GitCommitAndPushExecutor(myCheckinEnvironment) : null; myExecutableValidator = new GitExecutableValidator(myProject); + myNotificationService = notificationService; } @@ -362,11 +365,11 @@ public void showErrors(@Nonnull List list, @Nonnull LocalizeValue StringBuilder buffer = new StringBuilder(); buffer.append("\n"); buffer.append(GitLocalize.errorListTitle(action)); - for (final VcsException exception : list) { + for (VcsException exception : list) { buffer.append("\n"); buffer.append(exception.getMessage()); } - final String msg = buffer.toString(); + String msg = buffer.toString(); UIUtil.invokeLaterIfNeeded(() -> Messages.showErrorDialog(myProject, msg, GitLocalize.errorDialogTitle().get())); } } @@ -375,10 +378,10 @@ public void showErrors(@Nonnull List list, @Nonnull LocalizeValue * Shows a plain message in the Version Control Console. */ public void showMessages(@Nonnull String message) { - if (message.length() == 0) { + if (message.isEmpty()) { return; } - showMessage(message, ConsoleViewContentType.NORMAL_OUTPUT); + showMessage(LocalizeValue.of(message), ConsoleViewContentType.NORMAL_OUTPUT); } /** @@ -391,23 +394,13 @@ private void showMessage(@Nonnull LocalizeValue message, @Nonnull ConsoleViewCon GitVcsConsoleWriter.getInstance(myProject).showMessage(message, contentType); } - /** - * Show message in the Version Control Console - * - * @param message a message to show - * @param contentType a style to use - */ - private void showMessage(@Nonnull String message, @Nonnull ConsoleViewContentType contentType) { - GitVcsConsoleWriter.getInstance(myProject).showMessage(message, contentType); - } - /** * Checks Git version and updates the myVersion variable. * In the case of exception or unsupported version reports the problem. * Note that unsupported version is also applied - some functionality might not work (we warn about that), but no need to disable at all. */ public void checkVersion() { - final String executable = myGitExecutableManager.getPathToGit(myProject); + String executable = myGitExecutableManager.getPathToGit(myProject); try { myVersion = GitVersion.identifyVersion(executable); if (!myVersion.isSupported()) { @@ -420,10 +413,10 @@ public void checkVersion() { myVersion, GitVersion.MIN ); - VcsNotifier.getInstance(myProject).notifyError( - "Unsupported Git version", - message, - new NotificationListener.Adapter() { + myNotificationService.newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Unsupported Git version")) + .content(LocalizeValue.localizeTODO(message)) + .optionalHyperlinkListener(new NotificationListener.Adapter() { @Override protected void hyperlinkActivated(@Nonnull Notification notification, @Nonnull HyperlinkEvent e) { if (SETTINGS_LINK.equals(e.getDescription())) { @@ -433,13 +426,13 @@ else if (UPDATE_LINK.equals(e.getDescription())) { Platform.current().openInBrowser("http://git-scm.com"); } } - } - ); + }) + .notify(myProject); } } catch (Exception e) { if (getExecutableValidator().checkExecutableAndNotifyIfNeeded()) { // check executable before notifying error - final String reason = (e.getCause() != null ? e.getCause() : e).getMessage(); + String reason = (e.getCause() != null ? e.getCause() : e).getMessage(); LocalizeValue message = GitLocalize.vcsUnableToRunGit(executable, reason); if (!myProject.isDefault()) { showMessage(message, ConsoleViewContentType.SYSTEM_OUTPUT); @@ -466,16 +459,15 @@ public GitVersion getVersion() { /** * Shows a command line message in the Version Control Console */ - public void showCommandLine(final String cmdLine) { - SimpleDateFormat f = new SimpleDateFormat("HH:mm:ss.SSS"); - showMessage(f.format(new Date()) + ": " + cmdLine, ConsoleViewContentType.SYSTEM_OUTPUT); + public void showCommandLine(String cmdLine) { + showMessage(LocalizeValue.of(DATE_FORMAT.format(new Date()) + ": " + cmdLine), ConsoleViewContentType.SYSTEM_OUTPUT); } /** * Shows error message in the Version Control Console */ - public void showErrorMessages(final String line) { - showMessage(line, ConsoleViewContentType.ERROR_OUTPUT); + public void showErrorMessages(String line) { + showMessage(LocalizeValue.of(line), ConsoleViewContentType.ERROR_OUTPUT); } @Override @@ -485,20 +477,20 @@ public boolean allowsNestedRoots() { @Nonnull @Override - public List filterUniqueRoots(@Nonnull List in, @Nonnull Function convertor) { - Collections.sort(in, Comparator.comparing(convertor, FilePathComparator.getInstance())); + public List filterUniqueRoots(@Nonnull List in, @Nonnull Function converter) { + Collections.sort(in, Comparator.comparing(converter, FilePathComparator.getInstance())); for (int i = 1; i < in.size(); i++) { - final S sChild = in.get(i); - final VirtualFile child = convertor.apply(sChild); - final VirtualFile childRoot = GitUtil.gitRootOrNull(child); + S sChild = in.get(i); + VirtualFile child = converter.apply(sChild); + VirtualFile childRoot = GitUtil.gitRootOrNull(child); if (childRoot == null) { // non-git file actually, skip it continue; } for (int j = i - 1; j >= 0; --j) { - final S sParent = in.get(j); - final VirtualFile parent = convertor.apply(sParent); + S sParent = in.get(j); + VirtualFile parent = converter.apply(sParent); // the method check both that parent is an ancestor of the child and that they share common git root if (VirtualFileUtil.isAncestor(parent, child, false) && VirtualFileUtil.isAncestor(childRoot, parent, false)) { in.remove(i); @@ -562,8 +554,8 @@ public boolean fileListenerIsSynchronous() { @Override @RequiredUIAccess public void enableIntegration() { - Application.get().executeOnPooledThread((Runnable)() -> { - Collection roots = ServiceManager.getService(myProject, VcsRootDetector.class).detect(); + myProject.getApplication().executeOnPooledThread((Runnable) () -> { + Collection roots = myProject.getInstance(VcsRootDetector.class).detect(); new GitIntegrationEnabler(GitVcs.this, myGit).enable(roots); }); } @@ -584,8 +576,8 @@ public boolean needsCaseSensitiveDirtyScope() { return true; } - @Override @Nonnull + @Override public VcsDirtyScopeBuilder createDirtyScope() { return new GitVcsDirtyScope(this, myProject); } diff --git a/plugin/src/main/java/git4idea/actions/BasicAction.java b/plugin/src/main/java/git4idea/actions/BasicAction.java index eaf91d7..d9f5a97 100644 --- a/plugin/src/main/java/git4idea/actions/BasicAction.java +++ b/plugin/src/main/java/git4idea/actions/BasicAction.java @@ -54,13 +54,12 @@ public abstract class BasicAction extends DumbAwareAction { @Override @RequiredUIAccess public void actionPerformed(@Nonnull AnActionEvent event) { - final Project project = event.getData(Project.KEY); - Application.get().runWriteAction(() -> FileDocumentManager.getInstance().saveAllDocuments()); - final VirtualFile[] vFiles = event.getData(VirtualFile.KEY_OF_ARRAY); + final Project project = event.getRequiredData(Project.KEY); + project.getApplication().runWriteAction(() -> FileDocumentManager.getInstance().saveAllDocuments()); + VirtualFile[] vFiles = event.getData(VirtualFile.KEY_OF_ARRAY); assert vFiles != null : "The action is only available when files are selected"; - assert project != null; - final GitVcs vcs = GitVcs.getInstance(project); + GitVcs vcs = GitVcs.getInstance(project); if (!ProjectLevelVcsManager.getInstance(project).checkAllFilesAreUnder(vcs, vFiles)) { return; } @@ -68,7 +67,7 @@ public void actionPerformed(@Nonnull AnActionEvent event) { final VirtualFile[] affectedFiles = collectAffectedFiles(project, vFiles); final List exceptions = new ArrayList<>(); - final boolean background = perform(project, vcs, exceptions, affectedFiles); + boolean background = perform(project, vcs, exceptions, affectedFiles); if (!background) { GitVcs.runInBackground(new Task.Backgroundable(project, actionName) { @Override @@ -176,14 +175,12 @@ protected boolean appliesTo(@Nonnull Project project, @Nonnull VirtualFile file) * @param e The update event */ @Override - @RequiredUIAccess public void update(@Nonnull AnActionEvent e) { super.update(e); Presentation presentation = e.getPresentation(); Project project = e.getData(Project.KEY); if (project == null) { - presentation.setEnabled(false); - presentation.setVisible(false); + presentation.setEnabledAndVisible(false); return; } diff --git a/plugin/src/main/java/git4idea/actions/GitInit.java b/plugin/src/main/java/git4idea/actions/GitInit.java index c253607..47f8586 100644 --- a/plugin/src/main/java/git4idea/actions/GitInit.java +++ b/plugin/src/main/java/git4idea/actions/GitInit.java @@ -98,7 +98,7 @@ private static void doInit(final Project project, FileChooserDescriptor fcd, Vir if (vcs != null && vcs.getExecutableValidator().checkExecutableAndNotifyIfNeeded()) { NotificationService.getInstance().newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) .title(LocalizeValue.localizeTODO("Git init failed")) - .content(LocalizeValue.localizeTODO(result.getErrorOutputAsHtmlString())) + .content(result.getErrorOutputAsHtmlValue()) .notify(project); } return; diff --git a/plugin/src/main/java/git4idea/actions/GitPull.java b/plugin/src/main/java/git4idea/actions/GitPull.java index 1708bbe..3bd29f2 100644 --- a/plugin/src/main/java/git4idea/actions/GitPull.java +++ b/plugin/src/main/java/git4idea/actions/GitPull.java @@ -130,7 +130,12 @@ protected void onSuccess() { @Override protected void onFailure() { - GitUIUtil.notifyGitErrors(project, "Error pulling " + dialog.getRemote(), "", handler.errors()); + GitUIUtil.notifyGitErrors( + project, + LocalizeValue.localizeTODO("Error pulling " + dialog.getRemote()), + LocalizeValue.empty(), + handler.errors() + ); repositoryManager.updateRepository(root); } } diff --git a/plugin/src/main/java/git4idea/branch/DeepComparator.java b/plugin/src/main/java/git4idea/branch/DeepComparator.java index 9a3ec23..094736a 100644 --- a/plugin/src/main/java/git4idea/branch/DeepComparator.java +++ b/plugin/src/main/java/git4idea/branch/DeepComparator.java @@ -15,13 +15,13 @@ */ package git4idea.branch; -import consulo.application.ApplicationManager; import consulo.application.progress.ProgressIndicator; import consulo.application.progress.Task; import consulo.disposer.Disposable; import consulo.disposer.Disposer; import consulo.logging.Logger; import consulo.project.Project; +import consulo.ui.annotation.RequiredUIAccess; import consulo.util.dataholder.Key; import consulo.versionControlSystem.VcsException; import consulo.versionControlSystem.VcsNotifier; @@ -44,257 +44,277 @@ import java.util.Set; public class DeepComparator implements VcsLogDeepComparator, Disposable { - private static final Logger LOG = Logger.getInstance(DeepComparator.class); - - @Nonnull - private final Project myProject; - @Nonnull - private final GitRepositoryManager myRepositoryManager; - @Nonnull - private final VcsLogUi myUi; - - @Nullable - private MyTask myTask; - @Nullable - private Set myNonPickedCommits; - - public DeepComparator(@Nonnull Project project, @Nonnull GitRepositoryManager manager, @Nonnull VcsLogUi ui, @Nonnull Disposable parent) { - myProject = project; - myRepositoryManager = manager; - myUi = ui; - Disposer.register(parent, this); - } - - @Override - public void highlightInBackground(@Nonnull String branchToCompare, @Nonnull VcsLogDataProvider dataProvider) { - if (myTask != null) { - LOG.error("Shouldn't be possible"); - return; - } + private static final Logger LOG = Logger.getInstance(DeepComparator.class); - Map repositories = getRepositories(myUi.getDataPack().getLogProviders(), branchToCompare); - if (repositories.isEmpty()) { - removeHighlighting(); - return; - } + @Nonnull + private final Project myProject; + @Nonnull + private final GitRepositoryManager myRepositoryManager; + @Nonnull + private final VcsLogUi myUi; - myTask = new MyTask(myProject, repositories, dataProvider, branchToCompare); - myTask.queue(); - } - - @Nonnull - private Map getRepositories(@Nonnull Map providers, - @Nonnull String branchToCompare) { - Map repos = new HashMap<>(); - for (VirtualFile root : providers.keySet()) { - GitRepository repository = myRepositoryManager.getRepositoryForRoot(root); - if (repository == null || repository.getCurrentBranch() == null || - repository.getBranches().findBranchByName(branchToCompare) == null) { - continue; - } - repos.put(repository, repository.getCurrentBranch()); - } - return repos; - } - - @Override - public void stopAndUnhighlight() { - stopTask(); - removeHighlighting(); - } - - private void stopTask() { - if (myTask != null) { - myTask.cancel(); - myTask = null; - } - } - - private void removeHighlighting() { - ApplicationManager.getApplication().assertIsDispatchThread(); - myNonPickedCommits = null; - } - - @Override - public void dispose() { - stopAndUnhighlight(); - } - - @Override - public boolean hasHighlightingOrInProgress() { - return myTask != null; - } - - public static DeepComparator getInstance(@Nonnull Project project, @Nonnull VcsLogUi logUi) { - return project.getInstance(DeepComparatorHolder.class).getInstance(logUi); - } - - @Nonnull - @Override - public VcsLogHighlighter.VcsCommitStyle getStyle(@Nonnull VcsShortCommitDetails commitDetails, boolean isSelected) { - if (myNonPickedCommits == null) { - return VcsCommitStyle.DEFAULT; - } - return VcsCommitStyleFactory.foreground(!myNonPickedCommits.contains(new CommitId(commitDetails.getId(), - commitDetails.getRoot())) ? COMMIT_FOREGROUND : null); - } - - @Override - public void update(@Nonnull VcsLogDataPack dataPack, boolean refreshHappened) { - if (myTask == null) { // no task in progress => not interested in refresh events - return; + @Nullable + private MyTask myTask; + @Nullable + private Set myNonPickedCommits; + + public DeepComparator( + @Nonnull Project project, + @Nonnull GitRepositoryManager manager, + @Nonnull VcsLogUi ui, + @Nonnull Disposable parent + ) { + myProject = project; + myRepositoryManager = manager; + myUi = ui; + Disposer.register(parent, this); } - if (refreshHappened) { - // collect data - String comparedBranch = myTask.myComparedBranch; - Map repositoriesWithCurrentBranches = myTask.myRepositoriesWithCurrentBranches; - VcsLogDataProvider provider = myTask.myProvider; + @Override + @RequiredUIAccess + public void highlightInBackground(@Nonnull String branchToCompare, @Nonnull VcsLogDataProvider dataProvider) { + if (myTask != null) { + LOG.error("Shouldn't be possible"); + return; + } - stopTask(); + Map repositories = getRepositories(myUi.getDataPack().getLogProviders(), branchToCompare); + if (repositories.isEmpty()) { + removeHighlighting(); + return; + } - // highlight again - Map repositories = getRepositories(dataPack.getLogProviders(), comparedBranch); - if (repositories.equals(repositoriesWithCurrentBranches)) { // but not if current branch changed - highlightInBackground(comparedBranch, provider); - } + myTask = new MyTask(myProject, repositories, dataProvider, branchToCompare); + myTask.queue(); } - else { - VcsLogBranchFilter branchFilter = dataPack.getFilters().getBranchFilter(); - if (branchFilter == null || !myTask.myComparedBranch.equals(VcsLogUtil.getSingleFilteredBranch( - branchFilter, - dataPack.getRefs()))) { - stopAndUnhighlight(); - } - } - } - public static class Factory implements VcsLogHighlighterFactory { @Nonnull - private static final String ID = "CHERRY_PICKED_COMMITS"; + private Map getRepositories( + @Nonnull Map providers, + @Nonnull String branchToCompare + ) { + Map repos = new HashMap<>(); + for (VirtualFile root : providers.keySet()) { + GitRepository repository = myRepositoryManager.getRepositoryForRoot(root); + if (repository == null || repository.getCurrentBranch() == null + || repository.getBranches().findBranchByName(branchToCompare) == null) { + continue; + } + repos.put(repository, repository.getCurrentBranch()); + } + return repos; + } - @Nonnull @Override - public VcsLogHighlighter createHighlighter(@Nonnull VcsLogData logDataManager, @Nonnull VcsLogUi logUi) { - return getInstance(logDataManager.getProject(), logUi); + @RequiredUIAccess + public void stopAndUnhighlight() { + stopTask(); + removeHighlighting(); } - @Nonnull - @Override - public String getId() { - return ID; + private void stopTask() { + if (myTask != null) { + myTask.cancel(); + myTask = null; + } } - @Nonnull - @Override - public String getTitle() { - return "Cherry Picked Commits"; + @RequiredUIAccess + private void removeHighlighting() { + myProject.getApplication().assertIsDispatchThread(); + myNonPickedCommits = null; } @Override - public boolean showMenuItem() { - return false; + @RequiredUIAccess + public void dispose() { + stopAndUnhighlight(); } - } - - private class MyTask extends Task.Backgroundable { - @Nonnull - private final Project myProject; - @Nonnull - private final Map myRepositoriesWithCurrentBranches; - @Nonnull - private final VcsLogDataProvider myProvider; - @Nonnull - private final String myComparedBranch; + @Override + public boolean hasHighlightingOrInProgress() { + return myTask != null; + } - @Nonnull - private final Set myCollectedNonPickedCommits = new HashSet<>(); - @Nullable - private VcsException myException; - private boolean myCancelled; - - public MyTask(@Nonnull Project project, - @Nonnull Map repositoriesWithCurrentBranches, - @Nonnull VcsLogDataProvider dataProvider, - @Nonnull String branchToCompare) { - super(project, "Comparing Branches..."); - myProject = project; - myRepositoriesWithCurrentBranches = repositoriesWithCurrentBranches; - myProvider = dataProvider; - myComparedBranch = branchToCompare; + public static DeepComparator getInstance(@Nonnull Project project, @Nonnull VcsLogUi logUi) { + return project.getInstance(DeepComparatorHolder.class).getInstance(logUi); } + @Nonnull @Override - public void run(@Nonnull ProgressIndicator indicator) { - try { - for (Map.Entry entry : myRepositoriesWithCurrentBranches.entrySet()) { - GitRepository repo = entry.getKey(); - GitBranch currentBranch = entry.getValue(); - myCollectedNonPickedCommits.addAll(getNonPickedCommitsFromGit(myProject, - repo.getRoot(), - currentBranch.getName(), - myComparedBranch)); + public VcsLogHighlighter.VcsCommitStyle getStyle(@Nonnull VcsShortCommitDetails commitDetails, boolean isSelected) { + if (myNonPickedCommits == null) { + return VcsCommitStyle.DEFAULT; } - } - catch (VcsException e) { - LOG.warn(e); - myException = e; - } + return VcsCommitStyleFactory.foreground( + !myNonPickedCommits.contains(new CommitId(commitDetails.getId(), commitDetails.getRoot())) ? COMMIT_FOREGROUND : null + ); } @Override - public void onSuccess() { - if (myCancelled) { - return; - } - - removeHighlighting(); - - if (myException != null) { - VcsNotifier.getInstance(myProject).notifyError("Couldn't compare with branch " + myComparedBranch, myException.getMessage()); - return; - } - myNonPickedCommits = myCollectedNonPickedCommits; + @RequiredUIAccess + public void update(@Nonnull VcsLogDataPack dataPack, boolean refreshHappened) { + if (myTask == null) { // no task in progress => not interested in refresh events + return; + } + + if (refreshHappened) { + // collect data + String comparedBranch = myTask.myComparedBranch; + Map repositoriesWithCurrentBranches = myTask.myRepositoriesWithCurrentBranches; + VcsLogDataProvider provider = myTask.myProvider; + + stopTask(); + + // highlight again + Map repositories = getRepositories(dataPack.getLogProviders(), comparedBranch); + if (repositories.equals(repositoriesWithCurrentBranches)) { // but not if current branch changed + highlightInBackground(comparedBranch, provider); + } + } + else { + VcsLogBranchFilter branchFilter = dataPack.getFilters().getBranchFilter(); + if (branchFilter == null || !myTask.myComparedBranch.equals(VcsLogUtil.getSingleFilteredBranch( + branchFilter, + dataPack.getRefs() + ))) { + stopAndUnhighlight(); + } + } } - public void cancel() { - myCancelled = true; + public static class Factory implements VcsLogHighlighterFactory { + @Nonnull + private static final String ID = "CHERRY_PICKED_COMMITS"; + + @Nonnull + @Override + public VcsLogHighlighter createHighlighter(@Nonnull VcsLogData logDataManager, @Nonnull VcsLogUi logUi) { + return getInstance(logDataManager.getProject(), logUi); + } + + @Nonnull + @Override + public String getId() { + return ID; + } + + @Nonnull + @Override + public String getTitle() { + return "Cherry Picked Commits"; + } + + @Override + public boolean showMenuItem() { + return false; + } } - @Nonnull - private Set getNonPickedCommitsFromGit(@Nonnull Project project, - @Nonnull final VirtualFile root, - @Nonnull String currentBranch, - @Nonnull String comparedBranch) throws VcsException { - GitLineHandler handler = new GitLineHandler(project, root, GitCommand.CHERRY); - handler.addParameters(currentBranch, comparedBranch); // upstream - current branch; head - compared branch - - final Set pickedCommits = new HashSet<>(); - handler.addLineListener(new GitLineHandlerAdapter() { + private class MyTask extends Task.Backgroundable { + @Nonnull + private final Project myProject; + @Nonnull + private final Map myRepositoriesWithCurrentBranches; + @Nonnull + private final VcsLogDataProvider myProvider; + @Nonnull + private final String myComparedBranch; + + @Nonnull + private final Set myCollectedNonPickedCommits = new HashSet<>(); + @Nullable + private VcsException myException; + private boolean myCancelled; + + public MyTask( + @Nonnull Project project, + @Nonnull Map repositoriesWithCurrentBranches, + @Nonnull VcsLogDataProvider dataProvider, + @Nonnull String branchToCompare + ) { + super(project, "Comparing Branches..."); + myProject = project; + myRepositoriesWithCurrentBranches = repositoriesWithCurrentBranches; + myProvider = dataProvider; + myComparedBranch = branchToCompare; + } + @Override - public void onLineAvailable(String line, Key outputType) { - // + 645caac042ff7fb1a5e3f7d348f00e9ceea5c317 - // - c3b9b90f6c26affd7e597ebf65db96de8f7e5860 - if (line.startsWith("+")) { + public void run(@Nonnull ProgressIndicator indicator) { try { - line = line.substring(2).trim(); - int firstSpace = line.indexOf(' '); - if (firstSpace > 0) { - line = line.substring(0, firstSpace); // safety-check: take just the first word for sure - } - Hash hash = HashImpl.build(line); - pickedCommits.add(new CommitId(hash, root)); + for (Map.Entry entry : myRepositoriesWithCurrentBranches.entrySet()) { + GitRepository repo = entry.getKey(); + GitBranch currentBranch = entry.getValue(); + myCollectedNonPickedCommits.addAll(getNonPickedCommitsFromGit( + myProject, + repo.getRoot(), + currentBranch.getName(), + myComparedBranch + )); + } } - catch (Exception e) { - LOG.error("Couldn't parse line [" + line + "]"); + catch (VcsException e) { + LOG.warn(e); + myException = e; } - } } - }); - handler.runInCurrentThread(null); - return pickedCommits; - } - } + @Override + @RequiredUIAccess + public void onSuccess() { + if (myCancelled) { + return; + } + + removeHighlighting(); + + if (myException != null) { + VcsNotifier.getInstance(myProject) + .notifyError("Couldn't compare with branch " + myComparedBranch, myException.getMessage()); + return; + } + myNonPickedCommits = myCollectedNonPickedCommits; + } + + public void cancel() { + myCancelled = true; + } + + @Nonnull + private Set getNonPickedCommitsFromGit( + @Nonnull Project project, + @Nonnull final VirtualFile root, + @Nonnull String currentBranch, + @Nonnull String comparedBranch + ) throws VcsException { + GitLineHandler handler = new GitLineHandler(project, root, GitCommand.CHERRY); + handler.addParameters(currentBranch, comparedBranch); // upstream - current branch; head - compared branch + + final Set pickedCommits = new HashSet<>(); + handler.addLineListener(new GitLineHandlerAdapter() { + @Override + public void onLineAvailable(String line, Key outputType) { + // + 645caac042ff7fb1a5e3f7d348f00e9ceea5c317 + // - c3b9b90f6c26affd7e597ebf65db96de8f7e5860 + if (line.startsWith("+")) { + try { + line = line.substring(2).trim(); + int firstSpace = line.indexOf(' '); + if (firstSpace > 0) { + line = line.substring(0, firstSpace); // safety-check: take just the first word for sure + } + Hash hash = HashImpl.build(line); + pickedCommits.add(new CommitId(hash, root)); + } + catch (Exception e) { + LOG.error("Couldn't parse line [" + line + "]"); + } + } + } + }); + handler.runInCurrentThread(null); + return pickedCommits; + } + } } \ No newline at end of file diff --git a/plugin/src/main/java/git4idea/branch/GitBranchOperation.java b/plugin/src/main/java/git4idea/branch/GitBranchOperation.java index f376231..c3e0bee 100644 --- a/plugin/src/main/java/git4idea/branch/GitBranchOperation.java +++ b/plugin/src/main/java/git4idea/branch/GitBranchOperation.java @@ -16,11 +16,13 @@ package git4idea.branch; import consulo.application.Application; -import consulo.application.ApplicationManager; import consulo.application.progress.ProgressIndicator; import consulo.document.FileDocumentManager; +import consulo.localize.LocalizeValue; import consulo.logging.Logger; import consulo.project.Project; +import consulo.project.ui.notification.NotificationService; +import consulo.ui.annotation.RequiredUIAccess; import consulo.util.collection.ContainerUtil; import consulo.util.collection.MultiMap; import consulo.util.lang.Pair; @@ -54,6 +56,8 @@ abstract class GitBranchOperation { @Nonnull protected final Project myProject; @Nonnull + protected final NotificationService myNotificationService; + @Nonnull protected final Git myGit; @Nonnull protected final GitBranchUiHandler myUiHandler; @@ -73,12 +77,21 @@ abstract class GitBranchOperation { @Nonnull private final Collection myRemainingRepositories; - protected GitBranchOperation(@Nonnull Project project, @Nonnull Git git, @Nonnull GitBranchUiHandler uiHandler, @Nonnull Collection repositories) { + protected GitBranchOperation( + @Nonnull Project project, + @Nonnull Git git, + @Nonnull GitBranchUiHandler uiHandler, + @Nonnull Collection repositories + ) { myProject = project; + myNotificationService = NotificationService.getInstance(); myGit = git; myUiHandler = uiHandler; myRepositories = repositories; - myCurrentHeads = ContainerUtil.map2Map(repositories, it -> Pair.create(it, chooseNotNull(it.getCurrentBranchName(), it.getCurrentRevision()))); + myCurrentHeads = ContainerUtil.map2Map( + repositories, + it -> Pair.create(it, chooseNotNull(it.getCurrentBranchName(), it.getCurrentRevision())) + ); myInitialRevisions = ContainerUtil.map2Map(repositories, it -> Pair.create(it, it.getCurrentRevision())); mySuccessfulRepositories = new ArrayList<>(); mySkippedRepositories = new ArrayList<>(); @@ -91,7 +104,7 @@ protected GitBranchOperation(@Nonnull Project project, @Nonnull Git git, @Nonnul protected abstract void rollback(); @Nonnull - public abstract String getSuccessMessage(); + public abstract LocalizeValue getSuccessMessage(); @Nonnull protected abstract String getRollbackProposal(); @@ -176,28 +189,32 @@ protected Collection getRemainingRepositories() { } @Nonnull - protected List getRemainingRepositoriesExceptGiven(@Nonnull final GitRepository currentRepository) { + protected List getRemainingRepositoriesExceptGiven(@Nonnull GitRepository currentRepository) { List repositories = new ArrayList<>(myRemainingRepositories); repositories.remove(currentRepository); return repositories; } - protected void notifySuccess(@Nonnull String message) { - VcsNotifier.getInstance(myProject).notifySuccess(message); + protected void notifySuccess(@Nonnull LocalizeValue message) { + myNotificationService.newInfo(VcsNotifier.NOTIFICATION_GROUP_ID) + .content(message) + .notify(myProject); } protected void notifySuccess() { notifySuccess(getSuccessMessage()); } + @RequiredUIAccess protected final void saveAllDocuments() { - ApplicationManager.getApplication().invokeAndWait(() -> FileDocumentManager.getInstance().saveAllDocuments(), Application.get().getDefaultModalityState()); + Application application = myProject.getApplication(); + application.invokeAndWait(() -> FileDocumentManager.getInstance().saveAllDocuments(), application.getDefaultModalityState()); } /** * Show fatal error as a notification or as a dialog with rollback proposal. */ - protected void fatalError(@Nonnull String title, @Nonnull String message) { + protected void fatalError(@Nonnull LocalizeValue title, @Nonnull LocalizeValue message) { if (wereSuccessful()) { showFatalErrorDialogWithRollback(title, message); } @@ -206,19 +223,22 @@ protected void fatalError(@Nonnull String title, @Nonnull String message) { } } - protected void showFatalErrorDialogWithRollback(@Nonnull final String title, @Nonnull final String message) { - boolean rollback = myUiHandler.notifyErrorWithRollbackProposal(title, message, getRollbackProposal()); + protected void showFatalErrorDialogWithRollback(@Nonnull LocalizeValue title, @Nonnull LocalizeValue message) { + boolean rollback = myUiHandler.notifyErrorWithRollbackProposal(title.get(), message.get(), getRollbackProposal()); if (rollback) { rollback(); } } - protected void showFatalNotification(@Nonnull String title, @Nonnull String message) { + protected void showFatalNotification(@Nonnull LocalizeValue title, @Nonnull LocalizeValue message) { notifyError(title, message); } - protected void notifyError(@Nonnull String title, @Nonnull String message) { - VcsNotifier.getInstance(myProject).notifyError(title, message); + protected void notifyError(@Nonnull LocalizeValue title, @Nonnull LocalizeValue message) { + myNotificationService.newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(title) + .content(message) + .notify(myProject); } @Nonnull @@ -310,9 +330,9 @@ protected void refreshRoot(@Nonnull GitRepository repository) { } protected void fatalLocalChangesError(@Nonnull String reference) { - String title = String.format("Couldn't %s %s", getOperationName(), reference); + LocalizeValue title = LocalizeValue.localizeTODO(String.format("Couldn't %s %s", getOperationName(), reference)); if (wereSuccessful()) { - showFatalErrorDialogWithRollback(title, ""); + showFatalErrorDialogWithRollback(title, LocalizeValue.empty()); } } @@ -348,7 +368,11 @@ private void showUntrackedFilesDialogWithRollback(@Nonnull VirtualFile root, @No * local changes. */ @Nonnull - Map> collectLocalChangesConflictingWithBranch(@Nonnull Collection repositories, @Nonnull String currentBranch, @Nonnull String otherBranch) { + Map> collectLocalChangesConflictingWithBranch( + @Nonnull Collection repositories, + @Nonnull String currentBranch, + @Nonnull String otherBranch + ) { Map> changes = new HashMap<>(); for (GitRepository repository : repositories) { try { @@ -361,7 +385,10 @@ Map> collectLocalChangesConflictingWithBranch(@Nonnu catch (VcsException e) { // ignoring the exception: this is not fatal if we won't collect such a diff from other repositories. // At worst, use will get double dialog proposing the smart checkout. - LOG.warn(String.format("Couldn't collect diff between %s and %s in %s", currentBranch, otherBranch, repository.getRoot()), e); + LOG.warn( + String.format("Couldn't collect diff between %s and %s in %s", currentBranch, otherBranch, repository.getRoot()), + e + ); } } return changes; @@ -380,17 +407,26 @@ Map> collectLocalChangesConflictingWithBranch(@Nonnu * @return Repositories that have failed or would fail with the "local changes" error, together with these local changes. */ @Nonnull - protected Pair, List> getConflictingRepositoriesAndAffectedChanges(@Nonnull GitRepository currentRepository, - @Nonnull GitMessageWithFilesDetector localChangesOverwrittenBy, - String currentBranch, - String nextBranch) { - + protected Pair, List> getConflictingRepositoriesAndAffectedChanges( + @Nonnull GitRepository currentRepository, + @Nonnull GitMessageWithFilesDetector localChangesOverwrittenBy, + String currentBranch, + String nextBranch + ) { // get changes overwritten by checkout from the error message captured from Git - List affectedChanges = GitUtil.findLocalChangesForPaths(myProject, currentRepository.getRoot(), localChangesOverwrittenBy.getRelativeFilePaths(), true); + List affectedChanges = GitUtil.findLocalChangesForPaths( + myProject, + currentRepository.getRoot(), + localChangesOverwrittenBy.getRelativeFilePaths(), + true + ); // get all other conflicting changes // get changes in all other repositories (except those which already have succeeded) to avoid multiple dialogs proposing smart checkout - Map> conflictingChangesInRepositories = collectLocalChangesConflictingWithBranch(getRemainingRepositoriesExceptGiven(currentRepository), currentBranch, - nextBranch); + Map> conflictingChangesInRepositories = collectLocalChangesConflictingWithBranch( + getRemainingRepositoriesExceptGiven(currentRepository), + currentBranch, + nextBranch + ); Set otherProblematicRepositories = conflictingChangesInRepositories.keySet(); List allConflictingRepositories = new ArrayList<>(otherProblematicRepositories); @@ -408,11 +444,14 @@ protected static String stringifyBranchesByRepos(@Nonnull Map - { - String roots = StringUtil.join(entry.getValue(), file -> file.getName(), ", "); - return entry.getKey() + " (in " + roots + ")"; - }, "
"); + return StringUtil.join( + grouped.entrySet(), + entry -> { + String roots = StringUtil.join(entry.getValue(), VirtualFile::getName, ", "); + return entry.getKey() + " (in " + roots + ")"; + }, + "
" + ); } @Nonnull diff --git a/plugin/src/main/java/git4idea/branch/GitBranchUiHandlerImpl.java b/plugin/src/main/java/git4idea/branch/GitBranchUiHandlerImpl.java index c430a98..df4ba2d 100644 --- a/plugin/src/main/java/git4idea/branch/GitBranchUiHandlerImpl.java +++ b/plugin/src/main/java/git4idea/branch/GitBranchUiHandlerImpl.java @@ -16,11 +16,11 @@ package git4idea.branch; import consulo.application.Application; -import consulo.application.ApplicationManager; import consulo.application.progress.ProgressIndicator; +import consulo.localize.LocalizeValue; import consulo.project.Project; -import consulo.project.ui.notification.Notification; -import consulo.project.ui.notification.event.NotificationListener; +import consulo.project.ui.notification.NotificationService; +import consulo.ui.annotation.RequiredUIAccess; import consulo.ui.ex.awt.Messages; import consulo.ui.ex.awt.UIUtil; import consulo.util.lang.StringUtil; @@ -48,97 +48,113 @@ import java.util.concurrent.atomic.AtomicBoolean; public class GitBranchUiHandlerImpl implements GitBranchUiHandler { - @Nonnull private final Project myProject; @Nonnull + protected final NotificationService myNotificationService; + @Nonnull private final Git myGit; @Nonnull private final ProgressIndicator myProgressIndicator; public GitBranchUiHandlerImpl(@Nonnull Project project, @Nonnull Git git, @Nonnull ProgressIndicator indicator) { myProject = project; + myNotificationService = NotificationService.getInstance(); myGit = git; myProgressIndicator = indicator; } @Override - public boolean notifyErrorWithRollbackProposal(@Nonnull final String title, - @Nonnull final String message, - @Nonnull final String rollbackProposal) { - final AtomicBoolean ok = new AtomicBoolean(); - UIUtil.invokeAndWaitIfNeeded(new Runnable() { - @Override - public void run() { - StringBuilder description = new StringBuilder(); - if (!StringUtil.isEmptyOrSpaces(message)) { - description.append(message).append("
"); - } - description.append(rollbackProposal); - ok.set(Messages.YES == DialogManager.showOkCancelDialog(myProject, - XmlStringUtil.wrapInHtml(description), - title, - "Rollback", - "Don't rollback", - Messages.getErrorIcon())); + public boolean notifyErrorWithRollbackProposal( + @Nonnull String title, + @Nonnull String message, + @Nonnull String rollbackProposal + ) { + AtomicBoolean ok = new AtomicBoolean(); + UIUtil.invokeAndWaitIfNeeded((Runnable) () -> { + StringBuilder description = new StringBuilder(); + if (!StringUtil.isEmptyOrSpaces(message)) { + description.append(message).append("
"); } + description.append(rollbackProposal); + ok.set(Messages.YES == DialogManager.showOkCancelDialog( + myProject, + XmlStringUtil.wrapInHtml(description), + title, + "Rollback", + "Don't rollback", + UIUtil.getErrorIcon() + )); }); return ok.get(); } @Override - public void showUnmergedFilesNotification(@Nonnull final String operationName, @Nonnull final Collection repositories) { - String title = unmergedFilesErrorTitle(operationName); - String description = unmergedFilesErrorNotificationDescription(operationName); - VcsNotifier.getInstance(myProject).notifyError(title, description, new NotificationListener() { - @Override - public void hyperlinkUpdate(@Nonnull Notification notification, @Nonnull HyperlinkEvent event) { + public void showUnmergedFilesNotification(@Nonnull String operationName, @Nonnull Collection repositories) { + myNotificationService.newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(unmergedFilesErrorTitle(operationName)) + .content(unmergedFilesErrorNotificationDescription(operationName)) + .optionalHyperlinkListener((notification, event) -> { if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED && event.getDescription().equals("resolve")) { - GitConflictResolver.Params params = new GitConflictResolver.Params(). - setMergeDescription(String.format( + GitConflictResolver.Params params = new GitConflictResolver.Params() + .setMergeDescription(String.format( "The following files have unresolved conflicts. You need to resolve them before %s.", - operationName)). - setErrorNotificationTitle("Unresolved files remain."); + operationName + )) + .setErrorNotificationTitle("Unresolved files remain."); new GitConflictResolver(myProject, myGit, GitUtil.getRootsFromRepositories(repositories), params).merge(); } - } - }); + }) + .notify(myProject); } @Override - public boolean showUnmergedFilesMessageWithRollback(@Nonnull final String operationName, @Nonnull final String rollbackProposal) { - final AtomicBoolean ok = new AtomicBoolean(); - UIUtil.invokeAndWaitIfNeeded(new Runnable() { - @Override - public void run() { - String description = - String.format("You have to resolve all merge conflicts before %s.
%s", operationName, rollbackProposal); - // suppressing: this message looks ugly if capitalized by words - //noinspection DialogTitleCapitalization - ok.set(Messages.YES == DialogManager.showOkCancelDialog(myProject, - description, - unmergedFilesErrorTitle(operationName), - "Rollback", - "Don't rollback", - Messages.getErrorIcon())); - } + public boolean showUnmergedFilesMessageWithRollback(@Nonnull String operationName, @Nonnull String rollbackProposal) { + AtomicBoolean ok = new AtomicBoolean(); + UIUtil.invokeAndWaitIfNeeded((Runnable) () -> { + String description = String.format( + "You have to resolve all merge conflicts before %s.
%s", + operationName, + rollbackProposal + ); + // suppressing: this message looks ugly if capitalized by words + //noinspection DialogTitleCapitalization + ok.set(Messages.YES == DialogManager.showOkCancelDialog( + myProject, + description, + unmergedFilesErrorTitle(operationName).get(), + "Rollback", + "Don't rollback", + UIUtil.getErrorIcon() + )); }); return ok.get(); } @Override - public void showUntrackedFilesNotification(@Nonnull String operationName, - @Nonnull VirtualFile root, - @Nonnull Collection relativePaths) { + public void showUntrackedFilesNotification( + @Nonnull String operationName, + @Nonnull VirtualFile root, + @Nonnull Collection relativePaths + ) { GitUntrackedFilesHelper.notifyUntrackedFilesOverwrittenBy(myProject, root, relativePaths, operationName, null); } @Override - public boolean showUntrackedFilesDialogWithRollback(@Nonnull String operationName, - @Nonnull final String rollbackProposal, - @Nonnull VirtualFile root, - @Nonnull final Collection relativePaths) { - return GitUntrackedFilesHelper.showUntrackedFilesDialogWithRollback(myProject, operationName, rollbackProposal, root, relativePaths); + @RequiredUIAccess + public boolean showUntrackedFilesDialogWithRollback( + @Nonnull String operationName, + @Nonnull String rollbackProposal, + @Nonnull VirtualFile root, + @Nonnull Collection relativePaths + ) { + return GitUntrackedFilesHelper.showUntrackedFilesDialogWithRollback( + myProject, + operationName, + rollbackProposal, + root, + relativePaths + ); } @Nonnull @@ -148,11 +164,13 @@ public ProgressIndicator getProgressIndicator() { } @Override - public int showSmartOperationDialog(@Nonnull Project project, - @Nonnull List changes, - @Nonnull Collection paths, - @Nonnull String operation, - @Nullable String forceButtonTitle) { + public int showSmartOperationDialog( + @Nonnull Project project, + @Nonnull List changes, + @Nonnull Collection paths, + @Nonnull String operation, + @Nullable String forceButtonTitle + ) { JComponent fileBrowser; if (!changes.isEmpty()) { ChangesBrowserFactory browserFactory = project.getApplication().getInstance(ChangesBrowserFactory.class); @@ -165,28 +183,37 @@ public int showSmartOperationDialog(@Nonnull Project project, } @Override - public boolean showBranchIsNotFullyMergedDialog(@Nonnull Project project, - @Nonnull Map> history, - @Nonnull Map baseBranches, - @Nonnull String removedBranch) { + @RequiredUIAccess + public boolean showBranchIsNotFullyMergedDialog( + @Nonnull Project project, + @Nonnull Map> history, + @Nonnull Map baseBranches, + @Nonnull String removedBranch + ) { AtomicBoolean restore = new AtomicBoolean(); - ApplicationManager.getApplication() - .invokeAndWait(() -> restore.set(GitBranchIsNotFullyMergedDialog.showAndGetAnswer(myProject, - history, - baseBranches, - removedBranch)), - Application.get().getDefaultModalityState()); + Application application = myProject.getApplication(); + application.invokeAndWait( + () -> restore.set(GitBranchIsNotFullyMergedDialog.showAndGetAnswer( + myProject, + history, + baseBranches, + removedBranch + )), + application.getDefaultModalityState() + ); return restore.get(); } @Nonnull - private static String unmergedFilesErrorTitle(@Nonnull String operationName) { - return "Can't " + operationName + " because of unmerged files"; + private static LocalizeValue unmergedFilesErrorTitle(@Nonnull String operationName) { + return LocalizeValue.localizeTODO("Can't " + operationName + " because of unmerged files"); } @Nonnull - private static String unmergedFilesErrorNotificationDescription(String operationName) { - return "You have to resolve all merge conflicts before " + operationName + ".
" + - "After resolving conflicts you also probably would want to commit your files to the current branch."; + private static LocalizeValue unmergedFilesErrorNotificationDescription(String operationName) { + return LocalizeValue.localizeTODO( + "You have to resolve all merge conflicts before " + operationName + ".
" + + "After resolving conflicts you also probably would want to commit your files to the current branch." + ); } } diff --git a/plugin/src/main/java/git4idea/branch/GitBrancher.java b/plugin/src/main/java/git4idea/branch/GitBrancher.java index 1c926e8..92599aa 100644 --- a/plugin/src/main/java/git4idea/branch/GitBrancher.java +++ b/plugin/src/main/java/git4idea/branch/GitBrancher.java @@ -80,7 +80,7 @@ void createNewTag(@Nonnull String name, /** * Creates and checks out a new local branch starting from the given reference: - * {@code git checkout -b }.
+ * {@code git checkout -b }.
* Provides the "smart checkout" procedure the same as in {@link #checkout(String, boolean, List, Runnable)}. * * @param newBranchName the name of the new local branch. diff --git a/plugin/src/main/java/git4idea/branch/GitCheckoutNewBranchOperation.java b/plugin/src/main/java/git4idea/branch/GitCheckoutNewBranchOperation.java index 0d77d5e..d642bd3 100644 --- a/plugin/src/main/java/git4idea/branch/GitCheckoutNewBranchOperation.java +++ b/plugin/src/main/java/git4idea/branch/GitCheckoutNewBranchOperation.java @@ -15,7 +15,9 @@ */ package git4idea.branch; +import consulo.localize.LocalizeValue; import consulo.project.Project; +import consulo.project.ui.notification.NotificationService; import consulo.util.lang.StringUtil; import consulo.versionControlSystem.VcsNotifier; import git4idea.commands.Git; @@ -25,6 +27,7 @@ import git4idea.repo.GitRepository; import jakarta.annotation.Nonnull; + import java.util.Collection; import static git4idea.util.GitUIUtil.code; @@ -32,120 +35,122 @@ /** * Create new branch (starting from the current branch) and check it out. */ -class GitCheckoutNewBranchOperation extends GitBranchOperation -{ - - @Nonnull - private final Project myProject; - @Nonnull - private final String myNewBranchName; - - GitCheckoutNewBranchOperation(@Nonnull Project project, @Nonnull Git git, @Nonnull GitBranchUiHandler uiHandler, @Nonnull Collection repositories, @Nonnull String newBranchName) - { - super(project, git, uiHandler, repositories); - myNewBranchName = newBranchName; - myProject = project; - } - - @Override - protected void execute() - { - boolean fatalErrorHappened = false; - while(hasMoreRepositories() && !fatalErrorHappened) - { - final GitRepository repository = next(); - - GitSimpleEventDetector unmergedDetector = new GitSimpleEventDetector(GitSimpleEventDetector.Event.UNMERGED_PREVENTING_CHECKOUT); - GitCommandResult result = myGit.checkoutNewBranch(repository, myNewBranchName, unmergedDetector); - - if(result.success()) - { - refresh(repository); - markSuccessful(repository); - } - else if(unmergedDetector.hasHappened()) - { - fatalUnmergedFilesError(); - fatalErrorHappened = true; - } - else - { - fatalError("Couldn't create new branch " + myNewBranchName, result.getErrorOutputAsJoinedString()); - fatalErrorHappened = true; - } - } - - if(!fatalErrorHappened) - { - notifySuccess(); - updateRecentBranch(); - } - } - - private static void refresh(@Nonnull GitRepository repository) - { - repository.update(); - } - - @Nonnull - @Override - public String getSuccessMessage() - { - return String.format("Branch %s was created", myNewBranchName); - } - - @Nonnull - @Override - protected String getRollbackProposal() - { - return "However checkout has succeeded for the following " + repositories() + ":
" + - successfulRepositoriesJoined() + - "
You may rollback (checkout previous branch back, and delete " + myNewBranchName + ") not to let branches diverge."; - } - - @Nonnull - @Override - protected String getOperationName() - { - return "checkout"; - } - - @Override - protected void rollback() - { - GitCompoundResult checkoutResult = new GitCompoundResult(myProject); - GitCompoundResult deleteResult = new GitCompoundResult(myProject); - Collection repositories = getSuccessfulRepositories(); - for(GitRepository repository : repositories) - { - GitCommandResult result = myGit.checkout(repository, myCurrentHeads.get(repository), null, true, false); - checkoutResult.append(repository, result); - if(result.success()) - { - deleteResult.append(repository, myGit.branchDelete(repository, myNewBranchName, false)); - } - refresh(repository); - } - if(checkoutResult.totalSuccess() && deleteResult.totalSuccess()) - { - VcsNotifier.getInstance(myProject).notifySuccess("Rollback successful", String.format("Checked out %s and deleted %s on %s %s", stringifyBranchesByRepos(myCurrentHeads), - code(myNewBranchName), StringUtil.pluralize("root", repositories.size()), successfulRepositoriesJoined())); - } - else - { - StringBuilder message = new StringBuilder(); - if(!checkoutResult.totalSuccess()) - { - message.append("Errors during checkout: "); - message.append(checkoutResult.getErrorOutputWithReposIndication()); - } - if(!deleteResult.totalSuccess()) - { - message.append("Errors during deleting ").append(code(myNewBranchName)); - message.append(deleteResult.getErrorOutputWithReposIndication()); - } - VcsNotifier.getInstance(myProject).notifyError("Error during rollback", message.toString()); - } - } - +class GitCheckoutNewBranchOperation extends GitBranchOperation { + @Nonnull + private final Project myProject; + @Nonnull + private final NotificationService myNotificationService; + @Nonnull + private final String myNewBranchName; + + GitCheckoutNewBranchOperation( + @Nonnull Project project, + @Nonnull Git git, + @Nonnull GitBranchUiHandler uiHandler, + @Nonnull Collection repositories, + @Nonnull String newBranchName + ) { + super(project, git, uiHandler, repositories); + myNewBranchName = newBranchName; + myProject = project; + myNotificationService = NotificationService.getInstance(); + } + + @Override + protected void execute() { + boolean fatalErrorHappened = false; + while (hasMoreRepositories() && !fatalErrorHappened) { + GitRepository repository = next(); + + GitSimpleEventDetector unmergedDetector = new GitSimpleEventDetector(GitSimpleEventDetector.Event.UNMERGED_PREVENTING_CHECKOUT); + GitCommandResult result = myGit.checkoutNewBranch(repository, myNewBranchName, unmergedDetector); + + if (result.success()) { + refresh(repository); + markSuccessful(repository); + } + else if (unmergedDetector.hasHappened()) { + fatalUnmergedFilesError(); + fatalErrorHappened = true; + } + else { + fatalError( + LocalizeValue.localizeTODO("Couldn't create new branch " + myNewBranchName), + result.getErrorOutputAsJoinedValue() + ); + fatalErrorHappened = true; + } + } + + if (!fatalErrorHappened) { + notifySuccess(); + updateRecentBranch(); + } + } + + private static void refresh(@Nonnull GitRepository repository) { + repository.update(); + } + + @Nonnull + @Override + public LocalizeValue getSuccessMessage() { + return LocalizeValue.localizeTODO(String.format("Branch %s was created", myNewBranchName)); + } + + @Nonnull + @Override + protected String getRollbackProposal() { + return "However checkout has succeeded for the following " + repositories() + ":
" + + successfulRepositoriesJoined() + + "
You may rollback (checkout previous branch back, and delete " + myNewBranchName + ") not to let branches diverge."; + } + + @Nonnull + @Override + protected String getOperationName() { + return "checkout"; + } + + @Override + protected void rollback() { + GitCompoundResult checkoutResult = new GitCompoundResult(myProject); + GitCompoundResult deleteResult = new GitCompoundResult(myProject); + Collection repositories = getSuccessfulRepositories(); + for (GitRepository repository : repositories) { + GitCommandResult result = myGit.checkout(repository, myCurrentHeads.get(repository), null, true, false); + checkoutResult.append(repository, result); + if (result.success()) { + deleteResult.append(repository, myGit.branchDelete(repository, myNewBranchName, false)); + } + refresh(repository); + } + if (checkoutResult.totalSuccess() && deleteResult.totalSuccess()) { + myNotificationService.newInfo(VcsNotifier.NOTIFICATION_GROUP_ID) + .title(LocalizeValue.localizeTODO("Rollback successful")) + .content(LocalizeValue.localizeTODO(String.format( + "Checked out %s and deleted %s on %s %s", + stringifyBranchesByRepos(myCurrentHeads), + code(myNewBranchName), + StringUtil.pluralize("root", repositories.size()), + successfulRepositoriesJoined() + ))) + .notify(myProject); + } + else { + StringBuilder message = new StringBuilder(); + if (!checkoutResult.totalSuccess()) { + message.append("Errors during checkout: "); + message.append(checkoutResult.getErrorOutputWithReposIndication()); + } + if (!deleteResult.totalSuccess()) { + message.append("Errors during deleting: ").append(code(myNewBranchName)); + message.append(deleteResult.getErrorOutputWithReposIndication()); + } + myNotificationService.newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Error during rollback")) + .content(LocalizeValue.localizeTODO(message.toString())) + .notify(myProject); + } + } } diff --git a/plugin/src/main/java/git4idea/branch/GitCheckoutOperation.java b/plugin/src/main/java/git4idea/branch/GitCheckoutOperation.java index f6691c4..78465e4 100644 --- a/plugin/src/main/java/git4idea/branch/GitCheckoutOperation.java +++ b/plugin/src/main/java/git4idea/branch/GitCheckoutOperation.java @@ -17,9 +17,11 @@ import consulo.application.AccessToken; import consulo.application.progress.ProgressIndicator; +import consulo.localize.LocalizeValue; import consulo.project.Project; import consulo.project.ui.notification.Notification; import consulo.project.ui.notification.event.NotificationListener; +import consulo.ui.annotation.RequiredUIAccess; import consulo.util.collection.ArrayUtil; import consulo.util.lang.Pair; import consulo.versionControlSystem.VcsNotifier; @@ -34,6 +36,7 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; + import javax.swing.event.HyperlinkEvent; import java.util.Collection; import java.util.List; @@ -50,284 +53,285 @@ * * @author Kirill Likhodedov */ -class GitCheckoutOperation extends GitBranchOperation -{ - - public static final String ROLLBACK_PROPOSAL_FORMAT = "You may rollback (checkout back to previous branch) not to let branches diverge."; +class GitCheckoutOperation extends GitBranchOperation { + public static final String ROLLBACK_PROPOSAL_FORMAT = + "You may rollback (checkout back to previous branch) not to let branches diverge."; - @Nonnull - private final String myStartPointReference; - private final boolean myDetach; - private final boolean myRefShouldBeValid; - @Nullable - private final String myNewBranch; + @Nonnull + private final String myStartPointReference; + private final boolean myDetach; + private final boolean myRefShouldBeValid; + @Nullable + private final String myNewBranch; - GitCheckoutOperation(@Nonnull Project project, - @Nonnull Git git, - @Nonnull GitBranchUiHandler uiHandler, - @Nonnull Collection repositories, - @Nonnull String startPointReference, - boolean detach, - boolean refShouldBeValid, - @Nullable String newBranch) - { - super(project, git, uiHandler, repositories); - myStartPointReference = startPointReference; - myDetach = detach; - myRefShouldBeValid = refShouldBeValid; - myNewBranch = newBranch; - } + GitCheckoutOperation( + @Nonnull Project project, + @Nonnull Git git, + @Nonnull GitBranchUiHandler uiHandler, + @Nonnull Collection repositories, + @Nonnull String startPointReference, + boolean detach, + boolean refShouldBeValid, + @Nullable String newBranch + ) { + super(project, git, uiHandler, repositories); + myStartPointReference = startPointReference; + myDetach = detach; + myRefShouldBeValid = refShouldBeValid; + myNewBranch = newBranch; + } - @Override - protected void execute() - { - saveAllDocuments(); - boolean fatalErrorHappened = false; - try(AccessToken token = DvcsUtil.workingTreeChangeStarted(myProject, getOperationName())) - { - while(hasMoreRepositories() && !fatalErrorHappened) - { - final GitRepository repository = next(); + @Override + @RequiredUIAccess + protected void execute() { + saveAllDocuments(); + boolean fatalErrorHappened = false; + try (AccessToken token = DvcsUtil.workingTreeChangeStarted(myProject, getOperationName())) { + while (hasMoreRepositories() && !fatalErrorHappened) { + GitRepository repository = next(); - VirtualFile root = repository.getRoot(); - GitLocalChangesWouldBeOverwrittenDetector localChangesDetector = new GitLocalChangesWouldBeOverwrittenDetector(root, GitLocalChangesWouldBeOverwrittenDetector.Operation.CHECKOUT); - GitSimpleEventDetector unmergedFiles = new GitSimpleEventDetector(GitSimpleEventDetector.Event.UNMERGED_PREVENTING_CHECKOUT); - GitSimpleEventDetector unknownPathspec = new GitSimpleEventDetector(GitSimpleEventDetector.Event.INVALID_REFERENCE); - GitUntrackedFilesOverwrittenByOperationDetector untrackedOverwrittenByCheckout = new GitUntrackedFilesOverwrittenByOperationDetector(root); + VirtualFile root = repository.getRoot(); + GitLocalChangesWouldBeOverwrittenDetector localChangesDetector = + new GitLocalChangesWouldBeOverwrittenDetector(root, GitLocalChangesWouldBeOverwrittenDetector.Operation.CHECKOUT); + GitSimpleEventDetector unmergedFiles = + new GitSimpleEventDetector(GitSimpleEventDetector.Event.UNMERGED_PREVENTING_CHECKOUT); + GitSimpleEventDetector unknownPathspec = new GitSimpleEventDetector(GitSimpleEventDetector.Event.INVALID_REFERENCE); + GitUntrackedFilesOverwrittenByOperationDetector untrackedOverwrittenByCheckout = + new GitUntrackedFilesOverwrittenByOperationDetector(root); - GitCommandResult result = myGit.checkout(repository, myStartPointReference, myNewBranch, false, myDetach, localChangesDetector, unmergedFiles, unknownPathspec, - untrackedOverwrittenByCheckout); - if(result.success()) - { - refresh(repository); - markSuccessful(repository); - } - else if(unmergedFiles.hasHappened()) - { - fatalUnmergedFilesError(); - fatalErrorHappened = true; - } - else if(localChangesDetector.wasMessageDetected()) - { - boolean smartCheckoutSucceeded = smartCheckoutOrNotify(repository, localChangesDetector); - if(!smartCheckoutSucceeded) - { - fatalErrorHappened = true; - } - } - else if(untrackedOverwrittenByCheckout.wasMessageDetected()) - { - fatalUntrackedFilesError(repository.getRoot(), untrackedOverwrittenByCheckout.getRelativeFilePaths()); - fatalErrorHappened = true; - } - else if(!myRefShouldBeValid && unknownPathspec.hasHappened()) - { - markSkip(repository); - } - else - { - fatalError(getCommonErrorTitle(), result.getErrorOutputAsJoinedString()); - fatalErrorHappened = true; - } - } - } + GitCommandResult result = myGit.checkout( + repository, + myStartPointReference, + myNewBranch, + false, + myDetach, + localChangesDetector, + unmergedFiles, + unknownPathspec, + untrackedOverwrittenByCheckout + ); + if (result.success()) { + refresh(repository); + markSuccessful(repository); + } + else if (unmergedFiles.hasHappened()) { + fatalUnmergedFilesError(); + fatalErrorHappened = true; + } + else if (localChangesDetector.wasMessageDetected()) { + boolean smartCheckoutSucceeded = smartCheckoutOrNotify(repository, localChangesDetector); + if (!smartCheckoutSucceeded) { + fatalErrorHappened = true; + } + } + else if (untrackedOverwrittenByCheckout.wasMessageDetected()) { + fatalUntrackedFilesError(repository.getRoot(), untrackedOverwrittenByCheckout.getRelativeFilePaths()); + fatalErrorHappened = true; + } + else if (!myRefShouldBeValid && unknownPathspec.hasHappened()) { + markSkip(repository); + } + else { + fatalError(getCommonErrorTitle(), result.getErrorOutputAsJoinedValue()); + fatalErrorHappened = true; + } + } + } - if(!fatalErrorHappened) - { - if(wereSuccessful()) - { - if(!wereSkipped()) - { - notifySuccess(); - updateRecentBranch(); - } - else - { - String mentionSuccess = getSuccessMessage() + GitUtil.mention(getSuccessfulRepositories(), 4); - String mentionSkipped = wereSkipped() ? "
Revision not found" + GitUtil.mention(getSkippedRepositories(), 4) : ""; + if (!fatalErrorHappened) { + if (wereSuccessful()) { + if (!wereSkipped()) { + notifySuccess(); + updateRecentBranch(); + } + else { + String mentionSuccess = getSuccessMessage() + GitUtil.mention(getSuccessfulRepositories(), 4); + String mentionSkipped = wereSkipped() ? "
Revision not found" + GitUtil.mention(getSkippedRepositories(), 4) : ""; - VcsNotifier.getInstance(myProject).notifySuccess("", mentionSuccess + - mentionSkipped + - "
Rollback", new RollbackOperationNotificationListener()); - updateRecentBranch(); - } - } - else - { - LOG.assertTrue(!myRefShouldBeValid); - notifyError("Couldn't checkout " + myStartPointReference, "Revision not found" + GitUtil.mention(getSkippedRepositories(), 4)); - } - } - } + myNotificationService.newInfo(VcsNotifier.NOTIFICATION_GROUP_ID) + .content(LocalizeValue.localizeTODO(mentionSuccess + mentionSkipped + "
Rollback")) + .optionalHyperlinkListener(new RollbackOperationNotificationListener()) + .notify(myProject); + updateRecentBranch(); + } + } + else { + LOG.assertTrue(!myRefShouldBeValid); + notifyError( + LocalizeValue.localizeTODO("Couldn't checkout " + myStartPointReference), + LocalizeValue.localizeTODO("Revision not found" + GitUtil.mention(getSkippedRepositories(), 4)) + ); + } + } + } - private boolean smartCheckoutOrNotify(@Nonnull GitRepository repository, @Nonnull GitMessageWithFilesDetector localChangesOverwrittenByCheckout) - { - Pair, List> conflictingRepositoriesAndAffectedChanges = getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByCheckout, - myCurrentHeads.get(repository), myStartPointReference); - List allConflictingRepositories = conflictingRepositoriesAndAffectedChanges.getFirst(); - List affectedChanges = conflictingRepositoriesAndAffectedChanges.getSecond(); + private boolean smartCheckoutOrNotify( + @Nonnull GitRepository repository, + @Nonnull GitMessageWithFilesDetector localChangesOverwrittenByCheckout + ) { + Pair, List> conflictingRepositoriesAndAffectedChanges = + getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByCheckout, + myCurrentHeads.get(repository), myStartPointReference + ); + List allConflictingRepositories = conflictingRepositoriesAndAffectedChanges.getFirst(); + List affectedChanges = conflictingRepositoriesAndAffectedChanges.getSecond(); - Collection absolutePaths = GitUtil.toAbsolute(repository.getRoot(), localChangesOverwrittenByCheckout.getRelativeFilePaths()); - int smartCheckoutDecision = myUiHandler.showSmartOperationDialog(myProject, affectedChanges, absolutePaths, "checkout", "&Force Checkout"); - if(smartCheckoutDecision == GitSmartOperationDialog.SMART_EXIT_CODE) - { - boolean smartCheckedOutSuccessfully = smartCheckout(allConflictingRepositories, myStartPointReference, myNewBranch, getIndicator()); - if(smartCheckedOutSuccessfully) - { - for(GitRepository conflictingRepository : allConflictingRepositories) - { - markSuccessful(conflictingRepository); - refresh(conflictingRepository); - } - return true; - } - else - { - // notification is handled in smartCheckout() - return false; - } - } - else if(smartCheckoutDecision == GitSmartOperationDialog.FORCE_EXIT_CODE) - { - boolean forceCheckoutSucceeded = checkoutOrNotify(allConflictingRepositories, myStartPointReference, myNewBranch, true); - if(forceCheckoutSucceeded) - { - markSuccessful(ArrayUtil.toObjectArray(allConflictingRepositories, GitRepository.class)); - refresh(ArrayUtil.toObjectArray(allConflictingRepositories, GitRepository.class)); - } - return forceCheckoutSucceeded; - } - else - { - fatalLocalChangesError(myStartPointReference); - return false; - } - } + Collection absolutePaths = + GitUtil.toAbsolute(repository.getRoot(), localChangesOverwrittenByCheckout.getRelativeFilePaths()); + int smartCheckoutDecision = + myUiHandler.showSmartOperationDialog(myProject, affectedChanges, absolutePaths, "checkout", "&Force Checkout"); + if (smartCheckoutDecision == GitSmartOperationDialog.SMART_EXIT_CODE) { + boolean smartCheckedOutSuccessfully = + smartCheckout(allConflictingRepositories, myStartPointReference, myNewBranch, getIndicator()); + if (smartCheckedOutSuccessfully) { + for (GitRepository conflictingRepository : allConflictingRepositories) { + markSuccessful(conflictingRepository); + refresh(conflictingRepository); + } + return true; + } + else { + // notification is handled in smartCheckout() + return false; + } + } + else if (smartCheckoutDecision == GitSmartOperationDialog.FORCE_EXIT_CODE) { + boolean forceCheckoutSucceeded = checkoutOrNotify(allConflictingRepositories, myStartPointReference, myNewBranch, true); + if (forceCheckoutSucceeded) { + markSuccessful(ArrayUtil.toObjectArray(allConflictingRepositories, GitRepository.class)); + refresh(ArrayUtil.toObjectArray(allConflictingRepositories, GitRepository.class)); + } + return forceCheckoutSucceeded; + } + else { + fatalLocalChangesError(myStartPointReference); + return false; + } + } - @Nonnull - @Override - protected String getRollbackProposal() - { - return "However checkout has succeeded for the following " + repositories() + ":
" + - successfulRepositoriesJoined() + "
" + ROLLBACK_PROPOSAL_FORMAT; - } + @Nonnull + @Override + protected String getRollbackProposal() { + return "However checkout has succeeded for the following " + repositories() + ":
" + + successfulRepositoriesJoined() + "
" + ROLLBACK_PROPOSAL_FORMAT; + } - @Nonnull - @Override - protected String getOperationName() - { - return "checkout"; - } + @Nonnull + @Override + protected String getOperationName() { + return "checkout"; + } - @Override - protected void rollback() - { - GitCompoundResult checkoutResult = new GitCompoundResult(myProject); - GitCompoundResult deleteResult = new GitCompoundResult(myProject); - for(GitRepository repository : getSuccessfulRepositories()) - { - GitCommandResult result = myGit.checkout(repository, myCurrentHeads.get(repository), null, true, false); - checkoutResult.append(repository, result); - if(result.success() && myNewBranch != null) - { - /* - force delete is needed, because we create new branch from branch other that the current one - e.g. being on master create newBranch from feature, - then rollback => newBranch is not fully merged to master (although it is obviously fully merged to feature). - */ - deleteResult.append(repository, myGit.branchDelete(repository, myNewBranch, true)); - } - refresh(repository); - } - if(!checkoutResult.totalSuccess() || !deleteResult.totalSuccess()) - { - StringBuilder message = new StringBuilder(); - if(!checkoutResult.totalSuccess()) - { - message.append("Errors during checkout: "); - message.append(checkoutResult.getErrorOutputWithReposIndication()); - } - if(!deleteResult.totalSuccess()) - { - message.append("Errors during deleting ").append(code(myNewBranch)).append(": "); - message.append(deleteResult.getErrorOutputWithReposIndication()); - } - VcsNotifier.getInstance(myProject).notifyError("Error during rollback", message.toString()); - } - } + @Override + protected void rollback() { + GitCompoundResult checkoutResult = new GitCompoundResult(myProject); + GitCompoundResult deleteResult = new GitCompoundResult(myProject); + for (GitRepository repository : getSuccessfulRepositories()) { + GitCommandResult result = myGit.checkout(repository, myCurrentHeads.get(repository), null, true, false); + checkoutResult.append(repository, result); + if (result.success() && myNewBranch != null) { + /* + force delete is needed, because we create new branch from branch other that the current one + e.g. being on master create newBranch from feature, + then rollback => newBranch is not fully merged to master (although it is obviously fully merged to feature). + */ + deleteResult.append(repository, myGit.branchDelete(repository, myNewBranch, true)); + } + refresh(repository); + } + if (!checkoutResult.totalSuccess() || !deleteResult.totalSuccess()) { + StringBuilder message = new StringBuilder(); + if (!checkoutResult.totalSuccess()) { + message.append("Errors during checkout: "); + message.append(checkoutResult.getErrorOutputWithReposIndication()); + } + if (!deleteResult.totalSuccess()) { + message.append("Errors during deleting ").append(code(myNewBranch)).append(": "); + message.append(deleteResult.getErrorOutputWithReposIndication()); + } + myNotificationService.newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Error during rollback")) + .content(LocalizeValue.localizeTODO(message.toString())) + .notify(myProject); + } + } - @Nonnull - private String getCommonErrorTitle() - { - return "Couldn't checkout " + myStartPointReference; - } + @Nonnull + private LocalizeValue getCommonErrorTitle() { + return LocalizeValue.localizeTODO("Couldn't checkout " + myStartPointReference); + } - @Nonnull - @Override - public String getSuccessMessage() - { - if(myNewBranch == null) - { - return String.format("Checked out %s", myStartPointReference); - } - return String.format("Checked out new branch %s from %s", myNewBranch, myStartPointReference); - } + @Nonnull + @Override + public LocalizeValue getSuccessMessage() { + if (myNewBranch == null) { + return LocalizeValue.localizeTODO(String.format("Checked out %s", myStartPointReference)); + } + return LocalizeValue.localizeTODO(String.format( + "Checked out new branch %s from %s", + myNewBranch, + myStartPointReference + )); + } - // stash - checkout - unstash - private boolean smartCheckout(@Nonnull final List repositories, @Nonnull final String reference, @Nullable final String newBranch, @Nonnull ProgressIndicator indicator) - { - final AtomicBoolean result = new AtomicBoolean(); - GitPreservingProcess preservingProcess = new GitPreservingProcess(myProject, myGit, GitUtil.getRootsFromRepositories(repositories), "checkout", reference, - GitVcsSettings.UpdateChangesPolicy.STASH, indicator, new Runnable() - { - @Override - public void run() - { - result.set(checkoutOrNotify(repositories, reference, newBranch, false)); - } - }); - preservingProcess.execute(); - return result.get(); - } + // stash - checkout - unstash + private boolean smartCheckout( + @Nonnull List repositories, + @Nonnull String reference, + @Nullable String newBranch, + @Nonnull ProgressIndicator indicator + ) { + AtomicBoolean result = new AtomicBoolean(); + GitPreservingProcess preservingProcess = new GitPreservingProcess( + myProject, + myGit, + GitUtil.getRootsFromRepositories(repositories), + "checkout", + reference, + GitVcsSettings.UpdateChangesPolicy.STASH, + indicator, + () -> result.set(checkoutOrNotify(repositories, reference, newBranch, false)) + ); + preservingProcess.execute(); + return result.get(); + } - /** - * Checks out or shows an error message. - */ - private boolean checkoutOrNotify(@Nonnull List repositories, @Nonnull String reference, @Nullable String newBranch, boolean force) - { - GitCompoundResult compoundResult = new GitCompoundResult(myProject); - for(GitRepository repository : repositories) - { - compoundResult.append(repository, myGit.checkout(repository, reference, newBranch, force, myDetach)); - } - if(compoundResult.totalSuccess()) - { - return true; - } - notifyError("Couldn't checkout " + reference, compoundResult.getErrorOutputWithReposIndication()); - return false; - } + /** + * Checks out or shows an error message. + */ + private boolean checkoutOrNotify( + @Nonnull List repositories, + @Nonnull String reference, + @Nullable String newBranch, + boolean force + ) { + GitCompoundResult compoundResult = new GitCompoundResult(myProject); + for (GitRepository repository : repositories) { + compoundResult.append(repository, myGit.checkout(repository, reference, newBranch, force, myDetach)); + } + if (compoundResult.totalSuccess()) { + return true; + } + notifyError(LocalizeValue.localizeTODO("Couldn't checkout " + reference), compoundResult.getErrorOutputWithReposIndication()); + return false; + } - private void refresh(GitRepository... repositories) - { - for(GitRepository repository : repositories) - { - refreshRoot(repository); - // repository state will be auto-updated with this VFS refresh => in general there is no need to call GitRepository#update() - // but to avoid problems of the asynchronous refresh, let's force update the repository info. - repository.update(); - } - } + private void refresh(GitRepository... repositories) { + for (GitRepository repository : repositories) { + refreshRoot(repository); + // repository state will be auto-updated with this VFS refresh => in general there is no need to call GitRepository#update() + // but to avoid problems of the asynchronous refresh, let's force update the repository info. + repository.update(); + } + } - private class RollbackOperationNotificationListener implements NotificationListener - { - @Override - public void hyperlinkUpdate(@Nonnull Notification notification, @Nonnull HyperlinkEvent event) - { - if(event.getEventType() == HyperlinkEvent.EventType.ACTIVATED && event.getDescription().equalsIgnoreCase("rollback")) - { - rollback(); - } - } - } + private class RollbackOperationNotificationListener implements NotificationListener { + @Override + @RequiredUIAccess + public void hyperlinkUpdate(@Nonnull Notification notification, @Nonnull HyperlinkEvent event) { + if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED && event.getDescription().equalsIgnoreCase("rollback")) { + rollback(); + } + } + } } diff --git a/plugin/src/main/java/git4idea/branch/GitDeleteBranchOperation.java b/plugin/src/main/java/git4idea/branch/GitDeleteBranchOperation.java index 92b1d4c..0e24b7e 100644 --- a/plugin/src/main/java/git4idea/branch/GitDeleteBranchOperation.java +++ b/plugin/src/main/java/git4idea/branch/GitDeleteBranchOperation.java @@ -23,7 +23,7 @@ import consulo.project.Project; import consulo.project.ui.notification.Notification; import consulo.project.ui.notification.NotificationAction; -import consulo.project.ui.notification.NotificationType; +import consulo.ui.annotation.RequiredUIAccess; import consulo.ui.ex.action.AnActionEvent; import consulo.util.collection.ContainerUtil; import consulo.util.collection.MultiMap; @@ -55,7 +55,6 @@ * current branch are merged to, and makes force delete, if wanted. */ class GitDeleteBranchOperation extends GitBranchOperation { - private static final Logger LOG = Logger.getInstance(GitDeleteBranchOperation.class); static final LocalizeValue RESTORE = LocalizeValue.localizeTODO("Restore"); @@ -65,8 +64,6 @@ class GitDeleteBranchOperation extends GitBranchOperation { @Nonnull private final String myBranchName; @Nonnull - private final VcsNotifier myNotifier; - @Nonnull private final MultiMap myTrackedBranches; @Nonnull @@ -74,14 +71,15 @@ class GitDeleteBranchOperation extends GitBranchOperation { @Nonnull private final Map myDeletedBranchTips; - GitDeleteBranchOperation(@Nonnull Project project, - @Nonnull Git git, - @Nonnull GitBranchUiHandler uiHandler, - @Nonnull Collection repositories, - @Nonnull String branchName) { + GitDeleteBranchOperation( + @Nonnull Project project, + @Nonnull Git git, + @Nonnull GitBranchUiHandler uiHandler, + @Nonnull Collection repositories, + @Nonnull String branchName + ) { super(project, git, uiHandler, repositories); myBranchName = branchName; - myNotifier = VcsNotifier.getInstance(myProject); myTrackedBranches = groupByTrackedBranchName(branchName, repositories); myUnmergedToBranches = new HashMap<>(); myDeletedBranchTips = ContainerUtil.map2Map(repositories, (GitRepository repo) -> { @@ -95,11 +93,13 @@ class GitDeleteBranchOperation extends GitBranchOperation { public void execute() { boolean fatalErrorHappened = false; while (hasMoreRepositories() && !fatalErrorHappened) { - final GitRepository repository = next(); + GitRepository repository = next(); - GitSimpleEventDetector notFullyMergedDetector = new GitSimpleEventDetector(GitSimpleEventDetector.Event.BRANCH_NOT_FULLY_MERGED); + GitSimpleEventDetector notFullyMergedDetector = + new GitSimpleEventDetector(GitSimpleEventDetector.Event.BRANCH_NOT_FULLY_MERGED); GitBranchNotMergedToUpstreamDetector notMergedToUpstreamDetector = new GitBranchNotMergedToUpstreamDetector(); - GitCommandResult result = myGit.branchDelete(repository, myBranchName, false, notFullyMergedDetector, notMergedToUpstreamDetector); + GitCommandResult result = + myGit.branchDelete(repository, myBranchName, false, notFullyMergedDetector, notMergedToUpstreamDetector); if (result.success()) { refresh(repository); @@ -110,8 +110,10 @@ else if (notFullyMergedDetector.hasHappened()) { if (baseBranch == null) { // GitBranchNotMergedToUpstreamDetector didn't happen baseBranch = myCurrentHeads.get(repository); } - myUnmergedToBranches.put(repository, - new UnmergedBranchInfo(myDeletedBranchTips.get(repository), GitBranchUtil.stripRefsPrefix(baseBranch))); + myUnmergedToBranches.put( + repository, + new UnmergedBranchInfo(myDeletedBranchTips.get(repository), GitBranchUtil.stripRefsPrefix(baseBranch)) + ); GitCommandResult forceDeleteResult = myGit.branchDelete(repository, myBranchName, true); if (forceDeleteResult.success()) { @@ -119,12 +121,12 @@ else if (notFullyMergedDetector.hasHappened()) { markSuccessful(repository); } else { - fatalError(getErrorTitle(), forceDeleteResult.getErrorOutputAsHtmlString()); + fatalError(getErrorTitle(), forceDeleteResult.getErrorOutputAsHtmlValue()); fatalErrorHappened = true; } } else { - fatalError(getErrorTitle(), result.getErrorOutputAsJoinedString()); + fatalError(getErrorTitle(), result.getErrorOutputAsJoinedValue()); fatalErrorHappened = true; } } @@ -141,37 +143,44 @@ protected void notifySuccess() { if (unmergedCommits) { message += "
Unmerged commits were discarded"; } - Notification notification = STANDARD_NOTIFICATION.createNotification("", message, NotificationType.INFORMATION, null); - notification.addAction(new NotificationAction(RESTORE) { - @Override - public void actionPerformed(@Nonnull AnActionEvent e, @Nonnull Notification notification) { - restoreInBackground(notification); - } - }); + Notification.Builder builder = myNotificationService.newInfo(STANDARD_NOTIFICATION) + .content(LocalizeValue.localizeTODO(message)) + .addAction(new NotificationAction(RESTORE) { + @Override + @RequiredUIAccess + public void actionPerformed(@Nonnull AnActionEvent e, @Nonnull Notification notification) { + restoreInBackground(notification); + } + }); if (unmergedCommits) { - notification.addAction(new NotificationAction(VIEW_COMMITS) { + builder.addAction(new NotificationAction(VIEW_COMMITS) { @Override + @RequiredUIAccess public void actionPerformed(@Nonnull AnActionEvent e, @Nonnull Notification notification) { viewUnmergedCommitsInBackground(notification); } }); } if (!myTrackedBranches.isEmpty() && hasOnlyTrackingBranch(myTrackedBranches, myBranchName)) { - notification.addAction(new NotificationAction(DELETE_TRACKED_BRANCH) { + builder.addAction(new NotificationAction(DELETE_TRACKED_BRANCH) { @Override + @RequiredUIAccess public void actionPerformed(@Nonnull AnActionEvent e, @Nonnull Notification notification) { deleteTrackedBranchInBackground(); } }); } - myNotifier.notify(notification); + builder.notify(myProject); } private static boolean hasOnlyTrackingBranch(@Nonnull MultiMap trackedBranches, @Nonnull String localBranch) { for (String remoteBranch : trackedBranches.keySet()) { for (GitRepository repository : trackedBranches.get(remoteBranch)) { - if (exists(repository.getBranchTrackInfos(), - info -> !info.getLocalBranch().getName().equals(localBranch) && info.getRemoteBranch().getName().equals(remoteBranch))) { + if (exists( + repository.getBranchTrackInfos(), + info -> !info.getLocalBranch().getName().equals(localBranch) + && info.getRemoteBranch().getName().equals(remoteBranch) + )) { return false; } } @@ -189,7 +198,10 @@ private static void refresh(@Nonnull GitRepository... repositories) { protected void rollback() { GitCompoundResult result = doRollback(); if (!result.totalSuccess()) { - myNotifier.notifyError("Error during rollback of branch deletion", result.getErrorOutputWithReposIndication()); + myNotificationService.newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Error during rollback of branch deletion")) + .content(result.getErrorOutputWithReposIndication()) + .notify(myProject); } } @@ -204,8 +216,10 @@ private GitCompoundResult doRollback() { if (myTrackedBranches.get(trackedBranch).contains(repository)) { GitCommandResult setTrackResult = setUpTracking(repository, myBranchName, trackedBranch); if (!setTrackResult.success()) { - LOG.warn("Couldn't set " + myBranchName + " to track " + trackedBranch + " in " + repository.getRoot().getName() + ": " + - setTrackResult.getErrorOutputAsJoinedString()); + LOG.warn( + "Couldn't set " + myBranchName + " to track " + trackedBranch + " in " + repository.getRoot().getName() + + ": " + setTrackResult.getErrorOutputAsJoinedString() + ); } } } @@ -228,13 +242,14 @@ private GitCommandResult setUpTracking(@Nonnull GitRepository repository, @Nonnu } @Nonnull - private String getErrorTitle() { - return String.format("Branch %s wasn't deleted", myBranchName); + private LocalizeValue getErrorTitle() { + return LocalizeValue.localizeTODO(String.format("Branch %s wasn't deleted", myBranchName)); } @Nonnull - public String getSuccessMessage() { - return String.format("Deleted branch %s", formatBranchName(myBranchName)); + @Override + public LocalizeValue getSuccessMessage() { + return LocalizeValue.localizeTODO(String.format("Deleted branch %s", formatBranchName(myBranchName))); } @Nonnull @@ -270,22 +285,28 @@ private boolean showNotFullyMergedDialog(@Nonnull MapemptyList()); } } - Map baseBranches = ContainerUtil.map2Map(unmergedBranches.keySet(), it -> { - return Pair.create(it, unmergedBranches.get(it).myBaseBranch); - }); + Map baseBranches = ContainerUtil.map2Map( + unmergedBranches.keySet(), + it -> Pair.create(it, unmergedBranches.get(it).myBaseBranch) + ); return myUiHandler.showBranchIsNotFullyMergedDialog(myProject, history, baseBranches, myBranchName); } @Nonnull - private static List getUnmergedCommits(@Nonnull GitRepository repository, - @Nonnull String branchName, - @Nonnull String baseBranch) { + private static List getUnmergedCommits( + @Nonnull GitRepository repository, + @Nonnull String branchName, + @Nonnull String baseBranch + ) { String range = baseBranch + ".." + branchName; try { return GitHistoryUtils.history(repository.getProject(), repository.getRoot(), range); @@ -297,8 +318,10 @@ private static List getUnmergedCommits(@Nonnull GitRepository reposit } @Nonnull - private static MultiMap groupByTrackedBranchName(@Nonnull String branchName, - @Nonnull Collection repositories) { + private static MultiMap groupByTrackedBranchName( + @Nonnull String branchName, + @Nonnull Collection repositories + ) { MultiMap trackedBranchNames = MultiMap.createLinked(); for (GitRepository repository : repositories) { GitBranchTrackInfo trackInfo = GitBranchUtil.getTrackInfo(repository, branchName); @@ -354,7 +377,7 @@ public UnmergedBranchInfo(@Nonnull String tipOfDeletedUnmergedBranch, @Nonnull S } private void deleteTrackedBranchInBackground() { - new Task.Backgroundable(myProject, "Deleting Remote Branch " + myBranchName + "...") { + new Task.Backgroundable(myProject, LocalizeValue.localizeTODO("Deleting Remote Branch " + myBranchName + "...")) { @Override public void run(@Nonnull ProgressIndicator indicator) { GitBrancher brancher = ServiceManager.getService(getProject(), GitBrancher.class); @@ -366,7 +389,7 @@ public void run(@Nonnull ProgressIndicator indicator) { } private void restoreInBackground(@Nonnull Notification notification) { - new Task.Backgroundable(myProject, "Restoring Branch " + myBranchName + "...") { + new Task.Backgroundable(myProject, LocalizeValue.localizeTODO("Restoring Branch " + myBranchName + "...")) { @Override public void run(@Nonnull ProgressIndicator indicator) { rollbackBranchDeletion(notification); @@ -380,12 +403,15 @@ private void rollbackBranchDeletion(@Nonnull Notification notification) { notification.expire(); } else { - myNotifier.notifyError("Couldn't Restore " + formatBranchName(myBranchName), result.getErrorOutputWithReposIndication()); + myNotificationService.newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Couldn't Restore " + formatBranchName(myBranchName))) + .content(result.getErrorOutputWithReposIndication()) + .notifyAndGet(myProject); } } private void viewUnmergedCommitsInBackground(@Nonnull Notification notification) { - new Task.Backgroundable(myProject, "Collecting Unmerged Commits...") { + new Task.Backgroundable(myProject, LocalizeValue.localizeTODO("Collecting Unmerged Commits...")) { @Override public void run(@Nonnull ProgressIndicator indicator) { boolean restore = showNotFullyMergedDialog(myUnmergedToBranches); diff --git a/plugin/src/main/java/git4idea/branch/GitDeleteRemoteBranchOperation.java b/plugin/src/main/java/git4idea/branch/GitDeleteRemoteBranchOperation.java index 814daf2..a474366 100644 --- a/plugin/src/main/java/git4idea/branch/GitDeleteRemoteBranchOperation.java +++ b/plugin/src/main/java/git4idea/branch/GitDeleteRemoteBranchOperation.java @@ -30,9 +30,9 @@ import git4idea.repo.GitRemote; import git4idea.repo.GitRepository; import git4idea.ui.branch.GitMultiRootBranchConfig; - import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; + import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -40,221 +40,230 @@ import java.util.concurrent.atomic.AtomicReference; class GitDeleteRemoteBranchOperation extends GitBranchOperation { - private final String myBranchName; + private final String myBranchName; - public GitDeleteRemoteBranchOperation(@Nonnull Project project, - @Nonnull Git git, - @Nonnull GitBranchUiHandler handler, - @Nonnull List repositories, - @Nonnull String name) { - super(project, git, handler, repositories); - myBranchName = name; - } - - @Override - protected void execute() { - final Collection repositories = getRepositories(); - final Collection trackingBranches = findTrackingBranches(myBranchName, repositories); - String currentBranch = GitBranchUtil.getCurrentBranchOrRev(repositories); - boolean currentBranchTracksBranchToDelete = false; - if (trackingBranches.contains(currentBranch)) { - currentBranchTracksBranchToDelete = true; - trackingBranches.remove(currentBranch); + public GitDeleteRemoteBranchOperation( + @Nonnull Project project, + @Nonnull Git git, + @Nonnull GitBranchUiHandler handler, + @Nonnull List repositories, + @Nonnull String name + ) { + super(project, git, handler, repositories); + myBranchName = name; } - final AtomicReference decision = new AtomicReference<>(); - final boolean finalCurrentBranchTracksBranchToDelete = currentBranchTracksBranchToDelete; - UIUtil.invokeAndWaitIfNeeded(new Runnable() { - @Override - public void run() { - decision.set(confirmBranchDeletion(myBranchName, trackingBranches, finalCurrentBranchTracksBranchToDelete, repositories)); - } - }); + @Override + protected void execute() { + final Collection repositories = getRepositories(); + Collection trackingBranches = findTrackingBranches(myBranchName, repositories); + String currentBranch = GitBranchUtil.getCurrentBranchOrRev(repositories); + boolean currentBranchTracksBranchToDelete = false; + if (trackingBranches.contains(currentBranch)) { + currentBranchTracksBranchToDelete = true; + trackingBranches.remove(currentBranch); + } + AtomicReference decision = new AtomicReference<>(); + boolean finalCurrentBranchTracksBranchToDelete = currentBranchTracksBranchToDelete; + UIUtil.invokeAndWaitIfNeeded((Runnable) () -> decision.set(confirmBranchDeletion( + myBranchName, + trackingBranches, + finalCurrentBranchTracksBranchToDelete, + repositories + ))); - if (decision.get().delete()) { - boolean deletedSuccessfully = doDeleteRemote(myBranchName, repositories); - if (deletedSuccessfully) { - final Collection successfullyDeletedLocalBranches = new ArrayList<>(1); - if (decision.get().deleteTracking()) { - for (final String branch : trackingBranches) { - getIndicator().setText("Deleting " + branch); - new GitDeleteBranchOperation(myProject, myGit, myUiHandler, repositories, branch) { - @Override - protected void notifySuccess(@Nonnull String message) { - // do nothing - will display a combo notification for all deleted branches below - successfullyDeletedLocalBranches.add(branch); - } - }.execute(); - } + if (decision.get().delete()) { + boolean deletedSuccessfully = doDeleteRemote(myBranchName, repositories); + if (deletedSuccessfully) { + final Collection successfullyDeletedLocalBranches = new ArrayList<>(1); + if (decision.get().deleteTracking()) { + for (final String branch : trackingBranches) { + getIndicator().setText("Deleting " + branch); + new GitDeleteBranchOperation(myProject, myGit, myUiHandler, repositories, branch) { + @Override + protected void notifySuccess(@Nonnull LocalizeValue message) { + // do nothing - will display a combo notification for all deleted branches below + successfullyDeletedLocalBranches.add(branch); + } + }.execute(); + } + } + notifySuccessfulDeletion(myBranchName, successfullyDeletedLocalBranches); + } } - notifySuccessfulDeletion(myBranchName, successfullyDeletedLocalBranches); - } - } - } + } - @Override - protected void rollback() { - throw new UnsupportedOperationException(); - } + @Override + protected void rollback() { + throw new UnsupportedOperationException(); + } - @Nonnull - @Override - public String getSuccessMessage() { - throw new UnsupportedOperationException(); - } + @Nonnull + @Override + public LocalizeValue getSuccessMessage() { + throw new UnsupportedOperationException(); + } - @Nonnull - @Override - protected String getRollbackProposal() { - throw new UnsupportedOperationException(); - } + @Nonnull + @Override + protected String getRollbackProposal() { + throw new UnsupportedOperationException(); + } - @Nonnull - @Override - protected String getOperationName() { - throw new UnsupportedOperationException(); - } + @Nonnull + @Override + protected String getOperationName() { + throw new UnsupportedOperationException(); + } - @Nonnull - private static Collection findTrackingBranches(@Nonnull String remoteBranch, @Nonnull Collection repositories) { - return new GitMultiRootBranchConfig(repositories).getTrackingBranches(remoteBranch); - } + @Nonnull + private static Collection findTrackingBranches(@Nonnull String remoteBranch, @Nonnull Collection repositories) { + return new GitMultiRootBranchConfig(repositories).getTrackingBranches(remoteBranch); + } - private boolean doDeleteRemote(@Nonnull String branchName, @Nonnull Collection repositories) { - Couple pair = splitNameOfRemoteBranch(branchName); - String remoteName = pair.getFirst(); - String branch = pair.getSecond(); + private boolean doDeleteRemote(@Nonnull String branchName, @Nonnull Collection repositories) { + Couple pair = splitNameOfRemoteBranch(branchName); + String remoteName = pair.getFirst(); + String branch = pair.getSecond(); - GitCompoundResult result = new GitCompoundResult(myProject); - for (GitRepository repository : repositories) { - GitCommandResult res; - GitRemote remote = getRemoteByName(repository, remoteName); - if (remote == null) { - String error = "Couldn't find remote by name: " + remoteName; - LOG.error(error); - res = GitCommandResult.error(error); - } - else { - res = pushDeletion(repository, remote, branch); - if (!res.success() && isAlreadyDeletedError(res.getErrorOutputAsJoinedString())) { - res = myGit.remotePrune(repository, remote); + GitCompoundResult result = new GitCompoundResult(myProject); + for (GitRepository repository : repositories) { + GitCommandResult res; + GitRemote remote = getRemoteByName(repository, remoteName); + if (remote == null) { + String error = "Couldn't find remote by name: " + remoteName; + LOG.error(error); + res = GitCommandResult.error(error); + } + else { + res = pushDeletion(repository, remote, branch); + if (!res.success() && isAlreadyDeletedError(res.getErrorOutputAsJoinedString())) { + res = myGit.remotePrune(repository, remote); + } + } + result.append(repository, res); + repository.update(); } - } - result.append(repository, res); - repository.update(); - } - if (!result.totalSuccess()) { - VcsNotifier.getInstance(myProject) - .notifyError("Failed to delete remote branch " + branchName, result.getErrorOutputWithReposIndication()); + if (!result.totalSuccess()) { + myNotificationService.newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Failed to delete remote branch " + branchName)) + .content(result.getErrorOutputWithReposIndication()) + .notify(myProject); + } + return result.totalSuccess(); } - return result.totalSuccess(); - } - private static boolean isAlreadyDeletedError(@Nonnull String errorOutput) { - return errorOutput.contains("remote ref does not exist"); - } + private static boolean isAlreadyDeletedError(@Nonnull String errorOutput) { + return errorOutput.contains("remote ref does not exist"); + } - /** - * Returns the remote and the "local" name of a remote branch. - * Expects branch in format "origin/master", i.e. remote/branch - */ - private static Couple splitNameOfRemoteBranch(String branchName) { - int firstSlash = branchName.indexOf('/'); - String remoteName = firstSlash > -1 ? branchName.substring(0, firstSlash) : branchName; - String remoteBranchName = branchName.substring(firstSlash + 1); - return Couple.of(remoteName, remoteBranchName); - } + /** + * Returns the remote and the "local" name of a remote branch. + * Expects branch in format "origin/master", i.e. remote/branch + */ + private static Couple splitNameOfRemoteBranch(String branchName) { + int firstSlash = branchName.indexOf('/'); + String remoteName = firstSlash > -1 ? branchName.substring(0, firstSlash) : branchName; + String remoteBranchName = branchName.substring(firstSlash + 1); + return Couple.of(remoteName, remoteBranchName); + } - @Nonnull - private GitCommandResult pushDeletion(@Nonnull GitRepository repository, @Nonnull GitRemote remote, @Nonnull String branchName) { - return myGit.push(repository, remote, ":" + branchName, false, false, false, null); - } + @Nonnull + private GitCommandResult pushDeletion(@Nonnull GitRepository repository, @Nonnull GitRemote remote, @Nonnull String branchName) { + return myGit.push(repository, remote, ":" + branchName, false, false, false, null); + } - @Nullable - private static GitRemote getRemoteByName(@Nonnull GitRepository repository, @Nonnull String remoteName) { - for (GitRemote remote : repository.getRemotes()) { - if (remote.getName().equals(remoteName)) { - return remote; - } + @Nullable + private static GitRemote getRemoteByName(@Nonnull GitRepository repository, @Nonnull String remoteName) { + for (GitRemote remote : repository.getRemotes()) { + if (remote.getName().equals(remoteName)) { + return remote; + } + } + return null; } - return null; - } - private void notifySuccessfulDeletion(@Nonnull String remoteBranchName, @Nonnull Collection localBranches) { - String message = ""; - if (!localBranches.isEmpty()) { - message = "Also deleted local " + StringUtil.pluralize("branch", localBranches.size()) + ": " + StringUtil.join(localBranches, ", "); + private void notifySuccessfulDeletion(@Nonnull String remoteBranchName, @Nonnull Collection localBranches) { + String message = ""; + if (!localBranches.isEmpty()) { + message = + "Also deleted local " + StringUtil.pluralize("branch", localBranches.size()) + ": " + StringUtil.join(localBranches, ", "); + } + myNotificationService.newInfo(VcsNotifier.NOTIFICATION_GROUP_ID) + .title(LocalizeValue.localizeTODO("Deleted remote branch " + remoteBranchName)) + .content(LocalizeValue.localizeTODO(message)) + .notify(myProject); } - VcsNotifier.getInstance(myProject).notifySuccess("Deleted remote branch " + remoteBranchName, message); - } - private DeleteRemoteBranchDecision confirmBranchDeletion(@Nonnull String branchName, - @Nonnull Collection trackingBranches, - boolean currentBranchTracksBranchToDelete, - @Nonnull Collection repositories) { - String title = "Delete Remote Branch"; - String message = "Delete remote branch " + branchName; + private DeleteRemoteBranchDecision confirmBranchDeletion( + @Nonnull String branchName, + @Nonnull Collection trackingBranches, + boolean currentBranchTracksBranchToDelete, + @Nonnull Collection repositories + ) { + String title = "Delete Remote Branch"; + String message = "Delete remote branch " + branchName; - boolean delete; - final boolean deleteTracking; - if (trackingBranches.isEmpty()) { - delete = Messages.showYesNoDialog(myProject, message, title, "Delete", "Cancel", Messages.getQuestionIcon()) == Messages.YES; - deleteTracking = false; - } - else { - if (currentBranchTracksBranchToDelete) { - message += - "\n\nCurrent branch " + GitBranchUtil.getCurrentBranchOrRev(repositories) + " tracks " + branchName + " but won't be deleted."; - } - final LocalizeValue checkboxMessage; - if (trackingBranches.size() == 1) { - checkboxMessage = LocalizeValue.localizeTODO("Delete tracking local branch " + trackingBranches.iterator().next() + " as well"); - } - else { - checkboxMessage = LocalizeValue.localizeTODO("Delete tracking local branches " + StringUtil.join(trackingBranches, ", ")); - } + boolean delete; + boolean deleteTracking; + if (trackingBranches.isEmpty()) { + delete = Messages.showYesNoDialog(myProject, message, title, "Delete", "Cancel", UIUtil.getQuestionIcon()) == Messages.YES; + deleteTracking = false; + } + else { + if (currentBranchTracksBranchToDelete) { + message += "\n\nCurrent branch " + GitBranchUtil.getCurrentBranchOrRev(repositories) + + " tracks " + branchName + " but won't be deleted."; + } + final LocalizeValue checkboxMessage; + if (trackingBranches.size() == 1) { + checkboxMessage = + LocalizeValue.localizeTODO("Delete tracking local branch " + trackingBranches.iterator().next() + " as well"); + } + else { + checkboxMessage = LocalizeValue.localizeTODO("Delete tracking local branches " + StringUtil.join(trackingBranches, ", ")); + } - final AtomicBoolean deleteChoice = new AtomicBoolean(); - delete = MessageDialogBuilder.yesNo(title, message) - .project(myProject) - .yesText("Delete") - .noText("Cancel") - .doNotAsk(new DialogWrapper.DoNotAskOption.Adapter() { - @Override - public void rememberChoice(boolean isSelected, int exitCode) { - deleteChoice.set(isSelected); - } + final AtomicBoolean deleteChoice = new AtomicBoolean(); + delete = MessageDialogBuilder.yesNo(title, message) + .project(myProject) + .yesText("Delete") + .noText("Cancel") + .doNotAsk(new DialogWrapper.DoNotAskOption.Adapter() { + @Override + public void rememberChoice(boolean isSelected, int exitCode) { + deleteChoice.set(isSelected); + } - @Nonnull - @Override - public LocalizeValue getDoNotShowMessage() { - return checkboxMessage; - } - }) - .show() == Messages.YES; - deleteTracking = deleteChoice.get(); + @Nonnull + @Override + public LocalizeValue getDoNotShowMessage() { + return checkboxMessage; + } + }) + .show() == Messages.YES; + deleteTracking = deleteChoice.get(); + } + return new DeleteRemoteBranchDecision(delete, deleteTracking); } - return new DeleteRemoteBranchDecision(delete, deleteTracking); - } - private static class DeleteRemoteBranchDecision { - private final boolean delete; - private final boolean deleteTracking; + private static class DeleteRemoteBranchDecision { + private final boolean delete; + private final boolean deleteTracking; - private DeleteRemoteBranchDecision(boolean delete, boolean deleteTracking) { - this.delete = delete; - this.deleteTracking = deleteTracking; - } + private DeleteRemoteBranchDecision(boolean delete, boolean deleteTracking) { + this.delete = delete; + this.deleteTracking = deleteTracking; + } - public boolean delete() { - return delete; - } + public boolean delete() { + return delete; + } - public boolean deleteTracking() { - return deleteTracking; + public boolean deleteTracking() { + return deleteTracking; + } } - } - } \ No newline at end of file diff --git a/plugin/src/main/java/git4idea/branch/GitMergeOperation.java b/plugin/src/main/java/git4idea/branch/GitMergeOperation.java index 98140ef..e13ddaa 100644 --- a/plugin/src/main/java/git4idea/branch/GitMergeOperation.java +++ b/plugin/src/main/java/git4idea/branch/GitMergeOperation.java @@ -16,12 +16,13 @@ package git4idea.branch; import consulo.application.AccessToken; -import consulo.application.util.function.Computable; -import consulo.ide.ServiceManager; +import consulo.localize.LocalizeValue; import consulo.logging.Logger; import consulo.project.Project; import consulo.project.ui.notification.Notification; +import consulo.project.ui.notification.NotificationService; import consulo.project.ui.notification.event.NotificationListener; +import consulo.ui.annotation.RequiredUIAccess; import consulo.ui.ex.awt.UIUtil; import consulo.util.lang.Pair; import consulo.versionControlSystem.VcsNotifier; @@ -37,418 +38,389 @@ import git4idea.repo.GitRepository; import git4idea.reset.GitResetMode; import git4idea.util.GitPreservingProcess; - import jakarta.annotation.Nonnull; + import javax.swing.event.HyperlinkEvent; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; -class GitMergeOperation extends GitBranchOperation -{ - - private static final Logger LOG = Logger.getInstance(GitMergeOperation.class); - public static final String ROLLBACK_PROPOSAL = "You may rollback (reset to the commit before merging) not to let branches diverge."; - - @Nonnull - private final ChangeListManager myChangeListManager; - @Nonnull - private final String myBranchToMerge; - private final GitBrancher.DeleteOnMergeOption myDeleteOnMerge; - - // true in value, if we've stashed local changes before merge and will need to unstash after resolving conflicts. - @Nonnull - private final Map myConflictedRepositories = new HashMap<>(); - private GitPreservingProcess myPreservingProcess; - - GitMergeOperation(@Nonnull Project project, - @Nonnull Git git, - @Nonnull GitBranchUiHandler uiHandler, - @Nonnull Collection repositories, - @Nonnull String branchToMerge, - GitBrancher.DeleteOnMergeOption deleteOnMerge) - { - super(project, git, uiHandler, repositories); - myBranchToMerge = branchToMerge; - myDeleteOnMerge = deleteOnMerge; - myChangeListManager = ChangeListManager.getInstance(myProject); - } - - @Override - protected void execute() - { - LOG.info("starting"); - saveAllDocuments(); - boolean fatalErrorHappened = false; - int alreadyUpToDateRepositories = 0; - try(AccessToken token = DvcsUtil.workingTreeChangeStarted(myProject, getOperationName())) - { - while(hasMoreRepositories() && !fatalErrorHappened) - { - final GitRepository repository = next(); - LOG.info("next repository: " + repository); - - VirtualFile root = repository.getRoot(); - GitLocalChangesWouldBeOverwrittenDetector localChangesDetector = new GitLocalChangesWouldBeOverwrittenDetector(root, GitLocalChangesWouldBeOverwrittenDetector.Operation.MERGE); - GitSimpleEventDetector unmergedFiles = new GitSimpleEventDetector(GitSimpleEventDetector.Event.UNMERGED_PREVENTING_MERGE); - GitUntrackedFilesOverwrittenByOperationDetector untrackedOverwrittenByMerge = new GitUntrackedFilesOverwrittenByOperationDetector(root); - GitSimpleEventDetector mergeConflict = new GitSimpleEventDetector(GitSimpleEventDetector.Event.MERGE_CONFLICT); - GitSimpleEventDetector alreadyUpToDateDetector = new GitSimpleEventDetector(GitSimpleEventDetector.Event.ALREADY_UP_TO_DATE); - - GitCommandResult result = myGit.merge(repository, myBranchToMerge, Collections.emptyList(), localChangesDetector, unmergedFiles, untrackedOverwrittenByMerge, mergeConflict, - alreadyUpToDateDetector); - if(result.success()) - { - LOG.info("Merged successfully"); - refresh(repository); - markSuccessful(repository); - if(alreadyUpToDateDetector.hasHappened()) - { - alreadyUpToDateRepositories += 1; - } - } - else if(unmergedFiles.hasHappened()) - { - LOG.info("Unmerged files error!"); - fatalUnmergedFilesError(); - fatalErrorHappened = true; - } - else if(localChangesDetector.wasMessageDetected()) - { - LOG.info("Local changes would be overwritten by merge!"); - boolean smartMergeSucceeded = proposeSmartMergePerformAndNotify(repository, localChangesDetector); - if(!smartMergeSucceeded) - { - fatalErrorHappened = true; - } - } - else if(mergeConflict.hasHappened()) - { - LOG.info("Merge conflict"); - myConflictedRepositories.put(repository, Boolean.FALSE); - refresh(repository); - markSuccessful(repository); - } - else if(untrackedOverwrittenByMerge.wasMessageDetected()) - { - LOG.info("Untracked files would be overwritten by merge!"); - fatalUntrackedFilesError(repository.getRoot(), untrackedOverwrittenByMerge.getRelativeFilePaths()); - fatalErrorHappened = true; - } - else - { - LOG.info("Unknown error. " + result); - fatalError(getCommonErrorTitle(), result.getErrorOutputAsJoinedString()); - fatalErrorHappened = true; - } - } - - if(fatalErrorHappened) - { - notifyAboutRemainingConflicts(); - } - else - { - boolean allConflictsResolved = resolveConflicts(); - if(allConflictsResolved) - { - if(alreadyUpToDateRepositories < getRepositories().size()) - { - notifySuccess(); - } - else - { - notifySuccess("Already up-to-date"); - } - } - } - - restoreLocalChanges(); - } - } - - private void notifyAboutRemainingConflicts() - { - if(!myConflictedRepositories.isEmpty()) - { - new MyMergeConflictResolver().notifyUnresolvedRemain(); - } - } - - @Override - protected void notifySuccess(@Nonnull String message) - { - switch(myDeleteOnMerge) - { - case DELETE: - super.notifySuccess(message); - UIUtil.invokeLaterIfNeeded(new Runnable() - { // bg process needs to be started from the EDT - @Override - public void run() - { - GitBrancher brancher = ServiceManager.getService(myProject, GitBrancher.class); - brancher.deleteBranch(myBranchToMerge, new ArrayList<>(getRepositories())); - } - }); - break; - case PROPOSE: - String description = message + "
Delete " + myBranchToMerge + ""; - VcsNotifier.getInstance(myProject).notifySuccess("", description, new DeleteMergedLocalBranchNotificationListener()); - break; - case NOTHING: - super.notifySuccess(message); - break; - } - } - - private boolean resolveConflicts() - { - if(!myConflictedRepositories.isEmpty()) - { - return new MyMergeConflictResolver().merge(); - } - return true; - } - - private boolean proposeSmartMergePerformAndNotify(@Nonnull GitRepository repository, @Nonnull GitMessageWithFilesDetector localChangesOverwrittenByMerge) - { - Pair, List> conflictingRepositoriesAndAffectedChanges = getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByMerge, - myCurrentHeads.get(repository), myBranchToMerge); - List allConflictingRepositories = conflictingRepositoriesAndAffectedChanges.getFirst(); - List affectedChanges = conflictingRepositoriesAndAffectedChanges.getSecond(); - - Collection absolutePaths = GitUtil.toAbsolute(repository.getRoot(), localChangesOverwrittenByMerge.getRelativeFilePaths()); - int smartCheckoutDecision = myUiHandler.showSmartOperationDialog(myProject, affectedChanges, absolutePaths, "merge", null); - if(smartCheckoutDecision == GitSmartOperationDialog.SMART_EXIT_CODE) - { - return doSmartMerge(allConflictingRepositories); - } - else - { - fatalLocalChangesError(myBranchToMerge); - return false; - } - } - - private void restoreLocalChanges() - { - if(myPreservingProcess != null) - { - myPreservingProcess.load(); - } - } - - private boolean doSmartMerge(@Nonnull final Collection repositories) - { - final AtomicBoolean success = new AtomicBoolean(); - myPreservingProcess = new GitPreservingProcess(myProject, myGit, GitUtil.getRootsFromRepositories(repositories), "merge", myBranchToMerge, GitVcsSettings.UpdateChangesPolicy.STASH, - getIndicator(), new Runnable() - { - @Override - public void run() - { - success.set(doMerge(repositories)); - } - }); - myPreservingProcess.execute(new Computable() - { - @Override - public Boolean compute() - { - return myConflictedRepositories.isEmpty(); - } - }); - return success.get(); - } - - /** - * Performs merge in the given repositories. - * Handle only merge conflict situation: all other cases should have been handled before and are treated as errors. - * Conflict is treated as a success: the repository with conflict is remembered and will be handled later along with all other conflicts. - * If an error happens in one repository, the method doesn't go further in others, and shows a notification. - * - * @return true if merge has succeeded without errors (but possibly with conflicts) in all repositories; - * false if it failed at least in one of them. - */ - private boolean doMerge(@Nonnull Collection repositories) - { - for(GitRepository repository : repositories) - { - GitSimpleEventDetector mergeConflict = new GitSimpleEventDetector(GitSimpleEventDetector.Event.MERGE_CONFLICT); - GitCommandResult result = myGit.merge(repository, myBranchToMerge, Collections.emptyList(), mergeConflict); - if(!result.success()) - { - if(mergeConflict.hasHappened()) - { - myConflictedRepositories.put(repository, Boolean.TRUE); - refresh(repository); - markSuccessful(repository); - } - else - { - fatalError(getCommonErrorTitle(), result.getErrorOutputAsJoinedString()); - return false; - } - } - else - { - refresh(repository); - markSuccessful(repository); - } - } - return true; - } - - @Nonnull - private String getCommonErrorTitle() - { - return "Couldn't merge " + myBranchToMerge; - } - - @Override - protected void rollback() - { - LOG.info("starting rollback..."); - Collection repositoriesForSmartRollback = new ArrayList<>(); - Collection repositoriesForSimpleRollback = new ArrayList<>(); - Collection repositoriesForMergeRollback = new ArrayList<>(); - for(GitRepository repository : getSuccessfulRepositories()) - { - if(myConflictedRepositories.containsKey(repository)) - { - repositoriesForMergeRollback.add(repository); - } - else if(thereAreLocalChangesIn(repository)) - { - repositoriesForSmartRollback.add(repository); - } - else - { - repositoriesForSimpleRollback.add(repository); - } - } - - LOG.info("for smart rollback: " + DvcsUtil.getShortNames(repositoriesForSmartRollback) + - "; for simple rollback: " + DvcsUtil.getShortNames(repositoriesForSimpleRollback) + - "; for merge rollback: " + DvcsUtil.getShortNames(repositoriesForMergeRollback)); - - GitCompoundResult result = smartRollback(repositoriesForSmartRollback); - for(GitRepository repository : repositoriesForSimpleRollback) - { - result.append(repository, rollback(repository)); - } - for(GitRepository repository : repositoriesForMergeRollback) - { - result.append(repository, rollbackMerge(repository)); - } - myConflictedRepositories.clear(); - - if(!result.totalSuccess()) - { - VcsNotifier.getInstance(myProject).notifyError("Error during rollback", result.getErrorOutputWithReposIndication()); - } - LOG.info("rollback finished."); - } - - @Nonnull - private GitCompoundResult smartRollback(@Nonnull final Collection repositories) - { - LOG.info("Starting smart rollback..."); - final GitCompoundResult result = new GitCompoundResult(myProject); - Collection roots = GitUtil.getRootsFromRepositories(repositories); - GitPreservingProcess preservingProcess = new GitPreservingProcess(myProject, myGit, roots, "merge", myBranchToMerge, GitVcsSettings.UpdateChangesPolicy.STASH, getIndicator(), new Runnable() - { - @Override - public void run() - { - for(GitRepository repository : repositories) - { - result.append(repository, rollback(repository)); - } - } - }); - preservingProcess.execute(); - LOG.info("Smart rollback completed."); - return result; - } - - @Nonnull - private GitCommandResult rollback(@Nonnull GitRepository repository) - { - return myGit.reset(repository, GitResetMode.HARD, getInitialRevision(repository)); - } - - @Nonnull - private GitCommandResult rollbackMerge(@Nonnull GitRepository repository) - { - GitCommandResult result = myGit.resetMerge(repository, null); - refresh(repository); - return result; - } - - private boolean thereAreLocalChangesIn(@Nonnull GitRepository repository) - { - return !myChangeListManager.getChangesIn(repository.getRoot()).isEmpty(); - } - - @Nonnull - @Override - public String getSuccessMessage() - { - return String.format("Merged %s to %s", myBranchToMerge, stringifyBranchesByRepos(myCurrentHeads)); - } - - @Nonnull - @Override - protected String getRollbackProposal() - { - return "However merge has succeeded for the following " + repositories() + ":
" + - successfulRepositoriesJoined() + - "
" + ROLLBACK_PROPOSAL; - } - - @Nonnull - @Override - protected String getOperationName() - { - return "merge"; - } - - private void refresh(GitRepository... repositories) - { - for(GitRepository repository : repositories) - { - refreshRoot(repository); - repository.update(); - } - } - - private class MyMergeConflictResolver extends GitMergeCommittingConflictResolver - { - public MyMergeConflictResolver() - { - super(GitMergeOperation.this.myProject, myGit, new GitMerger(GitMergeOperation.this.myProject), GitUtil.getRootsFromRepositories(GitMergeOperation.this.myConflictedRepositories.keySet()) - , new Params(), true); - } - - @Override - protected void notifyUnresolvedRemain() - { - VcsNotifier.getInstance(myProject).notifyImportantWarning("Merged branch " + myBranchToMerge + " with conflicts", "Unresolved conflicts remain in the project. Resolve " + - "now.", getResolveLinkListener()); - } - } - - private class DeleteMergedLocalBranchNotificationListener implements NotificationListener - { - @Override - public void hyperlinkUpdate(@Nonnull Notification notification, @Nonnull HyperlinkEvent event) - { - if(event.getEventType() == HyperlinkEvent.EventType.ACTIVATED && event.getDescription().equalsIgnoreCase("delete")) - { - GitBrancher brancher = ServiceManager.getService(myProject, GitBrancher.class); - brancher.deleteBranch(myBranchToMerge, new ArrayList<>(getRepositories())); - } - } - } +class GitMergeOperation extends GitBranchOperation { + private static final Logger LOG = Logger.getInstance(GitMergeOperation.class); + public static final String ROLLBACK_PROPOSAL = "You may rollback (reset to the commit before merging) not to let branches diverge."; + + @Nonnull + private final ChangeListManager myChangeListManager; + @Nonnull + private final NotificationService myNotificationService; + @Nonnull + private final String myBranchToMerge; + private final GitBrancher.DeleteOnMergeOption myDeleteOnMerge; + + // true in value, if we've stashed local changes before merge and will need to unstash after resolving conflicts. + @Nonnull + private final Map myConflictedRepositories = new HashMap<>(); + private GitPreservingProcess myPreservingProcess; + + GitMergeOperation( + @Nonnull Project project, + @Nonnull Git git, + @Nonnull GitBranchUiHandler uiHandler, + @Nonnull Collection repositories, + @Nonnull String branchToMerge, + GitBrancher.DeleteOnMergeOption deleteOnMerge + ) { + super(project, git, uiHandler, repositories); + myBranchToMerge = branchToMerge; + myDeleteOnMerge = deleteOnMerge; + myChangeListManager = ChangeListManager.getInstance(myProject); + myNotificationService = NotificationService.getInstance(); + } + + @Override + @RequiredUIAccess + protected void execute() { + LOG.info("starting"); + saveAllDocuments(); + boolean fatalErrorHappened = false; + int alreadyUpToDateRepositories = 0; + try (AccessToken token = DvcsUtil.workingTreeChangeStarted(myProject, getOperationName())) { + while (hasMoreRepositories() && !fatalErrorHappened) { + GitRepository repository = next(); + LOG.info("next repository: " + repository); + + VirtualFile root = repository.getRoot(); + GitLocalChangesWouldBeOverwrittenDetector localChangesDetector = + new GitLocalChangesWouldBeOverwrittenDetector(root, GitLocalChangesWouldBeOverwrittenDetector.Operation.MERGE); + GitSimpleEventDetector unmergedFiles = new GitSimpleEventDetector(GitSimpleEventDetector.Event.UNMERGED_PREVENTING_MERGE); + GitUntrackedFilesOverwrittenByOperationDetector untrackedOverwrittenByMerge = + new GitUntrackedFilesOverwrittenByOperationDetector(root); + GitSimpleEventDetector mergeConflict = new GitSimpleEventDetector(GitSimpleEventDetector.Event.MERGE_CONFLICT); + GitSimpleEventDetector alreadyUpToDateDetector = + new GitSimpleEventDetector(GitSimpleEventDetector.Event.ALREADY_UP_TO_DATE); + + GitCommandResult result = myGit.merge( + repository, + myBranchToMerge, + Collections.emptyList(), + localChangesDetector, + unmergedFiles, + untrackedOverwrittenByMerge, + mergeConflict, + alreadyUpToDateDetector + ); + if (result.success()) { + LOG.info("Merged successfully"); + refresh(repository); + markSuccessful(repository); + if (alreadyUpToDateDetector.hasHappened()) { + alreadyUpToDateRepositories += 1; + } + } + else if (unmergedFiles.hasHappened()) { + LOG.info("Unmerged files error!"); + fatalUnmergedFilesError(); + fatalErrorHappened = true; + } + else if (localChangesDetector.wasMessageDetected()) { + LOG.info("Local changes would be overwritten by merge!"); + boolean smartMergeSucceeded = proposeSmartMergePerformAndNotify(repository, localChangesDetector); + if (!smartMergeSucceeded) { + fatalErrorHappened = true; + } + } + else if (mergeConflict.hasHappened()) { + LOG.info("Merge conflict"); + myConflictedRepositories.put(repository, Boolean.FALSE); + refresh(repository); + markSuccessful(repository); + } + else if (untrackedOverwrittenByMerge.wasMessageDetected()) { + LOG.info("Untracked files would be overwritten by merge!"); + fatalUntrackedFilesError(repository.getRoot(), untrackedOverwrittenByMerge.getRelativeFilePaths()); + fatalErrorHappened = true; + } + else { + LOG.info("Unknown error. " + result); + fatalError(getCommonErrorTitle(), result.getErrorOutputAsJoinedValue()); + fatalErrorHappened = true; + } + } + + if (fatalErrorHappened) { + notifyAboutRemainingConflicts(); + } + else { + boolean allConflictsResolved = resolveConflicts(); + if (allConflictsResolved) { + if (alreadyUpToDateRepositories < getRepositories().size()) { + notifySuccess(); + } + else { + notifySuccess(LocalizeValue.localizeTODO("Already up-to-date")); + } + } + } + + restoreLocalChanges(); + } + } + + private void notifyAboutRemainingConflicts() { + if (!myConflictedRepositories.isEmpty()) { + new MyMergeConflictResolver().notifyUnresolvedRemain(); + } + } + + @Override + protected void notifySuccess(@Nonnull LocalizeValue message) { + switch (myDeleteOnMerge) { + case DELETE: + super.notifySuccess(message); + // bg process needs to be started from the EDT + UIUtil.invokeLaterIfNeeded(() -> { + GitBrancher brancher = myProject.getInstance(GitBrancher.class); + brancher.deleteBranch(myBranchToMerge, new ArrayList<>(getRepositories())); + }); + break; + case PROPOSE: + myNotificationService.newInfo(VcsNotifier.NOTIFICATION_GROUP_ID) + .content(LocalizeValue.localizeTODO(message + "
Delete " + myBranchToMerge + "")) + .optionalHyperlinkListener(new DeleteMergedLocalBranchNotificationListener()) + .notify(myProject); + break; + case NOTHING: + super.notifySuccess(message); + break; + } + } + + private boolean resolveConflicts() { + return myConflictedRepositories.isEmpty() || new MyMergeConflictResolver().merge(); + } + + private boolean proposeSmartMergePerformAndNotify( + @Nonnull GitRepository repository, + @Nonnull GitMessageWithFilesDetector localChangesOverwrittenByMerge + ) { + Pair, List> conflictingRepositoriesAndAffectedChanges = + getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByMerge, + myCurrentHeads.get(repository), myBranchToMerge + ); + List allConflictingRepositories = conflictingRepositoriesAndAffectedChanges.getFirst(); + List affectedChanges = conflictingRepositoriesAndAffectedChanges.getSecond(); + + Collection absolutePaths = GitUtil.toAbsolute(repository.getRoot(), localChangesOverwrittenByMerge.getRelativeFilePaths()); + int smartCheckoutDecision = myUiHandler.showSmartOperationDialog(myProject, affectedChanges, absolutePaths, "merge", null); + if (smartCheckoutDecision == GitSmartOperationDialog.SMART_EXIT_CODE) { + return doSmartMerge(allConflictingRepositories); + } + else { + fatalLocalChangesError(myBranchToMerge); + return false; + } + } + + private void restoreLocalChanges() { + if (myPreservingProcess != null) { + myPreservingProcess.load(); + } + } + + private boolean doSmartMerge(@Nonnull Collection repositories) { + AtomicBoolean success = new AtomicBoolean(); + myPreservingProcess = new GitPreservingProcess( + myProject, + myGit, + GitUtil.getRootsFromRepositories(repositories), + "merge", + myBranchToMerge, + GitVcsSettings.UpdateChangesPolicy.STASH, + getIndicator(), + () -> success.set(doMerge(repositories)) + ); + myPreservingProcess.execute(myConflictedRepositories::isEmpty); + return success.get(); + } + + /** + * Performs merge in the given repositories. + * Handle only merge conflict situation: all other cases should have been handled before and are treated as errors. + * Conflict is treated as a success: the repository with conflict is remembered and will be handled later along with all other conflicts. + * If an error happens in one repository, the method doesn't go further in others, and shows a notification. + * + * @return true if merge has succeeded without errors (but possibly with conflicts) in all repositories; + * false if it failed at least in one of them. + */ + private boolean doMerge(@Nonnull Collection repositories) { + for (GitRepository repository : repositories) { + GitSimpleEventDetector mergeConflict = new GitSimpleEventDetector(GitSimpleEventDetector.Event.MERGE_CONFLICT); + GitCommandResult result = myGit.merge(repository, myBranchToMerge, Collections.emptyList(), mergeConflict); + if (!result.success()) { + if (mergeConflict.hasHappened()) { + myConflictedRepositories.put(repository, Boolean.TRUE); + refresh(repository); + markSuccessful(repository); + } + else { + fatalError(getCommonErrorTitle(), result.getErrorOutputAsJoinedValue()); + return false; + } + } + else { + refresh(repository); + markSuccessful(repository); + } + } + return true; + } + + @Nonnull + private LocalizeValue getCommonErrorTitle() { + return LocalizeValue.localizeTODO("Couldn't merge " + myBranchToMerge); + } + + @Override + protected void rollback() { + LOG.info("starting rollback..."); + Collection repositoriesForSmartRollback = new ArrayList<>(); + Collection repositoriesForSimpleRollback = new ArrayList<>(); + Collection repositoriesForMergeRollback = new ArrayList<>(); + for (GitRepository repository : getSuccessfulRepositories()) { + if (myConflictedRepositories.containsKey(repository)) { + repositoriesForMergeRollback.add(repository); + } + else if (thereAreLocalChangesIn(repository)) { + repositoriesForSmartRollback.add(repository); + } + else { + repositoriesForSimpleRollback.add(repository); + } + } + + LOG.info("for smart rollback: " + DvcsUtil.getShortNames(repositoriesForSmartRollback) + + "; for simple rollback: " + DvcsUtil.getShortNames(repositoriesForSimpleRollback) + + "; for merge rollback: " + DvcsUtil.getShortNames(repositoriesForMergeRollback)); + + GitCompoundResult result = smartRollback(repositoriesForSmartRollback); + for (GitRepository repository : repositoriesForSimpleRollback) { + result.append(repository, rollback(repository)); + } + for (GitRepository repository : repositoriesForMergeRollback) { + result.append(repository, rollbackMerge(repository)); + } + myConflictedRepositories.clear(); + + if (!result.totalSuccess()) { + myNotificationService.newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Error during rollback")) + .content(result.getErrorOutputWithReposIndication()) + .notify(myProject); + } + LOG.info("rollback finished."); + } + + @Nonnull + private GitCompoundResult smartRollback(@Nonnull Collection repositories) { + LOG.info("Starting smart rollback..."); + GitCompoundResult result = new GitCompoundResult(myProject); + Collection roots = GitUtil.getRootsFromRepositories(repositories); + GitPreservingProcess preservingProcess = new GitPreservingProcess( + myProject, + myGit, + roots, + "merge", + myBranchToMerge, + GitVcsSettings.UpdateChangesPolicy.STASH, + getIndicator(), + () -> { + for (GitRepository repository : repositories) { + result.append(repository, rollback(repository)); + } + } + ); + preservingProcess.execute(); + LOG.info("Smart rollback completed."); + return result; + } + + @Nonnull + private GitCommandResult rollback(@Nonnull GitRepository repository) { + return myGit.reset(repository, GitResetMode.HARD, getInitialRevision(repository)); + } + + @Nonnull + private GitCommandResult rollbackMerge(@Nonnull GitRepository repository) { + GitCommandResult result = myGit.resetMerge(repository, null); + refresh(repository); + return result; + } + + private boolean thereAreLocalChangesIn(@Nonnull GitRepository repository) { + return !myChangeListManager.getChangesIn(repository.getRoot()).isEmpty(); + } + + @Nonnull + @Override + public LocalizeValue getSuccessMessage() { + return LocalizeValue.localizeTODO(String.format( + "Merged %s to %s", + myBranchToMerge, + stringifyBranchesByRepos(myCurrentHeads) + )); + } + + @Nonnull + @Override + protected String getRollbackProposal() { + return "However merge has succeeded for the following " + repositories() + ":
" + + successfulRepositoriesJoined() + + "
" + ROLLBACK_PROPOSAL; + } + + @Nonnull + @Override + protected String getOperationName() { + return "merge"; + } + + private void refresh(GitRepository... repositories) { + for (GitRepository repository : repositories) { + refreshRoot(repository); + repository.update(); + } + } + + private class MyMergeConflictResolver extends GitMergeCommittingConflictResolver { + public MyMergeConflictResolver() { + super( + GitMergeOperation.this.myProject, + myGit, + new GitMerger(GitMergeOperation.this.myProject), + GitUtil.getRootsFromRepositories(GitMergeOperation.this.myConflictedRepositories.keySet()) + , + new Params(), + true + ); + } + + @Override + protected void notifyUnresolvedRemain() { + myNotificationService.newWarn(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Merged branch " + myBranchToMerge + " with conflicts")) + .content(LocalizeValue.localizeTODO("Unresolved conflicts remain in the project. Resolve now.")) + .optionalHyperlinkListener(getResolveLinkListener()) + .notify(myProject); + } + } + + private class DeleteMergedLocalBranchNotificationListener implements NotificationListener { + @Override + @RequiredUIAccess + public void hyperlinkUpdate(@Nonnull Notification notification, @Nonnull HyperlinkEvent event) { + if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED && event.getDescription().equalsIgnoreCase("delete")) { + GitBrancher brancher = myProject.getInstance(GitBrancher.class); + brancher.deleteBranch(myBranchToMerge, new ArrayList<>(getRepositories())); + } + } + } } diff --git a/plugin/src/main/java/git4idea/branch/GitRenameBranchOperation.java b/plugin/src/main/java/git4idea/branch/GitRenameBranchOperation.java index c91d4e3..115dfbe 100644 --- a/plugin/src/main/java/git4idea/branch/GitRenameBranchOperation.java +++ b/plugin/src/main/java/git4idea/branch/GitRenameBranchOperation.java @@ -15,6 +15,7 @@ */ package git4idea.branch; +import consulo.localize.LocalizeValue; import consulo.project.Project; import consulo.versionControlSystem.VcsNotifier; import git4idea.commands.Git; @@ -23,97 +24,96 @@ import git4idea.repo.GitRepository; import jakarta.annotation.Nonnull; + import java.util.Collection; import java.util.List; -public class GitRenameBranchOperation extends GitBranchOperation -{ - @Nonnull - private final VcsNotifier myNotifier; - @Nonnull - private final String myCurrentName; - @Nonnull - private final String myNewName; +public class GitRenameBranchOperation extends GitBranchOperation { + @Nonnull + private final String myCurrentName; + @Nonnull + private final String myNewName; - public GitRenameBranchOperation(@Nonnull Project project, - @Nonnull Git git, - @Nonnull GitBranchUiHandler uiHandler, - @Nonnull String currentName, - @Nonnull String newName, - @Nonnull List repositories) - { - super(project, git, uiHandler, repositories); - myCurrentName = currentName; - myNewName = newName; - myNotifier = VcsNotifier.getInstance(myProject); - } + public GitRenameBranchOperation( + @Nonnull Project project, + @Nonnull Git git, + @Nonnull GitBranchUiHandler uiHandler, + @Nonnull String currentName, + @Nonnull String newName, + @Nonnull List repositories + ) { + super(project, git, uiHandler, repositories); + myCurrentName = currentName; + myNewName = newName; + } - @Override - protected void execute() - { - while(hasMoreRepositories()) - { - GitRepository repository = next(); - GitCommandResult result = myGit.renameBranch(repository, myCurrentName, myNewName); - if(result.success()) - { - refresh(repository); - markSuccessful(repository); - } - else - { - fatalError("Couldn't rename " + myCurrentName + " to " + myNewName, result.getErrorOutputAsJoinedString()); - return; - } - } - notifySuccess(); - } + @Override + protected void execute() { + while (hasMoreRepositories()) { + GitRepository repository = next(); + GitCommandResult result = myGit.renameBranch(repository, myCurrentName, myNewName); + if (result.success()) { + refresh(repository); + markSuccessful(repository); + } + else { + fatalError( + LocalizeValue.localizeTODO("Couldn't rename " + myCurrentName + " to " + myNewName), + result.getErrorOutputAsJoinedValue() + ); + return; + } + } + notifySuccess(); + } - @Override - protected void rollback() - { - GitCompoundResult result = new GitCompoundResult(myProject); - Collection repositories = getSuccessfulRepositories(); - for(GitRepository repository : repositories) - { - result.append(repository, myGit.renameBranch(repository, myNewName, myCurrentName)); - refresh(repository); - } - if(result.totalSuccess()) - { - myNotifier.notifySuccess("Rollback Successful", "Renamed back to " + myCurrentName); - } - else - { - myNotifier.notifyError("Rollback Failed", result.getErrorOutputWithReposIndication()); - } - } + @Override + protected void rollback() { + GitCompoundResult result = new GitCompoundResult(myProject); + Collection repositories = getSuccessfulRepositories(); + for (GitRepository repository : repositories) { + result.append(repository, myGit.renameBranch(repository, myNewName, myCurrentName)); + refresh(repository); + } + if (result.totalSuccess()) { + myNotificationService.newInfo(VcsNotifier.NOTIFICATION_GROUP_ID) + .title(LocalizeValue.localizeTODO("Rollback Successful")) + .content(LocalizeValue.localizeTODO("Renamed back to " + myCurrentName)) + .notify(myProject); + } + else { + myNotificationService.newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Rollback Failed")) + .content(result.getErrorOutputWithReposIndication()) + .notify(myProject); + } + } - @Nonnull - @Override - public String getSuccessMessage() - { - return String.format("Branch %s was renamed to %s", myCurrentName, myNewName); - } + @Nonnull + @Override + public LocalizeValue getSuccessMessage() { + return LocalizeValue.localizeTODO(String.format( + "Branch %s was renamed to %s", + myCurrentName, + myNewName + )); + } - @Nonnull - @Override - protected String getRollbackProposal() - { - return "However rename has succeeded for the following " + repositories() + ":
" + - successfulRepositoriesJoined() + - "
You may rollback (rename branch back to " + myCurrentName + ") not to let branches diverge."; - } + @Nonnull + @Override + protected String getRollbackProposal() { + return "However rename has succeeded for the following " + repositories() + ":
" + + successfulRepositoriesJoined() + + "
You may rollback (rename branch back to " + myCurrentName + ") not to let branches diverge."; + } - @Nonnull - @Override - protected String getOperationName() - { - return "rename"; - } + @Nonnull + @Override + protected String getOperationName() { + return "rename"; + } - private static void refresh(@Nonnull GitRepository repository) - { - repository.update(); - } + private static void refresh(@Nonnull GitRepository repository) { + repository.update(); + } } diff --git a/plugin/src/main/java/git4idea/checkout/GitCheckoutProvider.java b/plugin/src/main/java/git4idea/checkout/GitCheckoutProvider.java index 6b6e50c..0a32946 100644 --- a/plugin/src/main/java/git4idea/checkout/GitCheckoutProvider.java +++ b/plugin/src/main/java/git4idea/checkout/GitCheckoutProvider.java @@ -19,7 +19,9 @@ import consulo.application.progress.ProgressIndicator; import consulo.application.progress.Task; import consulo.git.localize.GitLocalize; +import consulo.localize.LocalizeValue; import consulo.project.Project; +import consulo.project.ui.notification.NotificationService; import consulo.ui.annotation.RequiredUIAccess; import consulo.versionControlSystem.VcsNotifier; import consulo.versionControlSystem.change.VcsDirtyScopeManager; @@ -51,6 +53,7 @@ public GitCheckoutProvider(@Nonnull Git git) { myGit = git; } + @Nonnull @Override public String getVcsName() { return "_Git"; @@ -58,7 +61,7 @@ public String getVcsName() { @Override @RequiredUIAccess - public void doCheckout(@Nonnull final Project project, @Nullable final Listener listener) { + public void doCheckout(@Nonnull Project project, @Nullable Listener listener) { BasicAction.saveAll(); GitCloneDialog dialog = new GitCloneDialog(project); dialog.show(); @@ -66,8 +69,8 @@ public void doCheckout(@Nonnull final Project project, @Nullable final Listener return; } dialog.rememberSettings(); - final LocalFileSystem lfs = LocalFileSystem.getInstance(); - final File parent = new File(dialog.getParentDirectory()); + LocalFileSystem lfs = LocalFileSystem.getInstance(); + File parent = new File(dialog.getParentDirectory()); VirtualFile destinationParent = lfs.findFileByIoFile(parent); if (destinationParent == null) { destinationParent = lfs.refreshAndFindFileByIoFile(parent); @@ -75,10 +78,10 @@ public void doCheckout(@Nonnull final Project project, @Nullable final Listener if (destinationParent == null) { return; } - final String sourceRepositoryURL = dialog.getSourceRepositoryURL(); - final String directoryName = dialog.getDirectoryName(); - final String parentDirectory = dialog.getParentDirectory(); - final String puttyKey = dialog.getPuttyKeyFile(); + String sourceRepositoryURL = dialog.getSourceRepositoryURL(); + String directoryName = dialog.getDirectoryName(); + String parentDirectory = dialog.getParentDirectory(); + String puttyKey = dialog.getPuttyKeyFile(); clone(project, myGit, listener, destinationParent, sourceRepositoryURL, directoryName, parentDirectory, puttyKey); } @@ -111,7 +114,7 @@ public void onSuccess() { true, () -> { if (project.isOpen() && (!project.isDisposed()) && (!project.isDefault())) { - final VcsDirtyScopeManager mgr = VcsDirtyScopeManager.getInstance(project); + VcsDirtyScopeManager mgr = VcsDirtyScopeManager.getInstance(project); mgr.fileDirty(destinationParent); } } @@ -136,7 +139,7 @@ public static boolean doClone( private static boolean cloneNatively( @Nonnull Project project, - @Nonnull final ProgressIndicator indicator, + @Nonnull ProgressIndicator indicator, @Nonnull Git git, @Nonnull File directory, @Nonnull String url, @@ -149,7 +152,10 @@ private static boolean cloneNatively( if (result.success()) { return true; } - VcsNotifier.getInstance(project).notifyError("Clone failed", result.getErrorOutputAsHtmlString()); + NotificationService.getInstance().newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Clone failed")) + .content(result.getErrorOutputAsHtmlValue()) + .notify(project); return false; } } diff --git a/plugin/src/main/java/git4idea/checkout/GitCloneDialog.java b/plugin/src/main/java/git4idea/checkout/GitCloneDialog.java index 4f5007b..3ae130d 100644 --- a/plugin/src/main/java/git4idea/checkout/GitCloneDialog.java +++ b/plugin/src/main/java/git4idea/checkout/GitCloneDialog.java @@ -15,6 +15,9 @@ */ package git4idea.checkout; +import com.intellij.uiDesigner.core.GridConstraints; +import com.intellij.uiDesigner.core.GridLayoutManager; +import com.intellij.uiDesigner.core.Spacer; import consulo.container.boot.ContainerPathManager; import consulo.fileChooser.FileChooserDescriptor; import consulo.fileChooser.FileChooserDescriptorFactory; @@ -38,9 +41,11 @@ import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; +import java.awt.*; import java.io.File; import java.net.URI; import java.net.URISyntaxException; +import java.util.ResourceBundle; import java.util.regex.Pattern; /** @@ -56,10 +61,10 @@ public class GitCloneDialog extends DialogWrapper { static { // TODO make real URL pattern - final String ch = "[\\p{ASCII}&&[\\p{Graph}]&&[^@:/]]"; - final String host = ch + "+(?:\\." + ch + "+)*"; - final String path = "/?" + ch + "+(?:/" + ch + "+)*/?"; - final String all = "(?:" + ch + "+@)?" + host + ":" + path; + String ch = "[\\p{ASCII}&&[\\p{Graph}]&&[^@:/]]"; + String host = ch + "+(?:\\." + ch + "+)*"; + String path = "/?" + ch + "+(?:/" + ch + "+)*/?"; + String all = "(?:" + ch + "+@)?" + host + ":" + path; SSH_URL_PATTERN = Pattern.compile(all); } @@ -79,6 +84,7 @@ public class GitCloneDialog extends DialogWrapper { public GitCloneDialog(Project project) { super(project, true); myProject = project; + $$$setupUI$$$(); setTitle(GitLocalize.cloneTitle()); setOKButtonText(GitLocalize.cloneButton()); @@ -126,8 +132,8 @@ private void initListeners() { .withHideIgnored(false); myParentDirectory.addActionListener(new ComponentWithBrowseButton.BrowseFolderActionListener<>( - fcd.getTitle(), - fcd.getDescription(), + fcd.getTitleValue(), + fcd.getDescriptionValue(), myParentDirectory, myProject, fcd, @@ -147,7 +153,7 @@ protected VirtualFile getInitialFile() { } }); - final DocumentListener updateOkButtonListener = new DocumentAdapter() { + DocumentListener updateOkButtonListener = new DocumentAdapter() { @Override protected void textChanged(DocumentEvent e) { updateButtons(); @@ -168,6 +174,7 @@ protected void textChanged(DocumentEvent e) { myTestButton.setEnabled(false); } + @RequiredUIAccess private void test() { myTestURL = getCurrentUrlText(); boolean testResult = test(myTestURL); @@ -188,11 +195,12 @@ private void test() { /* * JGit doesn't have ls-remote command independent from repository yet. - * That way, we have a hack here: if http response asked for a password, then the url is at least valid and existant, and we consider + * That way, we have a hack here: if http response asked for a password, then the url is at least valid and existent, and we consider * that the test passed. */ + @RequiredUIAccess private boolean test(String url) { - final GitLineHandlerPasswordRequestAware handler = + GitLineHandlerPasswordRequestAware handler = new GitLineHandlerPasswordRequestAware(myProject, new File("."), GitCommand.LS_REMOTE); handler.setPuttyKey(getPuttyKeyFile()); handler.setUrl(url); @@ -300,13 +308,13 @@ private String getCurrentUrlText() { private void createUIComponents() { myRepositoryURL = new EditorComboBox(""); - final GitRememberedInputs rememberedInputs = GitRememberedInputs.getInstance(); + GitRememberedInputs rememberedInputs = GitRememberedInputs.getInstance(); myRepositoryURL.setHistory(ArrayUtil.toObjectArray(rememberedInputs.getVisitedUrls(), String.class)); myRepositoryURL.addDocumentListener(new consulo.document.event.DocumentAdapter() { @Override public void documentChanged(consulo.document.event.DocumentEvent e) { // enable test button only if something is entered in repository URL - final String url = getCurrentUrlText(); + String url = getCurrentUrlText(); myTestButton.setEnabled(url.length() != 0); if (myDefaultDirectoryName.equals(myDirectoryName.getText()) || myDirectoryName.getText().length() == 0) { // modify field if it was unmodified or blank @@ -318,12 +326,12 @@ public void documentChanged(consulo.document.event.DocumentEvent e) { }); } - public void prependToHistory(final String item) { + public void prependToHistory(String item) { myRepositoryURL.prependItem(item); } public void rememberSettings() { - final GitRememberedInputs rememberedInputs = GitRememberedInputs.getInstance(); + GitRememberedInputs rememberedInputs = GitRememberedInputs.getInstance(); rememberedInputs.addUrl(getSourceRepositoryURL()); rememberedInputs.setCloneParentDir(getParentDirectory()); rememberedInputs.setPuttyKey(getPuttyKeyFile()); @@ -335,7 +343,7 @@ public void rememberSettings() { * @param url an URL to checkout * @return a default repository name */ - private static String defaultDirectoryName(final String url) { + private static String defaultDirectoryName(String url) { String nonSystemName; if (url.endsWith("/" + GitUtil.DOT_GIT) || url.endsWith(File.separator + GitUtil.DOT_GIT)) { nonSystemName = url.substring(0, url.length() - 5); @@ -375,4 +383,305 @@ public JComponent getPreferredFocusedComponent() { protected String getHelpId() { return "reference.VersionControl.Git.CloneRepository"; } + + /** + * Method generated by Consulo GUI Designer + * >>> IMPORTANT!! <<< + * DO NOT edit this method OR call it in your code! + */ + private void $$$setupUI$$$() { + createUIComponents(); + myRootPanel = new JPanel(); + myRootPanel.setLayout(new GridLayoutManager(5, 4, JBUI.emptyInsets(), -1, -1)); + JLabel label1 = new JLabel(); + this.$$$loadLabelText$$$(label1, ResourceBundle.getBundle("git4idea/i18n/GitBundle").getString("clone.repository.url")); + myRootPanel.add( + label1, + new GridConstraints( + 0, + 0, + 1, + 1, + GridConstraints.ANCHOR_WEST, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 0, + false + ) + ); + Spacer spacer1 = new Spacer(); + myRootPanel.add( + spacer1, + new GridConstraints( + 4, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_VERTICAL, + 1, + GridConstraints.SIZEPOLICY_WANT_GROW, + null, + null, + null, + 0, + false + ) + ); + Spacer spacer2 = new Spacer(); + myRootPanel.add( + spacer2, + new GridConstraints( + 4, + 1, + 1, + 3, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_HORIZONTAL, + GridConstraints.SIZEPOLICY_WANT_GROW, + 1, + null, + null, + null, + 0, + false + ) + ); + myRootPanel.add( + myRepositoryURL, + new GridConstraints( + 0, + 1, + 1, + 2, + GridConstraints.ANCHOR_WEST, + GridConstraints.FILL_HORIZONTAL, + GridConstraints.SIZEPOLICY_WANT_GROW, + GridConstraints.SIZEPOLICY_FIXED, + null, + new Dimension(150, -1), + null, + 0, + false + ) + ); + JLabel label2 = new JLabel(); + this.$$$loadLabelText$$$(label2, ResourceBundle.getBundle("git4idea/i18n/GitBundle").getString("clone.parent.dir")); + myRootPanel.add( + label2, + new GridConstraints( + 2, + 0, + 1, + 1, + GridConstraints.ANCHOR_WEST, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 0, + false + ) + ); + myParentDirectory = new TextFieldWithBrowseButton(); + myRootPanel.add( + myParentDirectory, + new GridConstraints( + 2, + 1, + 1, + 3, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_HORIZONTAL, + GridConstraints.SIZEPOLICY_WANT_GROW, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 0, + false + ) + ); + JLabel label3 = new JLabel(); + this.$$$loadLabelText$$$(label3, ResourceBundle.getBundle("git4idea/i18n/GitBundle").getString("clone.dir.name")); + myRootPanel.add( + label3, + new GridConstraints( + 3, + 0, + 1, + 1, + GridConstraints.ANCHOR_WEST, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 0, + false + ) + ); + myTestButton = new JButton(); + this.$$$loadButtonText$$$(myTestButton, ResourceBundle.getBundle("git4idea/i18n/GitBundle").getString("clone.test")); + myRootPanel.add( + myTestButton, + new GridConstraints( + 0, + 3, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_HORIZONTAL, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 0, + false + ) + ); + myDirectoryName = new JTextField(); + myRootPanel.add( + myDirectoryName, + new GridConstraints( + 3, + 1, + 1, + 1, + GridConstraints.ANCHOR_WEST, + GridConstraints.FILL_HORIZONTAL, + GridConstraints.SIZEPOLICY_WANT_GROW, + GridConstraints.SIZEPOLICY_FIXED, + null, + new Dimension(150, -1), + null, + 0, + false + ) + ); + Spacer spacer3 = new Spacer(); + myRootPanel.add( + spacer3, + new GridConstraints( + 3, + 2, + 1, + 2, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_HORIZONTAL, + GridConstraints.SIZEPOLICY_WANT_GROW, + 1, + null, + null, + null, + 0, + false + ) + ); + myPuttyLabel = new JLabel(); + this.$$$loadLabelText$$$(myPuttyLabel, ResourceBundle.getBundle("git4idea/i18n/GitBundle").getString("clone.repository.putty.key")); + myRootPanel.add( + myPuttyLabel, + new GridConstraints( + 1, + 0, + 1, + 1, + GridConstraints.ANCHOR_WEST, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 0, + false + ) + ); + myPuttyKeyChooser = new TextFieldWithBrowseButton(); + myRootPanel.add( + myPuttyKeyChooser, + new GridConstraints( + 1, + 1, + 1, + 3, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_HORIZONTAL, + GridConstraints.SIZEPOLICY_WANT_GROW, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 0, + false + ) + ); + label1.setLabelFor(myRepositoryURL); + label3.setLabelFor(myDirectoryName); + } + + private void $$$loadLabelText$$$(JLabel component, String text) { + StringBuilder result = new StringBuilder(); + boolean haveMnemonic = false; + char mnemonic = '\0'; + int mnemonicIndex = -1; + for (int i = 0; i < text.length(); i++) { + if (text.charAt(i) == '&') { + i++; + if (i == text.length()) { + break; + } + if (!haveMnemonic && text.charAt(i) != '&') { + haveMnemonic = true; + mnemonic = text.charAt(i); + mnemonicIndex = result.length(); + } + } + result.append(text.charAt(i)); + } + component.setText(result.toString()); + if (haveMnemonic) { + component.setDisplayedMnemonic(mnemonic); + component.setDisplayedMnemonicIndex(mnemonicIndex); + } + } + + private void $$$loadButtonText$$$(AbstractButton component, String text) { + StringBuilder result = new StringBuilder(); + boolean haveMnemonic = false; + char mnemonic = '\0'; + int mnemonicIndex = -1; + for (int i = 0; i < text.length(); i++) { + if (text.charAt(i) == '&') { + i++; + if (i == text.length()) { + break; + } + if (!haveMnemonic && text.charAt(i) != '&') { + haveMnemonic = true; + mnemonic = text.charAt(i); + mnemonicIndex = result.length(); + } + } + result.append(text.charAt(i)); + } + component.setText(result.toString()); + if (haveMnemonic) { + component.setMnemonic(mnemonic); + component.setDisplayedMnemonicIndex(mnemonicIndex); + } + } + + public JComponent $$$getRootComponent$$$() { + return myRootPanel; + } } diff --git a/plugin/src/main/java/git4idea/cherrypick/GitCherryPicker.java b/plugin/src/main/java/git4idea/cherrypick/GitCherryPicker.java index 0699e10..b45bd45 100644 --- a/plugin/src/main/java/git4idea/cherrypick/GitCherryPicker.java +++ b/plugin/src/main/java/git4idea/cherrypick/GitCherryPicker.java @@ -18,16 +18,17 @@ import consulo.annotation.component.ExtensionImpl; import consulo.application.AccessToken; import consulo.application.Application; -import consulo.application.ApplicationManager; +import consulo.localize.LocalizeValue; import consulo.logging.Logger; import consulo.project.Project; import consulo.project.ui.notification.Notification; +import consulo.project.ui.notification.NotificationService; import consulo.project.ui.notification.event.NotificationListener; +import consulo.ui.annotation.RequiredUIAccess; import consulo.util.collection.ArrayUtil; import consulo.util.collection.ContainerUtil; import consulo.util.lang.ObjectUtil; import consulo.util.lang.StringUtil; -import consulo.util.lang.function.Condition; import consulo.versionControlSystem.AbstractVcsHelper; import consulo.versionControlSystem.FilePath; import consulo.versionControlSystem.VcsKey; @@ -56,10 +57,10 @@ import git4idea.repo.GitRepository; import git4idea.repo.GitRepositoryManager; import git4idea.util.GitUntrackedFilesHelper; -import jakarta.inject.Inject; - import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import jakarta.inject.Inject; + import javax.swing.event.HyperlinkEvent; import java.io.File; import java.io.IOException; @@ -69,7 +70,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; import static consulo.util.lang.StringUtil.pluralize; import static consulo.versionControlSystem.distributed.DvcsUtil.getShortRepositoryName; @@ -78,653 +78,716 @@ @ExtensionImpl public class GitCherryPicker extends VcsCherryPicker { + private static final Logger LOG = Logger.getInstance(GitCherryPicker.class); - private static final Logger LOG = Logger.getInstance(GitCherryPicker.class); - - @Nonnull - private final Project myProject; - @Nonnull - private final Git myGit; - @Nonnull - private final ChangeListManager myChangeListManager; - @Nonnull - private final GitRepositoryManager myRepositoryManager; - - @Inject - public GitCherryPicker(@Nonnull Project project, @Nonnull Git git) { - myProject = project; - myGit = git; - myChangeListManager = ChangeListManager.getInstance(myProject); - myRepositoryManager = GitUtil.getRepositoryManager(myProject); - } - - public void cherryPick(@Nonnull List commits) { - Map> commitsInRoots = DvcsUtil.groupCommitsByRoots(myRepositoryManager, commits); - LOG.info("Cherry-picking commits: " + toString(commitsInRoots)); - List successfulCommits = new ArrayList<>(); - List alreadyPicked = new ArrayList<>(); - try (AccessToken token = DvcsUtil.workingTreeChangeStarted(myProject, getActionTitle())) { - for (Map.Entry> entry : commitsInRoots.entrySet()) { - GitRepository repository = entry.getKey(); - boolean result = cherryPick(repository, entry.getValue(), successfulCommits, alreadyPicked); - repository.update(); - if (!result) { - return; - } - } - notifyResult(successfulCommits, alreadyPicked); - } - } - - @Nonnull - private static String toString(@Nonnull final Map> commitsInRoots) { - return StringUtil.join(commitsInRoots.keySet(), new Function() { - @Override - public String apply(@Nonnull GitRepository repository) { - String commits = StringUtil.join(commitsInRoots.get(repository), new Function() { - @Override - public String apply(VcsFullCommitDetails details) { - return details.getId().asString(); - } - }, ", "); - return getShortRepositoryName(repository) + ": [" + commits + "]"; - } - }, "; "); - } - - // return true to continue with other roots, false to break execution - private boolean cherryPick(@Nonnull GitRepository repository, - @Nonnull List commits, - @Nonnull List successfulCommits, - @Nonnull List alreadyPicked) { - for (VcsFullCommitDetails commit : commits) { - GitSimpleEventDetector conflictDetector = new GitSimpleEventDetector(CHERRY_PICK_CONFLICT); - GitSimpleEventDetector localChangesOverwrittenDetector = new GitSimpleEventDetector(LOCAL_CHANGES_OVERWRITTEN_BY_CHERRY_PICK); - GitUntrackedFilesOverwrittenByOperationDetector untrackedFilesDetector = - new GitUntrackedFilesOverwrittenByOperationDetector(repository.getRoot()); - boolean autoCommit = isAutoCommit(); - GitCommandResult result = myGit.cherryPick(repository, - commit.getId().asString(), - autoCommit, - conflictDetector, - localChangesOverwrittenDetector, - untrackedFilesDetector); - GitCommitWrapper commitWrapper = new GitCommitWrapper(commit); - if (result.success()) { - if (autoCommit) { - successfulCommits.add(commitWrapper); + @Nonnull + private final Project myProject; + @Nonnull + protected final NotificationService myNotificationService; + @Nonnull + private final Git myGit; + @Nonnull + private final ChangeListManager myChangeListManager; + @Nonnull + private final GitRepositoryManager myRepositoryManager; + + @Inject + public GitCherryPicker(@Nonnull Project project, @Nonnull Git git) { + myProject = project; + myNotificationService = NotificationService.getInstance(); + myGit = git; + myChangeListManager = ChangeListManager.getInstance(myProject); + myRepositoryManager = GitUtil.getRepositoryManager(myProject); + } + + @Override + @RequiredUIAccess + public void cherryPick(@Nonnull List commits) { + Map> commitsInRoots = DvcsUtil.groupCommitsByRoots(myRepositoryManager, commits); + LOG.info("Cherry-picking commits: " + toString(commitsInRoots)); + List successfulCommits = new ArrayList<>(); + List alreadyPicked = new ArrayList<>(); + try (AccessToken token = DvcsUtil.workingTreeChangeStarted(myProject, getActionTitle())) { + for (Map.Entry> entry : commitsInRoots.entrySet()) { + GitRepository repository = entry.getKey(); + boolean result = cherryPick(repository, entry.getValue(), successfulCommits, alreadyPicked); + repository.update(); + if (!result) { + return; + } + } + notifyResult(successfulCommits, alreadyPicked); } - else { - boolean committed = updateChangeListManagerShowCommitDialogAndRemoveChangeListOnSuccess(repository, - commitWrapper, - successfulCommits, - alreadyPicked); - if (!committed) { - notifyCommitCancelled(commitWrapper, successfulCommits); - return false; - } - } - } - else if (conflictDetector.hasHappened()) { - boolean mergeCompleted = - new CherryPickConflictResolver(myProject, myGit, repository.getRoot(), commit.getId().asString(), VcsUserUtil - .getShortPresentation(commit.getAuthor()), - commit.getSubject()).merge(); - - if (mergeCompleted) { - boolean committed = updateChangeListManagerShowCommitDialogAndRemoveChangeListOnSuccess(repository, - commitWrapper, - successfulCommits, - alreadyPicked); - if (!committed) { - notifyCommitCancelled(commitWrapper, successfulCommits); + } + + @Nonnull + private static String toString(@Nonnull Map> commitsInRoots) { + return StringUtil.join( + commitsInRoots.keySet(), + repository -> { + String commits = StringUtil.join( + commitsInRoots.get(repository), + details -> details.getId().asString(), + ", " + ); + return getShortRepositoryName(repository) + ": [" + commits + "]"; + }, + "; " + ); + } + + // return true to continue with other roots, false to break execution + @RequiredUIAccess + private boolean cherryPick( + @Nonnull GitRepository repository, + @Nonnull List commits, + @Nonnull List successfulCommits, + @Nonnull List alreadyPicked + ) { + for (VcsFullCommitDetails commit : commits) { + GitSimpleEventDetector conflictDetector = new GitSimpleEventDetector(CHERRY_PICK_CONFLICT); + GitSimpleEventDetector localChangesOverwrittenDetector = new GitSimpleEventDetector(LOCAL_CHANGES_OVERWRITTEN_BY_CHERRY_PICK); + GitUntrackedFilesOverwrittenByOperationDetector untrackedFilesDetector = + new GitUntrackedFilesOverwrittenByOperationDetector(repository.getRoot()); + boolean autoCommit = isAutoCommit(); + GitCommandResult result = myGit.cherryPick( + repository, + commit.getId().asString(), + autoCommit, + conflictDetector, + localChangesOverwrittenDetector, + untrackedFilesDetector + ); + GitCommitWrapper commitWrapper = new GitCommitWrapper(commit); + if (result.success()) { + if (autoCommit) { + successfulCommits.add(commitWrapper); + } + else { + boolean committed = updateChangeListManagerShowCommitDialogAndRemoveChangeListOnSuccess( + repository, + commitWrapper, + successfulCommits, + alreadyPicked + ); + if (!committed) { + notifyCommitCancelled(commitWrapper, successfulCommits); + return false; + } + } + } + else if (conflictDetector.hasHappened()) { + boolean mergeCompleted = new CherryPickConflictResolver( + myProject, + myGit, + repository.getRoot(), + commit.getId().asString(), + VcsUserUtil.getShortPresentation(commit.getAuthor()), + commit.getSubject() + ).merge(); + + if (mergeCompleted) { + boolean committed = updateChangeListManagerShowCommitDialogAndRemoveChangeListOnSuccess( + repository, + commitWrapper, + successfulCommits, + alreadyPicked + ); + if (!committed) { + notifyCommitCancelled(commitWrapper, successfulCommits); + return false; + } + } + else { + updateChangeListManager(commit); + notifyConflictWarning(repository, commitWrapper, successfulCommits); + return false; + } + } + else if (untrackedFilesDetector.wasMessageDetected()) { + String description = commitDetails(commitWrapper) + "
" + + "Some untracked working tree files would be overwritten by cherry-pick.
" + + "Please move, remove or add them before you can cherry-pick. View them"; + description += getSuccessfulCommitDetailsIfAny(successfulCommits); + + GitUntrackedFilesHelper.notifyUntrackedFilesOverwrittenBy( + myProject, + repository.getRoot(), + untrackedFilesDetector.getRelativeFilePaths(), + "cherry-pick", + description + ); + return false; + } + else if (localChangesOverwrittenDetector.hasHappened()) { + notifyError( + LocalizeValue.localizeTODO( + "Your local changes would be overwritten by cherry-pick.
Commit your changes or stash them to proceed." + ), + commitWrapper, + successfulCommits + ); + return false; + } + else if (isNothingToCommitMessage(result)) { + alreadyPicked.add(commitWrapper); + return true; + } + else { + notifyError(result.getErrorOutputAsHtmlValue(), commitWrapper, successfulCommits); + return false; + } + } + return true; + } + + private static boolean isNothingToCommitMessage(@Nonnull GitCommandResult result) { + if (!result.getErrorOutputAsJoinedString().isEmpty()) { return false; - } } - else { - updateChangeListManager(commit); - notifyConflictWarning(repository, commitWrapper, successfulCommits); - return false; + String stdout = result.getOutputAsJoinedString(); + return stdout.contains("nothing to commit") || stdout.contains("previous cherry-pick is now empty"); + } + + @RequiredUIAccess + private boolean updateChangeListManagerShowCommitDialogAndRemoveChangeListOnSuccess( + @Nonnull GitRepository repository, + @Nonnull GitCommitWrapper commit, + @Nonnull List successfulCommits, + @Nonnull List alreadyPicked + ) { + CherryPickData data = updateChangeListManager(commit.getCommit()); + if (data == null) { + alreadyPicked.add(commit); + return true; } - } - else if (untrackedFilesDetector.wasMessageDetected()) { + boolean committed = showCommitDialogAndWaitForCommit(repository, commit, data.myChangeList, data.myCommitMessage); + if (committed) { + myChangeListManager.removeChangeList(data.myChangeList); + successfulCommits.add(commit); + return true; + } + return false; + } + + private void notifyConflictWarning( + @Nonnull GitRepository repository, + @Nonnull GitCommitWrapper commit, + @Nonnull List successfulCommits + ) { + NotificationListener resolveLinkListener = new ResolveLinkListener( + myProject, + myGit, + repository.getRoot(), + commit.getCommit().getId().toShortString(), + VcsUserUtil.getShortPresentation(commit.getCommit().getAuthor()), + commit.getSubject() + ); String description = - commitDetails(commitWrapper) + "
Some untracked working tree files would be overwritten by cherry-pick.
" + "Please move, remove or add them before you " + - "can cherry-pick. View them"; + commitDetails(commit) + "
Unresolved conflicts remain in the working tree. Resolve them."; description += getSuccessfulCommitDetailsIfAny(successfulCommits); + myNotificationService.newWarn(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Cherry-picked with conflicts")) + .content(LocalizeValue.localizeTODO(description)) + .optionalHyperlinkListener(resolveLinkListener) + .notify(myProject); + } - GitUntrackedFilesHelper.notifyUntrackedFilesOverwrittenBy(myProject, - repository.getRoot(), - untrackedFilesDetector.getRelativeFilePaths(), - "cherry-pick", - description); - return false; - } - else if (localChangesOverwrittenDetector.hasHappened()) { - notifyError("Your local changes would be overwritten by cherry-pick.
Commit your changes or stash them to proceed.", - commitWrapper, - successfulCommits); - return false; - } - else if (isNothingToCommitMessage(result)) { - alreadyPicked.add(commitWrapper); - return true; - } - else { - notifyError(result.getErrorOutputAsHtmlString(), commitWrapper, successfulCommits); - return false; - } - } - return true; - } - - private static boolean isNothingToCommitMessage(@Nonnull GitCommandResult result) { - if (!result.getErrorOutputAsJoinedString().isEmpty()) { - return false; - } - String stdout = result.getOutputAsJoinedString(); - return stdout.contains("nothing to commit") || stdout.contains("previous cherry-pick is now empty"); - } - - private boolean updateChangeListManagerShowCommitDialogAndRemoveChangeListOnSuccess(@Nonnull GitRepository repository, - @Nonnull GitCommitWrapper commit, - @Nonnull List successfulCommits, - @Nonnull List alreadyPicked) { - CherryPickData data = updateChangeListManager(commit.getCommit()); - if (data == null) { - alreadyPicked.add(commit); - return true; - } - boolean committed = showCommitDialogAndWaitForCommit(repository, commit, data.myChangeList, data.myCommitMessage); - if (committed) { - myChangeListManager.removeChangeList(data.myChangeList); - successfulCommits.add(commit); - return true; - } - return false; - } - - private void notifyConflictWarning(@Nonnull GitRepository repository, - @Nonnull GitCommitWrapper commit, - @Nonnull List successfulCommits) { - NotificationListener resolveLinkListener = new ResolveLinkListener(myProject, - myGit, - repository.getRoot(), - commit.getCommit().getId().toShortString(), - VcsUserUtil.getShortPresentation( - commit - .getCommit().getAuthor()), - commit.getSubject()); - String description = - commitDetails(commit) + "
Unresolved conflicts remain in the working tree.
Resolve them."; - description += getSuccessfulCommitDetailsIfAny(successfulCommits); - VcsNotifier.getInstance(myProject).notifyImportantWarning("Cherry-picked with conflicts", description, resolveLinkListener); - } - - private void notifyCommitCancelled(@Nonnull GitCommitWrapper commit, @Nonnull List successfulCommits) { - if (successfulCommits.isEmpty()) { - // don't notify about cancelled commit. Notify just in the case when there were already successful commits in the queue. - return; - } - String description = commitDetails(commit); - description += getSuccessfulCommitDetailsIfAny(successfulCommits); - VcsNotifier.getInstance(myProject).notifyMinorWarning("Cherry-pick cancelled", description, null); - } - - @Nullable - private CherryPickData updateChangeListManager(@Nonnull final VcsFullCommitDetails commit) { - Collection changes = commit.getChanges(); - RefreshVFsSynchronously.updateChanges(changes); - final String commitMessage = createCommitMessage(commit); - final Collection paths = ChangesUtil.getPaths(changes); - LocalChangeList changeList = createChangeListAfterUpdate(commit, paths, commitMessage); - return changeList == null ? null : new CherryPickData(changeList, commitMessage); - } - - @Nullable - private LocalChangeList createChangeListAfterUpdate(@Nonnull final VcsFullCommitDetails commit, - @Nonnull final Collection paths, - @Nonnull final String commitMessage) { - final CountDownLatch waiter = new CountDownLatch(1); - final AtomicReference changeList = new AtomicReference<>(); - final Application application = ApplicationManager.getApplication(); - application.invokeAndWait(new Runnable() { - @Override - public void run() { - myChangeListManager.invokeAfterUpdate(new Runnable() { - public void run() { - changeList.set(createChangeListIfThereAreChanges(commit, commitMessage)); - waiter.countDown(); - } - }, - InvokeAfterUpdateMode.SILENT_CALLBACK_POOLED, - "Cherry-pick", - vcsDirtyScopeManager -> vcsDirtyScopeManager.filePathsDirty(paths, null), - application.getNoneModalityState()); - } - }, application.getNoneModalityState()); - try { - boolean success = waiter.await(100, TimeUnit.SECONDS); - if (!success) { - LOG.error("Couldn't await for changelist manager refresh"); - } - } - catch (InterruptedException e) { - LOG.error(e); - return null; - } - - return changeList.get(); - } - - @Nonnull - private static String createCommitMessage(@Nonnull VcsFullCommitDetails commit) { - return commit.getFullMessage() + "\n\n(cherry picked from commit " + commit.getId().toShortString() + ")"; - } - - private boolean showCommitDialogAndWaitForCommit(@Nonnull final GitRepository repository, - @Nonnull final GitCommitWrapper commit, - @Nonnull final LocalChangeList changeList, - @Nonnull final String commitMessage) { - final AtomicBoolean commitSucceeded = new AtomicBoolean(); - final Semaphore sem = new Semaphore(0); - ApplicationManager.getApplication().invokeAndWait(new Runnable() { - @Override - public void run() { + private void notifyCommitCancelled(@Nonnull GitCommitWrapper commit, @Nonnull List successfulCommits) { + if (successfulCommits.isEmpty()) { + // don't notify about cancelled commit. Notify just in the case when there were already successful commits in the queue. + return; + } + String description = commitDetails(commit); + description += getSuccessfulCommitDetailsIfAny(successfulCommits); + myNotificationService.newWarn(VcsNotifier.STANDARD_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Cherry-pick cancelled")) + .content(LocalizeValue.localizeTODO(description)) + .notify(myProject); + } + + @Nullable + @RequiredUIAccess + private CherryPickData updateChangeListManager(@Nonnull VcsFullCommitDetails commit) { + Collection changes = commit.getChanges(); + RefreshVFsSynchronously.updateChanges(changes); + String commitMessage = createCommitMessage(commit); + Collection paths = ChangesUtil.getPaths(changes); + LocalChangeList changeList = createChangeListAfterUpdate(commit, paths, commitMessage); + return changeList == null ? null : new CherryPickData(changeList, commitMessage); + } + + @Nullable + @RequiredUIAccess + private LocalChangeList createChangeListAfterUpdate( + @Nonnull VcsFullCommitDetails commit, + @Nonnull Collection paths, + @Nonnull String commitMessage + ) { + CountDownLatch waiter = new CountDownLatch(1); + AtomicReference changeList = new AtomicReference<>(); + Application application = myProject.getApplication(); + application.invokeAndWait( + () -> myChangeListManager.invokeAfterUpdate( + () -> { + changeList.set(createChangeListIfThereAreChanges(commit, commitMessage)); + waiter.countDown(); + }, + InvokeAfterUpdateMode.SILENT_CALLBACK_POOLED, + "Cherry-pick", + vcsDirtyScopeManager -> vcsDirtyScopeManager.filePathsDirty(paths, null), + application.getNoneModalityState() + ), + application.getNoneModalityState() + ); try { - cancelCherryPick(repository); - Collection changes = commit.getCommit().getChanges(); - boolean commitNotCancelled = - AbstractVcsHelper.getInstance(myProject).commitChanges(changes, changeList, commitMessage, new CommitResultHandler() { - @Override - public void onSuccess(@Nonnull String commitMessage) { - commit.setActualSubject(commitMessage); - commitSucceeded.set(true); - sem.release(); - } - - @Override - public void onFailure() { - commitSucceeded.set(false); - sem.release(); - } - }); - - if (!commitNotCancelled) { - commitSucceeded.set(false); - sem.release(); - } + boolean success = waiter.await(100, TimeUnit.SECONDS); + if (!success) { + LOG.error("Couldn't await for changelist manager refresh"); + } } - catch (Throwable t) { - LOG.error(t); - commitSucceeded.set(false); - sem.release(); + catch (InterruptedException e) { + LOG.error(e); + return null; } - } - }, ApplicationManager.getApplication().getNoneModalityState()); - // need additional waiting, because commitChanges is asynchronous - try { - sem.acquire(); + return changeList.get(); } - catch (InterruptedException e) { - LOG.error(e); - return false; - } - return commitSucceeded.get(); - } - /** - * We control the cherry-pick workflow ourselves + we want to use partial commits ('git commit --only'), which is prohibited during - * cherry-pick, i.e. until the CHERRY_PICK_HEAD exists. - */ - private void cancelCherryPick(@Nonnull GitRepository repository) { - if (isAutoCommit()) { - removeCherryPickHead(repository); - } - } - - private void removeCherryPickHead(@Nonnull GitRepository repository) { - File cherryPickHeadFile = repository.getRepositoryFiles().getCherryPickHead(); - final VirtualFile cherryPickHead = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(cherryPickHeadFile); - - if (cherryPickHead != null && cherryPickHead.exists()) { - ApplicationManager.getApplication().runWriteAction(new Runnable() { - @Override - public void run() { - try { - cherryPickHead.delete(this); - } - catch (IOException e) { - // if CHERRY_PICK_HEAD is not deleted, the partial commit will fail, and the user will be notified anyway. - // So here we just log the fact. It is happens relatively often, maybe some additional solution will follow. - LOG.error(e); - } - } - }); - } - else { - LOG.info("Cancel cherry-pick in " + repository.getPresentableUrl() + ": no CHERRY_PICK_HEAD found"); - } - } - - private void notifyError(@Nonnull String content, - @Nonnull GitCommitWrapper failedCommit, - @Nonnull List successfulCommits) { - String description = commitDetails(failedCommit) + "
" + content; - description += getSuccessfulCommitDetailsIfAny(successfulCommits); - VcsNotifier.getInstance(myProject).notifyError("Cherry-pick failed", description); - } - - @Nonnull - private static String getSuccessfulCommitDetailsIfAny(@Nonnull List successfulCommits) { - String description = ""; - if (!successfulCommits.isEmpty()) { - description += "
However cherry-pick succeeded for the following " + pluralize("commit", successfulCommits.size()) + ":
"; - description += getCommitsDetails(successfulCommits); - } - return description; - } - - private void notifyResult(@Nonnull List successfulCommits, @Nonnull List alreadyPicked) { - if (alreadyPicked.isEmpty()) { - VcsNotifier.getInstance(myProject).notifySuccess("Cherry-pick successful", getCommitsDetails(successfulCommits)); - } - else if (!successfulCommits.isEmpty()) { - String title = - String.format("Cherry-picked %d commits from %d", successfulCommits.size(), successfulCommits.size() + alreadyPicked.size()); - String description = getCommitsDetails(successfulCommits) + "
" + formAlreadyPickedDescription(alreadyPicked, true); - VcsNotifier.getInstance(myProject).notifySuccess(title, description); - } - else { - VcsNotifier.getInstance(myProject) - .notifyImportantWarning("Nothing to cherry-pick", formAlreadyPickedDescription(alreadyPicked, false)); - } - } - - @Nonnull - private static String formAlreadyPickedDescription(@Nonnull List alreadyPicked, boolean but) { - String hashes = StringUtil.join(alreadyPicked, commit -> commit.getCommit().getId().toShortString(), ", "); - if (but) { - String wasnt = alreadyPicked.size() == 1 ? "wasn't" : "weren't"; - String it = alreadyPicked.size() == 1 ? "it" : "them"; - return String.format("%s %s picked, because all changes from %s have already been applied.", hashes, wasnt, it); - } - return String.format("All changes from %s have already been applied", hashes); - } - - @Nonnull - private static String getCommitsDetails(@Nonnull List successfulCommits) { - String description = ""; - for (GitCommitWrapper commit : successfulCommits) { - description += commitDetails(commit) + "
"; - } - return description.substring(0, description.length() - "
".length()); - } - - @Nonnull - private static String commitDetails(@Nonnull GitCommitWrapper commit) { - return commit.getCommit().getId().toShortString() + " " + commit.getOriginalSubject(); - } - - @Nullable - private LocalChangeList createChangeListIfThereAreChanges(@Nonnull VcsFullCommitDetails commit, @Nonnull String commitMessage) { - Collection originalChanges = commit.getChanges(); - if (originalChanges.isEmpty()) { - LOG.info("Empty commit " + commit.getId()); - return null; - } - if (noChangesAfterCherryPick(originalChanges)) { - LOG.info("No changes after cherry-picking " + commit.getId()); - return null; - } - - String changeListName = createNameForChangeList(commitMessage, 0).replace('\n', ' '); - LocalChangeList createdChangeList = myChangeListManager.addChangeList(changeListName, commitMessage, commit); - LocalChangeList actualChangeList = moveChanges(originalChanges, createdChangeList); - if (actualChangeList != null && !actualChangeList.getChanges().isEmpty()) { - return createdChangeList; - } - LOG.warn("No changes were moved to the changelist. Changes from commit: " + originalChanges + "\nAll changes: " + myChangeListManager.getAllChanges()); - myChangeListManager.removeChangeList(createdChangeList); - return null; - } - - private boolean noChangesAfterCherryPick(@Nonnull Collection originalChanges) { - final Collection allChanges = myChangeListManager.getAllChanges(); - return !ContainerUtil.exists(originalChanges, new Condition() { - @Override - public boolean value(Change change) { - return allChanges.contains(change); - } - }); - } - - @Nullable - private LocalChangeList moveChanges(@Nonnull Collection originalChanges, @Nonnull final LocalChangeList targetChangeList) { - Collection localChanges = GitUtil.findCorrespondentLocalChanges(myChangeListManager, originalChanges); - - // 1. We have to listen to CLM changes, because moveChangesTo is asynchronous - // 2. We have to collect the real target change list, because the original target list (passed to moveChangesTo) is not updated in time. - final CountDownLatch moveChangesWaiter = new CountDownLatch(1); - final AtomicReference resultingChangeList = new AtomicReference<>(); - ChangeListAdapter listener = new ChangeListAdapter() { - @Override - public void changesMoved(Collection changes, ChangeList fromList, ChangeList toList) { - if (toList instanceof LocalChangeList && targetChangeList.getId().equals(((LocalChangeList)toList).getId())) { - resultingChangeList.set((LocalChangeList)toList); - moveChangesWaiter.countDown(); - } - } - }; - try { - myChangeListManager.addChangeListListener(listener); - myChangeListManager.moveChangesTo(targetChangeList, ArrayUtil.toObjectArray(localChanges, Change.class)); - boolean success = moveChangesWaiter.await(100, TimeUnit.SECONDS); - if (!success) { - LOG.error("Couldn't await for changes move."); - } - return resultingChangeList.get(); - } - catch (InterruptedException e) { - LOG.error(e); - return null; - } - finally { - myChangeListManager.removeChangeListListener(listener); - } - } - - @Nonnull - private String createNameForChangeList(@Nonnull String proposedName, int step) { - for (LocalChangeList list : myChangeListManager.getChangeLists()) { - if (list.getName().equals(nameWithStep(proposedName, step))) { - return createNameForChangeList(proposedName, step + 1); - } - } - return nameWithStep(proposedName, step); - } - - private static String nameWithStep(String name, int step) { - return step == 0 ? name : name + "-" + step; - } - - @Nonnull - @Override - public VcsKey getSupportedVcs() { - return GitVcs.getKey(); - } - - @Nonnull - @Override - public String getActionTitle() { - return "Cherry-Pick"; - } - - private boolean isAutoCommit() { - return GitVcsSettings.getInstance(myProject).isAutoCommitOnCherryPick(); - } - - @Override - public boolean canHandleForRoots(@Nonnull Collection roots) { - return roots.stream().allMatch(r -> myRepositoryManager.getRepositoryForRoot(r) != null); - } - - @Override - public String getInfo(@Nonnull VcsLog log, @Nonnull Map> commits) { - int commitsNum = commits.values().size(); - for (VirtualFile root : commits.keySet()) { - // all these roots already related to this cherry-picker - GitRepository repository = ObjectUtil.assertNotNull(myRepositoryManager.getRepositoryForRoot(root)); - for (Hash commit : commits.get(root)) { - GitLocalBranch currentBranch = repository.getCurrentBranch(); - Collection containingBranches = log.getContainingBranches(commit, root); - if (currentBranch != null && containingBranches != null && containingBranches.contains(currentBranch.getName())) { - // already in the current branch - return String.format("The current branch already contains %s the selected %s", - commitsNum > 1 ? "one of" : "", - pluralize("commit", commitsNum)); - } - } - } - return null; - } - - private static class CherryPickData { @Nonnull - private final LocalChangeList myChangeList; - @Nonnull - private final String myCommitMessage; - - private CherryPickData(@Nonnull LocalChangeList list, @Nonnull String message) { - myChangeList = list; - myCommitMessage = message; + private static String createCommitMessage(@Nonnull VcsFullCommitDetails commit) { + return commit.getFullMessage() + "\n\n(cherry picked from commit " + commit.getId().toShortString() + ")"; + } + + @RequiredUIAccess + private boolean showCommitDialogAndWaitForCommit( + @Nonnull GitRepository repository, + @Nonnull final GitCommitWrapper commit, + @Nonnull LocalChangeList changeList, + @Nonnull String commitMessage + ) { + final AtomicBoolean commitSucceeded = new AtomicBoolean(); + final Semaphore sem = new Semaphore(0); + myProject.getApplication().invokeAndWait( + () -> { + try { + cancelCherryPick(repository); + Collection changes = commit.getCommit().getChanges(); + boolean commitNotCancelled = AbstractVcsHelper.getInstance(myProject).commitChanges( + changes, + changeList, + commitMessage, + new CommitResultHandler() { + @Override + public void onSuccess(@Nonnull String commitMessage1) { + commit.setActualSubject(commitMessage1); + commitSucceeded.set(true); + sem.release(); + } + + @Override + public void onFailure() { + commitSucceeded.set(false); + sem.release(); + } + } + ); + + if (!commitNotCancelled) { + commitSucceeded.set(false); + sem.release(); + } + } + catch (Throwable t) { + LOG.error(t); + commitSucceeded.set(false); + sem.release(); + } + }, + myProject.getApplication().getNoneModalityState() + ); + + // need additional waiting, because commitChanges is asynchronous + try { + sem.acquire(); + } + catch (InterruptedException e) { + LOG.error(e); + return false; + } + return commitSucceeded.get(); } - } - private static class CherryPickConflictResolver extends GitConflictResolver { + /** + * We control the cherry-pick workflow ourselves + we want to use partial commits ('git commit --only'), which is prohibited during + * cherry-pick, i.e. until the CHERRY_PICK_HEAD exists. + */ + @RequiredUIAccess + private void cancelCherryPick(@Nonnull GitRepository repository) { + if (isAutoCommit()) { + removeCherryPickHead(repository); + } + } - public CherryPickConflictResolver(@Nonnull Project project, - @Nonnull Git git, - @Nonnull VirtualFile root, - @Nonnull String commitHash, - @Nonnull String commitAuthor, - @Nonnull String commitMessage) { - super(project, git, Collections.singleton(root), makeParams(commitHash, commitAuthor, commitMessage)); + @RequiredUIAccess + private void removeCherryPickHead(@Nonnull GitRepository repository) { + File cherryPickHeadFile = repository.getRepositoryFiles().getCherryPickHead(); + final VirtualFile cherryPickHead = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(cherryPickHeadFile); + + if (cherryPickHead != null && cherryPickHead.exists()) { + myProject.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + try { + cherryPickHead.delete(this); + } + catch (IOException e) { + // if CHERRY_PICK_HEAD is not deleted, the partial commit will fail, and the user will be notified anyway. + // So here we just log the fact. It is happens relatively often, maybe some additional solution will follow. + LOG.error(e); + } + } + }); + } + else { + LOG.info("Cancel cherry-pick in " + repository.getPresentableUrl() + ": no CHERRY_PICK_HEAD found"); + } } - private static Params makeParams(String commitHash, String commitAuthor, String commitMessage) { - Params params = new Params(); - params.setErrorNotificationTitle("Cherry-picked with conflicts"); - params.setMergeDialogCustomizer(new CherryPickMergeDialogCustomizer(commitHash, commitAuthor, commitMessage)); - return params; + private void notifyError( + @Nonnull LocalizeValue content, + @Nonnull GitCommitWrapper failedCommit, + @Nonnull List successfulCommits + ) { + String description = commitDetails(failedCommit) + "
" + content + getSuccessfulCommitDetailsIfAny(successfulCommits); + myNotificationService.newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Cherry-pick failed")) + .content(LocalizeValue.localizeTODO(description)) + .notify(myProject); } - @Override - protected void notifyUnresolvedRemain() { - // we show a [possibly] compound notification after cherry-picking all commits. + @Nonnull + private static String getSuccessfulCommitDetailsIfAny(@Nonnull List successfulCommits) { + String description = ""; + if (!successfulCommits.isEmpty()) { + description += + "
However cherry-pick succeeded for the following " + pluralize("commit", successfulCommits.size()) + ":
"; + description += getCommitsDetails(successfulCommits); + } + return description; } - } + private void notifyResult(@Nonnull List successfulCommits, @Nonnull List alreadyPicked) { + if (alreadyPicked.isEmpty()) { + myNotificationService.newInfo(VcsNotifier.NOTIFICATION_GROUP_ID) + .title(LocalizeValue.localizeTODO("Cherry-pick successful")) + .content(LocalizeValue.localizeTODO(getCommitsDetails(successfulCommits))) + .notify(myProject); + } + else if (!successfulCommits.isEmpty()) { + String title = String.format( + "Cherry-picked %d commits from %d", + successfulCommits.size(), + successfulCommits.size() + alreadyPicked.size() + ); + String description = getCommitsDetails(successfulCommits) + "
" + formAlreadyPickedDescription(alreadyPicked, true); + myNotificationService.newInfo(VcsNotifier.NOTIFICATION_GROUP_ID) + .title(LocalizeValue.localizeTODO(title)) + .content(LocalizeValue.localizeTODO(description)) + .notify(myProject); + } + else { + myNotificationService.newWarn(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Nothing to cherry-pick")) + .content(LocalizeValue.localizeTODO(formAlreadyPickedDescription(alreadyPicked, false))) + .notify(myProject); + } + } - private static class ResolveLinkListener implements NotificationListener { - @Nonnull - private final Project myProject; - @Nonnull - private final Git myGit; @Nonnull - private final VirtualFile myRoot; - @Nonnull - private final String myHash; + private static String formAlreadyPickedDescription(@Nonnull List alreadyPicked, boolean but) { + String hashes = StringUtil.join(alreadyPicked, commit -> commit.getCommit().getId().toShortString(), ", "); + if (but) { + String wasnt = alreadyPicked.size() == 1 ? "wasn't" : "weren't"; + String it = alreadyPicked.size() == 1 ? "it" : "them"; + return String.format("%s %s picked, because all changes from %s have already been applied.", hashes, wasnt, it); + } + return String.format("All changes from %s have already been applied", hashes); + } + @Nonnull - private final String myAuthor; + private static String getCommitsDetails(@Nonnull List successfulCommits) { + String description = ""; + for (GitCommitWrapper commit : successfulCommits) { + description += commitDetails(commit) + "
"; + } + return description.substring(0, description.length() - "
".length()); + } + @Nonnull - private final String myMessage; + private static String commitDetails(@Nonnull GitCommitWrapper commit) { + return commit.getCommit().getId().toShortString() + " " + commit.getOriginalSubject(); + } - public ResolveLinkListener(@Nonnull Project project, - @Nonnull Git git, - @Nonnull VirtualFile root, - @Nonnull String commitHash, - @Nonnull String commitAuthor, - @Nonnull String commitMessage) { + @Nullable + private LocalChangeList createChangeListIfThereAreChanges(@Nonnull VcsFullCommitDetails commit, @Nonnull String commitMessage) { + Collection originalChanges = commit.getChanges(); + if (originalChanges.isEmpty()) { + LOG.info("Empty commit " + commit.getId()); + return null; + } + if (noChangesAfterCherryPick(originalChanges)) { + LOG.info("No changes after cherry-picking " + commit.getId()); + return null; + } - myProject = project; - myGit = git; - myRoot = root; - myHash = commitHash; - myAuthor = commitAuthor; - myMessage = commitMessage; + String changeListName = createNameForChangeList(commitMessage, 0).replace('\n', ' '); + LocalChangeList createdChangeList = myChangeListManager.addChangeList(changeListName, commitMessage, commit); + LocalChangeList actualChangeList = moveChanges(originalChanges, createdChangeList); + if (actualChangeList != null && !actualChangeList.getChanges().isEmpty()) { + return createdChangeList; + } + LOG.warn( + "No changes were moved to the changelist. Changes from commit: " + originalChanges + "\n" + + "All changes: " + myChangeListManager.getAllChanges() + ); + myChangeListManager.removeChangeList(createdChangeList); + return null; + } + + private boolean noChangesAfterCherryPick(@Nonnull Collection originalChanges) { + Collection allChanges = myChangeListManager.getAllChanges(); + return !ContainerUtil.exists(originalChanges, allChanges::contains); + } + + @Nullable + private LocalChangeList moveChanges(@Nonnull Collection originalChanges, @Nonnull final LocalChangeList targetChangeList) { + Collection localChanges = GitUtil.findCorrespondentLocalChanges(myChangeListManager, originalChanges); + + // 1. We have to listen to CLM changes, because moveChangesTo is asynchronous + // 2. We have to collect the real target change list, + // because the original target list (passed to moveChangesTo) is not updated in time. + final CountDownLatch moveChangesWaiter = new CountDownLatch(1); + final AtomicReference resultingChangeList = new AtomicReference<>(); + ChangeListAdapter listener = new ChangeListAdapter() { + @Override + public void changesMoved(Collection changes, ChangeList fromList, ChangeList toList) { + if (toList instanceof LocalChangeList localChangeList && targetChangeList.getId().equals(localChangeList.getId())) { + resultingChangeList.set(localChangeList); + moveChangesWaiter.countDown(); + } + } + }; + try { + myChangeListManager.addChangeListListener(listener); + myChangeListManager.moveChangesTo(targetChangeList, ArrayUtil.toObjectArray(localChanges, Change.class)); + boolean success = moveChangesWaiter.await(100, TimeUnit.SECONDS); + if (!success) { + LOG.error("Couldn't await for changes move."); + } + return resultingChangeList.get(); + } + catch (InterruptedException e) { + LOG.error(e); + return null; + } + finally { + myChangeListManager.removeChangeListListener(listener); + } } - @Override - public void hyperlinkUpdate(@Nonnull Notification notification, @Nonnull HyperlinkEvent event) { - if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { - if (event.getDescription().equals("resolve")) { - new CherryPickConflictResolver(myProject, myGit, myRoot, myHash, myAuthor, myMessage).mergeNoProceed(); + @Nonnull + private String createNameForChangeList(@Nonnull String proposedName, int step) { + for (LocalChangeList list : myChangeListManager.getChangeLists()) { + if (list.getName().equals(nameWithStep(proposedName, step))) { + return createNameForChangeList(proposedName, step + 1); + } } - } + return nameWithStep(proposedName, step); } - } - - private static class CherryPickMergeDialogCustomizer extends MergeDialogCustomizer { - private String myCommitHash; - private String myCommitAuthor; - private String myCommitMessage; + private static String nameWithStep(String name, int step) { + return step == 0 ? name : name + "-" + step; + } - public CherryPickMergeDialogCustomizer(String commitHash, String commitAuthor, String commitMessage) { - myCommitHash = commitHash; - myCommitAuthor = commitAuthor; - myCommitMessage = commitMessage; + @Nonnull + @Override + public VcsKey getSupportedVcs() { + return GitVcs.getKey(); } + @Nonnull @Override - public String getMultipleFileMergeDescription(@Nonnull Collection files) { - return "Conflicts during cherry-picking commit " + myCommitHash + " made by " + myCommitAuthor + "
" + "\"" + myCommitMessage + "\""; + public String getActionTitle() { + return "Cherry-Pick"; + } + + private boolean isAutoCommit() { + return GitVcsSettings.getInstance(myProject).isAutoCommitOnCherryPick(); } @Override - public String getLeftPanelTitle(@Nonnull VirtualFile file) { - return "Local changes"; + public boolean canHandleForRoots(@Nonnull Collection roots) { + return roots.stream().allMatch(r -> myRepositoryManager.getRepositoryForRoot(r) != null); } @Override - public String getRightPanelTitle(@Nonnull VirtualFile file, VcsRevisionNumber revisionNumber) { - return "Changes from cherry-pick " + myCommitHash + ""; + public String getInfo(@Nonnull VcsLog log, @Nonnull Map> commits) { + int commitsNum = commits.values().size(); + for (VirtualFile root : commits.keySet()) { + // all these roots already related to this cherry-picker + GitRepository repository = ObjectUtil.assertNotNull(myRepositoryManager.getRepositoryForRoot(root)); + for (Hash commit : commits.get(root)) { + GitLocalBranch currentBranch = repository.getCurrentBranch(); + Collection containingBranches = log.getContainingBranches(commit, root); + if (currentBranch != null && containingBranches != null && containingBranches.contains(currentBranch.getName())) { + // already in the current branch + return String.format( + "The current branch already contains %s the selected %s", + commitsNum > 1 ? "one of" : "", + pluralize("commit", commitsNum) + ); + } + } + } + return null; } - } - /** - * This class is needed to hold both the original GitCommit, and the commit message which could be changed by the user. - * Only the subject of the commit message is needed. - */ - private static class GitCommitWrapper { - @Nonnull - private final VcsFullCommitDetails myOriginalCommit; - @Nonnull - private String myActualSubject; + private static class CherryPickData { + @Nonnull + private final LocalChangeList myChangeList; + @Nonnull + private final String myCommitMessage; - private GitCommitWrapper(@Nonnull VcsFullCommitDetails commit) { - myOriginalCommit = commit; - myActualSubject = commit.getSubject(); + private CherryPickData(@Nonnull LocalChangeList list, @Nonnull String message) { + myChangeList = list; + myCommitMessage = message; + } } - @Nonnull - public String getSubject() { - return myActualSubject; + private static class CherryPickConflictResolver extends GitConflictResolver { + public CherryPickConflictResolver( + @Nonnull Project project, + @Nonnull Git git, + @Nonnull VirtualFile root, + @Nonnull String commitHash, + @Nonnull String commitAuthor, + @Nonnull String commitMessage + ) { + super(project, git, Collections.singleton(root), makeParams(commitHash, commitAuthor, commitMessage)); + } + + private static Params makeParams(String commitHash, String commitAuthor, String commitMessage) { + Params params = new Params(); + params.setErrorNotificationTitle("Cherry-picked with conflicts"); + params.setMergeDialogCustomizer(new CherryPickMergeDialogCustomizer(commitHash, commitAuthor, commitMessage)); + return params; + } + + @Override + protected void notifyUnresolvedRemain() { + // we show a [possibly] compound notification after cherry-picking all commits. + } } - public void setActualSubject(@Nonnull String actualSubject) { - myActualSubject = actualSubject; + private static class ResolveLinkListener implements NotificationListener { + @Nonnull + private final Project myProject; + @Nonnull + private final Git myGit; + @Nonnull + private final VirtualFile myRoot; + @Nonnull + private final String myHash; + @Nonnull + private final String myAuthor; + @Nonnull + private final String myMessage; + + public ResolveLinkListener( + @Nonnull Project project, + @Nonnull Git git, + @Nonnull VirtualFile root, + @Nonnull String commitHash, + @Nonnull String commitAuthor, + @Nonnull String commitMessage + ) { + myProject = project; + myGit = git; + myRoot = root; + myHash = commitHash; + myAuthor = commitAuthor; + myMessage = commitMessage; + } + + @Override + @RequiredUIAccess + public void hyperlinkUpdate(@Nonnull Notification notification, @Nonnull HyperlinkEvent event) { + if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED && "resolve".equals(event.getDescription())) { + new CherryPickConflictResolver(myProject, myGit, myRoot, myHash, myAuthor, myMessage).mergeNoProceed(); + } + } } - @Nonnull - public VcsFullCommitDetails getCommit() { - return myOriginalCommit; + private static class CherryPickMergeDialogCustomizer extends MergeDialogCustomizer { + private String myCommitHash; + private String myCommitAuthor; + private String myCommitMessage; + + public CherryPickMergeDialogCustomizer(String commitHash, String commitAuthor, String commitMessage) { + myCommitHash = commitHash; + myCommitAuthor = commitAuthor; + myCommitMessage = commitMessage; + } + + @Override + public String getMultipleFileMergeDescription(@Nonnull Collection files) { + return "Conflicts during cherry-picking commit " + myCommitHash + " made by " + myCommitAuthor + "
" + + "\"" + myCommitMessage + "\""; + } + + @Override + public String getLeftPanelTitle(@Nonnull VirtualFile file) { + return "Local changes"; + } + + @Override + public String getRightPanelTitle(@Nonnull VirtualFile file, VcsRevisionNumber revisionNumber) { + return "Changes from cherry-pick " + myCommitHash + ""; + } } - public String getOriginalSubject() { - return myOriginalCommit.getSubject(); + /** + * This class is needed to hold both the original GitCommit, and the commit message which could be changed by the user. + * Only the subject of the commit message is needed. + */ + private static class GitCommitWrapper { + @Nonnull + private final VcsFullCommitDetails myOriginalCommit; + @Nonnull + private String myActualSubject; + + private GitCommitWrapper(@Nonnull VcsFullCommitDetails commit) { + myOriginalCommit = commit; + myActualSubject = commit.getSubject(); + } + + @Nonnull + public String getSubject() { + return myActualSubject; + } + + public void setActualSubject(@Nonnull String actualSubject) { + myActualSubject = actualSubject; + } + + @Nonnull + public VcsFullCommitDetails getCommit() { + return myOriginalCommit; + } + + public String getOriginalSubject() { + return myOriginalCommit.getSubject(); + } } - } } diff --git a/plugin/src/main/java/git4idea/commands/Git.java b/plugin/src/main/java/git4idea/commands/Git.java index 40b2f79..d44884b 100644 --- a/plugin/src/main/java/git4idea/commands/Git.java +++ b/plugin/src/main/java/git4idea/commands/Git.java @@ -28,6 +28,7 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; + import java.io.File; import java.util.Collection; import java.util.List; @@ -36,191 +37,224 @@ @ServiceAPI(ComponentScope.APPLICATION) public interface Git { - @Nonnull - static Git getInstance() { - return ServiceManager.getService(Git.class); - } - - /** - * A generic method to run a Git command, when existing methods like {@link #fetch(GitRepository, String, String, List, String...)} - * are not sufficient. - * - * @param handlerConstructor this is needed, since the operation may need to repeat (e.g. in case of authentication failure). - * make sure to supply a stateless constructor. - */ - @Nonnull - GitCommandResult runCommand(@Nonnull Supplier handlerConstructor); - - /** - * A generic method to run a Git command, when existing methods are not sufficient.
- * Can be used instead of {@link #runCommand(Supplier)} if the operation will not need to be repeated for sure - * (e.g. it is a completely local operation). - */ - @Nonnull - GitCommandResult runCommand(@Nonnull GitLineHandler handler); - - @Nonnull - GitCommandResult init(@Nonnull Project project, @Nonnull VirtualFile root, @Nonnull GitLineHandlerListener... listeners); - - @Nonnull - Set untrackedFiles(@Nonnull Project project, - @Nonnull VirtualFile root, - @Nullable Collection files) throws VcsException; - - // relativePaths are guaranteed to fit into command line length limitations. - @Nonnull - Collection untrackedFilesNoChunk(@Nonnull Project project, - @Nonnull VirtualFile root, - @Nullable List relativePaths) throws VcsException; - - @Nonnull - GitCommandResult clone(@Nonnull Project project, - @Nonnull File parentDirectory, - @Nonnull String url, - @Nullable String puttyKey, - @Nonnull String clonedDirectoryName, - @Nonnull GitLineHandlerListener... progressListeners); - - @Nonnull - GitCommandResult config(@Nonnull GitRepository repository, String... params); - - @Nonnull - GitCommandResult diff(@Nonnull GitRepository repository, @Nonnull List parameters, @Nonnull String range); - - @Nonnull - GitCommandResult merge(@Nonnull GitRepository repository, - @Nonnull String branchToMerge, - @Nullable List additionalParams, - @Nonnull GitLineHandlerListener... listeners); - - @Nonnull - GitCommandResult checkout(@Nonnull GitRepository repository, - @Nonnull String reference, - @Nullable String newBranch, - boolean force, - boolean detach, - @Nonnull GitLineHandlerListener... listeners); - - @Nonnull - GitCommandResult checkoutNewBranch(@Nonnull GitRepository repository, - @Nonnull String branchName, - @Nullable GitLineHandlerListener listener); - - @Nonnull - GitCommandResult createNewTag(@Nonnull GitRepository repository, - @Nonnull String tagName, - @Nullable GitLineHandlerListener listener, - @Nonnull String reference); - - @Nonnull - GitCommandResult branchDelete(@Nonnull GitRepository repository, - @Nonnull String branchName, - boolean force, - @Nonnull GitLineHandlerListener... listeners); - - @Nonnull - GitCommandResult branchContains(@Nonnull GitRepository repository, @Nonnull String commit); - - /** - * Create branch without checking it out:
- *
    git branch <branchName> <startPoint>
- */ - @Nonnull - GitCommandResult branchCreate(@Nonnull GitRepository repository, @Nonnull String branchName, @Nonnull String startPoint); - - @Nonnull - GitCommandResult renameBranch(@Nonnull GitRepository repository, - @Nonnull String currentName, - @Nonnull String newName, - @Nonnull GitLineHandlerListener... listeners); - - @Nonnull - GitCommandResult reset(@Nonnull GitRepository repository, - @Nonnull GitResetMode mode, - @Nonnull String target, - @Nonnull GitLineHandlerListener... listeners); - - @Nonnull - GitCommandResult resetMerge(@Nonnull GitRepository repository, @Nullable String revision); - - @Nonnull - GitCommandResult tip(@Nonnull GitRepository repository, @Nonnull String branchName); - - @Nonnull - GitCommandResult push(@Nonnull GitRepository repository, - @Nonnull String remote, - @Nullable String url, - @Nullable String puttyKey, - @Nonnull String spec, - boolean updateTracking, - @Nonnull GitLineHandlerListener... listeners); - - @Nonnull - GitCommandResult push(@Nonnull GitRepository repository, - @Nonnull GitRemote remote, - @Nonnull String spec, - boolean force, - boolean updateTracking, - boolean skipHook, - @Nullable String tagMode, - GitLineHandlerListener... listeners); - - @Nonnull - GitCommandResult show(@Nonnull GitRepository repository, @Nonnull String... params); - - @Nonnull - GitCommandResult cherryPick(@Nonnull GitRepository repository, - @Nonnull String hash, - boolean autoCommit, - @Nonnull GitLineHandlerListener... listeners); - - @Nonnull - GitCommandResult getUnmergedFiles(@Nonnull GitRepository repository); - - @Nonnull - GitCommandResult checkAttr(@Nonnull GitRepository repository, - @Nonnull Collection attributes, - @Nonnull Collection files); - - @Nonnull - GitCommandResult stashSave(@Nonnull GitRepository repository, @Nonnull String message); - - @Nonnull - GitCommandResult stashPop(@Nonnull GitRepository repository, @Nonnull GitLineHandlerListener... listeners); - - @Nonnull - GitCommandResult fetch(@Nonnull GitRepository repository, - @Nonnull GitRemote remote, - @Nonnull List listeners, - String... params); - - @Nonnull - GitCommandResult addRemote(@Nonnull GitRepository repository, @Nonnull String name, @Nonnull String url); - - @Nonnull - GitCommandResult lsRemote(@Nonnull Project project, @Nonnull File workingDir, @Nonnull String url); - - @Nonnull - GitCommandResult lsRemote(@Nonnull Project project, - @Nonnull VirtualFile workingDir, - @Nonnull GitRemote remote, - String... additionalParameters); - - @Nonnull - GitCommandResult remotePrune(@Nonnull GitRepository repository, @Nonnull GitRemote remote); - - @Nonnull - GitCommandResult rebase(@Nonnull GitRepository repository, - @Nonnull GitRebaseParams parameters, - @Nonnull GitLineHandlerListener... listeners); - - @Nonnull - GitCommandResult rebaseAbort(@Nonnull GitRepository repository, @Nonnull GitLineHandlerListener... listeners); - - @Nonnull - GitCommandResult rebaseContinue(@Nonnull GitRepository repository, @Nonnull GitLineHandlerListener... listeners); - - @Nonnull - GitCommandResult rebaseSkip(@Nonnull GitRepository repository, @Nonnull GitLineHandlerListener... listeners); - + @Nonnull + static Git getInstance() { + return ServiceManager.getService(Git.class); + } + + /** + * A generic method to run a Git command, when existing methods like {@link #fetch(GitRepository, String, String, List, String...)} + * are not sufficient. + * + * @param handlerConstructor this is needed, since the operation may need to repeat (e.g. in case of authentication failure). + * make sure to supply a stateless constructor. + */ + @Nonnull + GitCommandResult runCommand(@Nonnull Supplier handlerConstructor); + + /** + * A generic method to run a Git command, when existing methods are not sufficient.
+ * Can be used instead of {@link #runCommand(Supplier)} if the operation will not need to be repeated for sure + * (e.g. it is a completely local operation). + */ + @Nonnull + GitCommandResult runCommand(@Nonnull GitLineHandler handler); + + @Nonnull + GitCommandResult init(@Nonnull Project project, @Nonnull VirtualFile root, @Nonnull GitLineHandlerListener... listeners); + + @Nonnull + Set untrackedFiles( + @Nonnull Project project, + @Nonnull VirtualFile root, + @Nullable Collection files + ) throws VcsException; + + // relativePaths are guaranteed to fit into command line length limitations. + @Nonnull + Collection untrackedFilesNoChunk( + @Nonnull Project project, + @Nonnull VirtualFile root, + @Nullable List relativePaths + ) throws VcsException; + + @Nonnull + GitCommandResult clone( + @Nonnull Project project, + @Nonnull File parentDirectory, + @Nonnull String url, + @Nullable String puttyKey, + @Nonnull String clonedDirectoryName, + @Nonnull GitLineHandlerListener... progressListeners + ); + + @Nonnull + GitCommandResult config(@Nonnull GitRepository repository, String... params); + + @Nonnull + GitCommandResult diff(@Nonnull GitRepository repository, @Nonnull List parameters, @Nonnull String range); + + @Nonnull + GitCommandResult merge( + @Nonnull GitRepository repository, + @Nonnull String branchToMerge, + @Nullable List additionalParams, + @Nonnull GitLineHandlerListener... listeners + ); + + @Nonnull + GitCommandResult checkout( + @Nonnull GitRepository repository, + @Nonnull String reference, + @Nullable String newBranch, + boolean force, + boolean detach, + @Nonnull GitLineHandlerListener... listeners + ); + + @Nonnull + GitCommandResult checkoutNewBranch( + @Nonnull GitRepository repository, + @Nonnull String branchName, + @Nullable GitLineHandlerListener listener + ); + + @Nonnull + GitCommandResult createNewTag( + @Nonnull GitRepository repository, + @Nonnull String tagName, + @Nullable GitLineHandlerListener listener, + @Nonnull String reference + ); + + @Nonnull + GitCommandResult branchDelete( + @Nonnull GitRepository repository, + @Nonnull String branchName, + boolean force, + @Nonnull GitLineHandlerListener... listeners + ); + + @Nonnull + GitCommandResult branchContains(@Nonnull GitRepository repository, @Nonnull String commit); + + /** + * Create branch without checking it out:
+ *
    git branch <branchName> <startPoint>
+ */ + @Nonnull + GitCommandResult branchCreate(@Nonnull GitRepository repository, @Nonnull String branchName, @Nonnull String startPoint); + + @Nonnull + GitCommandResult renameBranch( + @Nonnull GitRepository repository, + @Nonnull String currentName, + @Nonnull String newName, + @Nonnull GitLineHandlerListener... listeners + ); + + @Nonnull + GitCommandResult reset( + @Nonnull GitRepository repository, + @Nonnull GitResetMode mode, + @Nonnull String target, + @Nonnull GitLineHandlerListener... listeners + ); + + @Nonnull + GitCommandResult resetMerge(@Nonnull GitRepository repository, @Nullable String revision); + + @Nonnull + GitCommandResult tip(@Nonnull GitRepository repository, @Nonnull String branchName); + + @Nonnull + GitCommandResult push( + @Nonnull GitRepository repository, + @Nonnull String remote, + @Nullable String url, + @Nullable String puttyKey, + @Nonnull String spec, + boolean updateTracking, + @Nonnull GitLineHandlerListener... listeners + ); + + @Nonnull + GitCommandResult push( + @Nonnull GitRepository repository, + @Nonnull GitRemote remote, + @Nonnull String spec, + boolean force, + boolean updateTracking, + boolean skipHook, + @Nullable String tagMode, + GitLineHandlerListener... listeners + ); + + @Nonnull + GitCommandResult show(@Nonnull GitRepository repository, @Nonnull String... params); + + @Nonnull + GitCommandResult cherryPick( + @Nonnull GitRepository repository, + @Nonnull String hash, + boolean autoCommit, + @Nonnull GitLineHandlerListener... listeners + ); + + @Nonnull + GitCommandResult getUnmergedFiles(@Nonnull GitRepository repository); + + @Nonnull + GitCommandResult checkAttr( + @Nonnull GitRepository repository, + @Nonnull Collection attributes, + @Nonnull Collection files + ); + + @Nonnull + GitCommandResult stashSave(@Nonnull GitRepository repository, @Nonnull String message); + + @Nonnull + GitCommandResult stashPop(@Nonnull GitRepository repository, @Nonnull GitLineHandlerListener... listeners); + + @Nonnull + GitCommandResult fetch( + @Nonnull GitRepository repository, + @Nonnull GitRemote remote, + @Nonnull List listeners, + String... params + ); + + @Nonnull + GitCommandResult addRemote(@Nonnull GitRepository repository, @Nonnull String name, @Nonnull String url); + + @Nonnull + GitCommandResult lsRemote(@Nonnull Project project, @Nonnull File workingDir, @Nonnull String url); + + @Nonnull + GitCommandResult lsRemote( + @Nonnull Project project, + @Nonnull VirtualFile workingDir, + @Nonnull GitRemote remote, + String... additionalParameters + ); + + @Nonnull + GitCommandResult remotePrune(@Nonnull GitRepository repository, @Nonnull GitRemote remote); + + @Nonnull + GitCommandResult rebase( + @Nonnull GitRepository repository, + @Nonnull GitRebaseParams parameters, + @Nonnull GitLineHandlerListener... listeners + ); + + @Nonnull + GitCommandResult rebaseAbort(@Nonnull GitRepository repository, @Nonnull GitLineHandlerListener... listeners); + + @Nonnull + GitCommandResult rebaseContinue(@Nonnull GitRepository repository, @Nonnull GitLineHandlerListener... listeners); + + @Nonnull + GitCommandResult rebaseSkip(@Nonnull GitRepository repository, @Nonnull GitLineHandlerListener... listeners); } diff --git a/plugin/src/main/java/git4idea/commands/GitBinaryHandler.java b/plugin/src/main/java/git4idea/commands/GitBinaryHandler.java index 11fd355..75575af 100644 --- a/plugin/src/main/java/git4idea/commands/GitBinaryHandler.java +++ b/plugin/src/main/java/git4idea/commands/GitBinaryHandler.java @@ -15,191 +15,157 @@ */ package git4idea.commands; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.concurrent.Semaphore; -import java.util.concurrent.atomic.AtomicReference; - +import consulo.ide.localize.IdeLocalize; import consulo.process.ExecutionException; -import consulo.ide.IdeBundle; import consulo.project.Project; import consulo.versionControlSystem.VcsException; import consulo.virtualFileSystem.VirtualFile; import git4idea.GitVcs; import jakarta.annotation.Nonnull; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicReference; + /** * The handler that allows consuming binary data as byte array */ -public class GitBinaryHandler extends GitHandler -{ - private static final int BUFFER_SIZE = 8 * 1024; +public class GitBinaryHandler extends GitHandler { + private static final int BUFFER_SIZE = 8 * 1024; - @Nonnull - private final ByteArrayOutputStream myStdout = new ByteArrayOutputStream(); - @Nonnull - private final ByteArrayOutputStream myStderr = new ByteArrayOutputStream(); - @Nonnull - private final Semaphore mySteamSemaphore = new Semaphore(0); // The semaphore that waits for stream processing - @Nonnull - private final AtomicReference myException = new AtomicReference(); + @Nonnull + private final ByteArrayOutputStream myStdout = new ByteArrayOutputStream(); + @Nonnull + private final ByteArrayOutputStream myStderr = new ByteArrayOutputStream(); + @Nonnull + private final Semaphore mySteamSemaphore = new Semaphore(0); // The semaphore that waits for stream processing + @Nonnull + private final AtomicReference myException = new AtomicReference<>(); - public GitBinaryHandler(final Project project, final VirtualFile vcsRoot, final GitCommand command) - { - super(project, vcsRoot, command); - } + public GitBinaryHandler(Project project, VirtualFile vcsRoot, GitCommand command) { + super(project, vcsRoot, command); + } - @Override - protected Process startProcess() throws ExecutionException - { - return myCommandLine.createProcess(); - } + @Override + protected Process startProcess() throws ExecutionException { + return myCommandLine.createProcess(); + } - @Override - protected void startHandlingStreams() - { - handleStream(myProcess.getErrorStream(), myStderr); - handleStream(myProcess.getInputStream(), myStdout); - } + @Override + protected void startHandlingStreams() { + handleStream(myProcess.getErrorStream(), myStderr); + handleStream(myProcess.getInputStream(), myStdout); + } - /** - * Handle the single stream - * - * @param in the standard input - * @param out the standard output - */ - private void handleStream(final InputStream in, final ByteArrayOutputStream out) - { - Thread t = new Thread(new Runnable() - { - @Override - public void run() - { - try - { - byte[] buffer = new byte[BUFFER_SIZE]; - while(true) - { - int rc = in.read(buffer); - if(rc == -1) - { - break; - } - out.write(buffer, 0, rc); - } - } - catch(IOException e) - { - //noinspection ThrowableInstanceNeverThrown - if(!myException.compareAndSet(null, new VcsException("Stream IO problem", e))) - { - LOG.error("Problem reading stream", e); - } - } - finally - { - mySteamSemaphore.release(1); - } - } - }, "Stream copy thread"); - t.setDaemon(true); - t.start(); - } + /** + * Handle the single stream + * + * @param in the standard input + * @param out the standard output + */ + private void handleStream(InputStream in, ByteArrayOutputStream out) { + Thread t = new Thread( + () -> { + try { + byte[] buffer = new byte[BUFFER_SIZE]; + while (true) { + int rc = in.read(buffer); + if (rc == -1) { + break; + } + out.write(buffer, 0, rc); + } + } + catch (IOException e) { + //noinspection ThrowableInstanceNeverThrown + if (!myException.compareAndSet(null, new VcsException("Stream IO problem", e))) { + LOG.error("Problem reading stream", e); + } + } + finally { + mySteamSemaphore.release(1); + } + }, + "Stream copy thread" + ); + t.setDaemon(true); + t.start(); + } - @Override - public void destroyProcess() - { - myProcess.destroy(); - } + @Override + public void destroyProcess() { + myProcess.destroy(); + } - @Override - protected void waitForProcess() - { - try - { - mySteamSemaphore.acquire(2); - myProcess.waitFor(); - int exitCode = myProcess.exitValue(); - setExitCode(exitCode); - } - catch(InterruptedException e) - { - if(LOG.isDebugEnabled()) - { - LOG.debug("Ignoring process exception: ", e); - } - setExitCode(255); - } - listeners().processTerminated(getExitCode()); - } + @Override + protected void waitForProcess() { + try { + mySteamSemaphore.acquire(2); + myProcess.waitFor(); + int exitCode = myProcess.exitValue(); + setExitCode(exitCode); + } + catch (InterruptedException e) { + if (LOG.isDebugEnabled()) { + LOG.debug("Ignoring process exception: ", e); + } + setExitCode(255); + } + listeners().processTerminated(getExitCode()); + } - /** - * Run in the current thread and return the data as array - * - * @return the binary data - * @throws VcsException in case of the problem with running git - */ - public byte[] run() throws VcsException - { - addListener(new GitHandlerListener() - { - @Override - public void processTerminated(int exitCode) - { - if(exitCode != 0 && !isIgnoredErrorCode(exitCode)) - { - Charset cs = getCharset(); - String message = new String(myStderr.toByteArray(), cs); - if(message.length() == 0) - { - //noinspection ThrowableResultOfMethodCallIgnored - if(myException.get() != null) - { - message = IdeBundle.message("finished.with.exit.code.text.message", exitCode); - } - else - { - message = null; - } - } - else - { - if(!isStderrSuppressed()) - { - GitVcs.getInstance(myProject).showErrorMessages(message); - } - } - if(message != null) - { - //noinspection ThrowableInstanceNeverThrown - VcsException e = myException.getAndSet(new VcsException(message)); - if(e != null) - { - LOG.warn("Dropping previous exception: ", e); - } - } - } - } + /** + * Run in the current thread and return the data as array + * + * @return the binary data + * @throws VcsException in case of the problem with running git + */ + public byte[] run() throws VcsException { + addListener(new GitHandlerListener() { + @Override + public void processTerminated(int exitCode) { + if (exitCode != 0 && !isIgnoredErrorCode(exitCode)) { + Charset cs = getCharset(); + String message = new String(myStderr.toByteArray(), cs); + if (message.length() == 0) { + //noinspection ThrowableResultOfMethodCallIgnored + if (myException.get() != null) { + message = IdeLocalize.finishedWithExitCodeTextMessage(exitCode).get(); + } + else { + message = null; + } + } + else if (!isStderrSuppressed()) { + GitVcs.getInstance(myProject).showErrorMessages(message); + } + if (message != null) { + //noinspection ThrowableInstanceNeverThrown + VcsException e = myException.getAndSet(new VcsException(message)); + if (e != null) { + LOG.warn("Dropping previous exception: ", e); + } + } + } + } - @Override - public void startFailed(Throwable exception) - { - //noinspection ThrowableInstanceNeverThrown - VcsException e = myException.getAndSet(new VcsException("Start failed: " + exception.getMessage(), exception)); - if(e != null) - { - LOG.warn("Dropping previous exception: ", e); - } - } - }); - GitHandlerUtil.runInCurrentThread(this, null); - //noinspection ThrowableResultOfMethodCallIgnored - if(myException.get() != null) - { - throw myException.get(); - } - return myStdout.toByteArray(); - } + @Override + public void startFailed(Throwable exception) { + //noinspection ThrowableInstanceNeverThrown + VcsException e = myException.getAndSet(new VcsException("Start failed: " + exception.getMessage(), exception)); + if (e != null) { + LOG.warn("Dropping previous exception: ", e); + } + } + }); + GitHandlerUtil.runInCurrentThread(this, null); + //noinspection ThrowableResultOfMethodCallIgnored + if (myException.get() != null) { + throw myException.get(); + } + return myStdout.toByteArray(); + } } diff --git a/plugin/src/main/java/git4idea/commands/GitCommand.java b/plugin/src/main/java/git4idea/commands/GitCommand.java index fb772a4..77fffa3 100644 --- a/plugin/src/main/java/git4idea/commands/GitCommand.java +++ b/plugin/src/main/java/git4idea/commands/GitCommand.java @@ -18,6 +18,7 @@ import consulo.application.util.registry.Registry; import org.jetbrains.annotations.NonNls; import jakarta.annotation.Nonnull; + import java.lang.Runnable; /** @@ -34,121 +35,108 @@ * write lock), which {@code git stash list} doesn't (and therefore no lock is needed). *

*/ -public class GitCommand -{ - - public static final GitCommand ADD = write("add"); - public static final GitCommand BLAME = read("blame"); - public static final GitCommand BRANCH = read("branch"); - public static final GitCommand CHECKOUT = write("checkout"); - public static final GitCommand CHECK_ATTR = read("check-attr"); - public static final GitCommand COMMIT = write("commit"); - public static final GitCommand CONFIG = read("config"); - public static final GitCommand CHERRY = read("cherry"); - public static final GitCommand CHERRY_PICK = write("cherry-pick"); - public static final GitCommand CLONE = write("clone"); - public static final GitCommand DIFF = read("diff"); - public static final GitCommand FETCH = read("fetch"); // fetch is a read-command, because it doesn't modify the index - public static final GitCommand INIT = write("init"); - public static final GitCommand LOG = read("log"); - public static final GitCommand LS_FILES = read("ls-files"); - public static final GitCommand LS_TREE = read("ls-tree"); - public static final GitCommand LS_REMOTE = read("ls-remote"); - public static final GitCommand MERGE = write("merge"); - public static final GitCommand MERGE_BASE = read("merge-base"); - public static final GitCommand MV = write("mv"); - public static final GitCommand PULL = write("pull"); - public static final GitCommand PUSH = write("push"); - public static final GitCommand REBASE = write("rebase"); - public static final GitCommand REMOTE = read("remote"); - public static final GitCommand RESET = write("reset"); - public static final GitCommand REV_LIST = read("rev-list"); - public static final GitCommand REV_PARSE = read("rev-parse"); - public static final GitCommand RM = write("rm"); - public static final GitCommand SHOW = read("show"); - public static final GitCommand STASH = write("stash"); - public static final GitCommand STATUS = Registry.is("git.status.write", true) ? write("status") : read("status"); - public static final GitCommand TAG = read("tag"); - public static final GitCommand UPDATE_INDEX = write("update-index"); +public class GitCommand { + public static final GitCommand ADD = write("add"); + public static final GitCommand BLAME = read("blame"); + public static final GitCommand BRANCH = read("branch"); + public static final GitCommand CHECKOUT = write("checkout"); + public static final GitCommand CHECK_ATTR = read("check-attr"); + public static final GitCommand COMMIT = write("commit"); + public static final GitCommand CONFIG = read("config"); + public static final GitCommand CHERRY = read("cherry"); + public static final GitCommand CHERRY_PICK = write("cherry-pick"); + public static final GitCommand CLONE = write("clone"); + public static final GitCommand DIFF = read("diff"); + public static final GitCommand FETCH = read("fetch"); // fetch is a read-command, because it doesn't modify the index + public static final GitCommand INIT = write("init"); + public static final GitCommand LOG = read("log"); + public static final GitCommand LS_FILES = read("ls-files"); + public static final GitCommand LS_TREE = read("ls-tree"); + public static final GitCommand LS_REMOTE = read("ls-remote"); + public static final GitCommand MERGE = write("merge"); + public static final GitCommand MERGE_BASE = read("merge-base"); + public static final GitCommand MV = write("mv"); + public static final GitCommand PULL = write("pull"); + public static final GitCommand PUSH = write("push"); + public static final GitCommand REBASE = write("rebase"); + public static final GitCommand REMOTE = read("remote"); + public static final GitCommand RESET = write("reset"); + public static final GitCommand REV_LIST = read("rev-list"); + public static final GitCommand REV_PARSE = read("rev-parse"); + public static final GitCommand RM = write("rm"); + public static final GitCommand SHOW = read("show"); + public static final GitCommand STASH = write("stash"); + public static final GitCommand STATUS = Registry.is("git.status.write", true) ? write("status") : read("status"); + public static final GitCommand TAG = read("tag"); + public static final GitCommand UPDATE_INDEX = write("update-index"); - /** - * Name of environment variable that specifies editor for the git - */ - public static final String GIT_EDITOR_ENV = "GIT_EDITOR"; + /** + * Name of environment variable that specifies editor for the git + */ + public static final String GIT_EDITOR_ENV = "GIT_EDITOR"; - enum LockingPolicy - { - READ, - WRITE - } + enum LockingPolicy { + READ, + WRITE + } - @Nonnull - @NonNls - private final String myName; // command name passed to git - @Nonnull - private final LockingPolicy myLocking; // Locking policy for the command + @Nonnull + private final String myName; // command name passed to git + @Nonnull + private final LockingPolicy myLocking; // Locking policy for the command - private GitCommand(@Nonnull String name, @Nonnull LockingPolicy lockingPolicy) - { - myLocking = lockingPolicy; - myName = name; - } + private GitCommand(@Nonnull String name, @Nonnull LockingPolicy lockingPolicy) { + myLocking = lockingPolicy; + myName = name; + } - /** - * Copy constructor with other locking policy. - */ - private GitCommand(@Nonnull GitCommand command, @Nonnull LockingPolicy lockingPolicy) - { - myName = command.name(); - myLocking = lockingPolicy; - } + /** + * Copy constructor with other locking policy. + */ + private GitCommand(@Nonnull GitCommand command, @Nonnull LockingPolicy lockingPolicy) { + myName = command.name(); + myLocking = lockingPolicy; + } - /** - *

Creates the clone of this git command, but with LockingPolicy different from the default one.

- *

This can be used for commands, which are considered to be "write" commands in general, but can be "read" commands when a certain - * set of arguments is given ({@code git stash list}, for instance).

- *

Use this constructor with care: specifying read-policy on a write operation may result in a conflict during simultaneous - * modification of index.

- */ - @Nonnull - public GitCommand readLockingCommand() - { - return new GitCommand(this, LockingPolicy.READ); - } + /** + *

Creates the clone of this git command, but with LockingPolicy different from the default one.

+ *

This can be used for commands, which are considered to be "write" commands in general, but can be "read" commands when a certain + * set of arguments is given ({@code git stash list}, for instance).

+ *

Use this constructor with care: specifying read-policy on a write operation may result in a conflict during simultaneous + * modification of index.

+ */ + @Nonnull + public GitCommand readLockingCommand() { + return new GitCommand(this, LockingPolicy.READ); + } - @Nonnull - public GitCommand writeLockingCommand() - { - return new GitCommand(this, LockingPolicy.WRITE); - } + @Nonnull + public GitCommand writeLockingCommand() { + return new GitCommand(this, LockingPolicy.WRITE); + } - @Nonnull - public static GitCommand read(@Nonnull String name) - { - return new GitCommand(name, LockingPolicy.READ); - } + @Nonnull + public static GitCommand read(@Nonnull String name) { + return new GitCommand(name, LockingPolicy.READ); + } - @Nonnull - public static GitCommand write(@Nonnull String name) - { - return new GitCommand(name, LockingPolicy.WRITE); - } + @Nonnull + public static GitCommand write(@Nonnull String name) { + return new GitCommand(name, LockingPolicy.WRITE); + } - @Nonnull - public String name() - { - return myName; - } + @Nonnull + public String name() { + return myName; + } - @Nonnull - public LockingPolicy lockingPolicy() - { - return myLocking; - } + @Nonnull + public LockingPolicy lockingPolicy() { + return myLocking; + } - @Override - public String toString() - { - return myName; - } + @Override + public String toString() { + return myName; + } } diff --git a/plugin/src/main/java/git4idea/commands/GitCommandResult.java b/plugin/src/main/java/git4idea/commands/GitCommandResult.java index 5d4d10c..9979f13 100644 --- a/plugin/src/main/java/git4idea/commands/GitCommandResult.java +++ b/plugin/src/main/java/git4idea/commands/GitCommandResult.java @@ -15,6 +15,7 @@ */ package git4idea.commands; +import consulo.localize.LocalizeValue; import consulo.util.collection.ContainerUtil; import consulo.util.lang.ObjectUtil; import consulo.util.lang.StringUtil; @@ -33,139 +34,143 @@ * * @author Kirill Likhodedov */ -public class GitCommandResult -{ - private final boolean mySuccess; - private final int myExitCode; // non-zero exit code doesn't necessarily mean an error - private final List myErrorOutput; - private final List myOutput; - @Nullable - private final Throwable myException; - - public GitCommandResult(boolean success, int exitCode, @Nonnull List errorOutput, @Nonnull List output, @Nullable Throwable exception) - { - myExitCode = exitCode; - mySuccess = success; - myErrorOutput = errorOutput; - myOutput = output; - myException = exception; - } - - @Nonnull - public static GitCommandResult merge(@Nullable GitCommandResult first, @Nonnull GitCommandResult second) - { - if(first == null) - { - return second; - } - - int mergedExitCode; - if(first.myExitCode == 0) - { - mergedExitCode = second.myExitCode; - } - else if(second.myExitCode == 0) - { - mergedExitCode = first.myExitCode; - } - else - { - mergedExitCode = second.myExitCode; // take exit code of the latest command - } - return new GitCommandResult(first.success() && second.success(), mergedExitCode, ContainerUtil.concat(first.myErrorOutput, second.myErrorOutput), ContainerUtil.concat(first.myOutput, - second.myOutput), ObjectUtil.chooseNotNull(second.myException, first.myException)); - } - - /** - * @return we think that the operation succeeded - */ - public boolean success() - { - return mySuccess; - } - - @Nonnull - public List getOutput() - { - return Collections.unmodifiableList(myOutput); - } - - public int getExitCode() - { - return myExitCode; - } - - @Nonnull - public List getErrorOutput() - { - return Collections.unmodifiableList(myErrorOutput); - } - - @Override - public String toString() - { - return String.format("{%d} %nOutput: %n%s %nError output: %n%s", myExitCode, myOutput, myErrorOutput); - } - - @Nonnull - public String getErrorOutputAsHtmlString() - { - return StringUtil.join(cleanup(getErrorOrStdOutput()), "
"); - } - - @Nonnull - public String getErrorOutputAsJoinedString() - { - return StringUtil.join(cleanup(getErrorOrStdOutput()), "\n"); - } - - // in some cases operation fails but no explicit error messages are given, in this case return the output to display something to user - @Nonnull - private List getErrorOrStdOutput() - { - return myErrorOutput.isEmpty() && !success() ? myOutput : myErrorOutput; - } - - @Nonnull - public String getOutputAsJoinedString() - { - return StringUtil.join(myOutput, "\n"); - } - - @Nullable - public Throwable getException() - { - return myException; - } - - @Nonnull - public static GitCommandResult error(@Nonnull String error) - { - return new GitCommandResult(false, 1, Collections.singletonList(error), Collections.emptyList(), null); - } - - public boolean cancelled() - { - return false; // will be implemented later - } - - @Nonnull - private static Collection cleanup(@Nonnull Collection errorOutput) - { - return ContainerUtil.map(errorOutput, errorMessage -> GitUtil.cleanupErrorPrefixes(errorMessage)); - } - - - /** - * Check if execution was successful and return textual result or throw exception - * - * @return result of {@link #getOutputAsJoinedString()} - * @throws VcsException with message from {@link #getErrorOutputAsJoinedString()} - */ - @Nonnull - public String getOutputOrThrow() throws VcsException - { - if (!success()) throw new VcsException(getErrorOutputAsJoinedString()); - return getOutputAsJoinedString(); - } +public class GitCommandResult { + private final boolean mySuccess; + private final int myExitCode; // non-zero exit code doesn't necessarily mean an error + private final List myErrorOutput; + private final List myOutput; + @Nullable + private final Throwable myException; + + public GitCommandResult( + boolean success, + int exitCode, + @Nonnull List errorOutput, + @Nonnull List output, + @Nullable Throwable exception + ) { + myExitCode = exitCode; + mySuccess = success; + myErrorOutput = errorOutput; + myOutput = output; + myException = exception; + } + + @Nonnull + public static GitCommandResult merge(@Nullable GitCommandResult first, @Nonnull GitCommandResult second) { + if (first == null) { + return second; + } + + int mergedExitCode; + if (first.myExitCode == 0) { + mergedExitCode = second.myExitCode; + } + else if (second.myExitCode == 0) { + mergedExitCode = first.myExitCode; + } + else { + mergedExitCode = second.myExitCode; // take exit code of the latest command + } + return new GitCommandResult( + first.success() && second.success(), + mergedExitCode, + ContainerUtil.concat(first.myErrorOutput, second.myErrorOutput), + ContainerUtil.concat( + first.myOutput, + second.myOutput + ), + ObjectUtil.chooseNotNull(second.myException, first.myException) + ); + } + + /** + * @return we think that the operation succeeded + */ + public boolean success() { + return mySuccess; + } + + @Nonnull + public List getOutput() { + return Collections.unmodifiableList(myOutput); + } + + public int getExitCode() { + return myExitCode; + } + + @Nonnull + public List getErrorOutput() { + return Collections.unmodifiableList(myErrorOutput); + } + + @Override + public String toString() { + return String.format("{%d} %nOutput: %n%s %nError output: %n%s", myExitCode, myOutput, myErrorOutput); + } + + @Nonnull + public LocalizeValue getErrorOutputAsHtmlValue() { + return LocalizeValue.of(getErrorOutputAsHtmlString()); + } + + @Nonnull + public String getErrorOutputAsHtmlString() { + return StringUtil.join(cleanup(getErrorOrStdOutput()), "
"); + } + + @Nonnull + public LocalizeValue getErrorOutputAsJoinedValue() { + return LocalizeValue.of(getErrorOutputAsJoinedString()); + } + + @Nonnull + public String getErrorOutputAsJoinedString() { + return StringUtil.join(cleanup(getErrorOrStdOutput()), "\n"); + } + + // in some cases operation fails but no explicit error messages are given, in this case return the output to display something to user + @Nonnull + private List getErrorOrStdOutput() { + return myErrorOutput.isEmpty() && !success() ? myOutput : myErrorOutput; + } + + @Nonnull + public String getOutputAsJoinedString() { + return StringUtil.join(myOutput, "\n"); + } + + @Nullable + public Throwable getException() { + return myException; + } + + @Nonnull + public static GitCommandResult error(@Nonnull String error) { + return new GitCommandResult(false, 1, Collections.singletonList(error), Collections.emptyList(), null); + } + + public boolean cancelled() { + return false; // will be implemented later + } + + @Nonnull + private static Collection cleanup(@Nonnull Collection errorOutput) { + return ContainerUtil.map(errorOutput, GitUtil::cleanupErrorPrefixes); + } + + /** + * Check if execution was successful and return textual result or throw exception + * + * @return result of {@link #getOutputAsJoinedString()} + * @throws VcsException with message from {@link #getErrorOutputAsJoinedValue()} + */ + @Nonnull + public String getOutputOrThrow() throws VcsException { + if (!success()) { + throw new VcsException(getErrorOutputAsJoinedValue()); + } + return getOutputAsJoinedString(); + } } diff --git a/plugin/src/main/java/git4idea/commands/GitCompoundResult.java b/plugin/src/main/java/git4idea/commands/GitCompoundResult.java index a4a5354..73990e6 100644 --- a/plugin/src/main/java/git4idea/commands/GitCompoundResult.java +++ b/plugin/src/main/java/git4idea/commands/GitCompoundResult.java @@ -15,6 +15,7 @@ */ package git4idea.commands; +import consulo.localize.LocalizeValue; import consulo.project.Project; import git4idea.GitUtil; import git4idea.repo.GitRepository; @@ -25,67 +26,67 @@ /** * Compound result of the Git command execution performed on several repositories. - * + * * @author Kirill Likhodedov */ public final class GitCompoundResult { - - private final Map resultsByRepos = new HashMap(1); - private final Project myProject; + private final Map resultsByRepos = new HashMap<>(1); + private final Project myProject; - public GitCompoundResult(Project project) { - myProject = project; - } + public GitCompoundResult(Project project) { + myProject = project; + } - public void append(GitRepository repository, GitCommandResult result) { - resultsByRepos.put(repository, result); - } + public void append(GitRepository repository, GitCommandResult result) { + resultsByRepos.put(repository, result); + } - public boolean totalSuccess() { - boolean success = true; - for (GitCommandResult result : resultsByRepos.values()) { - success &= result.success(); + public boolean totalSuccess() { + boolean success = true; + for (GitCommandResult result : resultsByRepos.values()) { + success &= result.success(); + } + return success; } - return success; - } - /** - * @return true if at least one, but not all repositories succeeded. - */ - public boolean partialSuccess() { - boolean successFound = false; - boolean failureFound = false; - for (GitCommandResult result : resultsByRepos.values()) { - if (result.success()) { - successFound = true; - } else { - failureFound = true; - } + /** + * @return true if at least one, but not all repositories succeeded. + */ + public boolean partialSuccess() { + boolean successFound = false; + boolean failureFound = false; + for (GitCommandResult result : resultsByRepos.values()) { + if (result.success()) { + successFound = true; + } + else { + failureFound = true; + } + } + return successFound && failureFound; } - return successFound && failureFound; - } - /** - * Constructs the HTML-formatted message from error outputs of failed repositories. - * If there is only 1 repository in the project, just returns the error without writing the repository url (to avoid confusion for people - * with only 1 root ever). - * Otherwise adds repository URL to the error that repository produced. - */ - @Nonnull - public String getErrorOutputWithReposIndication() { - StringBuilder sb = new StringBuilder(); - for (Map.Entry entry : resultsByRepos.entrySet()) { - GitRepository repository = entry.getKey(); - GitCommandResult result = entry.getValue(); - if (!result.success()) { - sb.append("

"); - if (!GitUtil.justOneGitRepository(myProject)) { - sb.append("" + repository.getPresentableUrl() + ":
"); + /** + * Constructs the HTML-formatted message from error outputs of failed repositories. + * If there is only 1 repository in the project, just returns the error without writing the repository url (to avoid confusion for people + * with only 1 root ever). + * Otherwise adds repository URL to the error that repository produced. + */ + @Nonnull + public LocalizeValue getErrorOutputWithReposIndication() { + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : resultsByRepos.entrySet()) { + GitRepository repository = entry.getKey(); + GitCommandResult result = entry.getValue(); + if (!result.success()) { + sb.append("

"); + if (!GitUtil.justOneGitRepository(myProject)) { + sb.append("").append(repository.getPresentableUrl()).append(":
"); + } + sb.append(result.getErrorOutputAsHtmlValue()); + sb.append("

"); + } } - sb.append(result.getErrorOutputAsHtmlString()); - sb.append("

"); - } + return LocalizeValue.localizeTODO(sb.toString()); } - return sb.toString(); - } } diff --git a/plugin/src/main/java/git4idea/commands/GitHandler.java b/plugin/src/main/java/git4idea/commands/GitHandler.java index 3e0ab88..e91a5ca 100644 --- a/plugin/src/main/java/git4idea/commands/GitHandler.java +++ b/plugin/src/main/java/git4idea/commands/GitHandler.java @@ -15,14 +15,12 @@ */ package git4idea.commands; -import consulo.application.ApplicationManager; -import consulo.application.util.SystemInfo; -import consulo.application.util.function.Processor; import consulo.component.ProcessCanceledException; import consulo.container.plugin.PluginManager; import consulo.http.HttpProxyManager; import consulo.ide.ServiceManager; import consulo.logging.Logger; +import consulo.platform.Platform; import consulo.process.ExecutionException; import consulo.process.cmd.GeneralCommandLine; import consulo.process.local.EnvironmentUtil; @@ -30,7 +28,6 @@ import consulo.proxy.EventDispatcher; import consulo.util.collection.ContainerUtil; import consulo.util.dataholder.Key; -import consulo.util.io.CharsetToolkit; import consulo.util.io.FileUtil; import consulo.util.lang.ObjectUtil; import consulo.util.lang.StringUtil; @@ -47,18 +44,19 @@ import git4idea.config.GitVcsApplicationSettings; import git4idea.config.GitVcsSettings; import git4idea.config.GitVersionSpecialty; -import org.jetbrains.annotations.NonNls; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import org.jetbrains.git4idea.rt.http.GitAskPassXmlRpcHandler; import org.jetbrains.git4idea.rt.ssh.GitSSHHandler; import org.jetbrains.git4idea.ssh.GitXmlRpcSshService; -import jakarta.annotation.Nonnull; -import jakarta.annotation.Nullable; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.*; +import java.util.function.Predicate; import static git4idea.commands.GitCommand.LockingPolicy.WRITE; import static java.util.Collections.singletonList; @@ -67,752 +65,753 @@ * A handler for git commands */ public abstract class GitHandler { - protected static final Logger LOG = Logger.getInstance(GitHandler.class); - protected static final Logger OUTPUT_LOG = Logger.getInstance("#output." + GitHandler.class.getName()); - private static final Logger TIME_LOG = Logger.getInstance("#time." + GitHandler.class.getName()); - - protected final Project myProject; - protected final GitCommand myCommand; - - private final HashSet myIgnoredErrorCodes = new HashSet<>(); // Error codes that are ignored for the handler - private final List myErrors = Collections.synchronizedList(new ArrayList()); - private final List myLastOutput = Collections.synchronizedList(new ArrayList()); - private final int LAST_OUTPUT_SIZE = 5; - final GeneralCommandLine myCommandLine; - @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"}) - Process myProcess; - - private boolean myStdoutSuppressed; // If true, the standard output is not copied to version control console - private boolean myStderrSuppressed; // If true, the standard error is not copied to version control console - private final File myWorkingDirectory; - - private boolean myEnvironmentCleanedUp = true; + protected static final Logger LOG = Logger.getInstance(GitHandler.class); + protected static final Logger OUTPUT_LOG = Logger.getInstance("#output." + GitHandler.class.getName()); + private static final Logger TIME_LOG = Logger.getInstance("#time." + GitHandler.class.getName()); + + @Nonnull + protected final Project myProject; + protected final GitCommand myCommand; + + private final HashSet myIgnoredErrorCodes = new HashSet<>(); // Error codes that are ignored for the handler + private final List myErrors = Collections.synchronizedList(new ArrayList()); + private final List myLastOutput = Collections.synchronizedList(new ArrayList()); + private final int LAST_OUTPUT_SIZE = 5; + final GeneralCommandLine myCommandLine; + @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"}) + Process myProcess; + + private boolean myStdoutSuppressed; // If true, the standard output is not copied to version control console + private boolean myStderrSuppressed; // If true, the standard error is not copied to version control console + private final File myWorkingDirectory; + + private boolean myEnvironmentCleanedUp = true; // the flag indicating that environment has been cleaned up, by default is true because there is nothing to clean - private UUID mySshHandler; - private UUID myHttpHandler; - private Processor myInputProcessor; // The processor for stdin - - // if true process might be cancelled - // note that access is safe because it accessed in unsynchronized block only after process is started, and it does not change after that - @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"}) - private boolean myIsCancellable = true; - - private Integer myExitCode; // exit code or null if exit code is not yet available - - @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"}) - @NonNls - @Nonnull - private Charset myCharset = CharsetToolkit.UTF8_CHARSET; // Character set to use for IO - - private final EventDispatcher myListeners = EventDispatcher.create(ProcessEventListener.class); - @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"}) - protected boolean mySilent; // if true, the command execution is not logged in version control view - - protected final GitVcs myVcs; - private final Map myEnv; - private GitVcsApplicationSettings myAppSettings; - private GitVcsSettings myProjectSettings; - - private long myStartTime; // git execution start timestamp - private static final long LONG_TIME = 10 * 1000; - @Nullable - private Collection myUrls; - @Nullable - private String myPuttyKey; - private boolean myHttpAuthFailed; - - - /** - * A constructor - * - * @param project a project - * @param directory a process directory - * @param command a command to execute (if empty string, the parameter is ignored) - */ - protected GitHandler(@Nonnull Project project, @Nonnull File directory, @Nonnull GitCommand command) { - myProject = project; - myCommand = command; - myAppSettings = GitVcsApplicationSettings.getInstance(); - myProjectSettings = GitVcsSettings.getInstance(myProject); - myEnv = new HashMap<>(EnvironmentUtil.getEnvironmentMap()); - myVcs = ObjectUtil.assertNotNull(GitVcs.getInstance(project)); - myWorkingDirectory = directory; - myCommandLine = new GeneralCommandLine(); - if (myAppSettings != null) { - myCommandLine.setExePath(GitExecutableManager.getInstance().getPathToGit(project)); - } - myCommandLine.setWorkDirectory(myWorkingDirectory); - if (GitVersionSpecialty.CAN_OVERRIDE_GIT_CONFIG_FOR_COMMAND.existsIn(myVcs.getVersion())) { - myCommandLine.addParameters("-c", "core.quotepath=false"); - } - myCommandLine.addParameter(command.name()); - myStdoutSuppressed = true; - mySilent = myCommand.lockingPolicy() == GitCommand.LockingPolicy.READ; - } - - /** - * A constructor - * - * @param project a project - * @param vcsRoot a process directory - * @param command a command to execute - */ - protected GitHandler(final Project project, final VirtualFile vcsRoot, final GitCommand command) { - this(project, VirtualFileUtil.virtualToIoFile(vcsRoot), command); - } - - /** - * @return multicaster for listeners - */ - protected ProcessEventListener listeners() { - return myListeners.getMulticaster(); - } - - /** - * Add error code to ignored list - * - * @param code the code to ignore - */ - public void ignoreErrorCode(int code) { - myIgnoredErrorCodes.add(code); - } - - /** - * Check if error code should be ignored - * - * @param code a code to check - * @return true if error code is ignorable - */ - public boolean isIgnoredErrorCode(int code) { - return myIgnoredErrorCodes.contains(code); - } - - - /** - * add error to the error list - * - * @param ex an error to add to the list - */ - public void addError(VcsException ex) { - myErrors.add(ex); - } - - public void addLastOutput(String line) { - if (myLastOutput.size() < LAST_OUTPUT_SIZE) { - myLastOutput.add(line); - } - else { - myLastOutput.add(0, line); - Collections.rotate(myLastOutput, -1); - } - } - - public List getLastOutput() { - return myLastOutput; - } - - /** - * @return unmodifiable list of errors. - */ - public List errors() { - return Collections.unmodifiableList(myErrors); - } - - /** - * @return a context project - */ - public Project project() { - return myProject; - } - - /** - * @return the current working directory - */ - public File workingDirectory() { - return myWorkingDirectory; - } - - /** - * @return the current working directory - */ - public VirtualFile workingDirectoryFile() { - final VirtualFile file = LocalFileSystem.getInstance().findFileByIoFile(workingDirectory()); - if (file == null) { - throw new IllegalStateException("The working directly should be available: " + workingDirectory()); - } - return file; - } - - public void setPuttyKey(@Nullable String key) { - myPuttyKey = key; - } - - public void setUrl(@Nonnull String url) { - setUrls(singletonList(url)); - } - - public void setUrls(@Nonnull Collection urls) { - myUrls = urls; - } - - protected boolean isRemote() { - return myUrls != null; - } - - /** - * Add listener to handler - * - * @param listener a listener - */ - protected void addListener(ProcessEventListener listener) { - myListeners.addListener(listener); - } - - /** - * End option parameters and start file paths. The method adds {@code "--"} parameter. - */ - public void endOptions() { - myCommandLine.addParameter("--"); - } - - /** - * Add string parameters - * - * @param parameters a parameters to add - */ - @SuppressWarnings({"WeakerAccess"}) - public void addParameters(@NonNls @Nonnull String... parameters) { - addParameters(Arrays.asList(parameters)); - } - - /** - * Add parameters from the list - * - * @param parameters the parameters to add - */ - public void addParameters(List parameters) { - checkNotStarted(); - for (String parameter : parameters) { - myCommandLine.addParameter(escapeParameterIfNeeded(parameter)); - } - } - - @Nonnull - private String escapeParameterIfNeeded(@Nonnull String parameter) { - if (escapeNeeded(parameter)) { - return parameter.replaceAll("\\^", "^^^^"); - } - return parameter; - } - - private boolean escapeNeeded(@Nonnull String parameter) { - return SystemInfo.isWindows && isCmd() && parameter.contains("^"); - } - - private boolean isCmd() { - return myAppSettings.getPathToGit().toLowerCase().endsWith("cmd"); - } - - @Nonnull - private String unescapeCommandLine(@Nonnull String commandLine) { - if (escapeNeeded(commandLine)) { - return commandLine.replaceAll("\\^\\^\\^\\^", "^"); - } - return commandLine; - } - - /** - * Add file path parameters. The parameters are made relative to the working directory - * - * @param parameters a parameters to add - * @throws IllegalArgumentException if some path is not under root. - */ - public void addRelativePaths(@Nonnull FilePath... parameters) { - addRelativePaths(Arrays.asList(parameters)); - } - - /** - * Add file path parameters. The parameters are made relative to the working directory - * - * @param filePaths a parameters to add - * @throws IllegalArgumentException if some path is not under root. - */ - @SuppressWarnings({"WeakerAccess"}) - public void addRelativePaths(@Nonnull final Collection filePaths) { - checkNotStarted(); - for (FilePath path : filePaths) { - myCommandLine.addParameter(VcsFileUtil.relativePath(myWorkingDirectory, path)); - } - } - - /** - * Add virtual file parameters. The parameters are made relative to the working directory - * - * @param files a parameters to add - * @throws IllegalArgumentException if some path is not under root. - */ - @SuppressWarnings({"WeakerAccess"}) - public void addRelativeFiles(@Nonnull final Collection files) { - checkNotStarted(); - for (VirtualFile file : files) { - myCommandLine.addParameter(VcsFileUtil.relativePath(myWorkingDirectory, file)); - } - } - - /** - * Adds "--progress" parameter. Usable for long operations, such as clone or fetch. - * - * @return is "--progress" parameter supported by this version of Git. - */ - public boolean addProgressParameter() { - if (GitVersionSpecialty.ABLE_TO_USE_PROGRESS_IN_REMOTE_COMMANDS.existsIn(myVcs.getVersion())) { - addParameters("--progress"); - return true; - } - return false; - } - - /** - * check that process is not started yet - * - * @throws IllegalStateException if process has been already started - */ - private void checkNotStarted() { - if (isStarted()) { - throw new IllegalStateException("The process has been already started"); - } - } - - /** - * check that process is started - * - * @throws IllegalStateException if process has not been started - */ - protected final void checkStarted() { - if (!isStarted()) { - throw new IllegalStateException("The process is not started yet"); - } - } - - /** - * @return true if process is started - */ - public final synchronized boolean isStarted() { - return myProcess != null; - } - - - /** - * Set new value of cancellable flag (by default true) - * - * @param value a new value of the flag - */ - public void setCancellable(boolean value) { - checkNotStarted(); - myIsCancellable = value; - } - - /** - * @return cancellable state - */ - public boolean isCancellable() { - return myIsCancellable; - } - - /** - * Start process - */ - public synchronized void start() { - checkNotStarted(); - - try { - myStartTime = System.currentTimeMillis(); - if (!myProject.isDefault() && !mySilent && (myVcs != null)) { - myVcs.showCommandLine("[" + stringifyWorkingDir() + "] " + printableCommandLine()); - LOG.info("[" + stringifyWorkingDir() + "] " + printableCommandLine()); - } - else { - LOG.debug("[" + stringifyWorkingDir() + "] " + printableCommandLine()); - } - - // setup environment - if (isRemote()) { - switch (myProjectSettings.getAppSettings().getSshExecutableType()) { - case IDEA_SSH: - setupSshAuthenticator(); - break; - case NATIVE_SSH: - setupHttpAuthenticator(); - break; - case PUTTY: - setupPuttyAuthenticator(); - break; - } - } - setUpLocale(); - unsetGitTrace(); - myCommandLine.getEnvironment().clear(); - myCommandLine.getEnvironment().putAll(myEnv); - // start process - myProcess = startProcess(); - startHandlingStreams(); - } - catch (ProcessCanceledException pce) { - cleanupEnv(); - } - catch (Throwable t) { - if (!ApplicationManager.getApplication().isUnitTestMode() || !myProject.isDisposed()) { - LOG.error(t); // will surely happen if called during unit test disposal, because the working dir is simply removed then - } - cleanupEnv(); - myListeners.getMulticaster().startFailed(t); - } - } - - private void setUpLocale() { - myEnv.putAll(VcsLocaleHelper.getDefaultLocaleEnvironmentVars("git")); - } - - private void unsetGitTrace() { - myEnv.put("GIT_TRACE", "0"); - } - - private void setupHttpAuthenticator() throws IOException { - GitHttpAuthService service = ServiceManager.getService(GitHttpAuthService.class); - myEnv.put(GitAskPassXmlRpcHandler.GIT_ASK_PASS_ENV, service.getScriptPath().getPath()); - GitHttpAuthenticator httpAuthenticator = service.createAuthenticator(myProject, myCommand, ObjectUtil.assertNotNull(myUrls)); - myHttpHandler = service.registerHandler(httpAuthenticator, myProject); - myEnvironmentCleanedUp = false; - myEnv.put(GitAskPassXmlRpcHandler.GIT_ASK_PASS_HANDLER_ENV, myHttpHandler.toString()); - int port = service.getXmlRcpPort(); - myEnv.put(GitAskPassXmlRpcHandler.GIT_ASK_PASS_PORT_ENV, Integer.toString(port)); - LOG.debug(String.format("handler=%s, port=%s", myHttpHandler, port)); - addAuthListener(httpAuthenticator); - } - - private void setupPuttyAuthenticator() { - Collection urls = ObjectUtil.assertNotNull(myUrls); - String url = ContainerUtil.getFirstItem(urls); - - GitRemoteProtocol remoteProtocol = GitRemoteProtocol.fromUrl(url); - if (remoteProtocol != null) { - myEnv.put(GitSSHHandler.GIT_SSH_ENV, new File(PluginManager.getPluginPath(Git.class), "putty/plink.exe").getAbsolutePath()); - StringBuilder builder = new StringBuilder(); - builder.append("-noagent "); - if (myPuttyKey != null) { - builder.append("-i ").append(FileUtil.toSystemDependentName(myPuttyKey)); - } - else { - throw new ProcessCanceledException(); - } - myEnv.put("PLINK_ARGS", builder.toString()); - } - } - - private void setupSshAuthenticator() throws IOException { - GitXmlRpcSshService ssh = ServiceManager.getService(GitXmlRpcSshService.class); - myEnv.put(GitSSHHandler.GIT_SSH_ENV, ssh.getScriptPath().getPath()); - mySshHandler = ssh.registerHandler(new GitSSHGUIHandler(myProject), myProject); - myEnvironmentCleanedUp = false; - myEnv.put(GitSSHHandler.SSH_HANDLER_ENV, mySshHandler.toString()); - int port = ssh.getXmlRcpPort(); - myEnv.put(GitSSHHandler.SSH_PORT_ENV, Integer.toString(port)); - LOG.debug(String.format("handler=%s, port=%s", mySshHandler, port)); - - final HttpProxyManager httpProxyManager = HttpProxyManager.getInstance(); - boolean useHttpProxy = httpProxyManager.isHttpProxyEnabled() && !isSshUrlExcluded(httpProxyManager, ObjectUtil.assertNotNull(myUrls)); - myEnv.put(GitSSHHandler.SSH_USE_PROXY_ENV, String.valueOf(useHttpProxy)); - - if (useHttpProxy) { - myEnv.put(GitSSHHandler.SSH_PROXY_HOST_ENV, StringUtil.notNullize(httpProxyManager.getProxyHost())); - myEnv.put(GitSSHHandler.SSH_PROXY_PORT_ENV, String.valueOf(httpProxyManager.getProxyPort())); - boolean proxyAuthentication = httpProxyManager.isProxyAuthenticationEnabled(); - myEnv.put(GitSSHHandler.SSH_PROXY_AUTHENTICATION_ENV, String.valueOf(proxyAuthentication)); - - if (proxyAuthentication) { - myEnv.put(GitSSHHandler.SSH_PROXY_USER_ENV, StringUtil.notNullize(httpProxyManager.getProxyLogin())); - myEnv.put(GitSSHHandler.SSH_PROXY_PASSWORD_ENV, StringUtil.notNullize(httpProxyManager.getPlainProxyPassword())); - } - } - } - - protected static boolean isSshUrlExcluded(@Nonnull final HttpProxyManager httpProxyManager, @Nonnull Collection urls) { - return ContainerUtil.exists(urls, url -> !httpProxyManager.isHttpProxyEnabledForUrl(url)); - } - - private void addAuthListener(@Nonnull final GitHttpAuthenticator authenticator) { - // TODO this code should be located in GitLineHandler, and the other remote code should be move there as well - if (this instanceof GitLineHandler) { - ((GitLineHandler)this).addLineListener(new GitLineHandlerAdapter() { - @Override - public void onLineAvailable(@NonNls String line, Key outputType) { - String lowerCaseLine = line.toLowerCase(); - if (lowerCaseLine.contains("authentication failed") || lowerCaseLine.contains("403 forbidden")) { - LOG.debug("auth listener: auth failure detected: " + line); - myHttpAuthFailed = true; - } - } - - @Override - public void processTerminated(int exitCode) { - LOG.debug("auth listener: process terminated. auth failed=" + myHttpAuthFailed + ", cancelled=" + authenticator.wasCancelled()); - if (!authenticator.wasCancelled()) { - if (myHttpAuthFailed) { - authenticator.forgetPassword(); + private UUID mySshHandler; + private UUID myHttpHandler; + private Predicate myInputProcessor; // The processor for stdin + + // if true process might be cancelled + // note that access is safe because it accessed in unsynchronized block only after process is started, and it does not change after that + @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"}) + private boolean myIsCancellable = true; + + private Integer myExitCode; // exit code or null if exit code is not yet available + + @Nonnull + @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"}) + private Charset myCharset = StandardCharsets.UTF_8; // Character set to use for IO + + private final EventDispatcher myListeners = EventDispatcher.create(ProcessEventListener.class); + @SuppressWarnings({"FieldAccessedSynchronizedAndUnsynchronized"}) + protected boolean mySilent; // if true, the command execution is not logged in version control view + + protected final GitVcs myVcs; + private final Map myEnv; + private GitVcsApplicationSettings myAppSettings; + private GitVcsSettings myProjectSettings; + + private long myStartTime; // git execution start timestamp + private static final long LONG_TIME = 10 * 1000; + @Nullable + private Collection myUrls; + @Nullable + private String myPuttyKey; + private boolean myHttpAuthFailed; + + /** + * A constructor + * + * @param project a project + * @param directory a process directory + * @param command a command to execute (if empty string, the parameter is ignored) + */ + protected GitHandler(@Nonnull Project project, @Nonnull File directory, @Nonnull GitCommand command) { + myProject = project; + myCommand = command; + myAppSettings = GitVcsApplicationSettings.getInstance(); + myProjectSettings = GitVcsSettings.getInstance(myProject); + myEnv = new HashMap<>(EnvironmentUtil.getEnvironmentMap()); + myVcs = ObjectUtil.assertNotNull(GitVcs.getInstance(project)); + myWorkingDirectory = directory; + myCommandLine = new GeneralCommandLine(); + if (myAppSettings != null) { + myCommandLine.setExePath(GitExecutableManager.getInstance().getPathToGit(project)); + } + myCommandLine.setWorkDirectory(myWorkingDirectory); + if (GitVersionSpecialty.CAN_OVERRIDE_GIT_CONFIG_FOR_COMMAND.existsIn(myVcs.getVersion())) { + myCommandLine.addParameters("-c", "core.quotepath=false"); + } + myCommandLine.addParameter(command.name()); + myStdoutSuppressed = true; + mySilent = myCommand.lockingPolicy() == GitCommand.LockingPolicy.READ; + } + + /** + * A constructor + * + * @param project a project + * @param vcsRoot a process directory + * @param command a command to execute + */ + protected GitHandler(Project project, VirtualFile vcsRoot, GitCommand command) { + this(project, VirtualFileUtil.virtualToIoFile(vcsRoot), command); + } + + /** + * @return multicaster for listeners + */ + protected ProcessEventListener listeners() { + return myListeners.getMulticaster(); + } + + /** + * Add error code to ignored list + * + * @param code the code to ignore + */ + public void ignoreErrorCode(int code) { + myIgnoredErrorCodes.add(code); + } + + /** + * Check if error code should be ignored + * + * @param code a code to check + * @return true if error code is ignorable + */ + public boolean isIgnoredErrorCode(int code) { + return myIgnoredErrorCodes.contains(code); + } + + /** + * add error to the error list + * + * @param ex an error to add to the list + */ + public void addError(VcsException ex) { + myErrors.add(ex); + } + + public void addLastOutput(String line) { + if (myLastOutput.size() < LAST_OUTPUT_SIZE) { + myLastOutput.add(line); + } + else { + myLastOutput.add(0, line); + Collections.rotate(myLastOutput, -1); + } + } + + public List getLastOutput() { + return myLastOutput; + } + + /** + * @return unmodifiable list of errors. + */ + public List errors() { + return Collections.unmodifiableList(myErrors); + } + + /** + * @return a context project + */ + public Project project() { + return myProject; + } + + /** + * @return the current working directory + */ + public File workingDirectory() { + return myWorkingDirectory; + } + + /** + * @return the current working directory + */ + public VirtualFile workingDirectoryFile() { + VirtualFile file = LocalFileSystem.getInstance().findFileByIoFile(workingDirectory()); + if (file == null) { + throw new IllegalStateException("The working directly should be available: " + workingDirectory()); + } + return file; + } + + public void setPuttyKey(@Nullable String key) { + myPuttyKey = key; + } + + public void setUrl(@Nonnull String url) { + setUrls(singletonList(url)); + } + + public void setUrls(@Nonnull Collection urls) { + myUrls = urls; + } + + protected boolean isRemote() { + return myUrls != null; + } + + /** + * Add listener to handler + * + * @param listener a listener + */ + protected void addListener(ProcessEventListener listener) { + myListeners.addListener(listener); + } + + /** + * End option parameters and start file paths. The method adds {@code "--"} parameter. + */ + public void endOptions() { + myCommandLine.addParameter("--"); + } + + /** + * Add string parameters + * + * @param parameters a parameters to add + */ + @SuppressWarnings({"WeakerAccess"}) + public void addParameters(@Nonnull String... parameters) { + addParameters(Arrays.asList(parameters)); + } + + /** + * Add parameters from the list + * + * @param parameters the parameters to add + */ + public void addParameters(List parameters) { + checkNotStarted(); + for (String parameter : parameters) { + myCommandLine.addParameter(escapeParameterIfNeeded(parameter)); + } + } + + @Nonnull + private String escapeParameterIfNeeded(@Nonnull String parameter) { + if (escapeNeeded(parameter)) { + return parameter.replaceAll("\\^", "^^^^"); + } + return parameter; + } + + private boolean escapeNeeded(@Nonnull String parameter) { + return Platform.current().os().isWindows() && isCmd() && parameter.contains("^"); + } + + private boolean isCmd() { + return myAppSettings.getPathToGit().toLowerCase().endsWith("cmd"); + } + + @Nonnull + private String unescapeCommandLine(@Nonnull String commandLine) { + if (escapeNeeded(commandLine)) { + return commandLine.replaceAll("\\^\\^\\^\\^", "^"); + } + return commandLine; + } + + /** + * Add file path parameters. The parameters are made relative to the working directory + * + * @param parameters a parameters to add + * @throws IllegalArgumentException if some path is not under root. + */ + public void addRelativePaths(@Nonnull FilePath... parameters) { + addRelativePaths(Arrays.asList(parameters)); + } + + /** + * Add file path parameters. The parameters are made relative to the working directory + * + * @param filePaths a parameters to add + * @throws IllegalArgumentException if some path is not under root. + */ + @SuppressWarnings({"WeakerAccess"}) + public void addRelativePaths(@Nonnull Collection filePaths) { + checkNotStarted(); + for (FilePath path : filePaths) { + myCommandLine.addParameter(VcsFileUtil.relativePath(myWorkingDirectory, path)); + } + } + + /** + * Add virtual file parameters. The parameters are made relative to the working directory + * + * @param files a parameters to add + * @throws IllegalArgumentException if some path is not under root. + */ + @SuppressWarnings({"WeakerAccess"}) + public void addRelativeFiles(@Nonnull Collection files) { + checkNotStarted(); + for (VirtualFile file : files) { + myCommandLine.addParameter(VcsFileUtil.relativePath(myWorkingDirectory, file)); + } + } + + /** + * Adds "--progress" parameter. Usable for long operations, such as clone or fetch. + * + * @return is "--progress" parameter supported by this version of Git. + */ + public boolean addProgressParameter() { + if (GitVersionSpecialty.ABLE_TO_USE_PROGRESS_IN_REMOTE_COMMANDS.existsIn(myVcs.getVersion())) { + addParameters("--progress"); + return true; + } + return false; + } + + /** + * check that process is not started yet + * + * @throws IllegalStateException if process has been already started + */ + private void checkNotStarted() { + if (isStarted()) { + throw new IllegalStateException("The process has been already started"); + } + } + + /** + * check that process is started + * + * @throws IllegalStateException if process has not been started + */ + protected final void checkStarted() { + if (!isStarted()) { + throw new IllegalStateException("The process is not started yet"); + } + } + + /** + * @return true if process is started + */ + public final synchronized boolean isStarted() { + return myProcess != null; + } + + /** + * Set new value of cancellable flag (by default true) + * + * @param value a new value of the flag + */ + public void setCancellable(boolean value) { + checkNotStarted(); + myIsCancellable = value; + } + + /** + * @return cancellable state + */ + public boolean isCancellable() { + return myIsCancellable; + } + + /** + * Start process + */ + public synchronized void start() { + checkNotStarted(); + + try { + myStartTime = System.currentTimeMillis(); + if (!myProject.isDefault() && !mySilent && (myVcs != null)) { + myVcs.showCommandLine("[" + stringifyWorkingDir() + "] " + printableCommandLine()); + LOG.info("[" + stringifyWorkingDir() + "] " + printableCommandLine()); + } + else { + LOG.debug("[" + stringifyWorkingDir() + "] " + printableCommandLine()); + } + + // setup environment + if (isRemote()) { + switch (myProjectSettings.getAppSettings().getSshExecutableType()) { + case IDEA_SSH: + setupSshAuthenticator(); + break; + case NATIVE_SSH: + setupHttpAuthenticator(); + break; + case PUTTY: + setupPuttyAuthenticator(); + break; + } + } + setUpLocale(); + unsetGitTrace(); + myCommandLine.getEnvironment().clear(); + myCommandLine.getEnvironment().putAll(myEnv); + // start process + myProcess = startProcess(); + startHandlingStreams(); + } + catch (ProcessCanceledException pce) { + cleanupEnv(); + } + catch (Throwable t) { + if (!myProject.getApplication().isUnitTestMode() || !myProject.isDisposed()) { + LOG.error(t); // will surely happen if called during unit test disposal, because the working dir is simply removed then + } + cleanupEnv(); + myListeners.getMulticaster().startFailed(t); + } + } + + private void setUpLocale() { + myEnv.putAll(VcsLocaleHelper.getDefaultLocaleEnvironmentVars("git")); + } + + private void unsetGitTrace() { + myEnv.put("GIT_TRACE", "0"); + } + + private void setupHttpAuthenticator() throws IOException { + GitHttpAuthService service = ServiceManager.getService(GitHttpAuthService.class); + myEnv.put(GitAskPassXmlRpcHandler.GIT_ASK_PASS_ENV, service.getScriptPath().getPath()); + GitHttpAuthenticator httpAuthenticator = service.createAuthenticator(myProject, myCommand, ObjectUtil.assertNotNull(myUrls)); + myHttpHandler = service.registerHandler(httpAuthenticator, myProject); + myEnvironmentCleanedUp = false; + myEnv.put(GitAskPassXmlRpcHandler.GIT_ASK_PASS_HANDLER_ENV, myHttpHandler.toString()); + int port = service.getXmlRcpPort(); + myEnv.put(GitAskPassXmlRpcHandler.GIT_ASK_PASS_PORT_ENV, Integer.toString(port)); + LOG.debug(String.format("handler=%s, port=%s", myHttpHandler, port)); + addAuthListener(httpAuthenticator); + } + + private void setupPuttyAuthenticator() { + Collection urls = ObjectUtil.assertNotNull(myUrls); + String url = ContainerUtil.getFirstItem(urls); + + GitRemoteProtocol remoteProtocol = GitRemoteProtocol.fromUrl(url); + if (remoteProtocol != null) { + myEnv.put(GitSSHHandler.GIT_SSH_ENV, new File(PluginManager.getPluginPath(Git.class), "putty/plink.exe").getAbsolutePath()); + StringBuilder builder = new StringBuilder(); + builder.append("-noagent "); + if (myPuttyKey != null) { + builder.append("-i ").append(FileUtil.toSystemDependentName(myPuttyKey)); + } + else { + throw new ProcessCanceledException(); + } + myEnv.put("PLINK_ARGS", builder.toString()); + } + } + + private void setupSshAuthenticator() throws IOException { + GitXmlRpcSshService ssh = ServiceManager.getService(GitXmlRpcSshService.class); + myEnv.put(GitSSHHandler.GIT_SSH_ENV, ssh.getScriptPath().getPath()); + mySshHandler = ssh.registerHandler(new GitSSHGUIHandler(myProject), myProject); + myEnvironmentCleanedUp = false; + myEnv.put(GitSSHHandler.SSH_HANDLER_ENV, mySshHandler.toString()); + int port = ssh.getXmlRcpPort(); + myEnv.put(GitSSHHandler.SSH_PORT_ENV, Integer.toString(port)); + LOG.debug(String.format("handler=%s, port=%s", mySshHandler, port)); + + HttpProxyManager httpProxyManager = HttpProxyManager.getInstance(); + boolean useHttpProxy = + httpProxyManager.isHttpProxyEnabled() && !isSshUrlExcluded(httpProxyManager, ObjectUtil.assertNotNull(myUrls)); + myEnv.put(GitSSHHandler.SSH_USE_PROXY_ENV, String.valueOf(useHttpProxy)); + + if (useHttpProxy) { + myEnv.put(GitSSHHandler.SSH_PROXY_HOST_ENV, StringUtil.notNullize(httpProxyManager.getProxyHost())); + myEnv.put(GitSSHHandler.SSH_PROXY_PORT_ENV, String.valueOf(httpProxyManager.getProxyPort())); + boolean proxyAuthentication = httpProxyManager.isProxyAuthenticationEnabled(); + myEnv.put(GitSSHHandler.SSH_PROXY_AUTHENTICATION_ENV, String.valueOf(proxyAuthentication)); + + if (proxyAuthentication) { + myEnv.put(GitSSHHandler.SSH_PROXY_USER_ENV, StringUtil.notNullize(httpProxyManager.getProxyLogin())); + myEnv.put(GitSSHHandler.SSH_PROXY_PASSWORD_ENV, StringUtil.notNullize(httpProxyManager.getPlainProxyPassword())); + } + } + } + + protected static boolean isSshUrlExcluded(@Nonnull HttpProxyManager httpProxyManager, @Nonnull Collection urls) { + return ContainerUtil.exists(urls, url -> !httpProxyManager.isHttpProxyEnabledForUrl(url)); + } + + private void addAuthListener(@Nonnull final GitHttpAuthenticator authenticator) { + // TODO this code should be located in GitLineHandler, and the other remote code should be move there as well + if (this instanceof GitLineHandler lineHandler) { + lineHandler.addLineListener(new GitLineHandlerAdapter() { + @Override + public void onLineAvailable(String line, Key outputType) { + String lowerCaseLine = line.toLowerCase(); + if (lowerCaseLine.contains("authentication failed") || lowerCaseLine.contains("403 forbidden")) { + LOG.debug("auth listener: auth failure detected: " + line); + myHttpAuthFailed = true; + } + } + + @Override + public void processTerminated(int exitCode) { + LOG.debug("auth listener: process terminated. auth failed=" + myHttpAuthFailed + ", cancelled=" + authenticator.wasCancelled()); + if (authenticator.wasCancelled()) { + myHttpAuthFailed = false; + } + else if (myHttpAuthFailed) { + authenticator.forgetPassword(); + } + else { + authenticator.saveAuthData(); + } + } + }); + } + } + + public boolean hasHttpAuthFailed() { + return myHttpAuthFailed; + } + + protected abstract Process startProcess() throws ExecutionException; + + /** + * Start handling process output streams for the handler. + */ + protected abstract void startHandlingStreams(); + + /** + * @return a command line with full path to executable replace to "git" + */ + public String printableCommandLine() { + return unescapeCommandLine(myCommandLine.getCommandLineString("git")); + } + + /** + * Cancel activity + */ + public synchronized void cancel() { + checkStarted(); + if (!myIsCancellable) { + throw new IllegalStateException("The process is not cancellable."); + } + destroyProcess(); + } + + /** + * Destroy process + */ + public abstract void destroyProcess(); + + /** + * @return exit code for process if it is available + */ + public synchronized int getExitCode() { + if (myExitCode == null) { + throw new IllegalStateException("Exit code is not yet available"); + } + return myExitCode; + } + + /** + * @param exitCode a exit code for process + */ + protected synchronized void setExitCode(int exitCode) { + if (myExitCode == null) { + myExitCode = exitCode; + } + else { + LOG.info("Not setting exit code " + exitCode + ", because it was already set to " + myExitCode); + } + } + + /** + * Cleanup environment + */ + protected synchronized void cleanupEnv() { + if (myEnvironmentCleanedUp) { + return; + } + if (mySshHandler != null) { + ServiceManager.getService(GitXmlRpcSshService.class).unregisterHandler(mySshHandler); + } + if (myHttpHandler != null) { + ServiceManager.getService(GitHttpAuthService.class).unregisterHandler(myHttpHandler); + } + myEnvironmentCleanedUp = true; + } + + /** + * Wait for process termination + */ + public void waitFor() { + checkStarted(); + try { + if (myInputProcessor != null && myProcess != null) { + myInputProcessor.test(myProcess.getOutputStream()); + } + } + finally { + waitForProcess(); + } + } + + /** + * Wait for process + */ + protected abstract void waitForProcess(); + + /** + * Set silent mode. When handler is silent, it does not logs command in version control console. + * Note that this option also suppresses stderr and stdout copying. + * + * @param silent a new value of the flag + * @see #setStderrSuppressed(boolean) + * @see #setStdoutSuppressed(boolean) + */ + @SuppressWarnings({"SameParameterValue"}) + public void setSilent(boolean silent) { + checkNotStarted(); + mySilent = silent; + if (silent) { + setStderrSuppressed(true); + setStdoutSuppressed(true); + } + } + + /** + * @return a character set to use for IO + */ + @Nonnull + public Charset getCharset() { + return myCharset; + } + + /** + * Set character set for IO + * + * @param charset a character set + */ + @SuppressWarnings({"SameParameterValue"}) + public void setCharset(@Nonnull Charset charset) { + myCharset = charset; + } + + /** + * @return true if standard output is not copied to the console + */ + public boolean isStdoutSuppressed() { + return myStdoutSuppressed; + } + + /** + * Set flag specifying if stdout should be copied to the console + * + * @param stdoutSuppressed true if output is not copied to the console + */ + public void setStdoutSuppressed(boolean stdoutSuppressed) { + checkNotStarted(); + myStdoutSuppressed = stdoutSuppressed; + } + + /** + * @return true if standard output is not copied to the console + */ + public boolean isStderrSuppressed() { + return myStderrSuppressed; + } + + /** + * Set flag specifying if stderr should be copied to the console + * + * @param stderrSuppressed true if error output is not copied to the console + */ + public void setStderrSuppressed(boolean stderrSuppressed) { + checkNotStarted(); + myStderrSuppressed = stderrSuppressed; + } + + /** + * Set environment variable + * + * @param name the variable name + * @param value the variable value + */ + public void setEnvironment(String name, String value) { + myEnv.put(name, value); + } + + /** + * @return true if the command line is too big + */ + public boolean isLargeCommandLine() { + return myCommandLine.getCommandLineString().length() > VcsFileUtil.FILE_PATH_LIMIT; + } + + public void runInCurrentThread(@Nullable Runnable postStartAction) { + //LOG.assertTrue(!ApplicationManager.getApplication().isDispatchThread(), "Git process should never start in the dispatch thread."); + + GitVcs vcs = GitVcs.getInstance(myProject); + if (vcs == null) { + return; + } + + if (WRITE == myCommand.lockingPolicy()) { + // need to lock only write operations: reads can be performed even when a write operation is going on + vcs.getCommandLock().writeLock().lock(); + } + try { + start(); + if (isStarted()) { + if (postStartAction != null) { + postStartAction.run(); + } + waitFor(); + } + } + finally { + if (WRITE == myCommand.lockingPolicy()) { + vcs.getCommandLock().writeLock().unlock(); + } + + logTime(); + } + } + + @Nonnull + private String stringifyWorkingDir() { + String basePath = myProject.getBasePath(); + if (basePath != null) { + String relPath = FileUtil.getRelativePath(basePath, FileUtil.toSystemIndependentName(myWorkingDirectory.getPath()), '/'); + if (".".equals(relPath)) { + return myWorkingDirectory.getName(); + } + else if (relPath != null) { + return FileUtil.toSystemDependentName(relPath); + } + } + return myWorkingDirectory.getPath(); + } + + private void logTime() { + if (myStartTime > 0) { + long time = System.currentTimeMillis() - myStartTime; + if (!TIME_LOG.isDebugEnabled() && time > LONG_TIME) { + LOG.info(String.format( + "git %s took %s ms. Command parameters: %n%s", + myCommand, + time, + myCommandLine.getCommandLineString() + )); } else { - authenticator.saveAuthData(); + TIME_LOG.debug(String.format("git %s took %s ms", myCommand, time)); } - } - else { - myHttpAuthFailed = false; - } - } - }); - } - } - - public boolean hasHttpAuthFailed() { - return myHttpAuthFailed; - } - - protected abstract Process startProcess() throws ExecutionException; - - /** - * Start handling process output streams for the handler. - */ - protected abstract void startHandlingStreams(); - - /** - * @return a command line with full path to executable replace to "git" - */ - public String printableCommandLine() { - return unescapeCommandLine(myCommandLine.getCommandLineString("git")); - } - - /** - * Cancel activity - */ - public synchronized void cancel() { - checkStarted(); - if (!myIsCancellable) { - throw new IllegalStateException("The process is not cancellable."); - } - destroyProcess(); - } - - /** - * Destroy process - */ - public abstract void destroyProcess(); - - /** - * @return exit code for process if it is available - */ - public synchronized int getExitCode() { - if (myExitCode == null) { - throw new IllegalStateException("Exit code is not yet available"); - } - return myExitCode.intValue(); - } - - /** - * @param exitCode a exit code for process - */ - protected synchronized void setExitCode(int exitCode) { - if (myExitCode == null) { - myExitCode = exitCode; - } - else { - LOG.info("Not setting exit code " + exitCode + ", because it was already set to " + myExitCode); - } - } - - /** - * Cleanup environment - */ - protected synchronized void cleanupEnv() { - if (myEnvironmentCleanedUp) { - return; - } - if (mySshHandler != null) { - ServiceManager.getService(GitXmlRpcSshService.class).unregisterHandler(mySshHandler); - } - if (myHttpHandler != null) { - ServiceManager.getService(GitHttpAuthService.class).unregisterHandler(myHttpHandler); - } - myEnvironmentCleanedUp = true; - } - - /** - * Wait for process termination - */ - public void waitFor() { - checkStarted(); - try { - if (myInputProcessor != null && myProcess != null) { - myInputProcessor.process(myProcess.getOutputStream()); - } - } - finally { - waitForProcess(); - } - } - - /** - * Wait for process - */ - protected abstract void waitForProcess(); - - /** - * Set silent mode. When handler is silent, it does not logs command in version control console. - * Note that this option also suppresses stderr and stdout copying. - * - * @param silent a new value of the flag - * @see #setStderrSuppressed(boolean) - * @see #setStdoutSuppressed(boolean) - */ - @SuppressWarnings({"SameParameterValue"}) - public void setSilent(final boolean silent) { - checkNotStarted(); - mySilent = silent; - if (silent) { - setStderrSuppressed(true); - setStdoutSuppressed(true); - } - } - - /** - * @return a character set to use for IO - */ - @Nonnull - public Charset getCharset() { - return myCharset; - } - - /** - * Set character set for IO - * - * @param charset a character set - */ - @SuppressWarnings({"SameParameterValue"}) - public void setCharset(@Nonnull Charset charset) { - myCharset = charset; - } - - /** - * @return true if standard output is not copied to the console - */ - public boolean isStdoutSuppressed() { - return myStdoutSuppressed; - } - - /** - * Set flag specifying if stdout should be copied to the console - * - * @param stdoutSuppressed true if output is not copied to the console - */ - public void setStdoutSuppressed(final boolean stdoutSuppressed) { - checkNotStarted(); - myStdoutSuppressed = stdoutSuppressed; - } - - /** - * @return true if standard output is not copied to the console - */ - public boolean isStderrSuppressed() { - return myStderrSuppressed; - } - - /** - * Set flag specifying if stderr should be copied to the console - * - * @param stderrSuppressed true if error output is not copied to the console - */ - public void setStderrSuppressed(final boolean stderrSuppressed) { - checkNotStarted(); - myStderrSuppressed = stderrSuppressed; - } - - /** - * Set environment variable - * - * @param name the variable name - * @param value the variable value - */ - public void setEnvironment(String name, String value) { - myEnv.put(name, value); - } - - /** - * @return true if the command line is too big - */ - public boolean isLargeCommandLine() { - return myCommandLine.getCommandLineString().length() > VcsFileUtil.FILE_PATH_LIMIT; - } - - public void runInCurrentThread(@Nullable Runnable postStartAction) { - //LOG.assertTrue(!ApplicationManager.getApplication().isDispatchThread(), "Git process should never start in the dispatch thread."); - - final GitVcs vcs = GitVcs.getInstance(myProject); - if (vcs == null) { - return; - } - - if (WRITE == myCommand.lockingPolicy()) { - // need to lock only write operations: reads can be performed even when a write operation is going on - vcs.getCommandLock().writeLock().lock(); - } - try { - start(); - if (isStarted()) { - if (postStartAction != null) { - postStartAction.run(); - } - waitFor(); - } - } - finally { - if (WRITE == myCommand.lockingPolicy()) { - vcs.getCommandLock().writeLock().unlock(); - } - - logTime(); - } - } - - @Nonnull - private String stringifyWorkingDir() { - String basePath = myProject.getBasePath(); - if (basePath != null) { - String relPath = FileUtil.getRelativePath(basePath, FileUtil.toSystemIndependentName(myWorkingDirectory.getPath()), '/'); - if (".".equals(relPath)) { - return myWorkingDirectory.getName(); - } - else if (relPath != null) { - return FileUtil.toSystemDependentName(relPath); - } - } - return myWorkingDirectory.getPath(); - } - - private void logTime() { - if (myStartTime > 0) { - long time = System.currentTimeMillis() - myStartTime; - if (!TIME_LOG.isDebugEnabled() && time > LONG_TIME) { - LOG.info(String.format("git %s took %s ms. Command parameters: %n%s", myCommand, time, myCommandLine.getCommandLineString())); - } - else { - TIME_LOG.debug(String.format("git %s took %s ms", myCommand, time)); - } - } - else { - LOG.debug(String.format("git %s finished.", myCommand)); - } - } - - @Override - public String toString() { - return myCommandLine.toString(); - } + } + else { + LOG.debug(String.format("git %s finished.", myCommand)); + } + } + + @Override + public String toString() { + return myCommandLine.toString(); + } } diff --git a/plugin/src/main/java/git4idea/commands/GitHandlerListener.java b/plugin/src/main/java/git4idea/commands/GitHandlerListener.java index cd93ae4..e2bb9de 100644 --- a/plugin/src/main/java/git4idea/commands/GitHandlerListener.java +++ b/plugin/src/main/java/git4idea/commands/GitHandlerListener.java @@ -20,6 +20,5 @@ /** * Listener for event common for all handlers */ -public interface GitHandlerListener extends ProcessEventListener -{ +public interface GitHandlerListener extends ProcessEventListener { } diff --git a/plugin/src/main/java/git4idea/commands/GitHandlerUtil.java b/plugin/src/main/java/git4idea/commands/GitHandlerUtil.java index 9e5a5f5..4dc90cf 100644 --- a/plugin/src/main/java/git4idea/commands/GitHandlerUtil.java +++ b/plugin/src/main/java/git4idea/commands/GitHandlerUtil.java @@ -81,7 +81,7 @@ protected String getErrorText() { * @return An exit code */ public static int doSynchronously( - final GitLineHandler handler, + GitLineHandler handler, @Nonnull LocalizeValue operationTitle, @Nonnull LocalizeValue operationName ) { @@ -123,10 +123,10 @@ public static int doSynchronously( final boolean showErrors, final boolean setIndeterminateFlag ) { - final ProgressManager manager = ProgressManager.getInstance(); + ProgressManager manager = ProgressManager.getInstance(); manager.run(new Task.Modal(handler.project(), operationTitle, false) { @Override - public void run(@Nonnull final ProgressIndicator indicator) { + public void run(@Nonnull ProgressIndicator indicator) { handler.addLineListener(new GitLineHandlerListenerProgress(indicator, handler, operationName, showErrors)); runInCurrentThread(handler, indicator, setIndeterminateFlag, operationTitle); } @@ -165,10 +165,10 @@ private static void runHandlerSynchronously( * @param operationName */ public static void runInCurrentThread( - final GitHandler handler, - final ProgressIndicator indicator, - final boolean setIndeterminateFlag, - @Nonnull final LocalizeValue operationName + GitHandler handler, + ProgressIndicator indicator, + boolean setIndeterminateFlag, + @Nonnull LocalizeValue operationName ) { runInCurrentThread( handler, @@ -194,7 +194,7 @@ public static void runInCurrentThread( * @param handler a handler to run * @param postStartAction an action that is executed */ - public static void runInCurrentThread(final GitHandler handler, @Nullable final Runnable postStartAction) { + public static void runInCurrentThread(GitHandler handler, @Nullable Runnable postStartAction) { handler.runInCurrentThread(postStartAction); } @@ -204,8 +204,8 @@ public static void runInCurrentThread(final GitHandler handler, @Nullable final * @param handler a handler to use * @return the collection of exception collected during operation */ - public static Collection doSynchronouslyWithExceptions(final GitLineHandler handler) { - final ProgressIndicator progressIndicator = ProgressManager.getInstance().getProgressIndicator(); + public static Collection doSynchronouslyWithExceptions(GitLineHandler handler) { + ProgressIndicator progressIndicator = ProgressManager.getInstance().getProgressIndicator(); return doSynchronouslyWithExceptions(handler, progressIndicator, LocalizeValue.empty()); } @@ -218,8 +218,8 @@ public static Collection doSynchronouslyWithExceptions(final GitLi * @return the collection of exception collected during operation */ public static Collection doSynchronouslyWithExceptions( - final GitLineHandler handler, - final ProgressIndicator progressIndicator, + GitLineHandler handler, + ProgressIndicator progressIndicator, @Nonnull LocalizeValue operationName ) { handler.addLineListener(new GitLineHandlerListenerProgress(progressIndicator, handler, operationName, false)); @@ -254,7 +254,7 @@ private abstract static class GitHandlerListenerBase implements GitHandlerListen * @param handler a handler instance * @param operationName an operation name */ - public GitHandlerListenerBase(final GitHandler handler, @Nonnull LocalizeValue operationName) { + public GitHandlerListenerBase(GitHandler handler, @Nonnull LocalizeValue operationName) { this(handler, operationName, true); } @@ -275,7 +275,7 @@ public GitHandlerListenerBase(GitHandler handler, @Nonnull LocalizeValue operati * {@inheritDoc} */ @Override - public void processTerminated(final int exitCode) { + public void processTerminated(int exitCode) { if (exitCode != 0 && !myHandler.isIgnoredErrorCode(exitCode)) { ensureError(exitCode); if (myShowErrors) { @@ -289,7 +289,7 @@ public void processTerminated(final int exitCode) { * * @param exitCode the exit code of the process */ - protected void ensureError(final int exitCode) { + protected void ensureError(int exitCode) { if (myHandler.errors().isEmpty()) { String text = getErrorText(); if (StringUtil.isEmpty(text) && myHandler.errors().isEmpty()) { @@ -312,7 +312,7 @@ protected void ensureError(final int exitCode) { * {@inheritDoc} */ @Override - public void startFailed(final Throwable exception) { + public void startFailed(Throwable exception) { //noinspection ThrowableInstanceNeverThrown myHandler.addError(new VcsException("Git start failed: " + exception.getMessage(), exception)); if (myShowErrors) { @@ -359,7 +359,7 @@ public static class GitLineHandlerListenerProgress extends GitLineHandlerListene * @param showErrors if true, the errors are shown when process is terminated */ public GitLineHandlerListenerProgress( - final ProgressIndicator manager, + ProgressIndicator manager, GitHandler handler, @Nonnull LocalizeValue operationName, boolean showErrors @@ -381,7 +381,7 @@ protected String getErrorText() { * {@inheritDoc} */ @Override - public void onLineAvailable(final String line, final Key outputType) { + public void onLineAvailable(String line, Key outputType) { if (isErrorLine(line.trim())) { //noinspection ThrowableInstanceNeverThrown myHandler.addError(new VcsException(line)); diff --git a/plugin/src/main/java/git4idea/commands/GitTask.java b/plugin/src/main/java/git4idea/commands/GitTask.java index 14c563c..858e9c9 100644 --- a/plugin/src/main/java/git4idea/commands/GitTask.java +++ b/plugin/src/main/java/git4idea/commands/GitTask.java @@ -94,12 +94,12 @@ public void executeModal(GitTaskResultHandler resultHandler) { * @param resultHandler callback called after the task has finished or was cancelled by user or automatically. */ @RequiredUIAccess - public void executeAsync(final GitTaskResultHandler resultHandler) { + public void executeAsync(GitTaskResultHandler resultHandler) { execute(false, false, resultHandler); } @RequiredUIAccess - public void executeInBackground(boolean sync, final GitTaskResultHandler resultHandler) { + public void executeInBackground(boolean sync, GitTaskResultHandler resultHandler) { execute(sync, false, resultHandler); } @@ -132,7 +132,7 @@ public void execute(boolean sync, boolean modal, final GitTaskResultHandler resu final AtomicBoolean completed = new AtomicBoolean(); if (modal) { - final ModalTask task = new ModalTask(myProject, myHandler, myTitle) { + ModalTask task = new ModalTask(myProject, myHandler, myTitle) { @Override @RequiredUIAccess public void onSuccess() { @@ -151,7 +151,7 @@ public void onCancel() { application.invokeAndWait(() -> ProgressManager.getInstance().run(task), application.getDefaultModalityState()); } else { - final BackgroundableTask task = new BackgroundableTask(myProject, myHandler, myTitle) { + BackgroundableTask task = new BackgroundableTask(myProject, myHandler, myTitle) { @Override @RequiredUIAccess public void onSuccess() { @@ -188,7 +188,7 @@ public void onCancel() { } } - private void commonOnSuccess(final Object LOCK, final GitTaskResultHandler resultHandler) { + private void commonOnSuccess(Object LOCK, GitTaskResultHandler resultHandler) { GitTaskResult res = !myHandler.errors().isEmpty() ? GitTaskResult.GIT_ERROR : GitTaskResult.OK; resultHandler.run(res); synchronized (LOCK) { @@ -196,7 +196,7 @@ private void commonOnSuccess(final Object LOCK, final GitTaskResultHandler resul } } - private void commonOnCancel(final Object LOCK, final GitTaskResultHandler resultHandler) { + private void commonOnCancel(Object LOCK, GitTaskResultHandler resultHandler) { resultHandler.run(GitTaskResult.CANCELLED); synchronized (LOCK) { LOCK.notifyAll(); @@ -208,7 +208,7 @@ private void addListeners(final TaskExecution task, final ProgressIndicator indi indicator.setIndeterminate(myProgressAnalyzer == null); } // When receives an error line, adds a VcsException to the GitHandler. - final GitLineHandlerListener listener = new GitLineHandlerListener() { + GitLineHandlerListener listener = new GitLineHandlerListener() { @Override public void processTerminated(int exitCode) { if (exitCode != 0 && !myHandler.isIgnoredErrorCode(exitCode)) { @@ -235,7 +235,7 @@ else if (!StringUtil.isEmptyOrSpaces(line)) { indicator.setText2(line); } if (myProgressAnalyzer != null && indicator != null) { - final double fraction = myProgressAnalyzer.analyzeProgress(line); + double fraction = myProgressAnalyzer.analyzeProgress(line); if (fraction >= 0) { indicator.setFraction(fraction); } @@ -243,8 +243,8 @@ else if (!StringUtil.isEmptyOrSpaces(line)) { } }; - if (myHandler instanceof GitLineHandler) { - ((GitLineHandler)myHandler).addLineListener(listener); + if (myHandler instanceof GitLineHandler lineHandler) { + lineHandler.addLineListener(listener); } else { myHandler.addListener(listener); @@ -289,7 +289,7 @@ private interface TaskExecution { private abstract class BackgroundableTask extends Task.Backgroundable implements TaskExecution { private GitTaskDelegate myDelegate; - public BackgroundableTask(@Nullable final Project project, @Nonnull GitHandler handler, @Nonnull final LocalizeValue processTitle) { + public BackgroundableTask(@Nullable Project project, @Nonnull GitHandler handler, @Nonnull LocalizeValue processTitle) { super(project, processTitle, true); myDelegate = new GitTaskDelegate(project, handler, this); } @@ -303,7 +303,7 @@ public final void run(@Nonnull ProgressIndicator indicator) { public final void runAlone() { Application application = Application.get(); if (application.isDispatchThread()) { - application.executeOnPooledThread((Runnable)this::justRun); + application.executeOnPooledThread((Runnable) this::justRun); } else { justRun(); @@ -339,7 +339,7 @@ public void dispose() { private abstract class ModalTask extends Task.Modal implements TaskExecution { private GitTaskDelegate myDelegate; - public ModalTask(@Nullable final Project project, @Nonnull GitHandler handler, @Nonnull final LocalizeValue processTitle) { + public ModalTask(@Nullable Project project, @Nonnull GitHandler handler, @Nonnull LocalizeValue processTitle) { super(project, processTitle, true); myDelegate = new GitTaskDelegate(project, handler, this); } @@ -362,7 +362,7 @@ public void dispose() { } /** - * Does the work which is common for BackgrounableTask and ModalTask. + * Does the work which is common for BackgroundableTask and ModalTask. * Actually - starts a timer which checks if current progress indicator is cancelled. * If yes, kills the GitHandler. */ diff --git a/plugin/src/main/java/git4idea/commands/GitTaskResultNotificationHandler.java b/plugin/src/main/java/git4idea/commands/GitTaskResultNotificationHandler.java index 90ea1f2..fc4fcb8 100644 --- a/plugin/src/main/java/git4idea/commands/GitTaskResultNotificationHandler.java +++ b/plugin/src/main/java/git4idea/commands/GitTaskResultNotificationHandler.java @@ -21,13 +21,14 @@ import jakarta.annotation.Nonnull; public class GitTaskResultNotificationHandler extends GitTaskResultHandlerAdapter { + @Nonnull private final Project myProject; private final LocalizeValue mySuccessMessage; private final LocalizeValue myCancelMessage; private final LocalizeValue myErrorMessage; public GitTaskResultNotificationHandler( - Project project, + @Nonnull Project project, @Nonnull LocalizeValue successMessage, @Nonnull LocalizeValue cancelMessage, @Nonnull LocalizeValue errorMessage diff --git a/plugin/src/main/java/git4idea/history/GitDiffFromHistoryHandler.java b/plugin/src/main/java/git4idea/history/GitDiffFromHistoryHandler.java index 53de8f5..c09c16e 100644 --- a/plugin/src/main/java/git4idea/history/GitDiffFromHistoryHandler.java +++ b/plugin/src/main/java/git4idea/history/GitDiffFromHistoryHandler.java @@ -18,7 +18,7 @@ import consulo.application.progress.ProgressIndicator; import consulo.application.progress.Task; import consulo.dataContext.DataContext; -import consulo.ide.ServiceManager; +import consulo.localize.LocalizeValue; import consulo.logging.Logger; import consulo.project.Project; import consulo.project.ui.notification.NotificationType; @@ -28,7 +28,6 @@ import consulo.ui.ex.popup.JBPopupFactory; import consulo.ui.ex.popup.ListPopup; import consulo.util.collection.ArrayUtil; -import consulo.util.collection.ContainerUtil; import consulo.util.io.FileUtil; import consulo.versionControlSystem.FilePath; import consulo.versionControlSystem.VcsException; @@ -84,7 +83,13 @@ public GitDiffFromHistoryHandler(@Nonnull Project project) { } @Override - public void showDiffForOne(@Nonnull AnActionEvent e, @Nonnull Project project, @Nonnull FilePath filePath, @Nonnull VcsFileRevision previousRevision, @Nonnull VcsFileRevision revision) { + public void showDiffForOne( + @Nonnull AnActionEvent e, + @Nonnull Project project, + @Nonnull FilePath filePath, + @Nonnull VcsFileRevision previousRevision, + @Nonnull VcsFileRevision revision + ) { GitFileRevision rev = (GitFileRevision) revision; Collection parents = rev.getParents(); if (parents.size() < 2) { @@ -97,12 +102,22 @@ public void showDiffForOne(@Nonnull AnActionEvent e, @Nonnull Project project, @ @Nonnull @Override - protected List getChangesBetweenRevisions(@Nonnull FilePath path, @Nonnull GitFileRevision rev1, @Nullable GitFileRevision rev2) throws VcsException { + protected List getChangesBetweenRevisions( + @Nonnull FilePath path, + @Nonnull GitFileRevision rev1, + @Nullable GitFileRevision rev2 + ) throws VcsException { GitRepository repository = getRepository(path); String hash1 = rev1.getHash(); String hash2 = rev2 != null ? rev2.getHash() : null; - return ContainerUtil.newArrayList(GitChangeUtils.getDiff(repository.getProject(), repository.getRoot(), hash1, hash2, Collections.singletonList(path))); + return new ArrayList<>(GitChangeUtils.getDiff( + repository.getProject(), + repository.getRoot(), + hash1, + hash2, + Collections.singletonList(path) + )); } @Nonnull @@ -110,7 +125,14 @@ protected List getChangesBetweenRevisions(@Nonnull FilePath path, @Nonnu protected List getAffectedChanges(@Nonnull FilePath path, @Nonnull GitFileRevision rev) throws VcsException { GitRepository repository = getRepository(path); - return ContainerUtil.newArrayList(GitChangeUtils.getRevisionChanges(repository.getProject(), repository.getRoot(), rev.getHash(), false, true, true).getChanges()); + return new ArrayList<>(GitChangeUtils.getRevisionChanges( + repository.getProject(), + repository.getRoot(), + rev.getHash(), + false, + true, + true + ).getChanges()); } @Nonnull @@ -126,18 +148,31 @@ private GitRepository getRepository(@Nonnull FilePath path) { return repository; } - private void showDiffForMergeCommit(@Nonnull final AnActionEvent event, @Nonnull final FilePath filePath, @Nonnull final GitFileRevision rev, @Nonnull final Collection parents) { - - checkIfFileWasTouchedAndFindParentsInBackground(filePath, rev, parents, new Consumer<>() { - @Override - public void accept(MergeCommitPreCheckInfo info) { + private void showDiffForMergeCommit( + @Nonnull AnActionEvent event, + @Nonnull FilePath filePath, + @Nonnull GitFileRevision rev, + @Nonnull Collection parents + ) { + checkIfFileWasTouchedAndFindParentsInBackground( + filePath, + rev, + parents, + info -> { if (!info.wasFileTouched()) { - String message = String.format("There were no changes in %s in this merge commit, besides those which were made in both branches", filePath.getName()); - VcsBalloonProblemNotifier.showOverVersionControlView(GitDiffFromHistoryHandler.this.myProject, message, NotificationType.INFORMATION); + String message = String.format( + "There were no changes in %s in this merge commit, besides those which were made in both branches", + filePath.getName() + ); + VcsBalloonProblemNotifier.showOverVersionControlView( + GitDiffFromHistoryHandler.this.myProject, + message, + NotificationType.INFORMATION + ); } showPopup(event, rev, filePath, info.getParents()); } - }); + ); } private static class MergeCommitPreCheckInfo { @@ -158,11 +193,13 @@ public Collection getParents() { } } - private void checkIfFileWasTouchedAndFindParentsInBackground(@Nonnull final FilePath filePath, - @Nonnull final GitFileRevision rev, - @Nonnull final Collection parentHashes, - @Nonnull final Consumer resultHandler) { - new Task.Backgroundable(myProject, "Loading changes...", true) { + private void checkIfFileWasTouchedAndFindParentsInBackground( + @Nonnull final FilePath filePath, + @Nonnull final GitFileRevision rev, + @Nonnull final Collection parentHashes, + @Nonnull final Consumer resultHandler + ) { + new Task.Backgroundable(myProject, LocalizeValue.localizeTODO("Loading changes..."), true) { private MergeCommitPreCheckInfo myInfo; @Override @@ -190,7 +227,11 @@ public void onSuccess() { } @Nonnull - private Collection findParentRevisions(@Nonnull GitRepository repository, @Nonnull GitFileRevision currentRevision, @Nonnull Collection parentHashes) throws VcsException { + private Collection findParentRevisions( + @Nonnull GitRepository repository, + @Nonnull GitFileRevision currentRevision, + @Nonnull Collection parentHashes + ) throws VcsException { // currentRevision is a merge revision. // the file could be renamed in one of the branches, i.e. the name in one of the parent revisions may be different from the name // in currentRevision. It can be different even in both parents, but it would a rename-rename conflict, and we don't handle such anyway. @@ -203,7 +244,11 @@ private Collection findParentRevisions(@Nonnull GitRepository r } @Nonnull - private GitFileRevision createParentRevision(@Nonnull GitRepository repository, @Nonnull GitFileRevision currentRevision, @Nonnull String parentHash) throws VcsException { + private GitFileRevision createParentRevision( + @Nonnull GitRepository repository, + @Nonnull GitFileRevision currentRevision, + @Nonnull String parentHash + ) throws VcsException { FilePath currentRevisionPath = currentRevision.getPath(); if (currentRevisionPath.isDirectory()) { // for directories the history doesn't follow renames @@ -221,21 +266,37 @@ private GitFileRevision createParentRevision(@Nonnull GitRepository repository, return new GitFileRevision(myProject, path, new GitRevisionNumber(parentHash)); } } - LOG.error(String.format("Could not find parent revision. Will use the path from parent revision. Current revision: %s, parent hash: %s", currentRevision, parentHash)); + LOG.error(String.format( + "Could not find parent revision. Will use the path from parent revision. Current revision: %s, parent hash: %s", + currentRevision, + parentHash + )); return makeRevisionFromHash(currentRevisionPath, parentHash); } - private void showPopup(@Nonnull AnActionEvent event, @Nonnull GitFileRevision rev, @Nonnull FilePath filePath, @Nonnull Collection parents) { + private void showPopup( + @Nonnull AnActionEvent event, + @Nonnull GitFileRevision rev, + @Nonnull FilePath filePath, + @Nonnull Collection parents + ) { ActionGroup parentActions = createActionGroup(rev, filePath, parents); DataContext dataContext = DataContext.builder().add(Project.KEY, myProject).build(); - ListPopup popup = JBPopupFactory.getInstance().createActionGroupPopup("Choose parent to compare", parentActions, dataContext, JBPopupFactory.ActionSelectionAid.NUMBERING, true); + ListPopup popup = JBPopupFactory.getInstance() + .createActionGroupPopup( + "Choose parent to compare", + parentActions, + dataContext, + JBPopupFactory.ActionSelectionAid.NUMBERING, + true + ); showPopupInBestPosition(popup, event, dataContext); } private static void showPopupInBestPosition(@Nonnull ListPopup popup, @Nonnull AnActionEvent event, @Nonnull DataContext dataContext) { - if (event.getInputEvent() instanceof MouseEvent) { + if (event.getInputEvent() instanceof MouseEvent mouseEvent) { if (!event.getPlace().equals(ActionPlaces.UPDATE_POPUP)) { - popup.show(new RelativePoint((MouseEvent) event.getInputEvent())); + popup.show(new RelativePoint(mouseEvent)); } else { // quick fix for invoking from the context menu: coordinates are calculated incorrectly there. popup.showInBestPositionFor(dataContext); @@ -247,7 +308,11 @@ private static void showPopupInBestPosition(@Nonnull ListPopup popup, @Nonnull A } @Nonnull - private ActionGroup createActionGroup(@Nonnull GitFileRevision rev, @Nonnull FilePath filePath, @Nonnull Collection parents) { + private ActionGroup createActionGroup( + @Nonnull GitFileRevision rev, + @Nonnull FilePath filePath, + @Nonnull Collection parents + ) { Collection actions = new ArrayList<>(2); for (GitFileRevision parent : parents) { actions.add(createParentAction(rev, filePath, parent)); @@ -270,7 +335,7 @@ private boolean wasFileTouched(@Nonnull GitRepository repository, @Nonnull GitFi if (result.success()) { return isFilePresentInOutput(repository, rev.getPath(), result.getOutput()); } - throw new VcsException(result.getErrorOutputAsJoinedString()); + throw new VcsException(result.getErrorOutputAsJoinedValue()); } private static boolean isFilePresentInOutput(@Nonnull GitRepository repository, @Nonnull FilePath path, @Nonnull List output) { @@ -306,11 +371,10 @@ public ShowDiffWithParentAction(@Nonnull FilePath filePath, @Nonnull GitFileRevi myParentRevision = parent; } - @RequiredUIAccess @Override - public void actionPerformed(AnActionEvent e) { + @RequiredUIAccess + public void actionPerformed(@Nonnull AnActionEvent e) { doShowDiff(myFilePath, myParentRevision, myRevision); } - } } diff --git a/plugin/src/main/java/git4idea/history/GitHistoryUtils.java b/plugin/src/main/java/git4idea/history/GitHistoryUtils.java index b6dbb3c..7620af4 100644 --- a/plugin/src/main/java/git4idea/history/GitHistoryUtils.java +++ b/plugin/src/main/java/git4idea/history/GitHistoryUtils.java @@ -191,7 +191,7 @@ public static ItemLatestState getLastRevision(@Nonnull Project project, @Nonnull /* === Smart full log with renames === - 'git log --follow' does detect renames, but it has a bug - merge commits aren't handled properly: they just dissapear from the history. + 'git log --follow' does detect renames, but it has a bug - merge commits aren't handled properly: they just disappear from the history. See http://kerneltrap.org/mailarchive/git/2009/1/30/4861054 and the whole thread about that: --follow is buggy, but maybe it won't be fixed. To get the whole history through renames we do the following: 1. 'git log ' - and we get the history since the first rename, if there was one. diff --git a/plugin/src/main/java/git4idea/merge/GitConflictResolver.java b/plugin/src/main/java/git4idea/merge/GitConflictResolver.java index d4d2f5c..bb874a6 100644 --- a/plugin/src/main/java/git4idea/merge/GitConflictResolver.java +++ b/plugin/src/main/java/git4idea/merge/GitConflictResolver.java @@ -16,11 +16,13 @@ package git4idea.merge; import consulo.application.Application; -import consulo.application.ApplicationManager; +import consulo.localize.LocalizeValue; import consulo.logging.Logger; import consulo.project.Project; import consulo.project.ui.notification.Notification; +import consulo.project.ui.notification.NotificationService; import consulo.project.ui.notification.event.NotificationListener; +import consulo.ui.annotation.RequiredUIAccess; import consulo.util.collection.ContainerUtil; import consulo.util.lang.StringUtil; import consulo.versionControlSystem.AbstractVcsHelper; @@ -36,8 +38,8 @@ import git4idea.repo.GitRepository; import git4idea.repo.GitRepositoryManager; import git4idea.util.StringScanner; - import jakarta.annotation.Nonnull; + import javax.swing.event.HyperlinkEvent; import java.io.File; import java.util.*; @@ -50,280 +52,296 @@ * The class is highly customizable, since the procedure of resolving conflicts is very common in Git operations. */ public class GitConflictResolver { - private static final Logger LOG = Logger.getInstance(GitConflictResolver.class); - - @Nonnull - private final Collection myRoots; - @Nonnull - private final Params myParams; - - @Nonnull - protected final Project myProject; - @Nonnull - private final Git myGit; - @Nonnull - private final GitRepositoryManager myRepositoryManager; - @Nonnull - private final AbstractVcsHelper myVcsHelper; - @Nonnull - private final GitVcs myVcs; - - /** - * Customizing parameters - mostly String notification texts, etc. - */ - public static class Params { - private boolean reverse; - private String myErrorNotificationTitle = ""; - private String myErrorNotificationAdditionalDescription = ""; - private String myMergeDescription = ""; - private MergeDialogCustomizer myMergeDialogCustomizer = new MergeDialogCustomizer() { - @Override - public String getMultipleFileMergeDescription(@Nonnull Collection files) { - return myMergeDescription; - } - }; + private static final Logger LOG = Logger.getInstance(GitConflictResolver.class); + + @Nonnull + private final Collection myRoots; + @Nonnull + private final Params myParams; + + @Nonnull + protected final Project myProject; + @Nonnull + protected final NotificationService myNotificationService; + @Nonnull + private final Git myGit; + @Nonnull + private final GitRepositoryManager myRepositoryManager; + @Nonnull + private final AbstractVcsHelper myVcsHelper; + @Nonnull + private final GitVcs myVcs; + + /** + * Customizing parameters - mostly String notification texts, etc. + */ + public static class Params { + private boolean reverse; + private String myErrorNotificationTitle = ""; + private String myErrorNotificationAdditionalDescription = ""; + private String myMergeDescription = ""; + private MergeDialogCustomizer myMergeDialogCustomizer = new MergeDialogCustomizer() { + @Override + public String getMultipleFileMergeDescription(@Nonnull Collection files) { + return myMergeDescription; + } + }; + + /** + * @param reverseMerge specify {@code true} if reverse merge provider has to be used for merging - it is the case of rebase or stash. + */ + public Params setReverse(boolean reverseMerge) { + reverse = reverseMerge; + return this; + } + + public Params setErrorNotificationTitle(String errorNotificationTitle) { + myErrorNotificationTitle = errorNotificationTitle; + return this; + } + + public Params setErrorNotificationAdditionalDescription(String errorNotificationAdditionalDescription) { + myErrorNotificationAdditionalDescription = errorNotificationAdditionalDescription; + return this; + } + + public Params setMergeDescription(String mergeDescription) { + myMergeDescription = mergeDescription; + return this; + } + + public Params setMergeDialogCustomizer(MergeDialogCustomizer mergeDialogCustomizer) { + myMergeDialogCustomizer = mergeDialogCustomizer; + return this; + } + } + + public GitConflictResolver(@Nonnull Project project, @Nonnull Git git, @Nonnull Collection roots, @Nonnull Params params) { + myProject = project; + myNotificationService = NotificationService.getInstance(); + myGit = git; + myRoots = roots; + myParams = params; + myRepositoryManager = GitUtil.getRepositoryManager(myProject); + myVcsHelper = AbstractVcsHelper.getInstance(project); + myVcs = assertNotNull(GitVcs.getInstance(myProject)); + } /** - * @param reverseMerge specify {@code true} if reverse merge provider has to be used for merging - it is the case of rebase or stash. + *

+ * Goes throw the procedure of merging conflicts via MergeTool for different types of operations. + *

    + *
  • Checks if there are unmerged files. If not, executes {@link #proceedIfNothingToMerge()}
  • + *
  • Otherwise shows a {@link MultipleFileMergeDialog} where user is able to merge files.
  • + *
  • After the dialog is closed, checks if unmerged files remain. + * If everything is merged, executes {@link #proceedAfterAllMerged()}. Otherwise shows a notification.
  • + *
+ *

+ *

+ * If a Git error happens during seeking for unmerged files or in other cases, + * the method shows a notification and returns {@code false}. + *

+ * + * @return {@code true} if there is nothing to merge anymore, {@code false} if unmerged files remain or in the case of error. */ - public Params setReverse(boolean reverseMerge) { - reverse = reverseMerge; - return this; + @RequiredUIAccess + public final boolean merge() { + return merge(false); } - public Params setErrorNotificationTitle(String errorNotificationTitle) { - myErrorNotificationTitle = errorNotificationTitle; - return this; + /** + * This is executed from {@link #merge()} if the initial check tells that there is nothing to merge. + * In the basic implementation no action is performed, {@code true} is returned. + * + * @return Return value is returned from {@link #merge()} + */ + protected boolean proceedIfNothingToMerge() throws VcsException { + return true; } - public Params setErrorNotificationAdditionalDescription(String errorNotificationAdditionalDescription) { - myErrorNotificationAdditionalDescription = errorNotificationAdditionalDescription; - return this; + /** + * This is executed from {@link #merge()} after all conflicts are resolved. + * In the basic implementation no action is performed, {@code true} is returned. + * + * @return Return value is returned from {@link #merge()} + */ + protected boolean proceedAfterAllMerged() throws VcsException { + return true; } - public Params setMergeDescription(String mergeDescription) { - myMergeDescription = mergeDescription; - return this; + /** + * Invoke the merge dialog, but execute nothing after merge is completed. + * + * @return true if all changes were merged, false if unresolved merges remain. + */ + @RequiredUIAccess + public final boolean mergeNoProceed() { + return merge(true); } - public Params setMergeDialogCustomizer(MergeDialogCustomizer mergeDialogCustomizer) { - myMergeDialogCustomizer = mergeDialogCustomizer; - return this; + /** + * Shows notification that not all conflicts were resolved. + */ + protected void notifyUnresolvedRemain() { + notifyWarning( + myParams.myErrorNotificationTitle, + "You have to
resolve all conflicts first." + myParams.myErrorNotificationAdditionalDescription + ); } - } - - public GitConflictResolver(@Nonnull Project project, @Nonnull Git git, @Nonnull Collection roots, @Nonnull Params params) { - myProject = project; - myGit = git; - myRoots = roots; - myParams = params; - myRepositoryManager = GitUtil.getRepositoryManager(myProject); - myVcsHelper = AbstractVcsHelper.getInstance(project); - myVcs = assertNotNull(GitVcs.getInstance(myProject)); - } - - /** - *

- * Goes throw the procedure of merging conflicts via MergeTool for different types of operations. - *

    - *
  • Checks if there are unmerged files. If not, executes {@link #proceedIfNothingToMerge()}
  • - *
  • Otherwise shows a {@link MultipleFileMergeDialog} where user is able to merge files.
  • - *
  • After the dialog is closed, checks if unmerged files remain. - * If everything is merged, executes {@link #proceedAfterAllMerged()}. Otherwise shows a notification.
  • - *
- *

- *

- * If a Git error happens during seeking for unmerged files or in other cases, - * the method shows a notification and returns {@code false}. - *

- * - * @return {@code true} if there is nothing to merge anymore, {@code false} if unmerged files remain or in the case of error. - */ - public final boolean merge() { - return merge(false); - } - - /** - * This is executed from {@link #merge()} if the initial check tells that there is nothing to merge. - * In the basic implementation no action is performed, {@code true} is returned. - * - * @return Return value is returned from {@link #merge()} - */ - protected boolean proceedIfNothingToMerge() throws VcsException { - return true; - } - - /** - * This is executed from {@link #merge()} after all conflicts are resolved. - * In the basic implementation no action is performed, {@code true} is returned. - * - * @return Return value is returned from {@link #merge()} - */ - protected boolean proceedAfterAllMerged() throws VcsException { - return true; - } - - /** - * Invoke the merge dialog, but execute nothing after merge is completed. - * - * @return true if all changes were merged, false if unresolved merges remain. - */ - public final boolean mergeNoProceed() { - return merge(true); - } - - /** - * Shows notification that not all conflicts were resolved. - */ - protected void notifyUnresolvedRemain() { - notifyWarning(myParams.myErrorNotificationTitle, - "You have to resolve all conflicts first." + myParams.myErrorNotificationAdditionalDescription); - } - - /** - * Shows notification that some conflicts were still not resolved - after user invoked the conflict resolver by pressing the link on the - * notification. - */ - private void notifyUnresolvedRemainAfterNotification() { - notifyWarning("Not all conflicts resolved", - "You should resolve all conflicts before update.
" + myParams.myErrorNotificationAdditionalDescription); - } - - private void notifyWarning(String title, String content) { - VcsNotifier.getInstance(myProject).notifyImportantWarning(title, content, new ResolveNotificationListener()); - } - - private boolean merge(boolean mergeDialogInvokedFromNotification) { - try { - final Collection initiallyUnmergedFiles = getUnmergedFiles(myRoots); - if (initiallyUnmergedFiles.isEmpty()) { - LOG.info("merge: no unmerged files"); - return mergeDialogInvokedFromNotification ? true : proceedIfNothingToMerge(); - } - else { - showMergeDialog(initiallyUnmergedFiles); - - final Collection unmergedFilesAfterResolve = getUnmergedFiles(myRoots); - if (unmergedFilesAfterResolve.isEmpty()) { - LOG.info("merge no more unmerged files"); - return mergeDialogInvokedFromNotification ? true : proceedAfterAllMerged(); + /** + * Shows notification that some conflicts were still not resolved - after user invoked the conflict resolver by pressing the link on the + * notification. + */ + private void notifyUnresolvedRemainAfterNotification() { + notifyWarning( + "Not all conflicts resolved", + "You should resolve all conflicts before update.
" + myParams.myErrorNotificationAdditionalDescription + ); + } + + private void notifyWarning(String title, String content) { + myNotificationService.newWarn(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO(title)) + .content(LocalizeValue.localizeTODO(content)) + .optionalHyperlinkListener(new ResolveNotificationListener()) + .notify(myProject); + } + + @RequiredUIAccess + private boolean merge(boolean mergeDialogInvokedFromNotification) { + try { + Collection initiallyUnmergedFiles = getUnmergedFiles(myRoots); + if (initiallyUnmergedFiles.isEmpty()) { + LOG.info("merge: no unmerged files"); + return mergeDialogInvokedFromNotification || proceedIfNothingToMerge(); + } + else { + showMergeDialog(initiallyUnmergedFiles); + + Collection unmergedFilesAfterResolve = getUnmergedFiles(myRoots); + if (unmergedFilesAfterResolve.isEmpty()) { + LOG.info("merge no more unmerged files"); + return mergeDialogInvokedFromNotification || proceedAfterAllMerged(); + } + else { + LOG.info("mergeFiles unmerged files remain: " + unmergedFilesAfterResolve); + if (mergeDialogInvokedFromNotification) { + notifyUnresolvedRemainAfterNotification(); + } + else { + notifyUnresolvedRemain(); + } + } + } } - else { - LOG.info("mergeFiles unmerged files remain: " + unmergedFilesAfterResolve); - if (mergeDialogInvokedFromNotification) { - notifyUnresolvedRemainAfterNotification(); - } - else { - notifyUnresolvedRemain(); - } + catch (VcsException e) { + if (myVcs.getExecutableValidator().checkExecutableAndNotifyIfNeeded()) { + notifyException(e); + } } - } - } - catch (VcsException e) { - if (myVcs.getExecutableValidator().checkExecutableAndNotifyIfNeeded()) { - notifyException(e); - } + return false; + } - return false; - - } - - private void showMergeDialog(@Nonnull final Collection initiallyUnmergedFiles) { - Application application = Application.get(); - application.invokeAndWait(() -> { - MergeProvider mergeProvider = new GitMergeProvider(myProject, myParams.reverse); - myVcsHelper.showMergeDialog(new ArrayList<>(initiallyUnmergedFiles), mergeProvider, myParams.myMergeDialogCustomizer); - }, application.getDefaultModalityState()); - } - - private void notifyException(VcsException e) { - LOG.info("mergeFiles ", e); - final String description = "Couldn't check the working tree for unmerged files because of an error."; - VcsNotifier.getInstance(myProject) - .notifyError(myParams.myErrorNotificationTitle, description + myParams.myErrorNotificationAdditionalDescription + "
" + - e.getLocalizedMessage(), new ResolveNotificationListener()); - } - - - @Nonnull - protected NotificationListener getResolveLinkListener() { - return new ResolveNotificationListener(); - } - - private class ResolveNotificationListener implements NotificationListener { - @Override - public void hyperlinkUpdate(@Nonnull final Notification notification, @Nonnull HyperlinkEvent event) { - if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED && event.getDescription().equals("resolve")) { - notification.expire(); - ApplicationManager.getApplication().executeOnPooledThread(new Runnable() { - @Override - public void run() { - mergeNoProceed(); - } - }); - } + + @RequiredUIAccess + private void showMergeDialog(@Nonnull Collection initiallyUnmergedFiles) { + Application application = myProject.getApplication(); + application.invokeAndWait( + () -> { + MergeProvider mergeProvider = new GitMergeProvider(myProject, myParams.reverse); + myVcsHelper.showMergeDialog(new ArrayList<>(initiallyUnmergedFiles), mergeProvider, myParams.myMergeDialogCustomizer); + }, + application.getDefaultModalityState() + ); } - } - - /** - * @return unmerged files in the given Git roots, all in a single collection. - * @see #getUnmergedFiles(VirtualFile) - */ - private Collection getUnmergedFiles(@Nonnull Collection roots) throws VcsException { - final Collection unmergedFiles = new HashSet<>(); - for (VirtualFile root : roots) { - unmergedFiles.addAll(getUnmergedFiles(root)); + + private void notifyException(VcsException e) { + LOG.info("mergeFiles ", e); + myNotificationService.newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO(myParams.myErrorNotificationTitle)) + .content(LocalizeValue.localizeTODO( + "Couldn't check the working tree for unmerged files because of an error." + + myParams.myErrorNotificationAdditionalDescription + "
" + e.getLocalizedMessage() + )) + .optionalHyperlinkListener(new ResolveNotificationListener()) + .notify(myProject); } - return unmergedFiles; - } - - /** - * @return unmerged files in the given Git root. - * @see #getUnmergedFiles(Collection - */ - private Collection getUnmergedFiles(@Nonnull VirtualFile root) throws VcsException { - return unmergedFiles(root); - } - - /** - * Parse changes from lines - * - * @param root the git root - * @return a set of unmerged files - * @throws VcsException if the input format does not matches expected format - */ - private List unmergedFiles(final VirtualFile root) throws VcsException { - GitRepository repository = myRepositoryManager.getRepositoryForRoot(root); - if (repository == null) { - LOG.error("Repository not found for root " + root); - return Collections.emptyList(); + + @Nonnull + protected NotificationListener getResolveLinkListener() { + return new ResolveNotificationListener(); } - GitCommandResult result = myGit.getUnmergedFiles(repository); - if (!result.success()) { - throw new VcsException(result.getErrorOutputAsJoinedString()); + private class ResolveNotificationListener implements NotificationListener { + @Override + @RequiredUIAccess + public void hyperlinkUpdate(@Nonnull Notification notification, @Nonnull HyperlinkEvent event) { + if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED && event.getDescription().equals("resolve")) { + notification.expire(); + myProject.getApplication().executeOnPooledThread((Runnable) GitConflictResolver.this::mergeNoProceed); + } + } } - String output = StringUtil.join(result.getOutput(), "\n"); - HashSet unmergedPaths = new HashSet<>(); - for (StringScanner s = new StringScanner(output); s.hasMoreData(); ) { - if (s.isEol()) { - s.nextLine(); - continue; - } - s.boundedToken('\t'); - String relative = s.line(); - unmergedPaths.add(GitUtil.unescapePath(relative)); + /** + * @return unmerged files in the given Git roots, all in a single collection. + * @see #getUnmergedFiles(VirtualFile) + */ + private Collection getUnmergedFiles(@Nonnull Collection roots) throws VcsException { + Collection unmergedFiles = new HashSet<>(); + for (VirtualFile root : roots) { + unmergedFiles.addAll(getUnmergedFiles(root)); + } + return unmergedFiles; } - if (unmergedPaths.size() == 0) { - return Collections.emptyList(); + /** + * @return unmerged files in the given Git root. + * @see #getUnmergedFiles(Collection + */ + private Collection getUnmergedFiles(@Nonnull VirtualFile root) throws VcsException { + return unmergedFiles(root); } - else { - List files = ContainerUtil.map(unmergedPaths, path -> new File(root.getPath(), path)); - return sortVirtualFilesByPresentation(findVirtualFilesWithRefresh(files)); + + /** + * Parse changes from lines + * + * @param root the git root + * @return a set of unmerged files + * @throws VcsException if the input format does not matches expected format + */ + private List unmergedFiles(VirtualFile root) throws VcsException { + GitRepository repository = myRepositoryManager.getRepositoryForRoot(root); + if (repository == null) { + LOG.error("Repository not found for root " + root); + return Collections.emptyList(); + } + + GitCommandResult result = myGit.getUnmergedFiles(repository); + if (!result.success()) { + throw new VcsException(result.getErrorOutputAsJoinedValue()); + } + + String output = StringUtil.join(result.getOutput(), "\n"); + HashSet unmergedPaths = new HashSet<>(); + for (StringScanner s = new StringScanner(output); s.hasMoreData(); ) { + if (s.isEol()) { + s.nextLine(); + continue; + } + s.boundedToken('\t'); + String relative = s.line(); + unmergedPaths.add(GitUtil.unescapePath(relative)); + } + + if (unmergedPaths.size() == 0) { + return Collections.emptyList(); + } + else { + List files = ContainerUtil.map(unmergedPaths, path -> new File(root.getPath(), path)); + return sortVirtualFilesByPresentation(findVirtualFilesWithRefresh(files)); + } } - } } diff --git a/plugin/src/main/java/git4idea/push/GitPushResultNotification.java b/plugin/src/main/java/git4idea/push/GitPushResultNotification.java index a3b4821..335d1c4 100644 --- a/plugin/src/main/java/git4idea/push/GitPushResultNotification.java +++ b/plugin/src/main/java/git4idea/push/GitPushResultNotification.java @@ -15,7 +15,7 @@ */ package git4idea.push; -import consulo.application.ApplicationManager; +import consulo.application.Application; import consulo.localize.LocalizeValue; import consulo.logging.Logger; import consulo.project.Project; @@ -37,6 +37,7 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; + import java.util.List; import java.util.Map; @@ -44,20 +45,22 @@ class GitPushResultNotification extends Notification { public static final LocalizeValue VIEW_FILES_UPDATED_DURING_THE_PUSH = LocalizeValue.localizeTODO("View files updated during the push"); public static final String UPDATE_WITH_RESOLVED_CONFLICTS = - "push has been cancelled, because there were conflicts during update.
" + "Check that conflicts were resolved correctly, and " + - "invoke push again."; + "push has been cancelled, because there were conflicts during update.
" + + "Check that conflicts were resolved correctly, and invoke push again."; public static final String INCOMPLETE_UPDATE = - "push has been cancelled, because not all conflicts were resolved during update.
" + "Resolve the conflicts and " + - "invoke push again."; + "push has been cancelled, because not all conflicts were resolved during update.
" + + "Resolve the conflicts and invoke push again."; public static final String UPDATE_WITH_ERRORS = "push was rejected, and update failed with error."; public static final String UPDATE_CANCELLED = "push was rejected, and update was cancelled."; private static final Logger LOG = Logger.getInstance(GitPushResultNotification.class); - public GitPushResultNotification(@Nonnull NotificationGroup groupDisplayId, - @Nonnull String title, - @Nonnull String content, - @Nonnull NotificationType type) { + public GitPushResultNotification( + @Nonnull NotificationGroup groupDisplayId, + @Nonnull String title, + @Nonnull String content, + @Nonnull NotificationType type + ) { super(groupDisplayId, title, content, type); } @@ -99,50 +102,58 @@ else if (!grouped.rejected.isEmpty() || !grouped.customRejected.isEmpty()) { UpdatedFiles updatedFiles = pushResult.getUpdatedFiles(); if (!updatedFiles.isEmpty()) { - ApplicationManager.getApplication().invokeLater(() -> { - UpdateInfoTree tree = ProjectLevelVcsManager.getInstance(project) - .showUpdateProjectInfo(updatedFiles, - "Update", - ActionInfo.UPDATE, - false); + Application.get().invokeLater(() -> { + UpdateInfoTree tree = ProjectLevelVcsManager.getInstance(project).showUpdateProjectInfo( + updatedFiles, + "Update", + ActionInfo.UPDATE, + false + ); if (tree != null) { tree.setBefore(pushResult.getBeforeUpdateLabel()); tree.setAfter(pushResult.getAfterUpdateLabel()); - notification.addAction(new ViewUpdateInfoNotification(project, + notification.addAction(new ViewUpdateInfoNotification( + project, tree, VIEW_FILES_UPDATED_DURING_THE_PUSH, - notification)); + notification + )); } }); } return notification; } - private static String formDescription(@Nonnull Map results, final boolean multiRepoProject) { - List> entries = ContainerUtil.sorted(results.entrySet(), (o1, o2) -> - { - // successful first - int compareResultTypes = GitPushRepoResult.TYPE_COMPARATOR.compare(o1.getValue().getType(), o2.getValue().getType()); - if (compareResultTypes != 0) { - return compareResultTypes; + private static String formDescription(@Nonnull Map results, boolean multiRepoProject) { + List> entries = ContainerUtil.sorted( + results.entrySet(), + (o1, o2) -> { + // successful first + int compareResultTypes = GitPushRepoResult.TYPE_COMPARATOR.compare(o1.getValue().getType(), o2.getValue().getType()); + if (compareResultTypes != 0) { + return compareResultTypes; + } + return DvcsUtil.REPOSITORY_COMPARATOR.compare(o1.getKey(), o2.getKey()); } - return DvcsUtil.REPOSITORY_COMPARATOR.compare(o1.getKey(), o2.getKey()); - }); + ); - return StringUtil.join(entries, entry -> - { - GitRepository repository = entry.getKey(); - GitPushRepoResult result = entry.getValue(); + return StringUtil.join( + entries, + entry -> { + GitRepository repository = entry.getKey(); + GitPushRepoResult result = entry.getValue(); - String description = formRepoDescription(result); - if (!multiRepoProject) { - description = StringUtil.capitalize(description); - } - else { - description = DvcsUtil.getShortRepositoryName(repository) + ": " + description; - } - return description; - }, "
"); + String description = formRepoDescription(result); + if (!multiRepoProject) { + description = StringUtil.capitalize(description); + } + else { + description = DvcsUtil.getShortRepositoryName(repository) + ": " + description; + } + return description; + }, + "
" + ); } private static String formRepoDescription(@Nonnull GitPushRepoResult result) { diff --git a/plugin/src/main/java/git4idea/rebase/GitAbortRebaseProcess.java b/plugin/src/main/java/git4idea/rebase/GitAbortRebaseProcess.java index 6ac8123..cfe5dd2 100644 --- a/plugin/src/main/java/git4idea/rebase/GitAbortRebaseProcess.java +++ b/plugin/src/main/java/git4idea/rebase/GitAbortRebaseProcess.java @@ -17,14 +17,15 @@ import consulo.application.AccessToken; import consulo.application.Application; -import consulo.application.ApplicationManager; import consulo.application.progress.ProgressIndicator; import consulo.ide.ServiceManager; +import consulo.localize.LocalizeValue; import consulo.logging.Logger; import consulo.project.Project; +import consulo.project.ui.notification.NotificationService; +import consulo.ui.annotation.RequiredUIAccess; import consulo.ui.ex.awt.Messages; -import consulo.util.collection.ContainerUtil; -import consulo.util.lang.ref.Ref; +import consulo.util.lang.ref.SimpleReference; import consulo.versionControlSystem.VcsNotifier; import consulo.versionControlSystem.distributed.DvcsUtil; import consulo.virtualFileSystem.util.VirtualFileUtil; @@ -39,6 +40,7 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -49,173 +51,187 @@ import static git4idea.rebase.GitRebaseUtils.mentionLocalChangesRemainingInStash; class GitAbortRebaseProcess { - private static final Logger LOG = Logger.getInstance(GitAbortRebaseProcess.class); - - @Nonnull - private final Project myProject; - @Nonnull - private final Git myGit; - @Nonnull - private final VcsNotifier myNotifier; - - @Nullable - private final GitRepository myRepositoryToAbort; - @Nonnull - private final Map myRepositoriesToRollback; - @Nonnull - private final Map myInitialCurrentBranches; - @Nonnull - private final ProgressIndicator myIndicator; - @Nullable - private final GitChangesSaver mySaver; - - GitAbortRebaseProcess(@Nonnull Project project, - @Nullable GitRepository repositoryToAbort, - @Nonnull Map repositoriesToRollback, - @Nonnull Map initialCurrentBranches, - @Nonnull ProgressIndicator progressIndicator, - @Nullable GitChangesSaver changesSaver) { - myProject = project; - myRepositoryToAbort = repositoryToAbort; - myRepositoriesToRollback = repositoriesToRollback; - myInitialCurrentBranches = initialCurrentBranches; - myIndicator = progressIndicator; - mySaver = changesSaver; - - myGit = ServiceManager.getService(Git.class); - myNotifier = VcsNotifier.getInstance(myProject); - } - - void abortWithConfirmation() { - LOG.info("Abort rebase. " + (myRepositoryToAbort == null ? "Nothing to abort" : getShortRepositoryName(myRepositoryToAbort)) + - ". Roots to rollback: " + DvcsUtil.joinShortNames(myRepositoriesToRollback.keySet())); - final Ref ref = Ref.create(); - Application application = ApplicationManager.getApplication(); - application.invokeAndWait(new Runnable() { - @Override - public void run() { - ref.set(confirmAbort()); - } - }, application.getDefaultModalityState()); - - LOG.info("User choice: " + ref.get()); - if (ref.get() == AbortChoice.ROLLBACK_AND_ABORT) { - doAbort(true); + private static final Logger LOG = Logger.getInstance(GitAbortRebaseProcess.class); + + @Nonnull + private final Project myProject; + @Nonnull + private final Git myGit; + @Nonnull + protected final NotificationService myNotificationService; + + @Nullable + private final GitRepository myRepositoryToAbort; + @Nonnull + private final Map myRepositoriesToRollback; + @Nonnull + private final Map myInitialCurrentBranches; + @Nonnull + private final ProgressIndicator myIndicator; + @Nullable + private final GitChangesSaver mySaver; + + GitAbortRebaseProcess( + @Nonnull Project project, + @Nullable GitRepository repositoryToAbort, + @Nonnull Map repositoriesToRollback, + @Nonnull Map initialCurrentBranches, + @Nonnull ProgressIndicator progressIndicator, + @Nullable GitChangesSaver changesSaver + ) { + myProject = project; + myNotificationService = NotificationService.getInstance(); + myRepositoryToAbort = repositoryToAbort; + myRepositoriesToRollback = repositoriesToRollback; + myInitialCurrentBranches = initialCurrentBranches; + myIndicator = progressIndicator; + mySaver = changesSaver; + + myGit = ServiceManager.getService(Git.class); } - else if (ref.get() == AbortChoice.ABORT) { - doAbort(false); - } - } - - @Nonnull - private AbortChoice confirmAbort() { - String title = "Abort Rebase"; - if (myRepositoryToAbort != null) { - if (myRepositoriesToRollback.isEmpty()) { - String message = "Are you sure you want to abort rebase" + GitUtil.mention(myRepositoryToAbort) + "?"; - int choice = DialogManager.showOkCancelDialog(myProject, message, title, "Abort", getCancelButtonText(), getQuestionIcon()); - if (choice == Messages.OK) { - return AbortChoice.ABORT; - } - } - else { - String message = "Do you want just to abort rebase" + GitUtil.mention(myRepositoryToAbort) + ",\n" + - "or also rollback the successful rebase" + GitUtil.mention(myRepositoriesToRollback.keySet()) + "?"; - int choice = DialogManager.showYesNoCancelDialog(myProject, - message, - title, - "Abort & Rollback", - "Abort", - getCancelButtonText(), - getQuestionIcon()); - if (choice == Messages.YES) { - return AbortChoice.ROLLBACK_AND_ABORT; + + @RequiredUIAccess + void abortWithConfirmation() { + LOG.info("Abort rebase. " + (myRepositoryToAbort == null ? "Nothing to abort" : getShortRepositoryName(myRepositoryToAbort)) + + ". Roots to rollback: " + DvcsUtil.joinShortNames(myRepositoriesToRollback.keySet())); + SimpleReference ref = SimpleReference.create(); + Application application = myProject.getApplication(); + application.invokeAndWait(() -> ref.set(confirmAbort()), application.getDefaultModalityState()); + + LOG.info("User choice: " + ref.get()); + if (ref.get() == AbortChoice.ROLLBACK_AND_ABORT) { + doAbort(true); } - else if (choice == Messages.NO) { - return AbortChoice.ABORT; + else if (ref.get() == AbortChoice.ABORT) { + doAbort(false); } - } } - else { - if (myRepositoriesToRollback.isEmpty()) { - LOG.error(new Throwable()); - } - else { - String description = "Do you want to rollback the successful rebase" + GitUtil.mention(myRepositoriesToRollback.keySet()) + "?"; - int choice = DialogManager.showOkCancelDialog(myProject, description, title, "Rollback", getCancelButtonText(), getQuestionIcon()); - if (choice == Messages.YES) { - return AbortChoice.ROLLBACK_AND_ABORT; + + @Nonnull + private AbortChoice confirmAbort() { + String title = "Abort Rebase"; + if (myRepositoryToAbort != null) { + if (myRepositoriesToRollback.isEmpty()) { + String message = "Are you sure you want to abort rebase" + GitUtil.mention(myRepositoryToAbort) + "?"; + int choice = DialogManager.showOkCancelDialog(myProject, message, title, "Abort", getCancelButtonText(), getQuestionIcon()); + if (choice == Messages.OK) { + return AbortChoice.ABORT; + } + } + else { + String message = "Do you want just to abort rebase" + GitUtil.mention(myRepositoryToAbort) + ",\n" + + "or also rollback the successful rebase" + GitUtil.mention(myRepositoriesToRollback.keySet()) + "?"; + int choice = DialogManager.showYesNoCancelDialog( + myProject, + message, + title, + "Abort & Rollback", + "Abort", + getCancelButtonText(), + getQuestionIcon() + ); + if (choice == Messages.YES) { + return AbortChoice.ROLLBACK_AND_ABORT; + } + else if (choice == Messages.NO) { + return AbortChoice.ABORT; + } + } } - } - } - return AbortChoice.CANCEL; - } - - enum AbortChoice { - ABORT, - ROLLBACK_AND_ABORT, - CANCEL - } - - private void doAbort(final boolean rollback) { - new GitFreezingProcess(myProject, "rebase", new Runnable() { - public void run() { - AccessToken token = DvcsUtil.workingTreeChangeStarted(myProject, "Rebase"); - List repositoriesToRefresh = ContainerUtil.newArrayList(); - try { - if (myRepositoryToAbort != null) { - myIndicator.setText2("git rebase --abort" + GitUtil.mention(myRepositoryToAbort)); - GitCommandResult result = myGit.rebaseAbort(myRepositoryToAbort); - repositoriesToRefresh.add(myRepositoryToAbort); - if (!result.success()) { - myNotifier.notifyError("Rebase Abort Failed", - result.getErrorOutputAsHtmlString() + mentionLocalChangesRemainingInStash(mySaver)); - return; + else { + if (myRepositoriesToRollback.isEmpty()) { + LOG.error(new Throwable()); } - } - - if (rollback) { - for (GitRepository repo : myRepositoriesToRollback.keySet()) { - myIndicator.setText2("git reset --keep" + GitUtil.mention(repo)); - GitCommandResult res = myGit.reset(repo, GitResetMode.KEEP, myRepositoriesToRollback.get(repo)); - repositoriesToRefresh.add(repo); - - if (res.success()) { - String initialBranchPosition = myInitialCurrentBranches.get(repo); - if (initialBranchPosition != null && !initialBranchPosition.equals(repo.getCurrentBranchName())) { - myIndicator.setText2("git checkout " + initialBranchPosition + GitUtil.mention(repo)); - res = myGit.checkout(repo, initialBranchPosition, null, true, false); + else { + String description = + "Do you want to rollback the successful rebase" + GitUtil.mention(myRepositoriesToRollback.keySet()) + "?"; + int choice = + DialogManager.showOkCancelDialog(myProject, description, title, "Rollback", getCancelButtonText(), getQuestionIcon()); + if (choice == Messages.YES) { + return AbortChoice.ROLLBACK_AND_ABORT; } - } + } + } + return AbortChoice.CANCEL; + } - if (!res.success()) { - String description = - myRepositoryToAbort != null ? "Rebase abort was successful" + GitUtil.mention(myRepositoryToAbort) + ", but rollback failed" : "Rollback failed"; - description += GitUtil.mention(repo) + ":" + res.getErrorOutputAsHtmlString() + - mentionLocalChangesRemainingInStash(mySaver); - myNotifier.notifyImportantWarning("Rebase Rollback Failed", description); - return; - } + enum AbortChoice { + ABORT, + ROLLBACK_AND_ABORT, + CANCEL + } + + private void doAbort(boolean rollback) { + new GitFreezingProcess( + myProject, + "rebase", + () -> { + AccessToken token = DvcsUtil.workingTreeChangeStarted(myProject, "Rebase"); + List repositoriesToRefresh = new ArrayList<>(); + try { + if (myRepositoryToAbort != null) { + myIndicator.setText2("git rebase --abort" + GitUtil.mention(myRepositoryToAbort)); + GitCommandResult result = myGit.rebaseAbort(myRepositoryToAbort); + repositoriesToRefresh.add(myRepositoryToAbort); + if (!result.success()) { + myNotificationService.newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Rebase Abort Failed")) + .content(LocalizeValue.localizeTODO( + result.getErrorOutputAsHtmlValue() + mentionLocalChangesRemainingInStash(mySaver) + )) + .notify(myProject); + return; + } + } + + if (rollback) { + for (GitRepository repo : myRepositoriesToRollback.keySet()) { + myIndicator.setText2("git reset --keep" + GitUtil.mention(repo)); + GitCommandResult res = myGit.reset(repo, GitResetMode.KEEP, myRepositoriesToRollback.get(repo)); + repositoriesToRefresh.add(repo); + + if (res.success()) { + String initialBranchPosition = myInitialCurrentBranches.get(repo); + if (initialBranchPosition != null && !initialBranchPosition.equals(repo.getCurrentBranchName())) { + myIndicator.setText2("git checkout " + initialBranchPosition + GitUtil.mention(repo)); + res = myGit.checkout(repo, initialBranchPosition, null, true, false); + } + } + + if (!res.success()) { + String description = myRepositoryToAbort != null + ? "Rebase abort was successful" + GitUtil.mention(myRepositoryToAbort) + ", but rollback failed" + : "Rollback failed"; + description += GitUtil.mention(repo) + ":" + res.getErrorOutputAsHtmlValue() + + mentionLocalChangesRemainingInStash(mySaver); + myNotificationService.newWarn(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Rebase Rollback Failed")) + .content(LocalizeValue.localizeTODO(description)) + .notify(myProject); + return; + } + } + } + + if (mySaver != null) { + mySaver.load(); + } + myNotificationService.newInfo(VcsNotifier.NOTIFICATION_GROUP_ID) + .content(LocalizeValue.localizeTODO("Rebase abort succeeded")) + .notify(myProject); + } + finally { + refresh(repositoriesToRefresh); + token.finish(); + } } - } + ).execute(); + } - if (mySaver != null) { - mySaver.load(); - } - myNotifier.notifySuccess("Rebase abort succeeded"); + private static void refresh(@Nonnull List toRefresh) { + for (GitRepository repository : toRefresh) { + repository.update(); } - finally { - refresh(repositoriesToRefresh); - token.finish(); - } - } - }).execute(); - } - - private static void refresh(@Nonnull List toRefresh) { - for (GitRepository repository : toRefresh) { - repository.update(); + VirtualFileUtil.markDirtyAndRefresh(false, true, false, VirtualFileUtil.toVirtualFileArray(getRootsFromRepositories(toRefresh))); } - VirtualFileUtil.markDirtyAndRefresh(false, true, false, VirtualFileUtil.toVirtualFileArray(getRootsFromRepositories(toRefresh))); - } } diff --git a/plugin/src/main/java/git4idea/rebase/GitRebaseProcess.java b/plugin/src/main/java/git4idea/rebase/GitRebaseProcess.java index 0cfd127..e585850 100644 --- a/plugin/src/main/java/git4idea/rebase/GitRebaseProcess.java +++ b/plugin/src/main/java/git4idea/rebase/GitRebaseProcess.java @@ -22,19 +22,22 @@ import consulo.component.ProcessCanceledException; import consulo.git.localize.GitLocalize; import consulo.ide.ServiceManager; +import consulo.localize.LocalizeValue; import consulo.logging.Logger; import consulo.project.Project; import consulo.project.ui.notification.Notification; +import consulo.project.ui.notification.NotificationService; import consulo.project.ui.notification.event.NotificationListener; +import consulo.ui.annotation.RequiredUIAccess; import consulo.util.collection.MultiMap; import consulo.util.lang.ExceptionUtil; import consulo.util.lang.StringUtil; import consulo.util.lang.ThreeState; -import consulo.util.lang.function.Condition; import consulo.versionControlSystem.VcsException; import consulo.versionControlSystem.VcsNotifier; import consulo.versionControlSystem.change.ChangeListManager; import consulo.versionControlSystem.distributed.DvcsUtil; +import consulo.versionControlSystem.distributed.repository.Repository; import consulo.virtualFileSystem.VirtualFile; import consulo.virtualFileSystem.util.VirtualFileUtil; import git4idea.GitUtil; @@ -64,608 +67,586 @@ import static git4idea.GitUtil.getRootsFromRepositories; import static java.util.Collections.singleton; -public class GitRebaseProcess -{ - private static final Logger LOG = Logger.getInstance(GitRebaseProcess.class); - - @Nonnull - private final Project myProject; - @Nonnull - private final Git myGit; - @Nonnull - private final ChangeListManager myChangeListManager; - @Nonnull - private final VcsNotifier myNotifier; - @Nonnull - private final GitRepositoryManager myRepositoryManager; - - @Nonnull - private final GitRebaseSpec myRebaseSpec; - @Nullable - private final GitRebaseResumeMode myCustomMode; - @Nonnull - private final GitChangesSaver mySaver; - @Nonnull - private final ProgressManager myProgressManager; - - public GitRebaseProcess(@Nonnull Project project, @Nonnull GitRebaseSpec rebaseSpec, @Nullable GitRebaseResumeMode customMode) - { - myProject = project; - myRebaseSpec = rebaseSpec; - myCustomMode = customMode; - mySaver = rebaseSpec.getSaver(); - - myGit = ServiceManager.getService(Git.class); - myChangeListManager = ChangeListManager.getInstance(myProject); - myNotifier = VcsNotifier.getInstance(myProject); - myRepositoryManager = GitUtil.getRepositoryManager(myProject); - myProgressManager = ProgressManager.getInstance(); - } - - public void rebase() - { - new GitFreezingProcess(myProject, "rebase", new Runnable() - { - public void run() - { - doRebase(); - } - }).execute(); - } - - /** - * Given a GitRebaseSpec this method either starts, or continues the ongoing rebase in multiple repositories. - *
    - *
  • It does nothing with "already successfully rebased repositories" (the ones which have {@link GitRebaseStatus} == SUCCESSFUL, - * and just remembers them to use in the resulting notification.
  • - *
  • If there is a repository with rebase in progress, it calls `git rebase --continue` (or `--skip`). - * It is assumed that there is only one such repository.
  • - *
  • For all remaining repositories rebase on which didn't start yet, it calls {@code git rebase }
  • - *
- */ - private void doRebase() - { - LOG.info("Started rebase"); - LOG.debug("Started rebase with the following spec: " + myRebaseSpec); - - Map statuses = new LinkedHashMap<>(myRebaseSpec.getStatuses()); - Collection toRefresh = new LinkedHashSet<>(); - List repositoriesToRebase = myRebaseSpec.getIncompleteRepositories(); - try(AccessToken ignored = DvcsUtil.workingTreeChangeStarted(myProject, "Rebase")) - { - if(!saveDirtyRootsInitially(repositoriesToRebase)) - { - return; - } - - GitRepository failed = null; - for(GitRepository repository : repositoriesToRebase) - { - GitRebaseResumeMode customMode = null; - if(repository == myRebaseSpec.getOngoingRebase()) - { - customMode = myCustomMode == null ? GitRebaseResumeMode.CONTINUE : myCustomMode; - } - - GitRebaseStatus rebaseStatus = rebaseSingleRoot(repository, customMode, getSuccessfulRepositories(statuses)); - repository.update(); // make the repo state info actual ASAP - statuses.put(repository, rebaseStatus); - if(shouldBeRefreshed(rebaseStatus)) - { - toRefresh.add(repository); - } - if(rebaseStatus.getType() != GitRebaseStatus.Type.SUCCESS) - { - failed = repository; - break; - } - } - - if(failed == null) - { - LOG.debug("Rebase completed successfully."); - mySaver.load(); - } - refresh(toRefresh); - if(failed == null) - { - notifySuccess(getSuccessfulRepositories(statuses), getSkippedCommits(statuses)); - } - - saveUpdatedSpec(statuses); - } - catch(ProcessCanceledException pce) - { - throw pce; - } - catch(Throwable e) - { - myRepositoryManager.setOngoingRebaseSpec(null); - ExceptionUtil.rethrowUnchecked(e); - } - } - - private void saveUpdatedSpec(@Nonnull Map statuses) - { - if(myRebaseSpec.shouldBeSaved()) - { - GitRebaseSpec newRebaseInfo = myRebaseSpec.cloneWithNewStatuses(statuses); - myRepositoryManager.setOngoingRebaseSpec(newRebaseInfo); - } - else - { - myRepositoryManager.setOngoingRebaseSpec(null); - } - } - - @Nonnull - private GitRebaseStatus rebaseSingleRoot(@Nonnull GitRepository repository, @Nullable GitRebaseResumeMode customMode, @Nonnull Map alreadyRebased) - { - VirtualFile root = repository.getRoot(); - String repoName = getShortRepositoryName(repository); - LOG.info("Rebasing root " + repoName + ", mode: " + notNull(customMode, "standard")); - - Collection skippedCommits = newArrayList(); - MultiMap allSkippedCommits = getSkippedCommits(alreadyRebased); - boolean retryWhenDirty = false; - - while(true) - { - GitRebaseProblemDetector rebaseDetector = new GitRebaseProblemDetector(); - GitUntrackedFilesOverwrittenByOperationDetector untrackedDetector = new GitUntrackedFilesOverwrittenByOperationDetector(root); - GitRebaseLineListener progressListener = new GitRebaseLineListener(); - GitCommandResult result = callRebase(repository, customMode, rebaseDetector, untrackedDetector, progressListener); - - boolean somethingRebased = customMode != null || progressListener.getResult().current > 1; - - if(result.success()) - { - if(rebaseDetector.hasStoppedForEditing()) - { - showStoppedForEditingMessage(repository); - return new GitRebaseStatus(GitRebaseStatus.Type.SUSPENDED, skippedCommits); - } - LOG.debug("Successfully rebased " + repoName); - return GitSuccessfulRebase.parseFromOutput(result.getOutput(), skippedCommits); - } - else if(result.cancelled()) - { - LOG.info("Rebase was cancelled"); - throw new ProcessCanceledException(); - } - else if(rebaseDetector.isDirtyTree() && customMode == null && !retryWhenDirty) - { - // if the initial dirty tree check doesn't find all local changes, we are still ready to stash-on-demand, - // but only once per repository (if the error happens again, that means that the previous stash attempt failed for some reason), - // and not in the case of --continue (where all local changes are expected to be committed) or --skip. - LOG.debug("Dirty tree detected in " + repoName); - String saveError = saveLocalChanges(singleton(repository.getRoot())); - if(saveError == null) - { - retryWhenDirty = true; // try same repository again - } - else - { - LOG.warn("Couldn't " + mySaver.getOperationName() + " root " + repository.getRoot() + ": " + saveError); - showFatalError(saveError, repository, somethingRebased, alreadyRebased.keySet(), allSkippedCommits); - GitRebaseStatus.Type type = somethingRebased ? GitRebaseStatus.Type.SUSPENDED : GitRebaseStatus.Type.ERROR; - return new GitRebaseStatus(type, skippedCommits); - } - } - else if(untrackedDetector.wasMessageDetected()) - { - LOG.info("Untracked files detected in " + repoName); - showUntrackedFilesError(untrackedDetector.getRelativeFilePaths(), repository, somethingRebased, alreadyRebased.keySet(), allSkippedCommits); - GitRebaseStatus.Type type = somethingRebased ? GitRebaseStatus.Type.SUSPENDED : GitRebaseStatus.Type.ERROR; - return new GitRebaseStatus(type, skippedCommits); - } - else if(rebaseDetector.isNoChangeError()) - { - LOG.info("'No changes' situation detected in " + repoName); - GitRebaseUtils.CommitInfo currentRebaseCommit = GitRebaseUtils.getCurrentRebaseCommit(myProject, root); - if(currentRebaseCommit != null) - { - skippedCommits.add(currentRebaseCommit); - } - customMode = GitRebaseResumeMode.SKIP; - } - else if(rebaseDetector.isMergeConflict()) - { - LOG.info("Merge conflict in " + repoName); - ResolveConflictResult resolveResult = showConflictResolver(repository, false); - if(resolveResult == ResolveConflictResult.ALL_RESOLVED) - { - customMode = GitRebaseResumeMode.CONTINUE; - } - else if(resolveResult == ResolveConflictResult.NOTHING_TO_MERGE) - { - // the output is the same for the cases: - // (1) "unresolved conflicts" - // (2) "manual editing of a file not followed by `git add` - // => we check if there are any unresolved conflicts, and if not, then it is the case #2 which we are not handling - LOG.info("Unmerged changes while rebasing root " + repoName + ": " + result.getErrorOutputAsJoinedString()); - showFatalError(result.getErrorOutputAsHtmlString(), repository, somethingRebased, alreadyRebased.keySet(), allSkippedCommits); - GitRebaseStatus.Type type = somethingRebased ? GitRebaseStatus.Type.SUSPENDED : GitRebaseStatus.Type.ERROR; - return new GitRebaseStatus(type, skippedCommits); - } - else - { - notifyNotAllConflictsResolved(repository, allSkippedCommits); - return new GitRebaseStatus(GitRebaseStatus.Type.SUSPENDED, skippedCommits); - } - } - else - { - LOG.info("Error rebasing root " + repoName + ": " + result.getErrorOutputAsJoinedString()); - showFatalError(result.getErrorOutputAsHtmlString(), repository, somethingRebased, alreadyRebased.keySet(), allSkippedCommits); - GitRebaseStatus.Type type = somethingRebased ? GitRebaseStatus.Type.SUSPENDED : GitRebaseStatus.Type.ERROR; - return new GitRebaseStatus(type, skippedCommits); - } - } - } - - @Nonnull - private GitCommandResult callRebase(@Nonnull GitRepository repository, @Nullable GitRebaseResumeMode mode, @Nonnull GitLineHandlerListener... listeners) - { - if(mode == null) - { - GitRebaseParams params = assertNotNull(myRebaseSpec.getParams()); - return myGit.rebase(repository, params, listeners); - } - else if(mode == GitRebaseResumeMode.SKIP) - { - return myGit.rebaseSkip(repository, listeners); - } - else - { - LOG.assertTrue(mode == GitRebaseResumeMode.CONTINUE, "Unexpected rebase mode: " + mode); - return myGit.rebaseContinue(repository, listeners); - } - } - - @Nonnull - protected Collection getDirtyRoots(@Nonnull Collection repositories) - { - return findRootsWithLocalChanges(repositories); - } - - private static boolean shouldBeRefreshed(@Nonnull GitRebaseStatus rebaseStatus) - { - return rebaseStatus.getType() != GitRebaseStatus.Type.SUCCESS || ((GitSuccessfulRebase) rebaseStatus).getSuccessType() != SuccessType.UP_TO_DATE; - } - - private static void refresh(@Nonnull Collection repositories) - { - GitUtil.updateRepositories(repositories); - // TODO use --diff-stat, and refresh only what's needed - VirtualFileUtil.markDirtyAndRefresh(false, true, false, VirtualFileUtil.toVirtualFileArray(getRootsFromRepositories(repositories))); - } - - private boolean saveDirtyRootsInitially(@Nonnull List repositories) - { - Collection repositoriesToSave = filter(repositories, new Condition() - { - @Override - public boolean value(GitRepository repository) - { - return !repository.equals(myRebaseSpec.getOngoingRebase()); // no need to save anything when --continue/--skip is to be called - } - }); - if(repositoriesToSave.isEmpty()) - { - return true; - } - Collection rootsToSave = getRootsFromRepositories(getDirtyRoots(repositoriesToSave)); - String error = saveLocalChanges(rootsToSave); - if(error != null) - { - myNotifier.notifyError("Rebase not Started", error); - return false; - } - return true; - } - - @Nullable - private String saveLocalChanges(@Nonnull Collection rootsToSave) - { - try - { - mySaver.saveLocalChanges(rootsToSave); - return null; - } - catch(VcsException e) - { - LOG.warn(e); - return "Couldn't " + mySaver.getSaverName() + " local uncommitted changes:
" + e.getMessage(); - } - } - - private Collection findRootsWithLocalChanges(@Nonnull Collection repositories) - { - return filter(repositories, new Condition() - { - @Override - public boolean value(GitRepository repository) - { - return myChangeListManager.haveChangesUnder(repository.getRoot()) != ThreeState.NO; - } - }); - } - - private void notifySuccess(@Nonnull Map successful, final MultiMap skippedCommits) - { - String rebasedBranch = getCommonCurrentBranchNameIfAllTheSame(myRebaseSpec.getAllRepositories()); - List successTypes = map(successful.values(), rebase -> rebase.getSuccessType()); - SuccessType commonType = getItemIfAllTheSame(successTypes, SuccessType.REBASED); - GitRebaseParams params = myRebaseSpec.getParams(); - String baseBranch = params == null ? null : notNull(params.getNewBase(), params.getUpstream()); - if("HEAD".equals(baseBranch)) - { - baseBranch = getItemIfAllTheSame(myRebaseSpec.getInitialBranchNames().values(), baseBranch); - } - String message = commonType.formatMessage(rebasedBranch, baseBranch, params != null && params.getBranch() != null); - message += mentionSkippedCommits(skippedCommits); - myNotifier.notifyMinorInfo("Rebase Successful", message, new NotificationListener.Adapter() - { - @Override - protected void hyperlinkActivated(@Nonnull Notification notification, @Nonnull HyperlinkEvent e) - { - handlePossibleCommitLinks(e.getDescription(), skippedCommits); - } - }); - } - - @Nullable - private static String getCommonCurrentBranchNameIfAllTheSame(@Nonnull Collection repositories) - { - return getItemIfAllTheSame(map(repositories, repository -> repository.getCurrentBranchName()), null); - } - - @Contract("_, !null -> !null") - private static T getItemIfAllTheSame(@Nonnull Collection collection, @Nullable T defaultItem) - { - return new HashSet<>(collection).size() == 1 ? getFirstItem(collection) : defaultItem; - } - - private void notifyNotAllConflictsResolved(@Nonnull GitRepository conflictingRepository, MultiMap skippedCommits) - { - String description = "You have to resolve the conflicts and continue rebase.
" + "If you want to start from the beginning, " + - "you can abort rebase."; - description += GitRebaseUtils.mentionLocalChangesRemainingInStash(mySaver); - myNotifier.notifyImportantWarning("Rebase Suspended", description, new RebaseNotificationListener(conflictingRepository, skippedCommits)); - } - - @Nonnull - private ResolveConflictResult showConflictResolver(@Nonnull GitRepository conflicting, boolean calledFromNotification) - { - GitConflictResolver.Params params = new GitConflictResolver.Params().setReverse(true); - RebaseConflictResolver conflictResolver = new RebaseConflictResolver(myProject, myGit, conflicting, params, calledFromNotification); - boolean allResolved = conflictResolver.merge(); - if(conflictResolver.myWasNothingToMerge) - { - return ResolveConflictResult.NOTHING_TO_MERGE; - } - if(allResolved) - { - return ResolveConflictResult.ALL_RESOLVED; - } - return ResolveConflictResult.UNRESOLVED_REMAIN; - } - - private void showStoppedForEditingMessage(@Nonnull GitRepository repository) - { - String description = "Once you are satisfied with your changes you may continue"; - myNotifier.notifyImportantInfo("Rebase Stopped for Editing", description, new RebaseNotificationListener(repository, MultiMap.empty())); - } - - private void showFatalError(@Nonnull final String error, - @Nonnull final GitRepository currentRepository, - boolean somethingWasRebased, - @Nonnull final Collection successful, - @Nonnull MultiMap skippedCommits) - { - String repo = myRepositoryManager.moreThanOneRoot() ? getShortRepositoryName(currentRepository) + ": " : ""; - String description = repo + error + "
" + - mentionRetryAndAbort(somethingWasRebased, successful) + - mentionSkippedCommits(skippedCommits) + - GitRebaseUtils.mentionLocalChangesRemainingInStash(mySaver); - String title = myRebaseSpec.getOngoingRebase() == null ? "Rebase Failed" : "Continue Rebase Failed"; - myNotifier.notifyError(title, description, new RebaseNotificationListener(currentRepository, skippedCommits)); - } - - private void showUntrackedFilesError(@Nonnull Set untrackedPaths, - @Nonnull GitRepository currentRepository, - boolean somethingWasRebased, - @Nonnull Collection successful, - MultiMap skippedCommits) - { - String message = GitUntrackedFilesHelper.createUntrackedFilesOverwrittenDescription("rebase", true) + - mentionRetryAndAbort(somethingWasRebased, successful) + - mentionSkippedCommits(skippedCommits) + - GitRebaseUtils.mentionLocalChangesRemainingInStash(mySaver); - GitUntrackedFilesHelper.notifyUntrackedFilesOverwrittenBy(myProject, currentRepository.getRoot(), untrackedPaths, "rebase", message); - } - - @Nonnull - private static String mentionRetryAndAbort(boolean somethingWasRebased, @Nonnull Collection successful) - { - return somethingWasRebased || !successful.isEmpty() ? "You can retry or abort rebase." : "Retry."; - } - - @Nonnull - private static String mentionSkippedCommits(@Nonnull MultiMap skippedCommits) - { - if(skippedCommits.isEmpty()) - { - return ""; - } - String message = "
"; - if(skippedCommits.values().size() == 1) - { - message += "The following commit was skipped during rebase:
"; - } - else - { - message += "The following commits were skipped during rebase:
"; - } - message += StringUtil.join(skippedCommits.values(), commitInfo -> - { - String commitMessage = StringUtil.shortenPathWithEllipsis(commitInfo.subject, 72, true); - String hash = commitInfo.revision.asString(); - String shortHash = DvcsUtil.getShortHash(commitInfo.revision.asString()); - return String.format("%s %s", hash, shortHash, commitMessage); - }, "
"); - return message; - } - - @Nonnull - private static MultiMap getSkippedCommits(@Nonnull Map statuses) - { - MultiMap map = MultiMap.create(); - for(GitRepository repository : statuses.keySet()) - { - map.put(repository, statuses.get(repository).getSkippedCommits()); - } - return map; - } - - @Nonnull - private static Map getSuccessfulRepositories(@Nonnull Map statuses) - { - Map map = new LinkedHashMap<>(); - for(GitRepository repository : statuses.keySet()) - { - GitRebaseStatus status = statuses.get(repository); - if(status instanceof GitSuccessfulRebase) - { - map.put(repository, (GitSuccessfulRebase) status); - } - } - return map; - } - - private class RebaseConflictResolver extends GitConflictResolver - { - private final boolean myCalledFromNotification; - private boolean myWasNothingToMerge; - - RebaseConflictResolver(@Nonnull Project project, @Nonnull Git git, @Nonnull GitRepository repository, @Nonnull Params params, boolean calledFromNotification) - { - super(project, git, singleton(repository.getRoot()), params); - myCalledFromNotification = calledFromNotification; - } - - @Override - protected void notifyUnresolvedRemain() - { - // will be handled in the common notification - } - - @Override - protected boolean proceedAfterAllMerged() throws VcsException - { - if(myCalledFromNotification) - { - retry(GitLocalize.actionRebaseContinueProgressTitle().get()); - } - return true; - } - - @Override - protected boolean proceedIfNothingToMerge() throws VcsException - { - myWasNothingToMerge = true; - return true; - } - } - - private enum ResolveConflictResult - { - ALL_RESOLVED, - NOTHING_TO_MERGE, - UNRESOLVED_REMAIN - } - - private class RebaseNotificationListener extends NotificationListener.Adapter - { - @Nonnull - private final GitRepository myCurrentRepository; - @Nonnull - private final MultiMap mySkippedCommits; - - RebaseNotificationListener(@Nonnull GitRepository currentRepository, @Nonnull MultiMap skippedCommits) - { - myCurrentRepository = currentRepository; - mySkippedCommits = skippedCommits; - } - - @Override - protected void hyperlinkActivated(@Nonnull Notification notification, @Nonnull final HyperlinkEvent e) - { - final String href = e.getDescription(); - if("abort".equals(href)) - { - abort(); - } - else if("continue".equals(href)) - { - retry(GitLocalize.actionRebaseContinueProgressTitle().get()); - } - else if("retry".equals(href)) - { - retry("Retry Rebase Process..."); - } - else if("resolve".equals(href)) - { - showConflictResolver(myCurrentRepository, true); - } - else if("stash".equals(href)) - { - mySaver.showSavedChanges(); - } - else - { - handlePossibleCommitLinks(href, mySkippedCommits); - } - } - } - - private void abort() - { - myProgressManager.run(new Task.Backgroundable(myProject, "Aborting Rebase Process...") - { - @Override - public void run(@Nonnull ProgressIndicator indicator) - { - GitRebaseUtils.abort((Project) myProject, indicator); - } - }); - } - - private void retry(@Nonnull final String processTitle) - { - myProgressManager.run(new Task.Backgroundable(myProject, processTitle, true) - { - @Override - public void run(@Nonnull ProgressIndicator indicator) - { - GitRebaseUtils.continueRebase((Project) myProject); - } - }); - } - - private void handlePossibleCommitLinks(@Nonnull String href, MultiMap skippedCommits) - { - GitRepository repository = findRootBySkippedCommit(href, skippedCommits); - if(repository != null) - { - GitUtil.showSubmittedFiles(myProject, href, repository.getRoot(), true, false); - } - } - - @Nullable - private static GitRepository findRootBySkippedCommit(@Nonnull final String hash, final MultiMap skippedCommits) - { - return find(skippedCommits.keySet(), repository -> exists(skippedCommits.get(repository), info -> info.revision.asString().equals(hash))); - } +public class GitRebaseProcess { + private static final Logger LOG = Logger.getInstance(GitRebaseProcess.class); + + @Nonnull + private final Project myProject; + @Nonnull + private final Git myGit; + @Nonnull + private final ChangeListManager myChangeListManager; + @Nonnull + protected final NotificationService myNotificationService; + @Nonnull + private final GitRepositoryManager myRepositoryManager; + + @Nonnull + private final GitRebaseSpec myRebaseSpec; + @Nullable + private final GitRebaseResumeMode myCustomMode; + @Nonnull + private final GitChangesSaver mySaver; + @Nonnull + private final ProgressManager myProgressManager; + + public GitRebaseProcess(@Nonnull Project project, @Nonnull GitRebaseSpec rebaseSpec, @Nullable GitRebaseResumeMode customMode) { + myProject = project; + myRebaseSpec = rebaseSpec; + myCustomMode = customMode; + mySaver = rebaseSpec.getSaver(); + + myGit = ServiceManager.getService(Git.class); + myChangeListManager = ChangeListManager.getInstance(myProject); + myNotificationService = NotificationService.getInstance(); + myRepositoryManager = GitUtil.getRepositoryManager(myProject); + myProgressManager = ProgressManager.getInstance(); + } + + public void rebase() { + new GitFreezingProcess(myProject, "rebase", this::doRebase).execute(); + } + + /** + * Given a GitRebaseSpec this method either starts, or continues the ongoing rebase in multiple repositories. + *
    + *
  • It does nothing with "already successfully rebased repositories" (the ones which have {@link GitRebaseStatus} == SUCCESSFUL, + * and just remembers them to use in the resulting notification.
  • + *
  • If there is a repository with rebase in progress, it calls `git rebase --continue` (or `--skip`). + * It is assumed that there is only one such repository.
  • + *
  • For all remaining repositories rebase on which didn't start yet, it calls {@code git rebase }
  • + *
+ */ + @RequiredUIAccess + private void doRebase() { + LOG.info("Started rebase"); + LOG.debug("Started rebase with the following spec: " + myRebaseSpec); + + Map statuses = new LinkedHashMap<>(myRebaseSpec.getStatuses()); + Collection toRefresh = new LinkedHashSet<>(); + List repositoriesToRebase = myRebaseSpec.getIncompleteRepositories(); + try (AccessToken ignored = DvcsUtil.workingTreeChangeStarted(myProject, "Rebase")) { + if (!saveDirtyRootsInitially(repositoriesToRebase)) { + return; + } + + GitRepository failed = null; + for (GitRepository repository : repositoriesToRebase) { + GitRebaseResumeMode customMode = null; + if (repository == myRebaseSpec.getOngoingRebase()) { + customMode = myCustomMode == null ? GitRebaseResumeMode.CONTINUE : myCustomMode; + } + + GitRebaseStatus rebaseStatus = rebaseSingleRoot(repository, customMode, getSuccessfulRepositories(statuses)); + repository.update(); // make the repo state info actual ASAP + statuses.put(repository, rebaseStatus); + if (shouldBeRefreshed(rebaseStatus)) { + toRefresh.add(repository); + } + if (rebaseStatus.getType() != GitRebaseStatus.Type.SUCCESS) { + failed = repository; + break; + } + } + + if (failed == null) { + LOG.debug("Rebase completed successfully."); + mySaver.load(); + } + refresh(toRefresh); + if (failed == null) { + notifySuccess(getSuccessfulRepositories(statuses), getSkippedCommits(statuses)); + } + + saveUpdatedSpec(statuses); + } + catch (ProcessCanceledException pce) { + throw pce; + } + catch (Throwable e) { + myRepositoryManager.setOngoingRebaseSpec(null); + ExceptionUtil.rethrowUnchecked(e); + } + } + + private void saveUpdatedSpec(@Nonnull Map statuses) { + if (myRebaseSpec.shouldBeSaved()) { + GitRebaseSpec newRebaseInfo = myRebaseSpec.cloneWithNewStatuses(statuses); + myRepositoryManager.setOngoingRebaseSpec(newRebaseInfo); + } + else { + myRepositoryManager.setOngoingRebaseSpec(null); + } + } + + @Nonnull + @RequiredUIAccess + private GitRebaseStatus rebaseSingleRoot( + @Nonnull GitRepository repository, + @Nullable GitRebaseResumeMode customMode, + @Nonnull Map alreadyRebased + ) { + VirtualFile root = repository.getRoot(); + String repoName = getShortRepositoryName(repository); + LOG.info("Rebasing root " + repoName + ", mode: " + notNull(customMode, "standard")); + + Collection skippedCommits = new ArrayList<>(); + MultiMap allSkippedCommits = getSkippedCommits(alreadyRebased); + boolean retryWhenDirty = false; + + while (true) { + GitRebaseProblemDetector rebaseDetector = new GitRebaseProblemDetector(); + GitUntrackedFilesOverwrittenByOperationDetector untrackedDetector = new GitUntrackedFilesOverwrittenByOperationDetector(root); + GitRebaseLineListener progressListener = new GitRebaseLineListener(); + GitCommandResult result = callRebase(repository, customMode, rebaseDetector, untrackedDetector, progressListener); + + boolean somethingRebased = customMode != null || progressListener.getResult().current > 1; + + if (result.success()) { + if (rebaseDetector.hasStoppedForEditing()) { + showStoppedForEditingMessage(repository); + return new GitRebaseStatus(GitRebaseStatus.Type.SUSPENDED, skippedCommits); + } + LOG.debug("Successfully rebased " + repoName); + return GitSuccessfulRebase.parseFromOutput(result.getOutput(), skippedCommits); + } + else if (result.cancelled()) { + LOG.info("Rebase was cancelled"); + throw new ProcessCanceledException(); + } + else if (rebaseDetector.isDirtyTree() && customMode == null && !retryWhenDirty) { + // if the initial dirty tree check doesn't find all local changes, we are still ready to stash-on-demand, + // but only once per repository (if the error happens again, that means that the previous stash attempt failed for some reason), + // and not in the case of --continue (where all local changes are expected to be committed) or --skip. + LOG.debug("Dirty tree detected in " + repoName); + String saveError = saveLocalChanges(singleton(repository.getRoot())); + if (saveError == null) { + retryWhenDirty = true; // try same repository again + } + else { + LOG.warn("Couldn't " + mySaver.getOperationName() + " root " + repository.getRoot() + ": " + saveError); + showFatalError( + LocalizeValue.localizeTODO(saveError), + repository, + somethingRebased, + alreadyRebased.keySet(), + allSkippedCommits + ); + GitRebaseStatus.Type type = somethingRebased ? GitRebaseStatus.Type.SUSPENDED : GitRebaseStatus.Type.ERROR; + return new GitRebaseStatus(type, skippedCommits); + } + } + else if (untrackedDetector.wasMessageDetected()) { + LOG.info("Untracked files detected in " + repoName); + showUntrackedFilesError( + untrackedDetector.getRelativeFilePaths(), + repository, + somethingRebased, + alreadyRebased.keySet(), + allSkippedCommits + ); + GitRebaseStatus.Type type = somethingRebased ? GitRebaseStatus.Type.SUSPENDED : GitRebaseStatus.Type.ERROR; + return new GitRebaseStatus(type, skippedCommits); + } + else if (rebaseDetector.isNoChangeError()) { + LOG.info("'No changes' situation detected in " + repoName); + GitRebaseUtils.CommitInfo currentRebaseCommit = GitRebaseUtils.getCurrentRebaseCommit(myProject, root); + if (currentRebaseCommit != null) { + skippedCommits.add(currentRebaseCommit); + } + customMode = GitRebaseResumeMode.SKIP; + } + else if (rebaseDetector.isMergeConflict()) { + LOG.info("Merge conflict in " + repoName); + ResolveConflictResult resolveResult = showConflictResolver(repository, false); + if (resolveResult == ResolveConflictResult.ALL_RESOLVED) { + customMode = GitRebaseResumeMode.CONTINUE; + } + else if (resolveResult == ResolveConflictResult.NOTHING_TO_MERGE) { + // the output is the same for the cases: + // (1) "unresolved conflicts" + // (2) "manual editing of a file not followed by `git add` + // => we check if there are any unresolved conflicts, and if not, then it is the case #2 which we are not handling + LOG.info("Unmerged changes while rebasing root " + repoName + ": " + result.getErrorOutputAsJoinedString()); + showFatalError( + result.getErrorOutputAsHtmlValue(), + repository, + somethingRebased, + alreadyRebased.keySet(), + allSkippedCommits + ); + GitRebaseStatus.Type type = somethingRebased ? GitRebaseStatus.Type.SUSPENDED : GitRebaseStatus.Type.ERROR; + return new GitRebaseStatus(type, skippedCommits); + } + else { + notifyNotAllConflictsResolved(repository, allSkippedCommits); + return new GitRebaseStatus(GitRebaseStatus.Type.SUSPENDED, skippedCommits); + } + } + else { + LOG.info("Error rebasing root " + repoName + ": " + result.getErrorOutputAsJoinedString()); + showFatalError( + result.getErrorOutputAsHtmlValue(), + repository, + somethingRebased, + alreadyRebased.keySet(), + allSkippedCommits + ); + GitRebaseStatus.Type type = somethingRebased ? GitRebaseStatus.Type.SUSPENDED : GitRebaseStatus.Type.ERROR; + return new GitRebaseStatus(type, skippedCommits); + } + } + } + + @Nonnull + private GitCommandResult callRebase( + @Nonnull GitRepository repository, + @Nullable GitRebaseResumeMode mode, + @Nonnull GitLineHandlerListener... listeners + ) { + if (mode == null) { + GitRebaseParams params = assertNotNull(myRebaseSpec.getParams()); + return myGit.rebase(repository, params, listeners); + } + else if (mode == GitRebaseResumeMode.SKIP) { + return myGit.rebaseSkip(repository, listeners); + } + else { + LOG.assertTrue(mode == GitRebaseResumeMode.CONTINUE, "Unexpected rebase mode: " + mode); + return myGit.rebaseContinue(repository, listeners); + } + } + + @Nonnull + protected Collection getDirtyRoots(@Nonnull Collection repositories) { + return findRootsWithLocalChanges(repositories); + } + + private static boolean shouldBeRefreshed(@Nonnull GitRebaseStatus rebaseStatus) { + return rebaseStatus.getType() != GitRebaseStatus.Type.SUCCESS || ((GitSuccessfulRebase) rebaseStatus).getSuccessType() != SuccessType.UP_TO_DATE; + } + + private static void refresh(@Nonnull Collection repositories) { + GitUtil.updateRepositories(repositories); + // TODO use --diff-stat, and refresh only what's needed + VirtualFileUtil.markDirtyAndRefresh(false, true, false, VirtualFileUtil.toVirtualFileArray(getRootsFromRepositories(repositories))); + } + + private boolean saveDirtyRootsInitially(@Nonnull List repositories) { + Collection repositoriesToSave = filter( + repositories, + // no need to save anything when --continue/--skip is to be called + repository -> !repository.equals(myRebaseSpec.getOngoingRebase()) + ); + if (repositoriesToSave.isEmpty()) { + return true; + } + Collection rootsToSave = getRootsFromRepositories(getDirtyRoots(repositoriesToSave)); + String error = saveLocalChanges(rootsToSave); + if (error != null) { + myNotificationService.newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Rebase not Started")) + .content(LocalizeValue.localizeTODO(error)) + .notify(myProject); + return false; + } + return true; + } + + @Nullable + private String saveLocalChanges(@Nonnull Collection rootsToSave) { + try { + mySaver.saveLocalChanges(rootsToSave); + return null; + } + catch (VcsException e) { + LOG.warn(e); + return "Couldn't " + mySaver.getSaverName() + " local uncommitted changes:
" + e.getMessage(); + } + } + + private Collection findRootsWithLocalChanges(@Nonnull Collection repositories) { + return filter(repositories, repository -> myChangeListManager.haveChangesUnder(repository.getRoot()) != ThreeState.NO); + } + + private void notifySuccess( + @Nonnull Map successful, + final MultiMap skippedCommits + ) { + String rebasedBranch = getCommonCurrentBranchNameIfAllTheSame(myRebaseSpec.getAllRepositories()); + List successTypes = map(successful.values(), GitSuccessfulRebase::getSuccessType); + SuccessType commonType = getItemIfAllTheSame(successTypes, SuccessType.REBASED); + GitRebaseParams params = myRebaseSpec.getParams(); + String baseBranch = params == null ? null : notNull(params.getNewBase(), params.getUpstream()); + if ("HEAD".equals(baseBranch)) { + baseBranch = getItemIfAllTheSame(myRebaseSpec.getInitialBranchNames().values(), baseBranch); + } + String message = commonType.formatMessage(rebasedBranch, baseBranch, params != null && params.getBranch() != null); + message += mentionSkippedCommits(skippedCommits); + myNotificationService.newInfo(VcsNotifier.STANDARD_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Rebase Successful")) + .content(LocalizeValue.localizeTODO(message)) + .optionalHyperlinkListener(new NotificationListener.Adapter() { + @Override + protected void hyperlinkActivated(@Nonnull Notification notification, @Nonnull HyperlinkEvent e) { + handlePossibleCommitLinks(e.getDescription(), skippedCommits); + } + }) + .notify(myProject); + } + + @Nullable + private static String getCommonCurrentBranchNameIfAllTheSame(@Nonnull Collection repositories) { + return getItemIfAllTheSame(map(repositories, Repository::getCurrentBranchName), null); + } + + @Contract("_, !null -> !null") + private static T getItemIfAllTheSame(@Nonnull Collection collection, @Nullable T defaultItem) { + return new HashSet<>(collection).size() == 1 ? getFirstItem(collection) : defaultItem; + } + + private void notifyNotAllConflictsResolved( + @Nonnull GitRepository conflictingRepository, + MultiMap skippedCommits + ) { + myNotificationService.newWarn(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Rebase Suspended")) + .content(LocalizeValue.localizeTODO( + "You have to resolve the conflicts and continue rebase.
" + + "If you want to start from the beginning, you can abort rebase." + + GitRebaseUtils.mentionLocalChangesRemainingInStash(mySaver) + )) + .optionalHyperlinkListener(new RebaseNotificationListener(conflictingRepository, skippedCommits)) + .notify(myProject); + } + + @Nonnull + @RequiredUIAccess + private ResolveConflictResult showConflictResolver(@Nonnull GitRepository conflicting, boolean calledFromNotification) { + GitConflictResolver.Params params = new GitConflictResolver.Params().setReverse(true); + RebaseConflictResolver conflictResolver = new RebaseConflictResolver(myProject, myGit, conflicting, params, calledFromNotification); + boolean allResolved = conflictResolver.merge(); + if (conflictResolver.myWasNothingToMerge) { + return ResolveConflictResult.NOTHING_TO_MERGE; + } + if (allResolved) { + return ResolveConflictResult.ALL_RESOLVED; + } + return ResolveConflictResult.UNRESOLVED_REMAIN; + } + + private void showStoppedForEditingMessage(@Nonnull GitRepository repository) { + myNotificationService.newInfo(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Rebase Stopped for Editing")) + .content(LocalizeValue.localizeTODO("Once you are satisfied with your changes you may continue")) + .optionalHyperlinkListener(new RebaseNotificationListener( + repository, + MultiMap.empty() + )) + .notify(myProject); + } + + private void showFatalError( + @Nonnull LocalizeValue error, + @Nonnull GitRepository currentRepository, + boolean somethingWasRebased, + @Nonnull Collection successful, + @Nonnull MultiMap skippedCommits + ) { + String repo = myRepositoryManager.moreThanOneRoot() ? getShortRepositoryName(currentRepository) + ": " : ""; + myNotificationService.newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO(myRebaseSpec.getOngoingRebase() == null ? "Rebase Failed" : "Continue Rebase Failed")) + .content(LocalizeValue.localizeTODO( + repo + error + "
" + + mentionRetryAndAbort(somethingWasRebased, successful) + + mentionSkippedCommits(skippedCommits) + + GitRebaseUtils.mentionLocalChangesRemainingInStash(mySaver) + )) + .optionalHyperlinkListener(new RebaseNotificationListener(currentRepository, skippedCommits)) + .notify(myProject); + } + + private void showUntrackedFilesError( + @Nonnull Set untrackedPaths, + @Nonnull GitRepository currentRepository, + boolean somethingWasRebased, + @Nonnull Collection successful, + MultiMap skippedCommits + ) { + String message = GitUntrackedFilesHelper.createUntrackedFilesOverwrittenDescription("rebase", true) + + mentionRetryAndAbort(somethingWasRebased, successful) + + mentionSkippedCommits(skippedCommits) + + GitRebaseUtils.mentionLocalChangesRemainingInStash(mySaver); + GitUntrackedFilesHelper.notifyUntrackedFilesOverwrittenBy( + myProject, + currentRepository.getRoot(), + untrackedPaths, + "rebase", + message + ); + } + + @Nonnull + private static String mentionRetryAndAbort(boolean somethingWasRebased, @Nonnull Collection successful) { + return somethingWasRebased || !successful.isEmpty() ? "You can retry or abort rebase." : "Retry."; + } + + @Nonnull + private static String mentionSkippedCommits(@Nonnull MultiMap skippedCommits) { + if (skippedCommits.isEmpty()) { + return ""; + } + String message = "
"; + if (skippedCommits.values().size() == 1) { + message += "The following commit was skipped during rebase:
"; + } + else { + message += "The following commits were skipped during rebase:
"; + } + message += StringUtil.join(skippedCommits.values(), commitInfo -> + { + String commitMessage = StringUtil.shortenPathWithEllipsis(commitInfo.subject, 72, true); + String hash = commitInfo.revision.asString(); + String shortHash = DvcsUtil.getShortHash(commitInfo.revision.asString()); + return String.format("%s %s", hash, shortHash, commitMessage); + }, "
"); + return message; + } + + @Nonnull + private static MultiMap getSkippedCommits(@Nonnull Map statuses) { + MultiMap map = MultiMap.create(); + for (GitRepository repository : statuses.keySet()) { + map.put(repository, statuses.get(repository).getSkippedCommits()); + } + return map; + } + + @Nonnull + private static Map getSuccessfulRepositories(@Nonnull Map statuses) { + Map map = new LinkedHashMap<>(); + for (GitRepository repository : statuses.keySet()) { + GitRebaseStatus status = statuses.get(repository); + if (status instanceof GitSuccessfulRebase successfulRebase) { + map.put(repository, successfulRebase); + } + } + return map; + } + + private class RebaseConflictResolver extends GitConflictResolver { + private final boolean myCalledFromNotification; + private boolean myWasNothingToMerge; + + RebaseConflictResolver( + @Nonnull Project project, + @Nonnull Git git, + @Nonnull GitRepository repository, + @Nonnull Params params, + boolean calledFromNotification + ) { + super(project, git, singleton(repository.getRoot()), params); + myCalledFromNotification = calledFromNotification; + } + + @Override + protected void notifyUnresolvedRemain() { + // will be handled in the common notification + } + + @Override + protected boolean proceedAfterAllMerged() throws VcsException { + if (myCalledFromNotification) { + retry(GitLocalize.actionRebaseContinueProgressTitle()); + } + return true; + } + + @Override + protected boolean proceedIfNothingToMerge() throws VcsException { + myWasNothingToMerge = true; + return true; + } + } + + private enum ResolveConflictResult { + ALL_RESOLVED, + NOTHING_TO_MERGE, + UNRESOLVED_REMAIN + } + + private class RebaseNotificationListener extends NotificationListener.Adapter { + @Nonnull + private final GitRepository myCurrentRepository; + @Nonnull + private final MultiMap mySkippedCommits; + + RebaseNotificationListener( + @Nonnull GitRepository currentRepository, + @Nonnull MultiMap skippedCommits + ) { + myCurrentRepository = currentRepository; + mySkippedCommits = skippedCommits; + } + + @Override + @RequiredUIAccess + protected void hyperlinkActivated(@Nonnull Notification notification, @Nonnull HyperlinkEvent e) { + String href = e.getDescription(); + if ("abort".equals(href)) { + abort(); + } + else if ("continue".equals(href)) { + retry(GitLocalize.actionRebaseContinueProgressTitle()); + } + else if ("retry".equals(href)) { + retry(LocalizeValue.localizeTODO("Retry Rebase Process...")); + } + else if ("resolve".equals(href)) { + showConflictResolver(myCurrentRepository, true); + } + else if ("stash".equals(href)) { + mySaver.showSavedChanges(); + } + else { + handlePossibleCommitLinks(href, mySkippedCommits); + } + } + } + + private void abort() { + myProgressManager.run(new Task.Backgroundable(myProject, "Aborting Rebase Process...") { + @Override + public void run(@Nonnull ProgressIndicator indicator) { + GitRebaseUtils.abort((Project) myProject, indicator); + } + }); + } + + private void retry(@Nonnull final LocalizeValue processTitle) { + myProgressManager.run(new Task.Backgroundable(myProject, processTitle, true) { + @Override + public void run(@Nonnull ProgressIndicator indicator) { + GitRebaseUtils.continueRebase((Project) myProject); + } + }); + } + + private void handlePossibleCommitLinks(@Nonnull String href, MultiMap skippedCommits) { + GitRepository repository = findRootBySkippedCommit(href, skippedCommits); + if (repository != null) { + GitUtil.showSubmittedFiles(myProject, href, repository.getRoot(), true, false); + } + } + + @Nullable + private static GitRepository findRootBySkippedCommit( + @Nonnull String hash, + MultiMap skippedCommits + ) { + return find( + skippedCommits.keySet(), + repository -> exists(skippedCommits.get(repository), info -> info.revision.asString().equals(hash)) + ); + } } diff --git a/plugin/src/main/java/git4idea/rebase/GitRebaseUnstructuredEditor.form b/plugin/src/main/java/git4idea/rebase/GitRebaseUnstructuredEditor.form deleted file mode 100644 index 0c9a618..0000000 --- a/plugin/src/main/java/git4idea/rebase/GitRebaseUnstructuredEditor.form +++ /dev/null @@ -1,56 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/plugin/src/main/java/git4idea/rebase/GitRebaseUnstructuredEditor.java b/plugin/src/main/java/git4idea/rebase/GitRebaseUnstructuredEditor.java index 9769628..ab738e4 100644 --- a/plugin/src/main/java/git4idea/rebase/GitRebaseUnstructuredEditor.java +++ b/plugin/src/main/java/git4idea/rebase/GitRebaseUnstructuredEditor.java @@ -15,15 +15,19 @@ */ package git4idea.rebase; +import com.intellij.uiDesigner.core.GridConstraints; +import com.intellij.uiDesigner.core.GridLayoutManager; import consulo.git.localize.GitLocalize; import consulo.project.Project; import consulo.ui.annotation.RequiredUIAccess; import consulo.ui.ex.awt.DialogWrapper; +import consulo.ui.ex.awt.JBScrollPane; import consulo.util.io.FileUtil; import consulo.virtualFileSystem.VirtualFile; import git4idea.config.GitConfigUtil; import javax.swing.*; +import java.awt.*; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; @@ -104,4 +108,137 @@ protected String getDimensionServiceKey() { public JComponent getPreferredFocusedComponent() { return myTextArea; } + + { +// GUI initializer generated by Consulo GUI Designer +// >>> IMPORTANT!! <<< +// DO NOT EDIT OR ADD ANY CODE HERE! + $$$setupUI$$$(); + } + + /** + * Method generated by Consulo GUI Designer + * >>> IMPORTANT!! <<< + * DO NOT edit this method OR call it in your code! + */ + private void $$$setupUI$$$() { + myPanel = new JPanel(); + myPanel.setLayout(new GridLayoutManager(3, 2, new Insets(0, 0, 0, 0), -1, -1)); + JLabel label1 = new JLabel(); + $$$loadLabelText$$$(label1, GitLocalize.rebaseUnstructuredEditorMessage().get()); + myPanel.add( + label1, + new GridConstraints( + 1, + 0, + 1, + 2, + GridConstraints.ANCHOR_WEST, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 0, + false + ) + ); + JBScrollPane jBScrollPane1 = new JBScrollPane(); + myPanel.add( + jBScrollPane1, + new GridConstraints( + 2, + 0, + 1, + 2, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, + null, + null, + null, + 0, + false + ) + ); + myTextArea = new JTextArea(); + myTextArea.setToolTipText(GitLocalize.rebaseUnstructuredEditorTooltip().get()); + jBScrollPane1.setViewportView(myTextArea); + JLabel label2 = new JLabel(); + $$$loadLabelText$$$(label2, GitLocalize.rebaseUnstructuredEditorGitRoot().get()); + myPanel.add( + label2, + new GridConstraints( + 0, + 0, + 1, + 1, + GridConstraints.ANCHOR_WEST, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 0, + false + ) + ); + myGitRootLabel = new JLabel(); + myGitRootLabel.setText(""); + myPanel.add( + myGitRootLabel, + new GridConstraints( + 0, + 1, + 1, + 1, + GridConstraints.ANCHOR_WEST, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_WANT_GROW, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 0, + false + ) + ); + label1.setLabelFor(myTextArea); + } + + /** + * @noinspection ALL + */ + private void $$$loadLabelText$$$(JLabel component, String text) { + StringBuffer result = new StringBuffer(); + boolean haveMnemonic = false; + char mnemonic = '\0'; + int mnemonicIndex = -1; + for (int i = 0; i < text.length(); i++) { + if (text.charAt(i) == '&') { + i++; + if (i == text.length()) { + break; + } + if (!haveMnemonic && text.charAt(i) != '&') { + haveMnemonic = true; + mnemonic = text.charAt(i); + mnemonicIndex = result.length(); + } + } + result.append(text.charAt(i)); + } + component.setText(result.toString()); + if (haveMnemonic) { + component.setDisplayedMnemonic(mnemonic); + component.setDisplayedMnemonicIndex(mnemonicIndex); + } + } + + public JComponent $$$getRootComponent$$$() { + return myPanel; + } } diff --git a/plugin/src/main/java/git4idea/rebase/GitRebaseUtils.java b/plugin/src/main/java/git4idea/rebase/GitRebaseUtils.java index 84b14a3..9d26f09 100644 --- a/plugin/src/main/java/git4idea/rebase/GitRebaseUtils.java +++ b/plugin/src/main/java/git4idea/rebase/GitRebaseUtils.java @@ -16,11 +16,12 @@ package git4idea.rebase; import consulo.application.progress.ProgressIndicator; +import consulo.localize.LocalizeValue; import consulo.logging.Logger; import consulo.project.Project; +import consulo.project.ui.notification.NotificationService; +import consulo.ui.annotation.RequiredUIAccess; import consulo.util.collection.ContainerUtil; -import consulo.util.io.CharsetToolkit; -import consulo.util.lang.function.Condition; import consulo.versionControlSystem.VcsNotifier; import consulo.versionControlSystem.distributed.repository.Repository; import consulo.virtualFileSystem.VirtualFile; @@ -29,7 +30,6 @@ import git4idea.branch.GitRebaseParams; import git4idea.repo.GitRepository; import git4idea.stash.GitChangesSaver; - import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; @@ -79,7 +79,10 @@ public static void continueRebase(@Nonnull Project project) { } else { LOG.warn("Refusing to continue: no rebase spec"); - VcsNotifier.getInstance(project).notifyError("Can't Continue Rebase", "No rebase in progress"); + NotificationService.getInstance().newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Can't Continue Rebase")) + .content(LocalizeValue.localizeTODO("No rebase in progress")) + .notify(project); } } @@ -90,7 +93,10 @@ public static void continueRebase(@Nonnull Project project, @Nonnull GitReposito } else { LOG.warn("Refusing to continue: no rebase spec"); - VcsNotifier.getInstance(project).notifyError("Can't Continue Rebase", "No rebase in progress"); + NotificationService.getInstance().newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Can't Continue Rebase")) + .content(LocalizeValue.localizeTODO("No rebase in progress")) + .notify(project); } } @@ -101,7 +107,10 @@ public static void skipRebase(@Nonnull Project project) { } else { LOG.warn("Refusing to skip: no rebase spec"); - VcsNotifier.getInstance(project).notifyError("Can't Continue Rebase", "No rebase in progress"); + NotificationService.getInstance().newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Can't Continue Rebase")) + .content(LocalizeValue.localizeTODO("No rebase in progress")) + .notify(project); } } @@ -112,7 +121,10 @@ public static void skipRebase(@Nonnull Project project, @Nonnull GitRepository r } else { LOG.warn("Refusing to skip: no rebase spec"); - VcsNotifier.getInstance(project).notifyError("Can't Continue Rebase", "No rebase in progress"); + NotificationService.getInstance().newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Can't Continue Rebase")) + .content(LocalizeValue.localizeTODO("No rebase in progress")) + .notify(project); } } @@ -122,6 +134,7 @@ public static void skipRebase(@Nonnull Project project, @Nonnull GitRepository r *

* Does nothing if no information about ongoing rebase is available, or if this information has become obsolete. */ + @RequiredUIAccess public static void abort(@Nonnull Project project, @Nonnull ProgressIndicator indicator) { GitRebaseSpec spec = GitUtil.getRepositoryManager(project).getOngoingRebaseSpec(); if (spec != null) { @@ -136,13 +149,17 @@ public static void abort(@Nonnull Project project, @Nonnull ProgressIndicator in } else { LOG.warn("Refusing to abort: no rebase spec"); - VcsNotifier.getInstance(project).notifyError("Can't Abort Rebase", "No rebase in progress"); + NotificationService.getInstance().newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Can't Abort Rebase")) + .content(LocalizeValue.localizeTODO("No rebase in progress")) + .notify(project); } } /** * Abort the ongoing rebase process in the given repository. */ + @RequiredUIAccess public static void abort(@Nonnull Project project, @Nullable GitRepository repository, @Nonnull ProgressIndicator indicator) { new GitAbortRebaseProcess( project, @@ -183,7 +200,10 @@ private static boolean isRebaseAllowed(@Nonnull Project project, @Nonnull Collec message = "Rebase is not possible" + in; } if (message != null) { - VcsNotifier.getInstance(project).notifyError("Rebase not Allowed", message); + NotificationService.getInstance().newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Rebase not Allowed")) + .content(LocalizeValue.localizeTODO(message)) + .notify(project); return false; } } @@ -249,7 +269,7 @@ public static CommitInfo getCurrentRebaseCommit(@Nonnull Project project, @Nonnu String hash = null; String subject = null; try { - BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(commitFile), CharsetToolkit.UTF8_CHARSET)); + BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(commitFile), StandardCharsets.UTF_8)); try { String line; while ((line = in.readLine()) != null) { @@ -281,7 +301,8 @@ public static CommitInfo getCurrentRebaseCommit(@Nonnull Project project, @Nonnu @Nonnull static String mentionLocalChangesRemainingInStash(@Nullable GitChangesSaver saver) { - return saver != null && saver.wereChangesSaved() ? "
Note that some local changes were " + toPast(saver.getOperationName()) + " before rebase." : ""; + return saver != null && saver.wereChangesSaved() + ? "
Note that some local changes were " + toPast(saver.getOperationName()) + " before rebase." : ""; } @Nonnull diff --git a/plugin/src/main/java/git4idea/rebase/GitRebaser.java b/plugin/src/main/java/git4idea/rebase/GitRebaser.java index d254299..1992459 100644 --- a/plugin/src/main/java/git4idea/rebase/GitRebaser.java +++ b/plugin/src/main/java/git4idea/rebase/GitRebaser.java @@ -76,17 +76,17 @@ public GitRebaser(@Nonnull Project project, @Nonnull Git git, @Nonnull ProgressI public GitUpdateResult rebase( @Nonnull VirtualFile root, @Nonnull List parameters, - @Nullable final Runnable onCancel, + @Nullable Runnable onCancel, @Nullable GitLineHandlerListener lineListener ) { - final GitLineHandler rebaseHandler = createHandler(root); + GitLineHandler rebaseHandler = createHandler(root); rebaseHandler.setStdoutSuppressed(false); rebaseHandler.addParameters(parameters); if (lineListener != null) { rebaseHandler.addLineListener(lineListener); } - final GitRebaseProblemDetector rebaseConflictDetector = new GitRebaseProblemDetector(); + GitRebaseProblemDetector rebaseConflictDetector = new GitRebaseProblemDetector(); rebaseHandler.addLineListener(rebaseConflictDetector); GitUntrackedFilesOverwrittenByOperationDetector untrackedFilesDetector = new GitUntrackedFilesOverwrittenByOperationDetector(root); GitLocalChangesWouldBeOverwrittenDetector localChangesDetector = new GitLocalChangesWouldBeOverwrittenDetector(root, CHECKOUT); @@ -122,7 +122,7 @@ protected GitLineHandler createHandler(VirtualFile root) { @RequiredUIAccess public void abortRebase(@Nonnull VirtualFile root) { LOG.info("abortRebase " + root); - final GitLineHandler rh = new GitLineHandler(myProject, root, GitCommand.REBASE); + GitLineHandler rh = new GitLineHandler(myProject, root, GitCommand.REBASE); rh.setStdoutSuppressed(false); rh.addParameters("--abort"); GitTask task = new GitTask(myProject, rh, GitLocalize.rebaseUpdateProjectAbortTaskTitle()); @@ -158,17 +158,17 @@ public boolean continueRebase(@Nonnull Collection rebasingRoots) { // start operation may be "--continue" or "--skip" depending on the situation. @RequiredUIAccess - private boolean continueRebase(final @Nonnull VirtualFile root, @Nonnull String startOperation) { + private boolean continueRebase(@Nonnull VirtualFile root, @Nonnull String startOperation) { LOG.info("continueRebase " + root + " " + startOperation); - final GitLineHandler rh = new GitLineHandler(myProject, root, GitCommand.REBASE); + GitLineHandler rh = new GitLineHandler(myProject, root, GitCommand.REBASE); rh.setStdoutSuppressed(false); rh.addParameters(startOperation); - final GitRebaseProblemDetector rebaseConflictDetector = new GitRebaseProblemDetector(); + GitRebaseProblemDetector rebaseConflictDetector = new GitRebaseProblemDetector(); rh.addLineListener(rebaseConflictDetector); makeContinueRebaseInteractiveEditor(root, rh); - final GitTask rebaseTask = new GitTask(myProject, rh, LocalizeValue.localizeTODO("git rebase " + startOperation)); + GitTask rebaseTask = new GitTask(myProject, rh, LocalizeValue.localizeTODO("git rebase " + startOperation)); rebaseTask.setProgressAnalyzer(new GitStandardProgressAnalyzer()); rebaseTask.setProgressIndicator(myProgressIndicator); return executeRebaseTaskInBackground(root, rh, rebaseConflictDetector, rebaseTask); @@ -188,7 +188,7 @@ protected void makeContinueRebaseInteractiveEditor(VirtualFile root, GitLineHand public @Nonnull Collection getRebasingRoots() { - final Collection rebasingRoots = new HashSet<>(); + Collection rebasingRoots = new HashSet<>(); for (VirtualFile root : ProjectLevelVcsManager.getInstance(myProject).getRootsUnderVcs(myVcs)) { if (GitRebaseUtils.isRebaseInTheProgress(myProject, root)) { rebasingRoots.add(root); @@ -206,7 +206,7 @@ Collection getRebasingRoots() { */ @RequiredUIAccess public boolean reoderCommitsIfNeeded( - @Nonnull final VirtualFile root, + @Nonnull VirtualFile root, @Nonnull String parentCommit, @Nonnull List olderCommits ) throws VcsException { @@ -216,7 +216,7 @@ public boolean reoderCommitsIfNeeded( return true; } - final GitLineHandler h = new GitLineHandler(myProject, root, GitCommand.REBASE); + GitLineHandler h = new GitLineHandler(myProject, root, GitCommand.REBASE); h.setStdoutSuppressed(false); Integer rebaseEditorNo = null; GitRebaseEditorService rebaseEditorService = GitRebaseEditorService.getInstance(); @@ -224,14 +224,14 @@ public boolean reoderCommitsIfNeeded( h.addParameters("-i", "-m", "-v"); h.addParameters(parentCommit); - final GitRebaseProblemDetector rebaseConflictDetector = new GitRebaseProblemDetector(); + GitRebaseProblemDetector rebaseConflictDetector = new GitRebaseProblemDetector(); h.addLineListener(rebaseConflictDetector); - final PushRebaseEditor pushRebaseEditor = new PushRebaseEditor(rebaseEditorService, root, olderCommits, false, h); + PushRebaseEditor pushRebaseEditor = new PushRebaseEditor(rebaseEditorService, root, olderCommits, false, h); rebaseEditorNo = pushRebaseEditor.getHandlerNo(); rebaseEditorService.configureHandler(h, rebaseEditorNo); - final GitTask rebaseTask = new GitTask(myProject, h, LocalizeValue.localizeTODO("Reordering commits")); + GitTask rebaseTask = new GitTask(myProject, h, LocalizeValue.localizeTODO("Reordering commits")); rebaseTask.setProgressIndicator(myProgressIndicator); return executeRebaseTaskInBackground(root, h, rebaseConflictDetector, rebaseTask); } @@ -281,7 +281,7 @@ protected void onFailure() { * @return true if the failure situation was resolved successfully, false if we failed to resolve the problem. */ @RequiredUIAccess - private boolean handleRebaseFailure(final VirtualFile root, final GitLineHandler h, GitRebaseProblemDetector rebaseConflictDetector) { + private boolean handleRebaseFailure(final VirtualFile root, GitLineHandler h, GitRebaseProblemDetector rebaseConflictDetector) { if (rebaseConflictDetector.isMergeConflict()) { LOG.info("handleRebaseFailure merge conflict"); return new GitConflictResolver(myProject, myGit, Collections.singleton(root), makeParamsForRebaseConflict()) { @@ -319,14 +319,18 @@ else if (GitUtil.hasLocalChanges(false, myProject, root)) { } catch (VcsException e) { LOG.info("Failed to work around 'no changes' error.", e); - String message = "Couldn't proceed with rebase. " + e.getMessage(); - GitUIUtil.notifyImportantError(myProject, "Error rebasing", message); + LocalizeValue message = LocalizeValue.localizeTODO("Couldn't proceed with rebase. " + e.getMessage()); + GitUIUtil.notifyImportantError(myProject, LocalizeValue.localizeTODO("Error rebasing"), message); return false; } } else { LOG.info("handleRebaseFailure error " + h.errors()); - GitUIUtil.notifyImportantError(myProject, "Error rebasing", GitUIUtil.stringifyErrors(h.errors())); + GitUIUtil.notifyImportantError( + myProject, + LocalizeValue.localizeTODO("Error rebasing"), + LocalizeValue.localizeTODO(GitUIUtil.stringifyErrors(h.errors())) + ); return false; } } @@ -375,7 +379,7 @@ public GitUpdateResult handleRebaseFailure( ) { if (rebaseConflictDetector.isMergeConflict()) { LOG.info("handleRebaseFailure merge conflict"); - final boolean allMerged = new GitRebaser.ConflictResolver(myProject, myGit, root, this).merge(); + boolean allMerged = new GitRebaser.ConflictResolver(myProject, myGit, root, this).merge(); return allMerged ? GitUpdateResult.SUCCESS_WITH_RESOLVED_CONFLICTS : GitUpdateResult.INCOMPLETE; } else if (untrackedWouldBeOverwrittenDetector.wasMessageDetected()) { @@ -400,7 +404,11 @@ else if (localChangesDetector.wasMessageDetected()) { } else { LOG.info("handleRebaseFailure error " + handler.errors()); - GitUIUtil.notifyImportantError(myProject, "Rebase error", GitUIUtil.stringifyErrors(handler.errors())); + GitUIUtil.notifyImportantError( + myProject, + LocalizeValue.localizeTODO("Rebase error"), + LocalizeValue.localizeTODO(GitUIUtil.stringifyErrors(handler.errors())) + ); return GitUpdateResult.ERROR; } } @@ -461,7 +469,7 @@ class PushRebaseEditor extends GitInteractiveRebaseEditorHandler { */ public PushRebaseEditor( GitRebaseEditorService rebaseEditorService, - final VirtualFile root, + VirtualFile root, List commits, boolean hasMerges, GitHandler h diff --git a/plugin/src/main/java/git4idea/reset/GitResetOperation.java b/plugin/src/main/java/git4idea/reset/GitResetOperation.java index 33ae809..f38bdf7 100644 --- a/plugin/src/main/java/git4idea/reset/GitResetOperation.java +++ b/plugin/src/main/java/git4idea/reset/GitResetOperation.java @@ -17,14 +17,16 @@ import consulo.application.AccessToken; import consulo.application.Application; -import consulo.application.ApplicationManager; import consulo.application.progress.ProgressIndicator; import consulo.document.FileDocumentManager; import consulo.ide.ServiceManager; +import consulo.localize.LocalizeValue; import consulo.project.Project; +import consulo.project.ui.notification.NotificationService; +import consulo.ui.annotation.RequiredUIAccess; import consulo.util.collection.MultiMap; import consulo.util.lang.StringUtil; -import consulo.util.lang.ref.Ref; +import consulo.util.lang.ref.SimpleReference; import consulo.versionControlSystem.VcsNotifier; import consulo.versionControlSystem.change.Change; import consulo.versionControlSystem.change.VcsDirtyScopeManager; @@ -48,146 +50,162 @@ import static git4idea.commands.GitLocalChangesWouldBeOverwrittenDetector.Operation.RESET; public class GitResetOperation { + @Nonnull + private final Project myProject; + @Nonnull + private final Map myCommits; + @Nonnull + private final GitResetMode myMode; + @Nonnull + private final ProgressIndicator myIndicator; + @Nonnull + private final Git myGit; + @Nonnull + protected final NotificationService myNotificationService; + @Nonnull + private final GitBranchUiHandlerImpl myUiHandler; + + public GitResetOperation( + @Nonnull Project project, + @Nonnull Map targetCommits, + @Nonnull GitResetMode mode, + @Nonnull ProgressIndicator indicator + ) { + myProject = project; + myCommits = targetCommits; + myMode = mode; + myIndicator = indicator; + myGit = ServiceManager.getService(Git.class); + myNotificationService = NotificationService.getInstance(); + myUiHandler = new GitBranchUiHandlerImpl(myProject, myGit, indicator); + } - @Nonnull - private final Project myProject; - @Nonnull - private final Map myCommits; - @Nonnull - private final GitResetMode myMode; - @Nonnull - private final ProgressIndicator myIndicator; - @Nonnull - private final Git myGit; - @Nonnull - private final VcsNotifier myNotifier; - @Nonnull - private final GitBranchUiHandlerImpl myUiHandler; - - public GitResetOperation(@Nonnull Project project, - @Nonnull Map targetCommits, - @Nonnull GitResetMode mode, - @Nonnull ProgressIndicator indicator) { - myProject = project; - myCommits = targetCommits; - myMode = mode; - myIndicator = indicator; - myGit = ServiceManager.getService(Git.class); - myNotifier = VcsNotifier.getInstance(project); - myUiHandler = new GitBranchUiHandlerImpl(myProject, myGit, indicator); - } - - public void execute() { - saveAllDocuments(); - Map results = new HashMap<>(); - try (AccessToken ignored = DvcsUtil.workingTreeChangeStarted(myProject, "Git Reset")) { - for (Map.Entry entry : myCommits.entrySet()) { - GitRepository repository = entry.getKey(); - VirtualFile root = repository.getRoot(); - String target = entry.getValue().asString(); - GitLocalChangesWouldBeOverwrittenDetector detector = new GitLocalChangesWouldBeOverwrittenDetector(root, RESET); - - GitCommandResult result = myGit.reset(repository, myMode, target, detector); - if (!result.success() && detector.wasMessageDetected()) { - GitCommandResult smartResult = proposeSmartReset(detector, repository, target); - if (smartResult != null) { - result = smartResult; - } + @RequiredUIAccess + public void execute() { + saveAllDocuments(); + Map results = new HashMap<>(); + try (AccessToken ignored = DvcsUtil.workingTreeChangeStarted(myProject, "Git Reset")) { + for (Map.Entry entry : myCommits.entrySet()) { + GitRepository repository = entry.getKey(); + VirtualFile root = repository.getRoot(); + String target = entry.getValue().asString(); + GitLocalChangesWouldBeOverwrittenDetector detector = new GitLocalChangesWouldBeOverwrittenDetector(root, RESET); + + GitCommandResult result = myGit.reset(repository, myMode, target, detector); + if (!result.success() && detector.wasMessageDetected()) { + GitCommandResult smartResult = proposeSmartReset(detector, repository, target); + if (smartResult != null) { + result = smartResult; + } + } + results.put(repository, result); + repository.update(); + VirtualFileUtil.markDirtyAndRefresh(false, true, false, root); + VcsDirtyScopeManager.getInstance(myProject).dirDirtyRecursively(root); + } } - results.put(repository, result); - repository.update(); - VirtualFileUtil.markDirtyAndRefresh(false, true, false, root); - VcsDirtyScopeManager.getInstance(myProject).dirDirtyRecursively(root); - } + notifyResult(results); } - notifyResult(results); - } - private GitCommandResult proposeSmartReset(@Nonnull GitLocalChangesWouldBeOverwrittenDetector detector, - @Nonnull final GitRepository repository, - @Nonnull final String target) { - Collection absolutePaths = GitUtil.toAbsolute(repository.getRoot(), detector.getRelativeFilePaths()); - List affectedChanges = GitUtil.findLocalChangesForPaths(myProject, repository.getRoot(), absolutePaths, false); - int choice = myUiHandler.showSmartOperationDialog(myProject, affectedChanges, absolutePaths, "reset", "&Hard Reset"); - if (choice == GitSmartOperationDialog.SMART_EXIT_CODE) { - final Ref result = Ref.create(); - new GitPreservingProcess(myProject, - myGit, - Collections.singleton(repository.getRoot()), - "reset", - target, - GitVcsSettings.UpdateChangesPolicy.STASH, - myIndicator, - new Runnable() { - @Override - public void run() { - result.set(myGit.reset(repository, myMode, target)); - } - }).execute(); - return result.get(); - } - if (choice == GitSmartOperationDialog.FORCE_EXIT_CODE) { - return myGit.reset(repository, GitResetMode.HARD, target); + private GitCommandResult proposeSmartReset( + @Nonnull GitLocalChangesWouldBeOverwrittenDetector detector, + @Nonnull GitRepository repository, + @Nonnull String target + ) { + Collection absolutePaths = GitUtil.toAbsolute(repository.getRoot(), detector.getRelativeFilePaths()); + List affectedChanges = GitUtil.findLocalChangesForPaths(myProject, repository.getRoot(), absolutePaths, false); + int choice = myUiHandler.showSmartOperationDialog(myProject, affectedChanges, absolutePaths, "reset", "&Hard Reset"); + if (choice == GitSmartOperationDialog.SMART_EXIT_CODE) { + SimpleReference result = SimpleReference.create(); + new GitPreservingProcess( + myProject, + myGit, + Collections.singleton(repository.getRoot()), + "reset", + target, + GitVcsSettings.UpdateChangesPolicy.STASH, + myIndicator, + () -> result.set(myGit.reset(repository, myMode, target)) + ).execute(); + return result.get(); + } + if (choice == GitSmartOperationDialog.FORCE_EXIT_CODE) { + return myGit.reset(repository, GitResetMode.HARD, target); + } + return null; } - return null; - } - private void notifyResult(@Nonnull Map results) { - Map successes = new HashMap<>(); - Map errors = new HashMap<>(); - for (Map.Entry entry : results.entrySet()) { - GitCommandResult result = entry.getValue(); - GitRepository repository = entry.getKey(); - if (result.success()) { - successes.put(repository, result); - } - else { - errors.put(repository, result); - } - } + private void notifyResult(@Nonnull Map results) { + Map successes = new HashMap<>(); + Map errors = new HashMap<>(); + for (Map.Entry entry : results.entrySet()) { + GitCommandResult result = entry.getValue(); + GitRepository repository = entry.getKey(); + if (result.success()) { + successes.put(repository, result); + } + else { + errors.put(repository, result); + } + } - if (errors.isEmpty()) { - myNotifier.notifySuccess("", "Reset successful"); - } - else if (!successes.isEmpty()) { - myNotifier.notifyImportantWarning("Reset partially failed", - "Reset was successful for " + joinRepos(successes.keySet()) + "
but failed for " + joinRepos( - errors.keySet()) + ":
" - + formErrorReport(errors)); - } - else { - myNotifier.notifyError("Reset Failed", formErrorReport(errors)); + if (errors.isEmpty()) { + myNotificationService.newInfo(VcsNotifier.NOTIFICATION_GROUP_ID) + .content(LocalizeValue.localizeTODO("Reset successful")) + .notify(myProject); + } + else if (!successes.isEmpty()) { + myNotificationService.newWarn(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Reset partially failed")) + .content(LocalizeValue.localizeTODO( + "Reset was successful for " + joinRepos(successes.keySet()) + "
" + + "but failed for " + joinRepos(errors.keySet()) + ":
" + + formErrorReport(errors) + )) + .notify(myProject); + } + else { + myNotificationService.newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Reset Failed")) + .content(formErrorReport(errors)) + .notify(myProject); + } } - } - @Nonnull - private static String formErrorReport(@Nonnull Map errorResults) { - MultiMap grouped = groupByResult(errorResults); - if (grouped.size() == 1) { - return "" + grouped.keySet().iterator().next() + ""; + @Nonnull + private static LocalizeValue formErrorReport(@Nonnull Map errorResults) { + MultiMap grouped = groupByResult(errorResults); + if (grouped.size() == 1) { + return grouped.keySet().iterator().next().map((localizeManager, string) -> "" + string + ""); + } + return LocalizeValue.localizeTODO(StringUtil.join( + grouped.entrySet(), + entry -> joinRepos(entry.getValue()) + ":
" + entry.getKey() + "", + "
" + )); } - return StringUtil.join(grouped.entrySet(), entry -> joinRepos(entry.getValue()) + ":
" + entry.getKey() + "", "
"); - } - // to avoid duplicate error reports if they are the same for different repositories - @Nonnull - private static MultiMap groupByResult(@Nonnull Map results) { - MultiMap grouped = MultiMap.create(); - for (Map.Entry entry : results.entrySet()) { - grouped.putValue(entry.getValue().getErrorOutputAsHtmlString(), entry.getKey()); + // to avoid duplicate error reports if they are the same for different repositories + @Nonnull + private static MultiMap groupByResult(@Nonnull Map results) { + MultiMap grouped = MultiMap.create(); + for (Map.Entry entry : results.entrySet()) { + grouped.putValue(entry.getValue().getErrorOutputAsHtmlValue(), entry.getKey()); + } + return grouped; } - return grouped; - } - - @Nonnull - private static String joinRepos(@Nonnull Collection repositories) { - return StringUtil.join(DvcsUtil.sortRepositories(repositories), ", "); - } - private static void saveAllDocuments() { - ApplicationManager.getApplication() - .invokeAndWait(() -> FileDocumentManager.getInstance().saveAllDocuments(), - Application.get().getDefaultModalityState()); - } + @Nonnull + private static String joinRepos(@Nonnull Collection repositories) { + return StringUtil.join(DvcsUtil.sortRepositories(repositories), ", "); + } + @RequiredUIAccess + private static void saveAllDocuments() { + Application application = Application.get(); + application.invokeAndWait( + () -> FileDocumentManager.getInstance().saveAllDocuments(), + application.getDefaultModalityState() + ); + } } diff --git a/plugin/src/main/java/git4idea/roots/GitIntegrationEnabler.java b/plugin/src/main/java/git4idea/roots/GitIntegrationEnabler.java index 14db381..25ee363 100644 --- a/plugin/src/main/java/git4idea/roots/GitIntegrationEnabler.java +++ b/plugin/src/main/java/git4idea/roots/GitIntegrationEnabler.java @@ -15,7 +15,9 @@ */ package git4idea.roots; +import consulo.localize.LocalizeValue; import consulo.logging.Logger; +import consulo.project.ui.notification.NotificationService; import consulo.versionControlSystem.VcsNotifier; import consulo.versionControlSystem.root.VcsIntegrationEnabler; import consulo.virtualFileSystem.VirtualFile; @@ -26,41 +28,35 @@ import jakarta.annotation.Nonnull; -public class GitIntegrationEnabler extends VcsIntegrationEnabler -{ - - private final - @Nonnull - Git myGit; - - private static final Logger LOG = Logger.getInstance(GitIntegrationEnabler.class); - - public GitIntegrationEnabler(@Nonnull GitVcs vcs, @Nonnull Git git) - { - super(vcs); - myGit = git; - } - - @Override - protected boolean initOrNotifyError(@Nonnull final VirtualFile projectDir) - { - VcsNotifier vcsNotifier = VcsNotifier.getInstance(myProject); - GitCommandResult result = myGit.init(myProject, projectDir); - if(result.success()) - { - refreshVcsDir(projectDir, GitUtil.DOT_GIT); - vcsNotifier.notifySuccess("Created Git repository in " + projectDir.getPresentableUrl()); - return true; - } - else - { - if(myVcs.getExecutableValidator().checkExecutableAndNotifyIfNeeded()) - { - vcsNotifier.notifyError("Couldn't git init " + projectDir.getPresentableUrl(), result.getErrorOutputAsHtmlString()); - LOG.info(result.getErrorOutputAsHtmlString()); - } - return false; - } - } - +public class GitIntegrationEnabler extends VcsIntegrationEnabler { + @Nonnull + private final Git myGit; + + private static final Logger LOG = Logger.getInstance(GitIntegrationEnabler.class); + + public GitIntegrationEnabler(@Nonnull GitVcs vcs, @Nonnull Git git) { + super(vcs); + myGit = git; + } + + @Override + protected boolean initOrNotifyError(@Nonnull VirtualFile projectDir) { + NotificationService notificationService = NotificationService.getInstance(); + GitCommandResult result = myGit.init(myProject, projectDir); + if (result.success()) { + refreshVcsDir(projectDir, GitUtil.DOT_GIT); + notificationService.newInfo(VcsNotifier.NOTIFICATION_GROUP_ID) + .content(LocalizeValue.localizeTODO("Created Git repository in " + projectDir.getPresentableUrl())) + .notifyAndGet(myProject); + return true; + } + else if (myVcs.getExecutableValidator().checkExecutableAndNotifyIfNeeded()) { + notificationService.newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Couldn't git init " + projectDir.getPresentableUrl())) + .content(result.getErrorOutputAsHtmlValue()) + .notifyAndGet(myProject); + LOG.info(result.getErrorOutputAsHtmlString()); + } + return false; + } } diff --git a/plugin/src/main/java/git4idea/stash/GitStashChangesSaver.java b/plugin/src/main/java/git4idea/stash/GitStashChangesSaver.java index 270c934..556bead 100644 --- a/plugin/src/main/java/git4idea/stash/GitStashChangesSaver.java +++ b/plugin/src/main/java/git4idea/stash/GitStashChangesSaver.java @@ -16,10 +16,9 @@ package git4idea.stash; import consulo.application.progress.ProgressIndicator; +import consulo.localize.LocalizeValue; import consulo.logging.Logger; import consulo.project.Project; -import consulo.project.ui.notification.Notification; -import consulo.project.ui.notification.event.NotificationListener; import consulo.util.lang.StringUtil; import consulo.versionControlSystem.VcsException; import consulo.versionControlSystem.VcsNotifier; @@ -37,228 +36,208 @@ import git4idea.repo.GitRepositoryManager; import git4idea.ui.GitUnstashDialog; import git4idea.util.GitUIUtil; - import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; + import javax.swing.event.HyperlinkEvent; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Set; -public class GitStashChangesSaver extends GitChangesSaver -{ - - private static final Logger LOG = Logger.getInstance(GitStashChangesSaver.class); - private static final String NO_LOCAL_CHANGES_TO_SAVE = "No local changes to save"; - - @Nonnull - private final GitRepositoryManager myRepositoryManager; - @Nonnull - private final Set myStashedRoots = new HashSet<>(); // save stashed roots to unstash only them - - public GitStashChangesSaver(@Nonnull Project project, @Nonnull Git git, @Nonnull ProgressIndicator progressIndicator, @Nonnull String stashMessage) - { - super(project, git, progressIndicator, stashMessage); - myRepositoryManager = GitUtil.getRepositoryManager(project); - } - - @Override - protected void save(@Nonnull Collection rootsToSave) throws VcsException - { - LOG.info("saving " + rootsToSave); - - for(VirtualFile root : rootsToSave) - { - final String message = GitHandlerUtil.formatOperationName("Stashing changes from", root); - LOG.info(message); - final String oldProgressTitle = myProgressIndicator.getText(); - myProgressIndicator.setText(message); - GitRepository repository = myRepositoryManager.getRepositoryForRoot(root); - if(repository == null) - { - LOG.error("Repository is null for root " + root); - } - else - { - GitCommandResult result = myGit.stashSave(repository, myStashMessage); - if(result.success() && somethingWasStashed(result)) - { - myStashedRoots.add(root); - } - else - { - String error = "stash " + repository.getRoot() + ": " + result.getErrorOutputAsJoinedString(); - if(!result.success()) - { - throw new VcsException(error); - } - else - { - LOG.warn(error); - } - } - } - myProgressIndicator.setText(oldProgressTitle); - } - } - - private static boolean somethingWasStashed(@Nonnull GitCommandResult result) - { - return !StringUtil.containsIgnoreCase(result.getErrorOutputAsJoinedString(), NO_LOCAL_CHANGES_TO_SAVE) && !StringUtil.containsIgnoreCase(result.getOutputAsJoinedString(), - NO_LOCAL_CHANGES_TO_SAVE); - } - - @Override - public void load() - { - for(VirtualFile root : myStashedRoots) - { - loadRoot(root); - } - - boolean conflictsResolved = new UnstashConflictResolver(myProject, myGit, myStashedRoots, myParams).merge(); - LOG.info("load: conflicts resolved status is " + conflictsResolved + " in roots " + myStashedRoots); - } - - @Override - public boolean wereChangesSaved() - { - return !myStashedRoots.isEmpty(); - } - - @Override - public String getSaverName() - { - return "stash"; - } - - @Nonnull - @Override - public String getOperationName() - { - return "stash"; - } - - @Override - public void showSavedChanges() - { - GitUnstashDialog.showUnstashDialog(myProject, new ArrayList<>(myStashedRoots), myStashedRoots.iterator().next()); - } - - /** - * Returns true if the root was loaded with conflict. - * False is returned in all other cases: in the case of success and in case of some other error. - */ - private boolean loadRoot(final VirtualFile root) - { - LOG.info("loadRoot " + root); - myProgressIndicator.setText(GitHandlerUtil.formatOperationName("Unstashing changes to", root)); - - GitRepository repository = myRepositoryManager.getRepositoryForRoot(root); - if(repository == null) - { - LOG.error("Repository is null for root " + root); - return false; - } - - GitSimpleEventDetector conflictDetector = new GitSimpleEventDetector(GitSimpleEventDetector.Event.MERGE_CONFLICT_ON_UNSTASH); - GitCommandResult result = myGit.stashPop(repository, conflictDetector); - VirtualFileUtil.markDirtyAndRefresh(false, true, false, root); - if(result.success()) - { - return false; - } - else if(conflictDetector.hasHappened()) - { - return true; - } - else - { - LOG.info("unstash failed " + result.getErrorOutputAsJoinedString()); - GitUIUtil.notifyImportantError(myProject, "Couldn't unstash", "
" + result.getErrorOutputAsHtmlString()); - return false; - } - } - - @Override - public String toString() - { - return "StashChangesSaver. Roots: " + myStashedRoots; - } - - private static class UnstashConflictResolver extends GitConflictResolver - { - - private final Set myStashedRoots; - - public UnstashConflictResolver(@Nonnull Project project, @Nonnull Git git, @Nonnull Set stashedRoots, @Nullable Params params) - { - super(project, git, stashedRoots, makeParamsOrUse(params)); - myStashedRoots = stashedRoots; - } - - private static Params makeParamsOrUse(@Nullable Params givenParams) - { - if(givenParams != null) - { - return givenParams; - } - Params params = new Params(); - params.setErrorNotificationTitle("Local changes were not restored"); - params.setMergeDialogCustomizer(new UnstashMergeDialogCustomizer()); - params.setReverse(true); - return params; - } - - - @Override - protected void notifyUnresolvedRemain() - { - VcsNotifier.getInstance(myProject).notifyImportantWarning("Local changes were restored with conflicts", "Your uncommitted changes were saved to stash.
" + - "Unstash is not complete, you have unresolved merges in your working tree
" + - "Resolve conflicts and drop the stash.", new NotificationListener() - { - @Override - public void hyperlinkUpdate(@Nonnull Notification notification, @Nonnull HyperlinkEvent event) - { - if(event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) - { - if(event.getDescription().equals("saver")) - { - // we don't use #showSavedChanges to specify unmerged root first - GitUnstashDialog.showUnstashDialog(myProject, new ArrayList<>(myStashedRoots), myStashedRoots.iterator().next()); - } - else if(event.getDescription().equals("resolve")) - { - mergeNoProceed(); - } - } - } - }); - } - - } - - private static class UnstashMergeDialogCustomizer extends MergeDialogCustomizer - { - - @Override - public String getMultipleFileMergeDescription(@Nonnull Collection files) - { - return "Uncommitted changes that were stashed before update have conflicts with updated files."; - } - - @Override - public String getLeftPanelTitle(@Nonnull VirtualFile file) - { - return getConflictLeftPanelTitle(); - } - - @Override - public String getRightPanelTitle(@Nonnull VirtualFile file, VcsRevisionNumber revisionNumber) - { - return getConflictRightPanelTitle(); - } - } +public class GitStashChangesSaver extends GitChangesSaver { + + private static final Logger LOG = Logger.getInstance(GitStashChangesSaver.class); + private static final String NO_LOCAL_CHANGES_TO_SAVE = "No local changes to save"; + + @Nonnull + private final GitRepositoryManager myRepositoryManager; + @Nonnull + private final Set myStashedRoots = new HashSet<>(); // save stashed roots to unstash only them + + public GitStashChangesSaver( + @Nonnull Project project, + @Nonnull Git git, + @Nonnull ProgressIndicator progressIndicator, + @Nonnull String stashMessage + ) { + super(project, git, progressIndicator, stashMessage); + myRepositoryManager = GitUtil.getRepositoryManager(project); + } + + @Override + protected void save(@Nonnull Collection rootsToSave) throws VcsException { + LOG.info("saving " + rootsToSave); + + for (VirtualFile root : rootsToSave) { + String message = GitHandlerUtil.formatOperationName("Stashing changes from", root); + LOG.info(message); + String oldProgressTitle = myProgressIndicator.getText(); + myProgressIndicator.setText(message); + GitRepository repository = myRepositoryManager.getRepositoryForRoot(root); + if (repository == null) { + LOG.error("Repository is null for root " + root); + } + else { + GitCommandResult result = myGit.stashSave(repository, myStashMessage); + if (result.success() && somethingWasStashed(result)) { + myStashedRoots.add(root); + } + else { + String error = "stash " + repository.getRoot() + ": " + result.getErrorOutputAsJoinedString(); + if (!result.success()) { + throw new VcsException(error); + } + else { + LOG.warn(error); + } + } + } + myProgressIndicator.setText(oldProgressTitle); + } + } + + private static boolean somethingWasStashed(@Nonnull GitCommandResult result) { + return !StringUtil.containsIgnoreCase(result.getErrorOutputAsJoinedString(), NO_LOCAL_CHANGES_TO_SAVE) + && !StringUtil.containsIgnoreCase(result.getOutputAsJoinedString(), NO_LOCAL_CHANGES_TO_SAVE); + } + + @Override + public void load() { + for (VirtualFile root : myStashedRoots) { + loadRoot(root); + } + + boolean conflictsResolved = new UnstashConflictResolver(myProject, myGit, myStashedRoots, myParams).merge(); + LOG.info("load: conflicts resolved status is " + conflictsResolved + " in roots " + myStashedRoots); + } + + @Override + public boolean wereChangesSaved() { + return !myStashedRoots.isEmpty(); + } + + @Override + public String getSaverName() { + return "stash"; + } + + @Nonnull + @Override + public String getOperationName() { + return "stash"; + } + + @Override + public void showSavedChanges() { + GitUnstashDialog.showUnstashDialog(myProject, new ArrayList<>(myStashedRoots), myStashedRoots.iterator().next()); + } + + /** + * Returns true if the root was loaded with conflict. + * False is returned in all other cases: in the case of success and in case of some other error. + */ + private boolean loadRoot(VirtualFile root) { + LOG.info("loadRoot " + root); + myProgressIndicator.setText(GitHandlerUtil.formatOperationName("Unstashing changes to", root)); + + GitRepository repository = myRepositoryManager.getRepositoryForRoot(root); + if (repository == null) { + LOG.error("Repository is null for root " + root); + return false; + } + + GitSimpleEventDetector conflictDetector = new GitSimpleEventDetector(GitSimpleEventDetector.Event.MERGE_CONFLICT_ON_UNSTASH); + GitCommandResult result = myGit.stashPop(repository, conflictDetector); + VirtualFileUtil.markDirtyAndRefresh(false, true, false, root); + if (result.success()) { + return false; + } + else if (conflictDetector.hasHappened()) { + return true; + } + else { + LOG.info("unstash failed " + result.getErrorOutputAsJoinedString()); + GitUIUtil.notifyImportantError( + myProject, + LocalizeValue.localizeTODO("Couldn't unstash"), + LocalizeValue.localizeTODO("
" + result.getErrorOutputAsHtmlValue()) + ); + return false; + } + } + + @Override + public String toString() { + return "StashChangesSaver. Roots: " + myStashedRoots; + } + + private static class UnstashConflictResolver extends GitConflictResolver { + + private final Set myStashedRoots; + + public UnstashConflictResolver( + @Nonnull Project project, + @Nonnull Git git, + @Nonnull Set stashedRoots, + @Nullable Params params + ) { + super(project, git, stashedRoots, makeParamsOrUse(params)); + myStashedRoots = stashedRoots; + } + + private static Params makeParamsOrUse(@Nullable Params givenParams) { + if (givenParams != null) { + return givenParams; + } + Params params = new Params(); + params.setErrorNotificationTitle("Local changes were not restored"); + params.setMergeDialogCustomizer(new UnstashMergeDialogCustomizer()); + params.setReverse(true); + return params; + } + + + @Override + protected void notifyUnresolvedRemain() { + VcsNotifier.getInstance(myProject).notifyImportantWarning( + "Local changes were restored with conflicts", + "Your uncommitted changes were saved to stash.
" + + "Unstash is not complete, you have unresolved merges in your working tree
" + + "Resolve conflicts and drop the stash.", + (notification, event) -> { + if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { + if (event.getDescription().equals("saver")) { + // we don't use #showSavedChanges to specify unmerged root first + GitUnstashDialog.showUnstashDialog( + myProject, + new ArrayList<>(myStashedRoots), + myStashedRoots.iterator().next() + ); + } + else if (event.getDescription().equals("resolve")) { + mergeNoProceed(); + } + } + } + ); + } + } + + private static class UnstashMergeDialogCustomizer extends MergeDialogCustomizer { + @Override + public String getMultipleFileMergeDescription(@Nonnull Collection files) { + return "Uncommitted changes that were stashed before update have conflicts with updated files."; + } + + @Override + public String getLeftPanelTitle(@Nonnull VirtualFile file) { + return getConflictLeftPanelTitle(); + } + + @Override + public String getRightPanelTitle(@Nonnull VirtualFile file, VcsRevisionNumber revisionNumber) { + return getConflictRightPanelTitle(); + } + } } diff --git a/plugin/src/main/java/git4idea/status/GitNewChangesCollector.java b/plugin/src/main/java/git4idea/status/GitNewChangesCollector.java index 7d6574f..8d53173 100644 --- a/plugin/src/main/java/git4idea/status/GitNewChangesCollector.java +++ b/plugin/src/main/java/git4idea/status/GitNewChangesCollector.java @@ -243,7 +243,7 @@ else if (yStatus == 'D') { // DD - unmerged, both deleted case 'U': if (yStatus == 'U' || yStatus == 'A' || yStatus == 'D' || yStatus == 'T') { - // UU - unmerged, both modified; UD - unmerged, deleted by them; UA - umerged, added by them + // UU - unmerged, both modified; UD - unmerged, deleted by them; UA - unmerged, added by them reportConflict(filepath, head); } else { diff --git a/plugin/src/main/java/git4idea/update/GitFetchResult.java b/plugin/src/main/java/git4idea/update/GitFetchResult.java index 23daf79..b468d2b 100644 --- a/plugin/src/main/java/git4idea/update/GitFetchResult.java +++ b/plugin/src/main/java/git4idea/update/GitFetchResult.java @@ -15,6 +15,7 @@ */ package git4idea.update; +import consulo.localize.LocalizeValue; import jakarta.annotation.Nonnull; import java.util.ArrayList; @@ -28,85 +29,85 @@ * @author Kirill Likhodedov */ public final class GitFetchResult { + private final Type myType; + private Collection myErrors = new ArrayList(); + private Collection myPrunedRefs = new ArrayList(); + + public enum Type { + SUCCESS, + CANCELLED, + NOT_AUTHORIZED, + ERROR + } + + public GitFetchResult(@Nonnull Type type) { + myType = type; + } + + @Nonnull + public static GitFetchResult success() { + return new GitFetchResult(Type.SUCCESS); + } + + @Nonnull + public static GitFetchResult cancel() { + return new GitFetchResult(Type.CANCELLED); + } + + @Nonnull + public static GitFetchResult error(Collection errors) { + GitFetchResult result = new GitFetchResult(Type.ERROR); + result.myErrors = errors; + return result; + } + + @Nonnull + public static GitFetchResult error(Exception error) { + return error(Collections.singletonList(error)); + } + + @Nonnull + public static GitFetchResult error(@Nonnull String errorMessage) { + return error(new Exception(errorMessage)); + } - private final Type myType; - private Collection myErrors = new ArrayList(); - private Collection myPrunedRefs = new ArrayList(); - - public enum Type { - SUCCESS, - CANCELLED, - NOT_AUTHORIZED, - ERROR - } - - public GitFetchResult(@Nonnull Type type) { - myType = type; - } - - @Nonnull - public static GitFetchResult success() { - return new GitFetchResult(Type.SUCCESS); - } - - @Nonnull - public static GitFetchResult cancel() { - return new GitFetchResult(Type.CANCELLED); - } - - @Nonnull - public static GitFetchResult error(Collection errors) { - GitFetchResult result = new GitFetchResult(Type.ERROR); - result.myErrors = errors; - return result; - } - - @Nonnull - public static GitFetchResult error(Exception error) { - return error(Collections.singletonList(error)); - } - - @Nonnull - public static GitFetchResult error(@Nonnull String errorMessage) { - return error(new Exception(errorMessage)); - } - - public boolean isSuccess() { - return myType == Type.SUCCESS; - } - - public boolean isCancelled() { - return myType == Type.CANCELLED; - } - - public boolean isNotAuthorized() { - return myType == Type.NOT_AUTHORIZED; - } - - public boolean isError() { - return myType == Type.ERROR; - } - - @Nonnull - public Collection getErrors() { - return myErrors; - } - - public void addPruneInfo(@Nonnull Collection prunedRefs) { - myPrunedRefs.addAll(prunedRefs); - } - - @Nonnull - public Collection getPrunedRefs() { - return myPrunedRefs; - } - - @Nonnull - public String getAdditionalInfo() { - if (!myPrunedRefs.isEmpty()) { - return "Pruned obsolete remote " + pluralize("reference", myPrunedRefs.size()) + ": " + join(myPrunedRefs, ", "); + public boolean isSuccess() { + return myType == Type.SUCCESS; } - return ""; - } + public boolean isCancelled() { + return myType == Type.CANCELLED; + } + + public boolean isNotAuthorized() { + return myType == Type.NOT_AUTHORIZED; + } + + public boolean isError() { + return myType == Type.ERROR; + } + + @Nonnull + public Collection getErrors() { + return myErrors; + } + + public void addPruneInfo(@Nonnull Collection prunedRefs) { + myPrunedRefs.addAll(prunedRefs); + } + + @Nonnull + public Collection getPrunedRefs() { + return myPrunedRefs; + } + + @Nonnull + public LocalizeValue getAdditionalInfo() { + if (!myPrunedRefs.isEmpty()) { + return LocalizeValue.localizeTODO( + "Pruned obsolete remote " + pluralize("reference", myPrunedRefs.size()) + ": " + join(myPrunedRefs, ", ") + ); + } + return LocalizeValue.empty(); + } } diff --git a/plugin/src/main/java/git4idea/update/GitFetcher.java b/plugin/src/main/java/git4idea/update/GitFetcher.java index 3bd6359..e5fb64e 100644 --- a/plugin/src/main/java/git4idea/update/GitFetcher.java +++ b/plugin/src/main/java/git4idea/update/GitFetcher.java @@ -19,6 +19,7 @@ import consulo.localize.LocalizeValue; import consulo.logging.Logger; import consulo.project.Project; +import consulo.project.ui.notification.NotificationService; import consulo.ui.annotation.RequiredUIAccess; import consulo.util.dataholder.Key; import consulo.util.lang.StringUtil; @@ -58,7 +59,10 @@ public class GitFetcher { private static final Logger LOG = Logger.getInstance(GitFetcher.class); + @Nonnull private final Project myProject; + @Nonnull + private final NotificationService myNotificationService; private final GitRepositoryManager myRepositoryManager; private final ProgressIndicator myProgressIndicator; private final boolean myFetchAll; @@ -72,6 +76,7 @@ public class GitFetcher { */ public GitFetcher(@Nonnull Project project, @Nonnull ProgressIndicator progressIndicator, boolean fetchAll) { myProject = project; + myNotificationService = NotificationService.getInstance(); myProgressIndicator = progressIndicator; myFetchAll = fetchAll; myRepositoryManager = GitUtil.getRepositoryManager(myProject); @@ -225,7 +230,7 @@ private GitFetchResult fetchNatively( h.addParameters(getFetchSpecForBranch(branch, remoteName)); } - final GitTask fetchTask = new GitTask(myProject, h, LocalizeValue.localizeTODO("Fetching " + remote.getFirstUrl())); + GitTask fetchTask = new GitTask(myProject, h, LocalizeValue.localizeTODO("Fetching " + remote.getFirstUrl())); fetchTask.setProgressIndicator(myProgressIndicator); fetchTask.setProgressAnalyzer(new GitStandardProgressAnalyzer()); @@ -252,7 +257,7 @@ protected void onFailure() { myErrors.addAll(h.errors()); } else { - myErrors.add(new VcsException("Authentication failed")); + myErrors.add(new VcsException(LocalizeValue.localizeTODO("Authentication failed"))); } result.set(GitFetchResult.error(myErrors)); } @@ -287,29 +292,32 @@ public static void displayFetchResult( @Nonnull Collection errors ) { if (result.isSuccess()) { - VcsNotifier.getInstance(project).notifySuccess("Fetched successfully" + result.getAdditionalInfo()); + NotificationService.getInstance().newInfo(VcsNotifier.NOTIFICATION_GROUP_ID) + .content(LocalizeValue.localizeTODO("Fetched successfully" + result.getAdditionalInfo())) + .notifyAndGet(project); } else if (result.isCancelled()) { - VcsNotifier.getInstance(project).notifyMinorWarning("", "Fetch cancelled by user" + result.getAdditionalInfo()); + NotificationService.getInstance().newWarn(VcsNotifier.STANDARD_NOTIFICATION) + .content(LocalizeValue.localizeTODO("Fetch cancelled by user" + result.getAdditionalInfo())) + .notifyAndGet(project); } else if (result.isNotAuthorized()) { - String title; - String description; + LocalizeValue title; + LocalizeValue description; if (errorNotificationTitle != null) { - title = errorNotificationTitle; - description = "Fetch failed: couldn't authorize"; + title = LocalizeValue.localizeTODO(errorNotificationTitle); + description = LocalizeValue.localizeTODO("Fetch failed: couldn't authorize" + result.getAdditionalInfo()); } else { - title = "Fetch failed"; - description = "Couldn't authorize"; + title = LocalizeValue.localizeTODO("Fetch failed"); + description = LocalizeValue.localizeTODO("Couldn't authorize" + result.getAdditionalInfo()); } - description += result.getAdditionalInfo(); GitUIUtil.notifyMessage(project, title, description, true, null); } else { GitVcs instance = GitVcs.getInstance(project); if (instance != null && instance.getExecutableValidator().isExecutableValid()) { - GitUIUtil.notifyMessage(project, "Fetch failed", result.getAdditionalInfo(), true, errors); + GitUIUtil.notifyMessage(project, LocalizeValue.localizeTODO("Fetch failed"), result.getAdditionalInfo(), true, errors); } } } @@ -335,9 +343,9 @@ public boolean fetchRootsAndNotify( for (GitRepository repository : roots) { LOG.info("fetching " + repository); GitFetchResult result = fetch(repository); - String ai = result.getAdditionalInfo(); - if (!StringUtil.isEmptyOrSpaces(ai)) { - additionalInfo.put(repository.getRoot(), ai); + LocalizeValue ai = result.getAdditionalInfo(); + if (ai != LocalizeValue.empty()) { + additionalInfo.put(repository.getRoot(), ai.get()); } if (!result.isSuccess()) { Collection errors = new ArrayList<>(getErrors()); @@ -347,12 +355,17 @@ public boolean fetchRootsAndNotify( } } if (notifySuccess) { - VcsNotifier.getInstance(myProject).notifySuccess("Fetched successfully"); + myNotificationService.newInfo(VcsNotifier.NOTIFICATION_GROUP_ID) + .content(LocalizeValue.localizeTODO("Fetched successfully")) + .notify(myProject); } String addInfo = makeAdditionalInfoByRoot(additionalInfo); if (!StringUtil.isEmptyOrSpaces(addInfo)) { - VcsNotifier.getInstance(myProject).notifyMinorInfo("Fetch details", addInfo); + myNotificationService.newInfo(VcsNotifier.STANDARD_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Fetch details")) + .content(LocalizeValue.localizeTODO(addInfo)) + .notify(myProject); } return true; diff --git a/plugin/src/main/java/git4idea/update/GitMergeUpdater.java b/plugin/src/main/java/git4idea/update/GitMergeUpdater.java index a24040f..687fbb8 100644 --- a/plugin/src/main/java/git4idea/update/GitMergeUpdater.java +++ b/plugin/src/main/java/git4idea/update/GitMergeUpdater.java @@ -18,8 +18,10 @@ import consulo.application.Application; import consulo.application.progress.ProgressIndicator; import consulo.component.ProcessCanceledException; +import consulo.localize.LocalizeValue; import consulo.logging.Logger; import consulo.project.Project; +import consulo.ui.annotation.RequiredUIAccess; import consulo.ui.ex.awt.UIUtil; import consulo.util.collection.ContainerUtil; import consulo.util.dataholder.Key; @@ -61,39 +63,44 @@ public class GitMergeUpdater extends GitUpdater { @Nonnull private final ChangeListManager myChangeListManager; - public GitMergeUpdater(@Nonnull Project project, - @Nonnull Git git, - @Nonnull GitRepository repository, - @Nonnull GitBranchPair branchAndTracked, - @Nonnull ProgressIndicator progressIndicator, - @Nonnull UpdatedFiles updatedFiles) { + public GitMergeUpdater( + @Nonnull Project project, + @Nonnull Git git, + @Nonnull GitRepository repository, + @Nonnull GitBranchPair branchAndTracked, + @Nonnull ProgressIndicator progressIndicator, + @Nonnull UpdatedFiles updatedFiles + ) { super(project, git, repository, branchAndTracked, progressIndicator, updatedFiles); myChangeListManager = ChangeListManager.getInstance(myProject); } - @Override @Nonnull + @Override + @RequiredUIAccess protected GitUpdateResult doUpdate() { LOG.info("doUpdate "); - final GitMerger merger = new GitMerger(myProject); + GitMerger merger = new GitMerger(myProject); MergeLineListener mergeLineListener = new MergeLineListener(); - GitUntrackedFilesOverwrittenByOperationDetector untrackedFilesDetector = new GitUntrackedFilesOverwrittenByOperationDetector(myRoot); + GitUntrackedFilesOverwrittenByOperationDetector untrackedFilesDetector = + new GitUntrackedFilesOverwrittenByOperationDetector(myRoot); String originalText = myProgressIndicator.getText(); myProgressIndicator.setText("Merging" + GitUtil.mention(myRepository) + "..."); try { - GitCommandResult result = myGit.merge(myRepository, + GitCommandResult result = myGit.merge( + myRepository, assertNotNull(myBranchPair.getDest()).getName(), asList("--no-stat", "-v"), mergeLineListener, untrackedFilesDetector, - GitStandardProgressAnalyzer.createListener(myProgressIndicator)); + GitStandardProgressAnalyzer.createListener(myProgressIndicator) + ); myProgressIndicator.setText(originalText); - return result.success() ? GitUpdateResult.SUCCESS : handleMergeFailure(mergeLineListener, - untrackedFilesDetector, - merger, - result.getErrorOutputAsJoinedString()); + return result.success() + ? GitUpdateResult.SUCCESS + : handleMergeFailure(mergeLineListener, untrackedFilesDetector, merger, result.getErrorOutputAsJoinedValue()); } catch (ProcessCanceledException pce) { cancel(); @@ -102,21 +109,24 @@ protected GitUpdateResult doUpdate() { } @Nonnull - private GitUpdateResult handleMergeFailure(MergeLineListener mergeLineListener, - GitMessageWithFilesDetector untrackedFilesWouldBeOverwrittenByMergeDetector, - final GitMerger merger, - String errorMessage) { - final MergeError error = mergeLineListener.getMergeError(); + @RequiredUIAccess + private GitUpdateResult handleMergeFailure( + MergeLineListener mergeLineListener, + GitMessageWithFilesDetector untrackedFilesWouldBeOverwrittenByMergeDetector, + GitMerger merger, + @Nonnull LocalizeValue errorMessage + ) { + MergeError error = mergeLineListener.getMergeError(); LOG.info("merge error: " + error); if (error == MergeError.CONFLICT) { LOG.info("Conflict detected"); - final boolean allMerged = new MyConflictResolver(myProject, myGit, merger, myRoot).merge(); + boolean allMerged = new MyConflictResolver(myProject, myGit, merger, myRoot).merge(); return allMerged ? GitUpdateResult.SUCCESS_WITH_RESOLVED_CONFLICTS : GitUpdateResult.INCOMPLETE; } else if (error == MergeError.LOCAL_CHANGES) { LOG.info("Local changes would be overwritten by merge"); - final List paths = getFilesOverwrittenByMerge(mergeLineListener.getOutput()); - final Collection changes = getLocalChangesFilteredByFiles(paths); + List paths = getFilesOverwrittenByMerge(mergeLineListener.getOutput()); + Collection changes = getLocalChangesFilteredByFiles(paths); LegacyComponentFactory componentFactory = Application.get().getInstance(LegacyComponentFactory.class); UIUtil.invokeAndWaitIfNeeded((Runnable) () -> { @@ -131,16 +141,18 @@ else if (error == MergeError.LOCAL_CHANGES) { } else if (untrackedFilesWouldBeOverwrittenByMergeDetector.wasMessageDetected()) { LOG.info("handleMergeFailure: untracked files would be overwritten by merge"); - GitUntrackedFilesHelper.notifyUntrackedFilesOverwrittenBy(myProject, + GitUntrackedFilesHelper.notifyUntrackedFilesOverwrittenBy( + myProject, myRoot, untrackedFilesWouldBeOverwrittenByMergeDetector.getRelativeFilePaths(), "merge", - null); + null + ); return GitUpdateResult.ERROR; } else { LOG.info("Unknown error: " + errorMessage); - GitUIUtil.notifyImportantError(myProject, "Error merging", errorMessage); + GitUIUtil.notifyImportantError(myProject, LocalizeValue.localizeTODO("Error merging"), errorMessage); return GitUpdateResult.ERROR; } } @@ -170,7 +182,10 @@ public boolean isSaveNeeded() { GitUtil.getPathsDiffBetweenRefs(Git.getInstance(), repository, currentBranch, remoteBranch); final List locallyChanged = myChangeListManager.getAffectedPaths(); for (final File localPath : locallyChanged) { - if (ContainerUtil.exists(remotelyChanged, remotelyChangedPath -> FileUtil.pathsEqual(localPath.getPath(), remotelyChangedPath))) { + if (ContainerUtil.exists( + remotelyChanged, + remotelyChangedPath -> FileUtil.pathsEqual(localPath.getPath(), remotelyChangedPath) + )) { // found a file which was changed locally and remotely => need to save return true; } @@ -189,13 +204,17 @@ private void cancel() { GitCommandResult result = Git.getInstance().runCommand(h); if (!result.success()) { LOG.info("cancel git reset --merge: " + result.getErrorOutputAsJoinedString()); - GitUIUtil.notifyImportantError(myProject, "Couldn't reset merge", result.getErrorOutputAsHtmlString()); + GitUIUtil.notifyImportantError( + myProject, + LocalizeValue.localizeTODO("Couldn't reset merge"), + result.getErrorOutputAsHtmlValue() + ); } } // parses the output of merge conflict returning files which would be overwritten by merge. These files will be stashed. private List getFilesOverwrittenByMerge(@Nonnull List mergeOutput) { - final List paths = new ArrayList<>(); + List paths = new ArrayList<>(); for (String line : mergeOutput) { if (StringUtil.isEmptyOrSpaces(line)) { continue; @@ -205,10 +224,10 @@ private List getFilesOverwrittenByMerge(@Nonnull List mergeOut } line = line.trim(); - final String path; + String path; try { path = myRoot.getPath() + "/" + GitUtil.unescapePath(line); - final File file = new File(path); + File file = new File(path); if (file.exists()) { paths.add(VcsUtil.getFilePath(file, false)); } @@ -220,12 +239,13 @@ private List getFilesOverwrittenByMerge(@Nonnull List mergeOut } private Collection getLocalChangesFilteredByFiles(List paths) { - final Collection changes = new HashSet<>(); + Collection changes = new HashSet<>(); for (LocalChangeList list : myChangeListManager.getChangeLists()) { for (Change change : list.getChanges()) { - final ContentRevision afterRevision = change.getAfterRevision(); - final ContentRevision beforeRevision = change.getBeforeRevision(); - if ((afterRevision != null && paths.contains(afterRevision.getFile())) || (beforeRevision != null && paths.contains(beforeRevision.getFile()))) { + ContentRevision afterRevision = change.getAfterRevision(); + ContentRevision beforeRevision = change.getBeforeRevision(); + if ((afterRevision != null && paths.contains(afterRevision.getFile())) || (beforeRevision != null && paths.contains( + beforeRevision.getFile()))) { changes.add(change); } } diff --git a/plugin/src/main/java/git4idea/update/GitRebaseUpdater.java b/plugin/src/main/java/git4idea/update/GitRebaseUpdater.java index d1d37dc..7240503 100644 --- a/plugin/src/main/java/git4idea/update/GitRebaseUpdater.java +++ b/plugin/src/main/java/git4idea/update/GitRebaseUpdater.java @@ -16,8 +16,11 @@ package git4idea.update; import consulo.application.progress.ProgressIndicator; +import consulo.localize.LocalizeValue; import consulo.logging.Logger; import consulo.project.Project; +import consulo.project.ui.notification.NotificationService; +import consulo.ui.annotation.RequiredUIAccess; import consulo.util.collection.ContainerUtil; import consulo.versionControlSystem.ProjectLevelVcsManager; import consulo.versionControlSystem.VcsException; @@ -35,6 +38,7 @@ import git4idea.repo.GitRepository; import jakarta.annotation.Nonnull; + import java.util.Collection; import java.util.List; @@ -43,104 +47,104 @@ /** * Handles 'git pull --rebase' */ -public class GitRebaseUpdater extends GitUpdater -{ - private static final Logger LOG = Logger.getInstance(GitRebaseUpdater.class.getName()); - private final GitRebaser myRebaser; - private final ChangeListManager myChangeListManager; - private final ProjectLevelVcsManager myVcsManager; - - public GitRebaseUpdater(@Nonnull Project project, - @Nonnull Git git, - @Nonnull GitRepository repository, - @Nonnull GitBranchPair branchAndTracked, - @Nonnull ProgressIndicator progressIndicator, - @Nonnull UpdatedFiles updatedFiles) - { - super(project, git, repository, branchAndTracked, progressIndicator, updatedFiles); - myRebaser = new GitRebaser(myProject, git, myProgressIndicator); - myChangeListManager = ChangeListManager.getInstance(project); - myVcsManager = ProjectLevelVcsManager.getInstance(project); - } +public class GitRebaseUpdater extends GitUpdater { + private static final Logger LOG = Logger.getInstance(GitRebaseUpdater.class.getName()); + private final GitRebaser myRebaser; + private final ChangeListManager myChangeListManager; + private final ProjectLevelVcsManager myVcsManager; + @Nonnull + private final NotificationService myNotificationService; - @Override - public boolean isSaveNeeded() - { - Collection localChanges = new LocalChangesUnderRoots(myChangeListManager, myVcsManager).getChangesUnderRoots(singletonList(myRoot)).get(myRoot); - return !ContainerUtil.isEmpty(localChanges); - } + public GitRebaseUpdater( + @Nonnull Project project, + @Nonnull Git git, + @Nonnull GitRepository repository, + @Nonnull GitBranchPair branchAndTracked, + @Nonnull ProgressIndicator progressIndicator, + @Nonnull UpdatedFiles updatedFiles + ) { + super(project, git, repository, branchAndTracked, progressIndicator, updatedFiles); + myRebaser = new GitRebaser(myProject, git, myProgressIndicator); + myChangeListManager = ChangeListManager.getInstance(project); + myVcsManager = ProjectLevelVcsManager.getInstance(project); + myNotificationService = NotificationService.getInstance(); + } - @Nonnull - @Override - protected GitUpdateResult doUpdate() - { - LOG.info("doUpdate "); - String remoteBranch = getRemoteBranchToMerge(); - List params = singletonList(remoteBranch); - return myRebaser.rebase(myRoot, params, () -> cancel(), null); - } + @Override + public boolean isSaveNeeded() { + Collection localChanges = + new LocalChangesUnderRoots(myChangeListManager, myVcsManager).getChangesUnderRoots(singletonList(myRoot)).get(myRoot); + return !ContainerUtil.isEmpty(localChanges); + } - @Nonnull - private String getRemoteBranchToMerge() - { - GitBranch dest = myBranchPair.getDest(); - LOG.assertTrue(dest != null, String.format("Destination branch is null for source branch %s in %s", myBranchPair.getBranch().getName(), myRoot)); - return dest.getName(); - } + @Nonnull + @Override + protected GitUpdateResult doUpdate() { + LOG.info("doUpdate "); + String remoteBranch = getRemoteBranchToMerge(); + List params = singletonList(remoteBranch); + return myRebaser.rebase(myRoot, params, this::cancel, null); + } - public void cancel() - { - myRebaser.abortRebase(myRoot); - myProgressIndicator.setText2("Refreshing files for the root " + myRoot.getPath()); - myRoot.refresh(false, true); - } + @Nonnull + private String getRemoteBranchToMerge() { + GitBranch dest = myBranchPair.getDest(); + LOG.assertTrue( + dest != null, + String.format("Destination branch is null for source branch %s in %s", myBranchPair.getBranch().getName(), myRoot) + ); + return dest.getName(); + } - @Override - public String toString() - { - return "Rebase updater"; - } + @RequiredUIAccess + public void cancel() { + myRebaser.abortRebase(myRoot); + myProgressIndicator.setText2("Refreshing files for the root " + myRoot.getPath()); + myRoot.refresh(false, true); + } - /** - * Tries to execute {@code git merge --ff-only}. - * - * @return true, if everything is successful; false for any error (to let a usual "fair" update deal with it). - */ - public boolean fastForwardMerge() - { - LOG.info("Trying fast-forward merge for " + myRoot); - GitRepository repository = GitUtil.getRepositoryManager(myProject).getRepositoryForRoot(myRoot); - if(repository == null) - { - LOG.error("Repository is null for " + myRoot); - return false; - } - try - { - markStart(myRoot); - } - catch(VcsException e) - { - LOG.info("Couldn't mark start for repository " + myRoot, e); - return false; - } + @Override + public String toString() { + return "Rebase updater"; + } - GitCommandResult result = myGit.merge(repository, getRemoteBranchToMerge(), singletonList("--ff-only")); + /** + * Tries to execute {@code git merge --ff-only}. + * + * @return true, if everything is successful; false for any error (to let a usual "fair" update deal with it). + */ + public boolean fastForwardMerge() { + LOG.info("Trying fast-forward merge for " + myRoot); + GitRepository repository = GitUtil.getRepositoryManager(myProject).getRepositoryForRoot(myRoot); + if (repository == null) { + LOG.error("Repository is null for " + myRoot); + return false; + } + try { + markStart(myRoot); + } + catch (VcsException e) { + LOG.info("Couldn't mark start for repository " + myRoot, e); + return false; + } - try - { - markEnd(myRoot); - } - catch(VcsException e) - { - // this is not critical, and update has already happened, - // so we just notify the user about problems with collecting the updated changes. - LOG.info("Couldn't mark end for repository " + myRoot, e); - VcsNotifier.getInstance(myProject). - notifyMinorWarning("Couldn't collect the updated files info", String.format("Update of %s was successful, but we couldn't collect the updated changes because of an error", - myRoot), null); - } - return result.success(); - } + GitCommandResult result = myGit.merge(repository, getRemoteBranchToMerge(), singletonList("--ff-only")); + try { + markEnd(myRoot); + } + catch (VcsException e) { + // this is not critical, and update has already happened, + // so we just notify the user about problems with collecting the updated changes. + LOG.info("Couldn't mark end for repository " + myRoot, e); + myNotificationService.newWarn(VcsNotifier.STANDARD_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Couldn't collect the updated files info")) + .content(LocalizeValue.localizeTODO(String.format( + "Update of %s was successful, but we couldn't collect the updated changes because of an error", + myRoot + ))) + .notify(myProject); + } + return result.success(); + } } diff --git a/plugin/src/main/java/git4idea/update/GitUpdateConfigurable.java b/plugin/src/main/java/git4idea/update/GitUpdateConfigurable.java index a9523a4..16ebd85 100644 --- a/plugin/src/main/java/git4idea/update/GitUpdateConfigurable.java +++ b/plugin/src/main/java/git4idea/update/GitUpdateConfigurable.java @@ -21,6 +21,7 @@ import consulo.localize.LocalizeValue; import consulo.ui.annotation.RequiredUIAccess; import git4idea.config.GitVcsSettings; +import jakarta.annotation.Nonnull; import javax.swing.*; @@ -43,6 +44,7 @@ public GitUpdateConfigurable(GitVcsSettings settings) { /** * {@inheritDoc} */ + @Nonnull @Override public LocalizeValue getDisplayName() { return GitLocalize.updateOptionsDisplayName(); diff --git a/plugin/src/main/java/git4idea/update/GitUpdateEnvironment.java b/plugin/src/main/java/git4idea/update/GitUpdateEnvironment.java index d9f7ec9..416f614 100644 --- a/plugin/src/main/java/git4idea/update/GitUpdateEnvironment.java +++ b/plugin/src/main/java/git4idea/update/GitUpdateEnvironment.java @@ -24,6 +24,7 @@ import java.util.Collection; import java.util.Set; +import consulo.ui.annotation.RequiredUIAccess; import jakarta.annotation.Nonnull; import consulo.application.progress.ProgressIndicator; @@ -42,51 +43,56 @@ import jakarta.annotation.Nullable; -public class GitUpdateEnvironment implements UpdateEnvironment -{ - private final Project myProject; - private final GitVcsSettings mySettings; +public class GitUpdateEnvironment implements UpdateEnvironment { + private final Project myProject; + private final GitVcsSettings mySettings; - public GitUpdateEnvironment(@Nonnull Project project, @Nonnull GitVcsSettings settings) - { - myProject = project; - mySettings = settings; - } + public GitUpdateEnvironment(@Nonnull Project project, @Nonnull GitVcsSettings settings) { + myProject = project; + mySettings = settings; + } - public void fillGroups(UpdatedFiles updatedFiles) - { - //unused, there are no custom categories yet - } + @Override + public void fillGroups(UpdatedFiles updatedFiles) { + //unused, there are no custom categories yet + } - @Nonnull - public UpdateSession updateDirectories(@Nonnull FilePath[] filePaths, - UpdatedFiles updatedFiles, - ProgressIndicator progressIndicator, - @Nonnull Ref sequentialUpdatesContextRef) throws ProcessCanceledException - { - Set roots = gitRoots(Arrays.asList(filePaths)); - GitRepositoryManager repositoryManager = getRepositoryManager(myProject); - final GitUpdateProcess gitUpdateProcess = new GitUpdateProcess(myProject, progressIndicator, getRepositoriesFromRoots(repositoryManager, roots), updatedFiles, true, true); - boolean result = gitUpdateProcess.update(mySettings.getUpdateType()).isSuccess(); - return new GitUpdateSession(result); - } + @Nonnull + @Override + @RequiredUIAccess + public UpdateSession updateDirectories( + @Nonnull FilePath[] filePaths, + UpdatedFiles updatedFiles, + ProgressIndicator progressIndicator, + @Nonnull Ref sequentialUpdatesContextRef + ) throws ProcessCanceledException { + Set roots = gitRoots(Arrays.asList(filePaths)); + GitRepositoryManager repositoryManager = getRepositoryManager(myProject); + GitUpdateProcess gitUpdateProcess = new GitUpdateProcess( + myProject, + progressIndicator, + getRepositoriesFromRoots(repositoryManager, roots), + updatedFiles, + true, + true + ); + boolean result = gitUpdateProcess.update(mySettings.getUpdateType()).isSuccess(); + return new GitUpdateSession(result); + } + @Override + public boolean validateOptions(Collection filePaths) { + for (FilePath p : filePaths) { + if (!isUnderGit(p)) { + return false; + } + } + return true; + } - public boolean validateOptions(Collection filePaths) - { - for(FilePath p : filePaths) - { - if(!isUnderGit(p)) - { - return false; - } - } - return true; - } - - @Nullable - public Configurable createConfigurable(Collection files) - { - return new GitUpdateConfigurable(mySettings); - } + @Nullable + @Override + public Configurable createConfigurable(Collection files) { + return new GitUpdateConfigurable(mySettings); + } } diff --git a/plugin/src/main/java/git4idea/update/GitUpdateLocallyModifiedDialog.form b/plugin/src/main/java/git4idea/update/GitUpdateLocallyModifiedDialog.form deleted file mode 100644 index 8d9e6f2..0000000 --- a/plugin/src/main/java/git4idea/update/GitUpdateLocallyModifiedDialog.form +++ /dev/null @@ -1,71 +0,0 @@ - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/plugin/src/main/java/git4idea/update/GitUpdateLocallyModifiedDialog.java b/plugin/src/main/java/git4idea/update/GitUpdateLocallyModifiedDialog.java index 329a7b8..44abe79 100644 --- a/plugin/src/main/java/git4idea/update/GitUpdateLocallyModifiedDialog.java +++ b/plugin/src/main/java/git4idea/update/GitUpdateLocallyModifiedDialog.java @@ -15,11 +15,13 @@ */ package git4idea.update; +import com.intellij.uiDesigner.core.GridConstraints; +import com.intellij.uiDesigner.core.GridLayoutManager; import consulo.application.Application; import consulo.git.localize.GitLocalize; import consulo.project.Project; -import consulo.ui.ex.awt.DialogWrapper; -import consulo.ui.ex.awt.UIUtil; +import consulo.ui.annotation.RequiredUIAccess; +import consulo.ui.ex.awt.*; import consulo.versionControlSystem.FilePath; import consulo.versionControlSystem.VcsException; import consulo.versionControlSystem.util.VcsUtil; @@ -34,6 +36,7 @@ import javax.swing.*; import java.util.ArrayList; import java.util.List; +import java.util.ResourceBundle; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -70,12 +73,13 @@ public class GitUpdateLocallyModifiedDialog extends DialogWrapper { * @param root the vcs root * @param locallyModifiedFiles the collection of locally modified files to use */ - protected GitUpdateLocallyModifiedDialog(final Project project, final VirtualFile root, List locallyModifiedFiles) { + @RequiredUIAccess + protected GitUpdateLocallyModifiedDialog(Project project, VirtualFile root, List locallyModifiedFiles) { super(project, true); myLocallyModifiedFiles = locallyModifiedFiles; setTitle(GitLocalize.updateLocallyModifiedTitle()); myGitRoot.setText(root.getPresentableUrl()); - myFilesList.setModel(new DefaultListModel()); + myFilesList.setModel(new DefaultListModel<>()); setOKButtonText(GitLocalize.updateLocallyModifiedRevert()); syncListModel(); myRescanButton.addActionListener(e -> { @@ -96,7 +100,7 @@ protected GitUpdateLocallyModifiedDialog(final Project project, final VirtualFil */ @SuppressWarnings("unchecked") private void syncListModel() { - DefaultListModel listModel = (DefaultListModel)myFilesList.getModel(); + DefaultListModel listModel = (DefaultListModel) myFilesList.getModel(); listModel.removeAllElements(); for (String p : myLocallyModifiedFiles) { listModel.addElement(p); @@ -149,7 +153,6 @@ private static void scanFiles(Project project, VirtualFile root, List fi } } - /** * Show the dialog if needed * @@ -157,27 +160,25 @@ private static void scanFiles(Project project, VirtualFile root, List fi * @param root the vcs root * @return true if showing is not needed or operation completed successfully */ - public static boolean showIfNeeded(final Project project, final VirtualFile root) { - final ArrayList files = new ArrayList<>(); + public static boolean showIfNeeded(Project project, VirtualFile root) { + List files = new ArrayList<>(); try { scanFiles(project, root, files); - final AtomicBoolean rc = new AtomicBoolean(true); + AtomicBoolean rc = new AtomicBoolean(true); if (!files.isEmpty()) { - UIUtil.invokeAndWaitIfNeeded((Runnable)() -> { + UIUtil.invokeAndWaitIfNeeded((Runnable) () -> { GitUpdateLocallyModifiedDialog d = new GitUpdateLocallyModifiedDialog(project, root, files); d.show(); rc.set(d.isOK()); }); - if (rc.get()) { - if (!files.isEmpty()) { - revertFiles(project, root, files); - } + if (rc.get() && !files.isEmpty()) { + revertFiles(project, root, files); } } return rc.get(); } - catch (final VcsException e) { - UIUtil.invokeAndWaitIfNeeded((Runnable)() -> GitUIUtil.showOperationError(project, e, "Checking for locally modified files")); + catch (VcsException e) { + UIUtil.invokeAndWaitIfNeeded((Runnable) () -> GitUIUtil.showOperationError(project, e, "Checking for locally modified files")); return false; } } @@ -189,13 +190,211 @@ public static boolean showIfNeeded(final Project project, final VirtualFile root * @param root the vcs root * @param files the files to revert */ - private static void revertFiles(Project project, VirtualFile root, ArrayList files) throws VcsException { + private static void revertFiles(Project project, VirtualFile root, List files) throws VcsException { // TODO consider deleted files GitRollbackEnvironment rollback = GitRollbackEnvironment.getInstance(project); - ArrayList list = new ArrayList<>(files.size()); + List list = new ArrayList<>(files.size()); for (String p : files) { list.add(VcsUtil.getFilePath(p)); } rollback.revert(root, list); } + + { +// GUI initializer generated by Consulo GUI Designer +// >>> IMPORTANT!! <<< +// DO NOT EDIT OR ADD ANY CODE HERE! + $$$setupUI$$$(); + } + + /** + * Method generated by Consulo GUI Designer + * >>> IMPORTANT!! <<< + * DO NOT edit this method OR call it in your code! + */ + private void $$$setupUI$$$() { + myRootPanel = new JPanel(); + myRootPanel.setLayout(new GridLayoutManager(3, 3, JBUI.emptyInsets(), -1, -1)); + JLabel label1 = new JLabel(); + this.$$$loadLabelText$$$(label1, ResourceBundle.getBundle("git4idea/i18n/GitBundle").getString("update.locally.modified.git.root")); + myRootPanel.add( + label1, + new GridConstraints( + 0, + 0, + 1, + 1, + GridConstraints.ANCHOR_WEST, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 0, + false + ) + ); + myGitRoot = new JLabel(); + myGitRoot.setText(""); + myRootPanel.add( + myGitRoot, + new GridConstraints( + 0, + 1, + 1, + 2, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_FIXED, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 0, + false + ) + ); + myDescriptionLabel = new JLabel(); + myDescriptionLabel.setText(""); + myRootPanel.add( + myDescriptionLabel, + new GridConstraints( + 1, + 0, + 1, + 3, + GridConstraints.ANCHOR_WEST, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 0, + false + ) + ); + JBScrollPane jBScrollPane1 = new JBScrollPane(); + myRootPanel.add( + jBScrollPane1, + new GridConstraints( + 2, + 1, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, + null, + null, + null, + 0, + false + ) + ); + myFilesList = new JBList<>(); + myFilesList.setToolTipText(ResourceBundle.getBundle("git4idea/i18n/GitBundle").getString("update.locally.modified.files.tooltip")); + jBScrollPane1.setViewportView(myFilesList); + myRescanButton = new JButton(); + this.$$$loadButtonText$$$(myRescanButton, GitLocalize.updateLocallyModifiedRescan().get()); + myRescanButton.setToolTipText(GitLocalize.updateLocallyModifiedRescanTooltip().get()); + myRootPanel.add( + myRescanButton, + new GridConstraints( + 2, + 2, + 1, + 1, + GridConstraints.ANCHOR_NORTH, + GridConstraints.FILL_HORIZONTAL, + GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 0, + false + ) + ); + JLabel label2 = new JLabel(); + this.$$$loadLabelText$$$(label2, GitLocalize.updateLocallyModifiedFiles().get()); + label2.setVerticalAlignment(0); + myRootPanel.add( + label2, + new GridConstraints( + 2, + 0, + 1, + 1, + GridConstraints.ANCHOR_NORTHWEST, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 0, + false + ) + ); + label2.setLabelFor(jBScrollPane1); + } + + private void $$$loadLabelText$$$(JLabel component, String text) { + StringBuilder result = new StringBuilder(); + boolean haveMnemonic = false; + char mnemonic = '\0'; + int mnemonicIndex = -1; + for (int i = 0; i < text.length(); i++) { + if (text.charAt(i) == '&') { + i++; + if (i == text.length()) { + break; + } + if (!haveMnemonic && text.charAt(i) != '&') { + haveMnemonic = true; + mnemonic = text.charAt(i); + mnemonicIndex = result.length(); + } + } + result.append(text.charAt(i)); + } + component.setText(result.toString()); + if (haveMnemonic) { + component.setDisplayedMnemonic(mnemonic); + component.setDisplayedMnemonicIndex(mnemonicIndex); + } + } + + private void $$$loadButtonText$$$(AbstractButton component, String text) { + StringBuilder result = new StringBuilder(); + boolean haveMnemonic = false; + char mnemonic = '\0'; + int mnemonicIndex = -1; + for (int i = 0; i < text.length(); i++) { + if (text.charAt(i) == '&') { + i++; + if (i == text.length()) { + break; + } + if (!haveMnemonic && text.charAt(i) != '&') { + haveMnemonic = true; + mnemonic = text.charAt(i); + mnemonicIndex = result.length(); + } + } + result.append(text.charAt(i)); + } + component.setText(result.toString()); + if (haveMnemonic) { + component.setMnemonic(mnemonic); + component.setDisplayedMnemonicIndex(mnemonicIndex); + } + } + + public JComponent $$$getRootComponent$$$() { + return myRootPanel; + } } diff --git a/plugin/src/main/java/git4idea/update/GitUpdateOptionsPanel.java b/plugin/src/main/java/git4idea/update/GitUpdateOptionsPanel.java index ada1fb8..b5ec492 100644 --- a/plugin/src/main/java/git4idea/update/GitUpdateOptionsPanel.java +++ b/plugin/src/main/java/git4idea/update/GitUpdateOptionsPanel.java @@ -15,24 +15,19 @@ */ package git4idea.update; -import com.intellij.uiDesigner.core.GridConstraints; -import com.intellij.uiDesigner.core.GridLayoutManager; import consulo.application.Application; import consulo.git.localize.GitLocalize; import consulo.ui.RadioButton; -import consulo.ui.StaticPosition; import consulo.ui.ValueGroups; import consulo.ui.annotation.RequiredUIAccess; import consulo.ui.ex.awtUnsafe.TargetAWT; -import consulo.ui.layout.*; +import consulo.ui.layout.LabeledLayout; +import consulo.ui.layout.VerticalLayout; import git4idea.config.GitVcsSettings; import git4idea.config.UpdateMethod; -import git4idea.i18n.GitBundle; import jakarta.annotation.Nonnull; import javax.swing.*; -import java.awt.*; -import java.util.ResourceBundle; /** * Update options panel @@ -64,22 +59,27 @@ public GitUpdateOptionsPanel() { myStashRadioButton.setToolTipText(GitLocalize.updateOptionsSaveStashTooltip()); myShelveRadioButton.setValue(false); - VerticalLayout leftGroup = VerticalLayout.create(); - leftGroup.add(myForceMergeRadioButton); - leftGroup.add(myForceRebaseRadioButton); - leftGroup.add(myBranchDefaultRadioButton); + VerticalLayout leftGroup = VerticalLayout.create() + .add(myForceMergeRadioButton) + .add(myForceRebaseRadioButton) + .add(myBranchDefaultRadioButton); myPanel.add(LabeledLayout.create(GitLocalize.updateOptionsType(), leftGroup)); - VerticalLayout rightGroup = VerticalLayout.create(); - rightGroup.add(myStashRadioButton); - rightGroup.add(myShelveRadioButton); + VerticalLayout rightGroup = VerticalLayout.create() + .add(myStashRadioButton) + .add(myShelveRadioButton); myPanel.add(LabeledLayout.create(GitLocalize.updateOptionsSaveBeforeUpdate(), rightGroup)); - ValueGroups.boolGroup().add(myBranchDefaultRadioButton).add(myForceRebaseRadioButton).add(myForceMergeRadioButton); + ValueGroups.boolGroup() + .add(myBranchDefaultRadioButton) + .add(myForceRebaseRadioButton) + .add(myForceMergeRadioButton); - ValueGroups.boolGroup().add(myStashRadioButton).add(myShelveRadioButton); + ValueGroups.boolGroup() + .add(myStashRadioButton) + .add(myShelveRadioButton); } @Nonnull @@ -106,18 +106,17 @@ private GitVcsSettings.UpdateChangesPolicy updateSaveFilesPolicy() { */ @RequiredUIAccess private UpdateMethod getUpdateType() { - UpdateMethod type = null; if (myForceRebaseRadioButton.getValueOrError()) { - type = UpdateMethod.REBASE; + return UpdateMethod.REBASE; } else if (myForceMergeRadioButton.getValueOrError()) { - type = UpdateMethod.MERGE; + return UpdateMethod.MERGE; } else if (myBranchDefaultRadioButton.getValueOrError()) { - type = UpdateMethod.BRANCH_DEFAULT; + return UpdateMethod.BRANCH_DEFAULT; } - assert type != null; - return type; + assert false; + return null; } /** @@ -135,17 +134,9 @@ public void applyTo(GitVcsSettings settings) { @RequiredUIAccess public void updateFrom(GitVcsSettings settings) { switch (settings.getUpdateType()) { - case REBASE: - myForceRebaseRadioButton.setValue(true); - break; - case MERGE: - myForceMergeRadioButton.setValue(true); - break; - case BRANCH_DEFAULT: - myBranchDefaultRadioButton.setValue(true); - break; - default: - assert false : "Unknown value of update type: " + settings.getUpdateType(); + case REBASE -> myForceRebaseRadioButton.setValue(true); + case MERGE -> myForceMergeRadioButton.setValue(true); + case BRANCH_DEFAULT -> myBranchDefaultRadioButton.setValue(true); } UpdatePolicyUtils.updatePolicyItem(settings.updateChangesPolicy(), myStashRadioButton, myShelveRadioButton); } diff --git a/plugin/src/main/java/git4idea/update/GitUpdateProcess.java b/plugin/src/main/java/git4idea/update/GitUpdateProcess.java index 4ca33c3..e1eddc0 100644 --- a/plugin/src/main/java/git4idea/update/GitUpdateProcess.java +++ b/plugin/src/main/java/git4idea/update/GitUpdateProcess.java @@ -18,11 +18,13 @@ import consulo.application.AccessToken; import consulo.application.progress.EmptyProgressIndicator; import consulo.application.progress.ProgressIndicator; +import consulo.localize.LocalizeValue; import consulo.logging.Logger; import consulo.project.Project; +import consulo.ui.annotation.RequiredUIAccess; import consulo.util.collection.ContainerUtil; import consulo.util.lang.ObjectUtil; -import consulo.util.lang.ref.Ref; +import consulo.util.lang.ref.SimpleReference; import consulo.versionControlSystem.ProjectLevelVcsManager; import consulo.versionControlSystem.VcsException; import consulo.versionControlSystem.base.LocalChangesUnderRoots; @@ -46,13 +48,10 @@ import git4idea.repo.GitBranchTrackInfo; import git4idea.repo.GitRepository; import git4idea.util.GitPreservingProcess; - import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; + +import java.util.*; import static consulo.versionControlSystem.distributed.DvcsUtil.getShortRepositoryName; import static git4idea.GitUtil.getRootsFromRepositories; @@ -65,385 +64,417 @@ * @author Kirill Likhodedov */ public class GitUpdateProcess { - private static final Logger LOG = Logger.getInstance(GitUpdateProcess.class); - - @Nonnull - private final Project myProject; - @Nonnull - private final Git myGit; - @Nonnull - private final ProjectLevelVcsManager myVcsManager; - @Nonnull - private final ChangeListManager myChangeListManager; - - @Nonnull - private final List myRepositories; - private final boolean myCheckRebaseOverMergeProblem; - private final boolean myCheckForTrackedBranchExistence; - private final UpdatedFiles myUpdatedFiles; - @Nonnull - private final ProgressIndicator myProgressIndicator; - @Nonnull - private final GitMerger myMerger; - - public GitUpdateProcess(@Nonnull Project project, - @Nullable ProgressIndicator progressIndicator, - @Nonnull Collection repositories, - @Nonnull UpdatedFiles updatedFiles, - boolean checkRebaseOverMergeProblem, - boolean checkForTrackedBranchExistence) { - myProject = project; - myCheckRebaseOverMergeProblem = checkRebaseOverMergeProblem; - myCheckForTrackedBranchExistence = checkForTrackedBranchExistence; - myGit = Git.getInstance(); - myChangeListManager = ChangeListManager.getInstance(project); - myVcsManager = ProjectLevelVcsManager.getInstance(project); - myUpdatedFiles = updatedFiles; - - myRepositories = GitUtil.getRepositoryManager(project).sortByDependency(repositories); - myProgressIndicator = progressIndicator == null ? new EmptyProgressIndicator() : progressIndicator; - myMerger = new GitMerger(myProject); - } - - /** - * Checks if update is possible, saves local changes and updates all roots. - * In case of error shows notification and returns false. If update completes without errors, returns true. - *

- * Perform update on all roots. - * 0. Blocks reloading project on external change, saving/syncing on frame deactivation. - * 1. Checks if update is possible (rebase/merge in progress, no tracked branches...) and provides merge dialog to solve problems. - * 2. Finds updaters to use (merge or rebase). - * 3. Preserves local changes if needed (not needed for merge sometimes). - * 4. Updates via 'git pull' or equivalent. - * 5. Restores local changes if update completed or failed with error. If update is incomplete, i.e. some unmerged files remain, - * local changes are not restored. - */ - @Nonnull - public GitUpdateResult update(final UpdateMethod updateMethod) { - LOG.info("update started|" + updateMethod); - String oldText = myProgressIndicator.getText(); - myProgressIndicator.setText("Updating..."); - - for (GitRepository repository : myRepositories) { - repository.update(); + private static final Logger LOG = Logger.getInstance(GitUpdateProcess.class); + + @Nonnull + private final Project myProject; + @Nonnull + private final Git myGit; + @Nonnull + private final ProjectLevelVcsManager myVcsManager; + @Nonnull + private final ChangeListManager myChangeListManager; + + @Nonnull + private final List myRepositories; + private final boolean myCheckRebaseOverMergeProblem; + private final boolean myCheckForTrackedBranchExistence; + private final UpdatedFiles myUpdatedFiles; + @Nonnull + private final ProgressIndicator myProgressIndicator; + @Nonnull + private final GitMerger myMerger; + + public GitUpdateProcess( + @Nonnull Project project, + @Nullable ProgressIndicator progressIndicator, + @Nonnull Collection repositories, + @Nonnull UpdatedFiles updatedFiles, + boolean checkRebaseOverMergeProblem, + boolean checkForTrackedBranchExistence + ) { + myProject = project; + myCheckRebaseOverMergeProblem = checkRebaseOverMergeProblem; + myCheckForTrackedBranchExistence = checkForTrackedBranchExistence; + myGit = Git.getInstance(); + myChangeListManager = ChangeListManager.getInstance(project); + myVcsManager = ProjectLevelVcsManager.getInstance(project); + myUpdatedFiles = updatedFiles; + + myRepositories = GitUtil.getRepositoryManager(project).sortByDependency(repositories); + myProgressIndicator = progressIndicator == null ? new EmptyProgressIndicator() : progressIndicator; + myMerger = new GitMerger(myProject); } - // check if update is possible - if (checkRebaseInProgress() || isMergeInProgress() || areUnmergedFiles()) { - return GitUpdateResult.NOT_READY; - } - if (checkTrackedBranchesConfiguration() == null) { - return GitUpdateResult.NOT_READY; - } + /** + * Checks if update is possible, saves local changes and updates all roots. + * In case of error shows notification and returns false. If update completes without errors, returns true. + *

+ * Perform update on all roots. + * 0. Blocks reloading project on external change, saving/syncing on frame deactivation. + * 1. Checks if update is possible (rebase/merge in progress, no tracked branches...) and provides merge dialog to solve problems. + * 2. Finds updaters to use (merge or rebase). + * 3. Preserves local changes if needed (not needed for merge sometimes). + * 4. Updates via 'git pull' or equivalent. + * 5. Restores local changes if update completed or failed with error. If update is incomplete, i.e. some unmerged files remain, + * local changes are not restored. + */ + @Nonnull + @RequiredUIAccess + public GitUpdateResult update(UpdateMethod updateMethod) { + LOG.info("update started|" + updateMethod); + String oldText = myProgressIndicator.getText(); + myProgressIndicator.setText("Updating..."); + + for (GitRepository repository : myRepositories) { + repository.update(); + } - if (!fetchAndNotify()) { - return GitUpdateResult.NOT_READY; - } + // check if update is possible + if (checkRebaseInProgress() || isMergeInProgress() || areUnmergedFiles()) { + return GitUpdateResult.NOT_READY; + } + if (checkTrackedBranchesConfiguration() == null) { + return GitUpdateResult.NOT_READY; + } - GitUpdateResult result; - try (AccessToken ignored = DvcsUtil.workingTreeChangeStarted(myProject, "VCS Update")) { - result = updateImpl(updateMethod); - } - myProgressIndicator.setText(oldText); - return result; - } - - @Nonnull - private GitUpdateResult updateImpl(@Nonnull UpdateMethod updateMethod) { - Map trackedBranches = checkTrackedBranchesConfiguration(); - if (trackedBranches == null) { - return GitUpdateResult.NOT_READY; - } + if (!fetchAndNotify()) { + return GitUpdateResult.NOT_READY; + } - Map updaters; - try { - updaters = defineUpdaters(updateMethod, trackedBranches); - } - catch (VcsException e) { - LOG.info(e); - notifyError(myProject, "Git update failed", e.getMessage(), true, e); - return GitUpdateResult.ERROR; + GitUpdateResult result; + try (AccessToken ignored = DvcsUtil.workingTreeChangeStarted(myProject, "VCS Update")) { + result = updateImpl(updateMethod); + } + myProgressIndicator.setText(oldText); + return result; } - if (updaters.isEmpty()) { - return GitUpdateResult.NOTHING_TO_UPDATE; + @Nonnull + private GitUpdateResult updateImpl(@Nonnull UpdateMethod updateMethod) { + Map trackedBranches = checkTrackedBranchesConfiguration(); + if (trackedBranches == null) { + return GitUpdateResult.NOT_READY; + } + + Map updaters; + try { + updaters = defineUpdaters(updateMethod, trackedBranches); + } + catch (VcsException e) { + LOG.info(e); + notifyError(myProject, LocalizeValue.localizeTODO("Git update failed"), LocalizeValue.ofNullable(e.getMessage()), true, e); + return GitUpdateResult.ERROR; + } + + if (updaters.isEmpty()) { + return GitUpdateResult.NOTHING_TO_UPDATE; + } + + updaters = tryFastForwardMergeForRebaseUpdaters(updaters); + + if (updaters.isEmpty()) { + // everything was updated via the fast-forward merge + return GitUpdateResult.SUCCESS; + } + + if (myCheckRebaseOverMergeProblem) { + Collection problematicRoots = findRootsRebasingOverMerge(updaters); + if (!problematicRoots.isEmpty()) { + GitRebaseOverMergeProblem.Decision decision = GitRebaseOverMergeProblem.showDialog(); + if (decision == GitRebaseOverMergeProblem.Decision.MERGE_INSTEAD) { + for (GitRepository repo : problematicRoots) { + VirtualFile root = repo.getRoot(); + GitBranchPair branchAndTracked = trackedBranches.get(root); + if (branchAndTracked == null) { + LOG.error("No tracked branch information for root " + root); + continue; + } + updaters.put( + repo, + new GitMergeUpdater(myProject, myGit, repo, branchAndTracked, myProgressIndicator, myUpdatedFiles) + ); + } + } + else if (decision == GitRebaseOverMergeProblem.Decision.CANCEL_OPERATION) { + return GitUpdateResult.CANCEL; + } + } + } + + // save local changes if needed (update via merge may perform without saving). + Collection myRootsToSave = new ArrayList<>(); + LOG.info("updateImpl: identifying if save is needed..."); + for (Map.Entry entry : updaters.entrySet()) { + GitRepository repo = entry.getKey(); + GitUpdater updater = entry.getValue(); + if (updater.isSaveNeeded()) { + myRootsToSave.add(repo.getRoot()); + LOG.info("update| root " + repo + " needs save"); + } + } + + LOG.info("updateImpl: saving local changes..."); + SimpleReference incomplete = SimpleReference.create(false); + SimpleReference compoundResult = SimpleReference.create(); + Map finalUpdaters = updaters; + new GitPreservingProcess( + myProject, + myGit, + myRootsToSave, + "Update", + "Remote", + GitVcsSettings.getInstance(myProject).updateChangesPolicy(), + myProgressIndicator, + () -> { + LOG.info("updateImpl: updating..."); + GitRepository currentlyUpdatedRoot = null; + try { + for (GitRepository repo : myRepositories) { + GitUpdater updater = finalUpdaters.get(repo); + if (updater == null) { + continue; + } + currentlyUpdatedRoot = repo; + GitUpdateResult res = updater.update(); + LOG.info("updating root " + currentlyUpdatedRoot + " finished: " + res); + if (res == GitUpdateResult.INCOMPLETE) { + incomplete.set(true); + } + compoundResult.set(joinResults(compoundResult.get(), res)); + } + } + catch (VcsException e) { + String rootName = (currentlyUpdatedRoot == null) ? "" : getShortRepositoryName(currentlyUpdatedRoot); + LOG.info("Error updating changes for root " + currentlyUpdatedRoot, e); + notifyImportantError( + myProject, + LocalizeValue.localizeTODO("Error updating " + rootName), + LocalizeValue.localizeTODO("Updating " + rootName + " failed with an error: " + e.getLocalizedMessage()) + ); + } + } + ).execute(() -> { + // Note: compoundResult normally should not be null, because the updaters map was checked for non-emptiness. + // But if updater.update() fails with exception for the first root, then the value would not be assigned. + // In this case we don't restore local changes either, because update failed. + return !incomplete.get() && !compoundResult.isNull() && compoundResult.get().isSuccess(); + }); + // GitPreservingProcess#save may fail due index.lock presence + return ObjectUtil.notNull(compoundResult.get(), GitUpdateResult.ERROR); } - updaters = tryFastForwardMergeForRebaseUpdaters(updaters); + @Nonnull + private Collection findRootsRebasingOverMerge(@Nonnull Map updaters) { + return ContainerUtil.mapNotNull( + updaters.keySet(), + repo -> { + GitUpdater updater = updaters.get(repo); + if (updater instanceof GitRebaseUpdater) { + String currentRef = updater.getSourceAndTarget().getBranch().getFullName(); + String baseRef = ObjectUtil.assertNotNull(updater.getSourceAndTarget().getDest()).getFullName(); + return GitRebaseOverMergeProblem.hasProblem(myProject, repo.getRoot(), baseRef, currentRef) ? repo : null; + } + return null; + } + ); + } - if (updaters.isEmpty()) { - // everything was updated via the fast-forward merge - return GitUpdateResult.SUCCESS; + @Nonnull + private Map tryFastForwardMergeForRebaseUpdaters(@Nonnull Map updaters) { + Map modifiedUpdaters = new HashMap<>(); + Map> changesUnderRoots = + new LocalChangesUnderRoots(myChangeListManager, myVcsManager).getChangesUnderRoots(getRootsFromRepositories(updaters.keySet())); + for (GitRepository repository : myRepositories) { + GitUpdater updater = updaters.get(repository); + if (updater == null) { + continue; + } + Collection changes = changesUnderRoots.get(repository.getRoot()); + LOG.debug("Changes under root '" + getShortRepositoryName(repository) + "': " + changes); + // check only if there are local changes, otherwise stash won't happen anyway and there would be no optimization + if (updater instanceof GitRebaseUpdater rebaseUpdater + && changes != null && !changes.isEmpty() + && rebaseUpdater.fastForwardMerge()) { + continue; + } + modifiedUpdaters.put(repository, updater); + } + return modifiedUpdaters; } - if (myCheckRebaseOverMergeProblem) { - Collection problematicRoots = findRootsRebasingOverMerge(updaters); - if (!problematicRoots.isEmpty()) { - GitRebaseOverMergeProblem.Decision decision = GitRebaseOverMergeProblem.showDialog(); - if (decision == GitRebaseOverMergeProblem.Decision.MERGE_INSTEAD) { - for (GitRepository repo : problematicRoots) { - VirtualFile root = repo.getRoot(); + @Nonnull + private Map defineUpdaters( + @Nonnull UpdateMethod updateMethod, + @Nonnull Map trackedBranches + ) throws VcsException { + Map updaters = new HashMap<>(); + LOG.info("updateImpl: defining updaters..."); + for (GitRepository repository : myRepositories) { + VirtualFile root = repository.getRoot(); GitBranchPair branchAndTracked = trackedBranches.get(root); if (branchAndTracked == null) { - LOG.error("No tracked branch information for root " + root); - continue; + continue; + } + GitUpdater updater = + GitUpdater.getUpdater(myProject, myGit, branchAndTracked, repository, myProgressIndicator, myUpdatedFiles, updateMethod); + if (updater.isUpdateNeeded()) { + updaters.put(repository, updater); } - updaters.put(repo, new GitMergeUpdater(myProject, myGit, repo, branchAndTracked, myProgressIndicator, myUpdatedFiles)); - } + LOG.info("update| root=" + root + " ,updater=" + updater); } - else if (decision == GitRebaseOverMergeProblem.Decision.CANCEL_OPERATION) { - return GitUpdateResult.CANCEL; + return updaters; + } + + @Nonnull + private static GitUpdateResult joinResults(@Nullable GitUpdateResult compoundResult, GitUpdateResult result) { + if (compoundResult == null) { + return result; } - } + return compoundResult.join(result); } - // save local changes if needed (update via merge may perform without saving). - final Collection myRootsToSave = ContainerUtil.newArrayList(); - LOG.info("updateImpl: identifying if save is needed..."); - for (Map.Entry entry : updaters.entrySet()) { - GitRepository repo = entry.getKey(); - GitUpdater updater = entry.getValue(); - if (updater.isSaveNeeded()) { - myRootsToSave.add(repo.getRoot()); - LOG.info("update| root " + repo + " needs save"); - } + // fetch all roots. If an error happens, return false and notify about errors. + @RequiredUIAccess + private boolean fetchAndNotify() { + return new GitFetcher(myProject, myProgressIndicator, false).fetchRootsAndNotify(myRepositories, "Update failed", false); } - LOG.info("updateImpl: saving local changes..."); - final Ref incomplete = Ref.create(false); - final Ref compoundResult = Ref.create(); - final Map finalUpdaters = updaters; - new GitPreservingProcess(myProject, - myGit, - myRootsToSave, - "Update", - "Remote", - GitVcsSettings.getInstance(myProject).updateChangesPolicy(), - myProgressIndicator, - () -> - { - LOG.info("updateImpl: updating..."); - GitRepository currentlyUpdatedRoot = null; - try { - for (GitRepository repo : myRepositories) { - GitUpdater updater = finalUpdaters.get(repo); - if (updater == null) { - continue; - } - currentlyUpdatedRoot = repo; - GitUpdateResult res = updater.update(); - LOG.info("updating root " + currentlyUpdatedRoot + " finished: " + res); - if (res == GitUpdateResult.INCOMPLETE) { - incomplete.set(true); - } - compoundResult.set(joinResults(compoundResult.get(), res)); - } - } - catch (VcsException e) { - String rootName = (currentlyUpdatedRoot == null) ? "" : getShortRepositoryName(currentlyUpdatedRoot); - LOG.info("Error updating changes for root " + currentlyUpdatedRoot, e); - notifyImportantError(myProject, - "Error updating " + rootName, - "Updating " + rootName + " failed with an error: " + e.getLocalizedMessage()); - } - }).execute(() -> - { - // Note: compoundResult normally should not be null, because the updaters map was checked for non-emptiness. - // But if updater.update() fails with exception for the first root, then the value would not be assigned. - // In this case we don't restore local changes either, because update failed. - return !incomplete.get() && !compoundResult.isNull() && compoundResult.get().isSuccess(); - }); - // GitPreservingProcess#save may fail due index.lock presence - return ObjectUtil.notNull(compoundResult.get(), GitUpdateResult.ERROR); - } - - @Nonnull - private Collection findRootsRebasingOverMerge(@Nonnull Map updaters) { - return ContainerUtil.mapNotNull(updaters.keySet(), repo -> - { - GitUpdater updater = updaters.get(repo); - if (updater instanceof GitRebaseUpdater) { - String currentRef = updater.getSourceAndTarget().getBranch().getFullName(); - String baseRef = ObjectUtil.assertNotNull(updater.getSourceAndTarget().getDest()).getFullName(); - return GitRebaseOverMergeProblem.hasProblem(myProject, repo.getRoot(), baseRef, currentRef) ? repo : null; - } - return null; - }); - } - - @Nonnull - private Map tryFastForwardMergeForRebaseUpdaters(@Nonnull Map updaters) { - Map modifiedUpdaters = new HashMap<>(); - Map> changesUnderRoots = - new LocalChangesUnderRoots(myChangeListManager, myVcsManager).getChangesUnderRoots(getRootsFromRepositories(updaters.keySet())); - for (GitRepository repository : myRepositories) { - GitUpdater updater = updaters.get(repository); - if (updater == null) { - continue; - } - Collection changes = changesUnderRoots.get(repository.getRoot()); - LOG.debug("Changes under root '" + getShortRepositoryName(repository) + "': " + changes); - if (updater instanceof GitRebaseUpdater && changes != null && !changes.isEmpty()) { - // check only if there are local changes, otherwise stash won't happen anyway and there would be no optimization - GitRebaseUpdater rebaseUpdater = (GitRebaseUpdater)updater; - if (rebaseUpdater.fastForwardMerge()) { - continue; + /** + * For each root check that the repository is on branch, and this branch is tracking a remote branch, and the remote branch exists. + * If it is not true for at least one of roots, notify and return null. + * If branch configuration is OK for all roots, return the collected tracking branch information. + */ + @Nullable + private Map checkTrackedBranchesConfiguration() { + Map trackedBranches = new HashMap<>(); + LOG.info("checking tracked branch configuration..."); + for (GitRepository repository : myRepositories) { + VirtualFile root = repository.getRoot(); + GitLocalBranch branch = repository.getCurrentBranch(); + if (branch == null) { + LOG.info("checkTrackedBranchesConfigured: current branch is null in " + repository); + notifyImportantError( + myProject, + LocalizeValue.localizeTODO("Can't update: no current branch"), + LocalizeValue.localizeTODO( + "You are in 'detached HEAD' state, which means that you're not on any branch" + mention(repository) + "
" + + "Checkout a branch to make update possible." + ) + ); + return null; + } + GitBranchTrackInfo trackInfo = GitBranchUtil.getTrackInfoForBranch(repository, branch); + if (trackInfo == null) { + LOG.info(String.format("checkTrackedBranchesConfigured: no track info for current branch %s in %s", branch, repository)); + if (myCheckForTrackedBranchExistence) { + notifyImportantError( + repository.getProject(), + LocalizeValue.localizeTODO("Can't Update"), + getNoTrackedBranchError(repository, branch.getName()) + ); + return null; + } + } + else { + trackedBranches.put(root, new GitBranchPair(branch, trackInfo.getRemoteBranch())); + } } - } - modifiedUpdaters.put(repository, updater); + return trackedBranches; } - return modifiedUpdaters; - } - - @Nonnull - private Map defineUpdaters(@Nonnull UpdateMethod updateMethod, - @Nonnull Map trackedBranches) throws VcsException { - final Map updaters = new HashMap<>(); - LOG.info("updateImpl: defining updaters..."); - for (GitRepository repository : myRepositories) { - VirtualFile root = repository.getRoot(); - GitBranchPair branchAndTracked = trackedBranches.get(root); - if (branchAndTracked == null) { - continue; - } - GitUpdater updater = - GitUpdater.getUpdater(myProject, myGit, branchAndTracked, repository, myProgressIndicator, myUpdatedFiles, updateMethod); - if (updater.isUpdateNeeded()) { - updaters.put(repository, updater); - } - LOG.info("update| root=" + root + " ,updater=" + updater); + + @Nonnull + static LocalizeValue getNoTrackedBranchError(@Nonnull GitRepository repository, @Nonnull String branchName) { + String recommendedCommand = recommendSetupTrackingCommand(repository, branchName); + return LocalizeValue.localizeTODO( + "No tracked branch configured for branch " + code(branchName) + mention(repository) + " or the branch doesn't exist.
" + + "To make your branch track a remote branch call, for example,
" + "" + recommendedCommand + "" + ); } - return updaters; - } - @Nonnull - private static GitUpdateResult joinResults(@Nullable GitUpdateResult compoundResult, GitUpdateResult result) { - if (compoundResult == null) { - return result; + @Nonnull + private static String recommendSetupTrackingCommand(@Nonnull GitRepository repository, @Nonnull String branchName) { + return String.format( + GitVersionSpecialty.KNOWS_SET_UPSTREAM_TO.existsIn(repository.getVcs().getVersion()) + ? "git branch --set-upstream-to=origin/%1$s %1$s" + : "git branch --set-upstream %1$s origin/%1$s", + branchName + ); } - return compoundResult.join(result); - } - - // fetch all roots. If an error happens, return false and notify about errors. - private boolean fetchAndNotify() { - return new GitFetcher(myProject, myProgressIndicator, false).fetchRootsAndNotify(myRepositories, "Update failed", false); - } - - /** - * For each root check that the repository is on branch, and this branch is tracking a remote branch, and the remote branch exists. - * If it is not true for at least one of roots, notify and return null. - * If branch configuration is OK for all roots, return the collected tracking branch information. - */ - @Nullable - private Map checkTrackedBranchesConfiguration() { - Map trackedBranches = new HashMap<>(); - LOG.info("checking tracked branch configuration..."); - for (GitRepository repository : myRepositories) { - VirtualFile root = repository.getRoot(); - final GitLocalBranch branch = repository.getCurrentBranch(); - if (branch == null) { - LOG.info("checkTrackedBranchesConfigured: current branch is null in " + repository); - notifyImportantError(myProject, - "Can't update: no current branch", - "You are in 'detached HEAD' state, which means that you're not on any branch" + mention(repository) + "
" + - "Checkout a branch to make update possible."); - return null; - } - GitBranchTrackInfo trackInfo = GitBranchUtil.getTrackInfoForBranch(repository, branch); - if (trackInfo == null) { - LOG.info(String.format("checkTrackedBranchesConfigured: no track info for current branch %s in %s", branch, repository)); - if (myCheckForTrackedBranchExistence) { - notifyImportantError(repository.getProject(), "Can't Update", getNoTrackedBranchError(repository, branch.getName())); - return null; + + /** + * Check if merge is in progress, propose to resolve conflicts. + * + * @return true if merge is in progress, which means that update can't continue. + */ + private boolean isMergeInProgress() { + LOG.info("isMergeInProgress: checking if there is an unfinished merge process..."); + Collection mergingRoots = myMerger.getMergingRoots(); + if (mergingRoots.isEmpty()) { + return false; } - } - else { - trackedBranches.put(root, new GitBranchPair(branch, trackInfo.getRemoteBranch())); - } + LOG.info("isMergeInProgress: roots with unfinished merge: " + mergingRoots); + GitConflictResolver.Params params = new GitConflictResolver.Params(); + params.setErrorNotificationTitle("Can't update"); + params.setMergeDescription("You have unfinished merge. These conflicts must be resolved before update."); + return !new GitMergeCommittingConflictResolver(myProject, myGit, myMerger, mergingRoots, params, false).merge(); } - return trackedBranches; - } - - @Nonnull - static String getNoTrackedBranchError(@Nonnull GitRepository repository, @Nonnull String branchName) { - String recommendedCommand = recommendSetupTrackingCommand(repository, branchName); - return "No tracked branch configured for branch " + code(branchName) + mention(repository) + " or the branch doesn't exist.
" + "To make your branch track a remote branch call, for " + - "example,
" + "" + recommendedCommand + ""; - } - - @Nonnull - private static String recommendSetupTrackingCommand(@Nonnull GitRepository repository, @Nonnull String branchName) { - return String.format(GitVersionSpecialty.KNOWS_SET_UPSTREAM_TO.existsIn(repository.getVcs() - .getVersion()) ? "git branch --set-upstream-to=origin/%1$s %1$s" : "git branch --set-upstream %1$s " + - "origin/%1$s", branchName); - } - - /** - * Check if merge is in progress, propose to resolve conflicts. - * - * @return true if merge is in progress, which means that update can't continue. - */ - private boolean isMergeInProgress() { - LOG.info("isMergeInProgress: checking if there is an unfinished merge process..."); - final Collection mergingRoots = myMerger.getMergingRoots(); - if (mergingRoots.isEmpty()) { - return false; + + /** + * Checks if there are unmerged files (which may still be possible even if rebase or merge have finished) + * + * @return true if there are unmerged files at + */ + @RequiredUIAccess + private boolean areUnmergedFiles() { + LOG.info("areUnmergedFiles: checking if there are unmerged files..."); + GitConflictResolver.Params params = new GitConflictResolver.Params(); + params.setErrorNotificationTitle("Update was not started"); + params.setMergeDescription("Unmerged files detected. These conflicts must be resolved before update."); + return !new GitMergeCommittingConflictResolver( + myProject, + myGit, + myMerger, + getRootsFromRepositories(myRepositories), + params, + false + ).merge(); } - LOG.info("isMergeInProgress: roots with unfinished merge: " + mergingRoots); - GitConflictResolver.Params params = new GitConflictResolver.Params(); - params.setErrorNotificationTitle("Can't update"); - params.setMergeDescription("You have unfinished merge. These conflicts must be resolved before update."); - return !new GitMergeCommittingConflictResolver(myProject, myGit, myMerger, mergingRoots, params, false).merge(); - } - - /** - * Checks if there are unmerged files (which may still be possible even if rebase or merge have finished) - * - * @return true if there are unmerged files at - */ - private boolean areUnmergedFiles() { - LOG.info("areUnmergedFiles: checking if there are unmerged files..."); - GitConflictResolver.Params params = new GitConflictResolver.Params(); - params.setErrorNotificationTitle("Update was not started"); - params.setMergeDescription("Unmerged files detected. These conflicts must be resolved before update."); - return !new GitMergeCommittingConflictResolver(myProject, - myGit, - myMerger, - getRootsFromRepositories(myRepositories), - params, - false).merge(); - } - - /** - * Check if rebase is in progress, propose to resolve conflicts. - * - * @return true if rebase is in progress, which means that update can't continue. - */ - private boolean checkRebaseInProgress() { - LOG.info("checkRebaseInProgress: checking if there is an unfinished rebase process..."); - final GitRebaser rebaser = new GitRebaser(myProject, myGit, myProgressIndicator); - final Collection rebasingRoots = rebaser.getRebasingRoots(); - if (rebasingRoots.isEmpty()) { - return false; + + /** + * Check if rebase is in progress, propose to resolve conflicts. + * + * @return true if rebase is in progress, which means that update can't continue. + */ + @RequiredUIAccess + private boolean checkRebaseInProgress() { + LOG.info("checkRebaseInProgress: checking if there is an unfinished rebase process..."); + final GitRebaser rebaser = new GitRebaser(myProject, myGit, myProgressIndicator); + final Collection rebasingRoots = rebaser.getRebasingRoots(); + if (rebasingRoots.isEmpty()) { + return false; + } + LOG.info("checkRebaseInProgress: roots with unfinished rebase: " + rebasingRoots); + + GitConflictResolver.Params params = new GitConflictResolver.Params(); + params.setErrorNotificationTitle("Can't update"); + params.setMergeDescription("You have unfinished rebase process. These conflicts must be resolved before update."); + params.setErrorNotificationAdditionalDescription( + "Then you may continue rebase.
You also may abort rebase to restore the original branch and stop rebasing." + ); + params.setReverse(true); + return !new GitConflictResolver(myProject, myGit, rebasingRoots, params) { + @Override + @RequiredUIAccess + protected boolean proceedIfNothingToMerge() { + return rebaser.continueRebase(rebasingRoots); + } + + @Override + @RequiredUIAccess + protected boolean proceedAfterAllMerged() { + return rebaser.continueRebase(rebasingRoots); + } + }.merge(); } - LOG.info("checkRebaseInProgress: roots with unfinished rebase: " + rebasingRoots); - - GitConflictResolver.Params params = new GitConflictResolver.Params(); - params.setErrorNotificationTitle("Can't update"); - params.setMergeDescription("You have unfinished rebase process. These conflicts must be resolved before update."); - params.setErrorNotificationAdditionalDescription( - "Then you may continue rebase.
You also may abort rebase to restore the original branch and stop rebasing."); - params.setReverse(true); - return !new GitConflictResolver(myProject, myGit, rebasingRoots, params) { - @Override - protected boolean proceedIfNothingToMerge() { - return rebaser.continueRebase(rebasingRoots); - } - - @Override - protected boolean proceedAfterAllMerged() { - return rebaser.continueRebase(rebasingRoots); - } - }.merge(); - } } diff --git a/plugin/src/main/java/git4idea/update/GitUpdateResult.java b/plugin/src/main/java/git4idea/update/GitUpdateResult.java index f48a4d3..b47118d 100644 --- a/plugin/src/main/java/git4idea/update/GitUpdateResult.java +++ b/plugin/src/main/java/git4idea/update/GitUpdateResult.java @@ -21,37 +21,36 @@ * @author Kirill Likhodedov */ public enum GitUpdateResult { - /** Nothing to update. */ - NOTHING_TO_UPDATE(1), - /** Successful update, without merge conflict resolution during update. */ - SUCCESS(2), - /** Update introduced a merge conflict, that was immediately resolved by user. */ - SUCCESS_WITH_RESOLVED_CONFLICTS(3), - /** Update introduced a merge conflict that wasn't immediately resolved. */ - INCOMPLETE(4), - /** User cancelled update, everything that has changed was rolled back (git rebase/merge --abort) */ - CANCEL(5), - /** An error happened during update */ - ERROR(6), - /** Update is not possible due to a configuration error or because of a failed fetch. */ - NOT_READY(7); + /** Nothing to update. */ + NOTHING_TO_UPDATE(1), + /** Successful update, without merge conflict resolution during update. */ + SUCCESS(2), + /** Update introduced a merge conflict, that was immediately resolved by user. */ + SUCCESS_WITH_RESOLVED_CONFLICTS(3), + /** Update introduced a merge conflict that wasn't immediately resolved. */ + INCOMPLETE(4), + /** User cancelled update, everything that has changed was rolled back (git rebase/merge --abort) */ + CANCEL(5), + /** An error happened during update */ + ERROR(6), + /** Update is not possible due to a configuration error or because of a failed fetch. */ + NOT_READY(7); - private final int myPriority; + private final int myPriority; - GitUpdateResult(int priority) { - myPriority = priority; - } - - public boolean isSuccess() { - return this == SUCCESS || this == SUCCESS_WITH_RESOLVED_CONFLICTS || this == INCOMPLETE || this == NOTHING_TO_UPDATE; - } + GitUpdateResult(int priority) { + myPriority = priority; + } - @Nonnull - public GitUpdateResult join(@Nonnull GitUpdateResult next) { - if (myPriority >= next.myPriority) { - return this; + public boolean isSuccess() { + return this == SUCCESS || this == SUCCESS_WITH_RESOLVED_CONFLICTS || this == INCOMPLETE || this == NOTHING_TO_UPDATE; } - return next; - } + @Nonnull + public GitUpdateResult join(@Nonnull GitUpdateResult next) { + if (myPriority >= next.myPriority) { + return this; + } + return next; + } } diff --git a/plugin/src/main/java/git4idea/update/GitUpdateSession.java b/plugin/src/main/java/git4idea/update/GitUpdateSession.java index 2daf0ec..fd72b08 100644 --- a/plugin/src/main/java/git4idea/update/GitUpdateSession.java +++ b/plugin/src/main/java/git4idea/update/GitUpdateSession.java @@ -26,21 +26,24 @@ * Git update session implementation */ public class GitUpdateSession implements UpdateSession { - private final boolean myResult; + private final boolean myResult; - public GitUpdateSession(boolean result) { - myResult = result; - } + public GitUpdateSession(boolean result) { + myResult = result; + } - @Nonnull - public List getExceptions() { - return Collections.emptyList(); - } + @Nonnull + @Override + public List getExceptions() { + return Collections.emptyList(); + } - public void onRefreshFilesCompleted() { - } + @Override + public void onRefreshFilesCompleted() { + } - public boolean isCanceled() { - return !myResult; - } + @Override + public boolean isCanceled() { + return !myResult; + } } diff --git a/plugin/src/main/java/git4idea/update/GitUpdater.java b/plugin/src/main/java/git4idea/update/GitUpdater.java index 4264a5c..acdbfbe 100644 --- a/plugin/src/main/java/git4idea/update/GitUpdater.java +++ b/plugin/src/main/java/git4idea/update/GitUpdater.java @@ -20,6 +20,7 @@ import static git4idea.config.UpdateMethod.REBASE; import java.util.ArrayList; +import java.util.List; import jakarta.annotation.Nonnull; import consulo.logging.Logger; @@ -51,207 +52,188 @@ * @see GitRebaseUpdater * @see GitMergeUpdater */ -public abstract class GitUpdater -{ - private static final Logger LOG = Logger.getInstance(GitUpdater.class); - - @Nonnull - protected final Project myProject; - @Nonnull - protected final Git myGit; - @Nonnull - protected final VirtualFile myRoot; - @Nonnull - protected final GitRepository myRepository; - @Nonnull - protected final GitBranchPair myBranchPair; - @Nonnull - protected final ProgressIndicator myProgressIndicator; - @Nonnull - protected final UpdatedFiles myUpdatedFiles; - @Nonnull - protected final AbstractVcsHelper myVcsHelper; - @Nonnull - protected final GitRepositoryManager myRepositoryManager; - protected final GitVcs myVcs; - - protected GitRevisionNumber myBefore; // The revision that was before update - - protected GitUpdater(@Nonnull Project project, - @Nonnull Git git, - @Nonnull GitRepository repository, - @Nonnull GitBranchPair branchAndTracked, - @Nonnull ProgressIndicator progressIndicator, - @Nonnull UpdatedFiles updatedFiles) - { - myProject = project; - myGit = git; - myRoot = repository.getRoot(); - myRepository = repository; - myBranchPair = branchAndTracked; - myProgressIndicator = progressIndicator; - myUpdatedFiles = updatedFiles; - myVcsHelper = AbstractVcsHelper.getInstance(project); - myVcs = GitVcs.getInstance(project); - myRepositoryManager = GitUtil.getRepositoryManager(myProject); - } - - /** - * Returns proper updater based on the update policy (merge or rebase) selected by user or stored in his .git/config - * - * @return {@link GitMergeUpdater} or {@link GitRebaseUpdater}. - */ - @Nonnull - public static GitUpdater getUpdater(@Nonnull Project project, - @Nonnull Git git, - @Nonnull GitBranchPair trackedBranches, - @Nonnull GitRepository repository, - @Nonnull ProgressIndicator progressIndicator, - @Nonnull UpdatedFiles updatedFiles, - @Nonnull UpdateMethod updateMethod) - { - if(updateMethod == UpdateMethod.BRANCH_DEFAULT) - { - updateMethod = resolveUpdateMethod(repository); - } - return updateMethod == UpdateMethod.REBASE ? new GitRebaseUpdater(project, git, repository, trackedBranches, progressIndicator, updatedFiles) : new GitMergeUpdater(project, git, repository, - trackedBranches, progressIndicator, updatedFiles); - } - - @Nonnull - public static UpdateMethod resolveUpdateMethod(@Nonnull GitRepository repository) - { - Project project = repository.getProject(); - GitLocalBranch branch = repository.getCurrentBranch(); - if(branch != null) - { - String branchName = branch.getName(); - try - { - String rebaseValue = GitConfigUtil.getValue(project, repository.getRoot(), "branch." + branchName + ".rebase"); - if(rebaseValue != null) - { - if(isRebaseValue(rebaseValue)) - { - return REBASE; - } - if(GitConfigUtil.getBooleanValue(rebaseValue) == Boolean.FALSE) - { - // explicit override of a more generic pull.rebase config value - return MERGE; - } - LOG.warn("Unknown value for branch." + branchName + ".rebase: " + rebaseValue); - } - } - catch(VcsException e) - { - LOG.warn("Couldn't get git config branch." + branchName + ".rebase"); - } - } - - if(GitVersionSpecialty.KNOWS_PULL_REBASE.existsIn(GitVcs.getInstance(project).getVersion())) - { - try - { - String pullRebaseValue = GitConfigUtil.getValue(project, repository.getRoot(), "pull.rebase"); - if(pullRebaseValue != null && isRebaseValue(pullRebaseValue)) - { - return REBASE; - } - } - catch(VcsException e) - { - LOG.warn("Couldn't get git config pull.rebase"); - } - } - - return MERGE; - } - - private static boolean isRebaseValue(@Nonnull String configValue) - { - return GitConfigUtil.getBooleanValue(configValue) == Boolean.TRUE || configValue.equalsIgnoreCase("interactive") || configValue.equalsIgnoreCase("preserve"); // 'yes' is not specified in the - // man, but actually works - } - - @Nonnull - public GitUpdateResult update() throws VcsException - { - markStart(myRoot); - try - { - return doUpdate(); - } - finally - { - markEnd(myRoot); - } - } - - /** - * Checks the repository if local changes need to be saved before update. - * For rebase local changes need to be saved always, - * for merge - only in the case if merge affects the same files or there is something in the index. - * - * @return true if local changes from this root need to be saved, false if not. - */ - public abstract boolean isSaveNeeded(); - - /** - * Checks if update is needed, i.e. if there are remote changes that weren't merged into the current branch. - * - * @return true if update is needed, false otherwise. - */ - public boolean isUpdateNeeded() throws VcsException - { - GitBranch dest = myBranchPair.getDest(); - assert dest != null; - String remoteBranch = dest.getName(); - if(!hasRemoteChanges(remoteBranch)) - { - LOG.info("isUpdateNeeded: No remote changes, update is not needed"); - return false; - } - return true; - } - - /** - * Performs update (via rebase or merge - depending on the implementing classes). - */ - @Nonnull - protected abstract GitUpdateResult doUpdate(); - - @Nonnull - GitBranchPair getSourceAndTarget() - { - return myBranchPair; - } - - protected void markStart(VirtualFile root) throws VcsException - { - // remember the current position - myBefore = GitRevisionNumber.resolve(myProject, root, "HEAD"); - } - - protected void markEnd(VirtualFile root) throws VcsException - { - // find out what have changed, this is done even if the process was cancelled. - final MergeChangeCollector collector = new MergeChangeCollector(myProject, root, myBefore); - final ArrayList exceptions = new ArrayList<>(); - collector.collect(myUpdatedFiles, exceptions); - if(!exceptions.isEmpty()) - { - throw exceptions.get(0); - } - } - - protected boolean hasRemoteChanges(@Nonnull String remoteBranch) throws VcsException - { - GitLineHandler handler = new GitLineHandler(myProject, myRoot, GitCommand.REV_LIST); - handler.setSilent(true); - handler.addParameters("-1"); - handler.addParameters(HEAD + ".." + remoteBranch); - String output = myGit.runCommand(handler).getOutputOrThrow(); - return output != null && !output.isEmpty(); - } +public abstract class GitUpdater { + private static final Logger LOG = Logger.getInstance(GitUpdater.class); + + @Nonnull + protected final Project myProject; + @Nonnull + protected final Git myGit; + @Nonnull + protected final VirtualFile myRoot; + @Nonnull + protected final GitRepository myRepository; + @Nonnull + protected final GitBranchPair myBranchPair; + @Nonnull + protected final ProgressIndicator myProgressIndicator; + @Nonnull + protected final UpdatedFiles myUpdatedFiles; + @Nonnull + protected final AbstractVcsHelper myVcsHelper; + @Nonnull + protected final GitRepositoryManager myRepositoryManager; + protected final GitVcs myVcs; + + protected GitRevisionNumber myBefore; // The revision that was before update + + protected GitUpdater( + @Nonnull Project project, + @Nonnull Git git, + @Nonnull GitRepository repository, + @Nonnull GitBranchPair branchAndTracked, + @Nonnull ProgressIndicator progressIndicator, + @Nonnull UpdatedFiles updatedFiles + ) { + myProject = project; + myGit = git; + myRoot = repository.getRoot(); + myRepository = repository; + myBranchPair = branchAndTracked; + myProgressIndicator = progressIndicator; + myUpdatedFiles = updatedFiles; + myVcsHelper = AbstractVcsHelper.getInstance(project); + myVcs = GitVcs.getInstance(project); + myRepositoryManager = GitUtil.getRepositoryManager(myProject); + } + + /** + * Returns proper updater based on the update policy (merge or rebase) selected by user or stored in his .git/config + * + * @return {@link GitMergeUpdater} or {@link GitRebaseUpdater}. + */ + @Nonnull + public static GitUpdater getUpdater( + @Nonnull Project project, + @Nonnull Git git, + @Nonnull GitBranchPair trackedBranches, + @Nonnull GitRepository repository, + @Nonnull ProgressIndicator progressIndicator, + @Nonnull UpdatedFiles updatedFiles, + @Nonnull UpdateMethod updateMethod + ) { + if (updateMethod == UpdateMethod.BRANCH_DEFAULT) { + updateMethod = resolveUpdateMethod(repository); + } + return updateMethod == UpdateMethod.REBASE + ? new GitRebaseUpdater(project, git, repository, trackedBranches, progressIndicator, updatedFiles) + : new GitMergeUpdater(project, git, repository, trackedBranches, progressIndicator, updatedFiles); + } + + @Nonnull + public static UpdateMethod resolveUpdateMethod(@Nonnull GitRepository repository) { + Project project = repository.getProject(); + GitLocalBranch branch = repository.getCurrentBranch(); + if (branch != null) { + String branchName = branch.getName(); + try { + String rebaseValue = GitConfigUtil.getValue(project, repository.getRoot(), "branch." + branchName + ".rebase"); + if (rebaseValue != null) { + if (isRebaseValue(rebaseValue)) { + return REBASE; + } + if (GitConfigUtil.getBooleanValue(rebaseValue) == Boolean.FALSE) { + // explicit override of a more generic pull.rebase config value + return MERGE; + } + LOG.warn("Unknown value for branch." + branchName + ".rebase: " + rebaseValue); + } + } + catch (VcsException e) { + LOG.warn("Couldn't get git config branch." + branchName + ".rebase"); + } + } + + if (GitVersionSpecialty.KNOWS_PULL_REBASE.existsIn(GitVcs.getInstance(project).getVersion())) { + try { + String pullRebaseValue = GitConfigUtil.getValue(project, repository.getRoot(), "pull.rebase"); + if (pullRebaseValue != null && isRebaseValue(pullRebaseValue)) { + return REBASE; + } + } + catch (VcsException e) { + LOG.warn("Couldn't get git config pull.rebase"); + } + } + + return MERGE; + } + + private static boolean isRebaseValue(@Nonnull String configValue) { + // 'yes' is not specified in the man, but actually works + return GitConfigUtil.getBooleanValue(configValue) == Boolean.TRUE + || configValue.equalsIgnoreCase("interactive") + || configValue.equalsIgnoreCase("preserve"); + } + + @Nonnull + public GitUpdateResult update() throws VcsException { + markStart(myRoot); + try { + return doUpdate(); + } + finally { + markEnd(myRoot); + } + } + + /** + * Checks the repository if local changes need to be saved before update. + * For rebase local changes need to be saved always, + * for merge - only in the case if merge affects the same files or there is something in the index. + * + * @return true if local changes from this root need to be saved, false if not. + */ + public abstract boolean isSaveNeeded(); + + /** + * Checks if update is needed, i.e. if there are remote changes that weren't merged into the current branch. + * + * @return true if update is needed, false otherwise. + */ + public boolean isUpdateNeeded() throws VcsException { + GitBranch dest = myBranchPair.getDest(); + assert dest != null; + String remoteBranch = dest.getName(); + if (!hasRemoteChanges(remoteBranch)) { + LOG.info("isUpdateNeeded: No remote changes, update is not needed"); + return false; + } + return true; + } + + /** + * Performs update (via rebase or merge - depending on the implementing classes). + */ + @Nonnull + protected abstract GitUpdateResult doUpdate(); + + @Nonnull + GitBranchPair getSourceAndTarget() { + return myBranchPair; + } + + protected void markStart(VirtualFile root) throws VcsException { + // remember the current position + myBefore = GitRevisionNumber.resolve(myProject, root, "HEAD"); + } + + protected void markEnd(VirtualFile root) throws VcsException { + // find out what have changed, this is done even if the process was cancelled. + MergeChangeCollector collector = new MergeChangeCollector(myProject, root, myBefore); + List exceptions = new ArrayList<>(); + collector.collect(myUpdatedFiles, exceptions); + if (!exceptions.isEmpty()) { + throw exceptions.get(0); + } + } + + protected boolean hasRemoteChanges(@Nonnull String remoteBranch) throws VcsException { + GitLineHandler handler = new GitLineHandler(myProject, myRoot, GitCommand.REV_LIST); + handler.setSilent(true); + handler.addParameters("-1"); + handler.addParameters(HEAD + ".." + remoteBranch); + String output = myGit.runCommand(handler).getOutputOrThrow(); + return !output.isEmpty(); + } } diff --git a/plugin/src/main/java/git4idea/update/UpdatePolicyUtils.java b/plugin/src/main/java/git4idea/update/UpdatePolicyUtils.java index c9bf39a..fe29d04 100644 --- a/plugin/src/main/java/git4idea/update/UpdatePolicyUtils.java +++ b/plugin/src/main/java/git4idea/update/UpdatePolicyUtils.java @@ -24,53 +24,52 @@ * The utilities for update policy */ public class UpdatePolicyUtils { - /** - * The private constructor - */ - private UpdatePolicyUtils() { - } - - /** - * Set policy value to radio buttons - * - * @param updateChangesPolicy the policy value to set - * @param stashRadioButton the stash radio button - * @param shelveRadioButton the shelve radio button - */ - @RequiredUIAccess - public static void updatePolicyItem(GitVcsSettings.UpdateChangesPolicy updateChangesPolicy, - RadioButton stashRadioButton, - RadioButton shelveRadioButton) { - switch (updateChangesPolicy == null ? GitVcsSettings.UpdateChangesPolicy.STASH : updateChangesPolicy) { - case STASH: - stashRadioButton.setValue(true); - return; - case SHELVE: - shelveRadioButton.setValue(true); - return; - default: - assert false : "Unknown policy value: " + updateChangesPolicy; + /** + * The private constructor + */ + private UpdatePolicyUtils() { } - } - /** - * Get policy value from radio buttons - * - * @param stashRadioButton the stash radio button - * @param shelveRadioButton the shelve radio button - * @return the policy value - */ - @RequiredUIAccess - public static GitVcsSettings.UpdateChangesPolicy getUpdatePolicy(@Nonnull RadioButton stashRadioButton, - @Nonnull RadioButton shelveRadioButton) { + /** + * Set policy value to radio buttons + * + * @param updateChangesPolicy the policy value to set + * @param stashRadioButton the stash radio button + * @param shelveRadioButton the shelve radio button + */ + @RequiredUIAccess + public static void updatePolicyItem( + GitVcsSettings.UpdateChangesPolicy updateChangesPolicy, + RadioButton stashRadioButton, + RadioButton shelveRadioButton + ) { + switch (updateChangesPolicy == null ? GitVcsSettings.UpdateChangesPolicy.STASH : updateChangesPolicy) { + case STASH -> stashRadioButton.setValue(true); + case SHELVE -> shelveRadioButton.setValue(true); + } + } - if (stashRadioButton.getValueOrError()) { - return GitVcsSettings.UpdateChangesPolicy.STASH; - } else if (shelveRadioButton.getValueOrError()) { - return GitVcsSettings.UpdateChangesPolicy.SHELVE; - } else { - // the stash is a default policy, in case if the policy could not be determined - return GitVcsSettings.UpdateChangesPolicy.STASH; + /** + * Get policy value from radio buttons + * + * @param stashRadioButton the stash radio button + * @param shelveRadioButton the shelve radio button + * @return the policy value + */ + @RequiredUIAccess + public static GitVcsSettings.UpdateChangesPolicy getUpdatePolicy( + @Nonnull RadioButton stashRadioButton, + @Nonnull RadioButton shelveRadioButton + ) { + if (stashRadioButton.getValueOrError()) { + return GitVcsSettings.UpdateChangesPolicy.STASH; + } + else if (shelveRadioButton.getValueOrError()) { + return GitVcsSettings.UpdateChangesPolicy.SHELVE; + } + else { + // the stash is a default policy, in case if the policy could not be determined + return GitVcsSettings.UpdateChangesPolicy.STASH; + } } - } } diff --git a/plugin/src/main/java/git4idea/util/GitUIUtil.java b/plugin/src/main/java/git4idea/util/GitUIUtil.java index 09c90f6..ed02f9b 100644 --- a/plugin/src/main/java/git4idea/util/GitUIUtil.java +++ b/plugin/src/main/java/git4idea/util/GitUIUtil.java @@ -19,6 +19,8 @@ import consulo.git.localize.GitLocalize; import consulo.localize.LocalizeValue; import consulo.project.Project; +import consulo.project.ui.notification.NotificationService; +import consulo.project.ui.notification.NotificationType; import consulo.ui.annotation.RequiredUIAccess; import consulo.ui.ex.awt.ListCellRendererWrapper; import consulo.ui.ex.awt.Messages; @@ -54,28 +56,28 @@ private GitUIUtil() { public static void notifyMessages( @Nonnull Project project, - @Nonnull String title, - @Nullable String description, + @Nonnull LocalizeValue title, + @Nonnull LocalizeValue description, boolean important, @Nullable Collection messages ) { - String desc = (description != null ? description.replace("\n", "
") : ""); + LocalizeValue desc = description.map((localizeManager, string) -> string.replace("\n", "
")); if (messages != null && !messages.isEmpty()) { - desc += StringUtil.join(messages, "



"); - } - VcsNotifier notifier = VcsNotifier.getInstance(project); - if (important) { - notifier.notifyError(title, desc); - } - else { - notifier.notifyImportantWarning(title, desc, null); + desc = LocalizeValue.join(desc, LocalizeValue.of(StringUtil.join(messages, "

"))); } + NotificationService.getInstance().newOfType( + VcsNotifier.IMPORTANT_ERROR_NOTIFICATION, + important ? NotificationType.ERROR : NotificationType.WARNING + ) + .title(title) + .content(desc) + .notify(project); } public static void notifyMessage( Project project, - @Nonnull String title, - @Nullable String description, + @Nonnull LocalizeValue title, + @Nonnull LocalizeValue description, boolean important, @Nullable Collection errors ) { @@ -86,8 +88,8 @@ public static void notifyMessage( else { errorMessages = new HashSet<>(errors.size()); for (Exception error : errors) { - if (error instanceof VcsException) { - for (String message : ((VcsException)error).getMessages()) { + if (error instanceof VcsException vcsException) { + for (String message : vcsException.getMessages()) { errorMessages.add(message.replace("\n", "
")); } } @@ -99,7 +101,13 @@ public static void notifyMessage( notifyMessages(project, title, description, important, errorMessages); } - public static void notifyError(Project project, String title, String description, boolean important, @Nullable Exception error) { + public static void notifyError( + Project project, + @Nonnull LocalizeValue title, + @Nonnull LocalizeValue description, + boolean important, + @Nullable Exception error + ) { notifyMessage(project, title, description, important, error == null ? null : Collections.singleton(error)); } @@ -122,13 +130,18 @@ String stringifyErrors(@Nullable Collection errors) { return content.toString(); } - public static void notifyImportantError(Project project, String title, String description) { + public static void notifyImportantError(Project project, @Nonnull LocalizeValue title, @Nonnull LocalizeValue description) { notifyMessage(project, title, description, true, null); } - public static void notifyGitErrors(Project project, String title, String description, Collection gitErrors) { + public static void notifyGitErrors( + Project project, + @Nonnull LocalizeValue title, + @Nonnull LocalizeValue description, + Collection gitErrors + ) { StringBuilder content = new StringBuilder(); - if (!StringUtil.isEmptyOrSpaces(description)) { + if (description != LocalizeValue.empty()) { content.append(description); } if (!gitErrors.isEmpty()) { @@ -137,7 +150,7 @@ public static void notifyGitErrors(Project project, String title, String descrip for (VcsException e : gitErrors) { content.append(e.getLocalizedMessage()).append("
"); } - notifyMessage(project, title, content.toString(), false, null); + notifyMessage(project, title, LocalizeValue.of(content.toString()), false, null); } /** @@ -147,11 +160,11 @@ public static ListCellRendererWrapper getVirtualFileListCellRendere return new ListCellRendererWrapper<>() { @Override public void customize( - final JList list, - final VirtualFile file, - final int index, - final boolean selected, - final boolean hasFocus + JList list, + VirtualFile file, + int index, + boolean selected, + boolean hasFocus ) { setText(file == null ? "(invalid)" : file.getPresentableUrl()); } @@ -165,7 +178,7 @@ public void customize( * @return the text field reference */ public static JTextField getTextField(JComboBox comboBox) { - return (JTextField)comboBox.getEditor().getEditorComponent(); + return (JTextField) comboBox.getEditor().getEditorComponent(); } /** @@ -178,11 +191,11 @@ public static JTextField getTextField(JComboBox comboBox) { * @param currentBranchLabel current branch label (might be null) */ public static void setupRootChooser( - @Nonnull final Project project, - @Nonnull final List roots, - @Nullable final VirtualFile defaultRoot, - @Nonnull final JComboBox gitRootChooser, - @Nullable final JLabel currentBranchLabel + @Nonnull Project project, + @Nonnull List roots, + @Nullable VirtualFile defaultRoot, + @Nonnull JComboBox gitRootChooser, + @Nullable JLabel currentBranchLabel ) { for (VirtualFile root : roots) { gitRootChooser.addItem(root); @@ -190,8 +203,8 @@ public static void setupRootChooser( gitRootChooser.setRenderer(getVirtualFileListCellRenderer()); gitRootChooser.setSelectedItem(defaultRoot != null ? defaultRoot : roots.get(0)); if (currentBranchLabel != null) { - final ActionListener listener = e -> { - VirtualFile root = (VirtualFile)gitRootChooser.getSelectedItem(); + ActionListener listener = e -> { + VirtualFile root = (VirtualFile) gitRootChooser.getSelectedItem(); assert root != null : "The root must not be null"; GitRepository repo = GitUtil.getRepositoryManager(project).getRepositoryForRoot(root); assert repo != null : "The repository must not be null"; @@ -216,7 +229,7 @@ public static void setupRootChooser( * @param operation the operation name */ @RequiredUIAccess - public static void showOperationError(final Project project, final VcsException ex, @Nonnull final String operation) { + public static void showOperationError(Project project, VcsException ex, @Nonnull String operation) { showOperationError(project, operation, ex.getMessage()); } @@ -229,9 +242,9 @@ public static void showOperationError(final Project project, final VcsException */ @RequiredUIAccess public static void showOperationErrors( - final Project project, - final Collection exs, - @Nonnull final LocalizeValue operation + Project project, + Collection exs, + @Nonnull LocalizeValue operation ) { if (exs.size() == 1) { //noinspection ThrowableResultOfMethodCallIgnored @@ -269,7 +282,7 @@ public static void showOperationError(Project project, @Nonnull LocalizeValue op @Deprecated @DeprecationInfo("Use variant with LocalizeValue") @RequiredUIAccess - public static void showOperationError(final Project project, final String operation, final String message) { + public static void showOperationError(Project project, String operation, String message) { Messages.showErrorDialog(project, message, GitLocalize.errorOccurredDuring(operation).get()); } @@ -339,7 +352,7 @@ public static void exclusive(final JCheckBox first, final boolean firstState, fi * @param changed the changed control * @param impliedState the implied state */ - private void check(final JCheckBox checked, final boolean checkedState, final JCheckBox changed, final boolean impliedState) { + private void check(JCheckBox checked, boolean checkedState, JCheckBox changed, boolean impliedState) { if (checked.isSelected() == checkedState) { changed.setSelected(impliedState); changed.setEnabled(false); diff --git a/plugin/src/main/java/git4idea/util/GitUntrackedFilesHelper.java b/plugin/src/main/java/git4idea/util/GitUntrackedFilesHelper.java index b60884f..de0b11c 100644 --- a/plugin/src/main/java/git4idea/util/GitUntrackedFilesHelper.java +++ b/plugin/src/main/java/git4idea/util/GitUntrackedFilesHelper.java @@ -18,6 +18,8 @@ import consulo.application.Application; import consulo.localize.LocalizeValue; import consulo.project.Project; +import consulo.project.ui.notification.NotificationService; +import consulo.ui.annotation.RequiredUIAccess; import consulo.ui.ex.awt.DialogWrapper; import consulo.ui.ex.awt.JBLabel; import consulo.ui.ex.awt.ScrollPaneFactory; @@ -25,7 +27,7 @@ import consulo.ui.ex.awt.internal.laf.MultiLineLabelUI; import consulo.util.collection.ContainerUtil; import consulo.util.lang.StringUtil; -import consulo.util.lang.ref.Ref; +import consulo.util.lang.ref.SimpleReference; import consulo.util.lang.xml.XmlStringUtil; import consulo.versionControlSystem.VcsNotifier; import consulo.versionControlSystem.ui.awt.LegacyComponentFactory; @@ -44,7 +46,6 @@ import java.util.List; public class GitUntrackedFilesHelper { - private GitUntrackedFilesHelper() { } @@ -58,25 +59,23 @@ private GitUntrackedFilesHelper() { * @param description the content of the notification or null if the default content is to be used. */ public static void notifyUntrackedFilesOverwrittenBy( - @Nonnull final Project project, - @Nonnull final VirtualFile root, + @Nonnull Project project, + @Nonnull VirtualFile root, @Nonnull Collection relativePaths, - @Nonnull final String operation, + @Nonnull String operation, @Nullable String description ) { - final String notificationTitle = StringUtil.capitalize(operation) + " failed"; - final String notificationDesc = description == null ? createUntrackedFilesOverwrittenDescription(operation, true) : description; - - final Collection absolutePaths = GitUtil.toAbsolute(root, relativePaths); - final List untrackedFiles = - ContainerUtil.mapNotNull(absolutePaths, GitUtil::findRefreshFileOrLog); - - VcsNotifier.getInstance(project).notifyError( - notificationTitle, - notificationDesc, - (notification, event) -> { + Collection absolutePaths = GitUtil.toAbsolute(root, relativePaths); + List untrackedFiles = ContainerUtil.mapNotNull(absolutePaths, GitUtil::findRefreshFileOrLog); + + NotificationService.getInstance().newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO(StringUtil.capitalize(operation) + " failed")) + .content(LocalizeValue.localizeTODO( + description == null ? createUntrackedFilesOverwrittenDescription(operation, true) : description + )) + .optionalHyperlinkListener((notification, event) -> { if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { - final String dialogDesc = createUntrackedFilesOverwrittenDescription(operation, false); + String dialogDesc = createUntrackedFilesOverwrittenDescription(operation, false); String title = "Untracked Files Preventing " + StringUtil.capitalize(operation); if (untrackedFiles.isEmpty()) { GitUtil.showPathsInDialog(project, absolutePaths, title, dialogDesc); @@ -84,22 +83,29 @@ public static void notifyUntrackedFilesOverwrittenBy( else { LegacyComponentFactory componentFactory = Application.get().getInstance(LegacyComponentFactory.class); - LegacyDialog legacyDialog = - componentFactory.createSelectFilesDialogOnlyOk(project, new ArrayList<>(untrackedFiles), StringUtil.stripHtml(dialogDesc, true), null, false, false, true); + LegacyDialog legacyDialog = componentFactory.createSelectFilesDialogOnlyOk( + project, + new ArrayList<>(untrackedFiles), + StringUtil.stripHtml(dialogDesc, true), + null, + false, + false, + true + ); legacyDialog.setTitle(LocalizeValue.localizeTODO(title)); legacyDialog.show(); } } - } - ); + }) + .notify(project); } @Nonnull - public static String createUntrackedFilesOverwrittenDescription(@Nonnull final String operation, boolean addLinkToViewFiles) { - final String description1 = " untracked working tree files would be overwritten by " + operation + "."; - final String description2 = "Please move or remove them before you can " + operation + "."; - final String notificationDesc; + public static String createUntrackedFilesOverwrittenDescription(@Nonnull String operation, boolean addLinkToViewFiles) { + String description1 = " untracked working tree files would be overwritten by " + operation + "."; + String description2 = "Please move or remove them before you can " + operation + "."; + String notificationDesc; if (addLinkToViewFiles) { notificationDesc = "Some" + description1 + "
" + description2 + " View them"; } @@ -119,19 +125,19 @@ public static String createUntrackedFilesOverwrittenDescription(@Nonnull final S * * @return true if the user agrees to rollback, false if the user decides to keep things as is and simply close the dialog. */ + @RequiredUIAccess public static boolean showUntrackedFilesDialogWithRollback( - @Nonnull final Project project, - @Nonnull final String operationName, - @Nonnull final String rollbackProposal, + @Nonnull Project project, + @Nonnull String operationName, + @Nonnull String rollbackProposal, @Nonnull VirtualFile root, - @Nonnull final Collection relativePaths + @Nonnull Collection relativePaths ) { - final Collection absolutePaths = GitUtil.toAbsolute(root, relativePaths); - final List untrackedFiles = - ContainerUtil.mapNotNull(absolutePaths, GitUtil::findRefreshFileOrLog); + Collection absolutePaths = GitUtil.toAbsolute(root, relativePaths); + List untrackedFiles = ContainerUtil.mapNotNull(absolutePaths, GitUtil::findRefreshFileOrLog); - final Ref rollback = Ref.create(); - Application application = Application.get(); + SimpleReference rollback = SimpleReference.create(); + Application application = project.getApplication(); application.invokeAndWait( () -> { JComponent filesBrowser; @@ -141,8 +147,9 @@ public static boolean showUntrackedFilesDialogWithRollback( else { LegacyComponentFactory componentFactory = application.getInstance(LegacyComponentFactory.class); - filesBrowser = - ScrollPaneFactory.createScrollPane(componentFactory.createVirtualFileList(project, untrackedFiles, false, false).getComponent()); + filesBrowser = ScrollPaneFactory.createScrollPane( + componentFactory.createVirtualFileList(project, untrackedFiles, false, false).getComponent() + ); } String title = "Could not " + StringUtil.capitalize(operationName); String description = StringUtil.stripHtml(createUntrackedFilesOverwrittenDescription(operationName, false), true); @@ -157,7 +164,6 @@ public static boolean showUntrackedFilesDialogWithRollback( } private static class UntrackedFilesRollBackDialog extends DialogWrapper { - @Nonnull private final JComponent myFilesBrowser; @Nonnull @@ -181,6 +187,7 @@ public UntrackedFilesRollBackDialog( } @Override + @RequiredUIAccess protected JComponent createSouthPanel() { JComponent buttons = super.createSouthPanel(); JPanel panel = new JPanel(new VerticalFlowLayout()); diff --git a/plugin/src/main/java/git4idea/util/LocalChangesWouldBeOverwrittenHelper.java b/plugin/src/main/java/git4idea/util/LocalChangesWouldBeOverwrittenHelper.java index 98a4320..b293610 100644 --- a/plugin/src/main/java/git4idea/util/LocalChangesWouldBeOverwrittenHelper.java +++ b/plugin/src/main/java/git4idea/util/LocalChangesWouldBeOverwrittenHelper.java @@ -15,9 +15,12 @@ */ package git4idea.util; +import consulo.localize.LocalizeValue; import consulo.project.Project; import consulo.project.ui.notification.Notification; +import consulo.project.ui.notification.NotificationService; import consulo.project.ui.notification.event.NotificationListener; +import consulo.ui.annotation.RequiredUIAccess; import consulo.ui.ex.awt.DialogBuilder; import consulo.ui.ex.awt.MultiLineLabel; import consulo.util.lang.StringUtil; @@ -33,10 +36,9 @@ import java.util.List; public class LocalChangesWouldBeOverwrittenHelper { - @Nonnull - private static String getErrorNotificationDescription() { - return getErrorDescription(true); + private static LocalizeValue getErrorNotificationDescription() { + return LocalizeValue.localizeTODO(getErrorDescription(true)); } @Nonnull @@ -56,34 +58,46 @@ private static String getErrorDescription(boolean forNotification) { } } - public static void showErrorNotification(@Nonnull final Project project, - @Nonnull final VirtualFile root, - @Nonnull final String operationName, - @Nonnull final Collection relativeFilePaths) { + public static void showErrorNotification( + @Nonnull final Project project, + @Nonnull VirtualFile root, + @Nonnull final String operationName, + @Nonnull Collection relativeFilePaths + ) { final Collection absolutePaths = GitUtil.toAbsolute(root, relativeFilePaths); final List changes = GitUtil.findLocalChangesForPaths(project, root, absolutePaths, false); - String notificationTitle = "Git " + StringUtil.capitalize(operationName) + " Failed"; - VcsNotifier.getInstance(project).notifyError(notificationTitle, getErrorNotificationDescription(), new NotificationListener.Adapter() { - @Override - protected void hyperlinkActivated(@Nonnull Notification notification, @Nonnull HyperlinkEvent e) { - showErrorDialog(project, operationName, changes, absolutePaths); - } - }); + NotificationService.getInstance().newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Git " + StringUtil.capitalize(operationName) + " Failed")) + .content(getErrorNotificationDescription()) + .optionalHyperlinkListener(new NotificationListener.Adapter() { + @Override + @RequiredUIAccess + protected void hyperlinkActivated(@Nonnull Notification notification, @Nonnull HyperlinkEvent e) { + showErrorDialog(project, operationName, changes, absolutePaths); + } + }) + .notify(project); } - public static void showErrorDialog(@Nonnull Project project, - @Nonnull VirtualFile root, - @Nonnull String operationName, - @Nonnull Collection relativeFilePaths) { + @RequiredUIAccess + public static void showErrorDialog( + @Nonnull Project project, + @Nonnull VirtualFile root, + @Nonnull String operationName, + @Nonnull Collection relativeFilePaths + ) { Collection absolutePaths = GitUtil.toAbsolute(root, relativeFilePaths); List changes = GitUtil.findLocalChangesForPaths(project, root, absolutePaths, false); showErrorDialog(project, operationName, changes, absolutePaths); } - private static void showErrorDialog(@Nonnull Project project, - @Nonnull String operationName, - @Nonnull List changes, - @Nonnull Collection absolutePaths) { + @RequiredUIAccess + private static void showErrorDialog( + @Nonnull Project project, + @Nonnull String operationName, + @Nonnull List changes, + @Nonnull Collection absolutePaths + ) { String title = "Local Changes Prevent from " + StringUtil.capitalize(operationName); String description = getErrorDialogDescription(); if (changes.isEmpty()) { @@ -100,5 +114,4 @@ private static void showErrorDialog(@Nonnull Project project, builder.show(); } } - } diff --git a/plugin/src/main/java/git4idea/util/StringScanner.java b/plugin/src/main/java/git4idea/util/StringScanner.java index 9aff008..987f617 100644 --- a/plugin/src/main/java/git4idea/util/StringScanner.java +++ b/plugin/src/main/java/git4idea/util/StringScanner.java @@ -21,222 +21,222 @@ * A parser of strings that is oriented to scanning typical git outputs */ public class StringScanner { - /** - * The text to scan - */ - private final String myText; - /** - * The text position - */ - private int myPosition; - - /** - * The constructor from text - * - * @param text the text to scan - */ - public StringScanner(@Nonnull final String text) { - myText = text; - myPosition = 0; - } - - /** - * @return true if there are more data available - */ - public boolean hasMoreData() { - return myPosition < myText.length(); - } - - /** - * @return true if the current position is end of line or end of the file - */ - public boolean isEol() { - if (!hasMoreData()) { - return true; - } - final char ch = myText.charAt(myPosition); - return ch == '\n' || ch == '\r'; - } - - /** - * Continue to the next line, the rest of the current line is skipped - */ - public void nextLine() { - while (!isEol()) { - myPosition++; - } - if (hasMoreData()) { - final char ch = myText.charAt(myPosition++); - if (hasMoreData()) { - final char ch2 = myText.charAt(myPosition); - if (ch == '\n' && ch2 == '\r' || ch == '\r' && ch2 == '\n') { - myPosition++; + /** + * The text to scan + */ + private final String myText; + /** + * The text position + */ + private int myPosition; + + /** + * The constructor from text + * + * @param text the text to scan + */ + public StringScanner(@Nonnull String text) { + myText = text; + myPosition = 0; + } + + /** + * @return true if there are more data available + */ + public boolean hasMoreData() { + return myPosition < myText.length(); + } + + /** + * @return true if the current position is end of line or end of the file + */ + public boolean isEol() { + if (!hasMoreData()) { + return true; + } + char ch = myText.charAt(myPosition); + return ch == '\n' || ch == '\r'; + } + + /** + * Continue to the next line, the rest of the current line is skipped + */ + public void nextLine() { + while (!isEol()) { + myPosition++; + } + if (hasMoreData()) { + char ch = myText.charAt(myPosition++); + if (hasMoreData()) { + char ch2 = myText.charAt(myPosition); + if (ch == '\n' && ch2 == '\r' || ch == '\r' && ch2 == '\n') { + myPosition++; + } + } + } + } + + /** + * Gets next token that is ended by space or new line. Consumes space but not a new line. + * Start position is the current. So if the string starts with space a empty token is returned. + * + * @return a token + */ + public String spaceToken() { + return boundedToken(' '); + } + + /** + * Gets next token that is ended by tab or new line. Consumes tab but not a new line. + * Start position is the current. So if the string starts with space a empty token is returned. + * + * @return a token + */ + public String tabToken() { + return boundedToken('\t'); + } + + /** + * Gets next token that is ended by {@code boundaryChar} or new line. Consumes {@code boundaryChar} but not a new line. + * Start position is the current. So if the string starts with {@code boundaryChar} a empty token is returned. + * + * @param boundaryChar a boundary character + * @return a token + */ + public String boundedToken(char boundaryChar) { + return boundedToken(boundaryChar, false); + } + + /** + * Gets next token that is ended by {@code boundaryChar} or new line. Consumes {@code boundaryChar} but not a new line (if it is not ignored). + * Start position is the current. So if the string starts with {@code boundaryChar} a empty token is returned. + * + * @param boundaryChar a boundary character + * @param ignoreEol if true, the end of line is considered as normal character and consumed + * @return a token + */ + public String boundedToken(char boundaryChar, boolean ignoreEol) { + int start = myPosition; + for (; myPosition < myText.length(); myPosition++) { + char ch = myText.charAt(myPosition); + if (ch == boundaryChar) { + String rc = myText.substring(start, myPosition); + myPosition++; + return rc; + } + if (!ignoreEol && isEol()) { + return myText.substring(start, myPosition); + } + } + throw new IllegalStateException("Unexpected text end at " + myPosition); + } + + /** + * Check if the next character is the specified one + * + * @param c the expected character + * @return true if the character matches expected. + */ + public boolean startsWith(char c) { + return hasMoreData() && myText.charAt(myPosition) == c; + } + + /** + * Check if the rest of the string starts with the specified text + * + * @param text the text to check + * @return true if the text contains the string. + */ + public boolean startsWith(String text) { + return myText.startsWith(text, myPosition); + } + + /** + * Get text from the current position until the end of the line. After return, the current position is the start of the next line. + * + * @return the text until end of the line + */ + public String line() { + return line(false); + } + + /** + * Get text from the current position until the end of the line. After return, the current position is the start of the next line. + * + * @param includeNewLine include new line characters into included string + * @return the text until end of the line + */ + public String line(boolean includeNewLine) { + int start = myPosition; + while (!isEol()) { + myPosition++; + } + int end; + if (includeNewLine) { + nextLine(); + end = myPosition; + } + else { + end = myPosition; + nextLine(); + } + return myText.substring(start, end); + } + + /** + * Skip specified amount of characters + * + * @param n characters to skip + */ + public void skipChars(int n) { + if (n < 0) { + throw new IllegalArgumentException("Amount of chars to skip must be non-negative: " + n); + } + if (myPosition + n >= myText.length()) { + throw new IllegalArgumentException("Skipping beyond end of the text (" + myPosition + " + " + n + " >= " + myText.length() + ")"); + } + myPosition += n; + } + + /** + * Try string and consume it if it matches + * + * @param c a character to try + * @return true if the string was consumed. + */ + public boolean tryConsume(char c) { + if (startsWith(c)) { + skipChars(1); + return true; + } + return false; + } + + /** + * Try consuming a sequence of characters + * + * @param chars a sequence of characters + * @return true if consumed successfully + */ + public boolean tryConsume(String chars) { + if (startsWith(chars)) { + skipChars(chars.length()); + return true; } - } - } - } - - /** - * Gets next token that is ended by space or new line. Consumes space but not a new line. - * Start position is the current. So if the string starts with space a empty token is returned. - * - * @return a token - */ - public String spaceToken() { - return boundedToken(' '); - } - - /** - * Gets next token that is ended by tab or new line. Consumes tab but not a new line. - * Start position is the current. So if the string starts with space a empty token is returned. - * - * @return a token - */ - public String tabToken() { - return boundedToken('\t'); - } - - /** - * Gets next token that is ended by {@code boundaryChar} or new line. Consumes {@code boundaryChar} but not a new line. - * Start position is the current. So if the string starts with {@code boundaryChar} a empty token is returned. - * - * @param boundaryChar a boundary character - * @return a token - */ - public String boundedToken(final char boundaryChar) { - return boundedToken(boundaryChar, false); - } - - /** - * Gets next token that is ended by {@code boundaryChar} or new line. Consumes {@code boundaryChar} but not a new line (if it is not ignored). - * Start position is the current. So if the string starts with {@code boundaryChar} a empty token is returned. - * - * @param boundaryChar a boundary character - * @param ignoreEol if true, the end of line is considered as normal character and consumed - * @return a token - */ - public String boundedToken(char boundaryChar, boolean ignoreEol) { - int start = myPosition; - for (; myPosition < myText.length(); myPosition++) { - final char ch = myText.charAt(myPosition); - if (ch == boundaryChar) { - final String rc = myText.substring(start, myPosition); - myPosition++; - return rc; - } - if (!ignoreEol && isEol()) { - return myText.substring(start, myPosition); - } - } - throw new IllegalStateException("Unexpected text end at " + myPosition); - } - - /** - * Check if the next character is the specified one - * - * @param c the expected character - * @return true if the character matches expected. - */ - public boolean startsWith(final char c) { - return hasMoreData() && myText.charAt(myPosition) == c; - } - - /** - * Check if the rest of the string starts with the specified text - * - * @param text the text to check - * @return true if the text contains the string. - */ - public boolean startsWith(String text) { - return myText.startsWith(text, myPosition); - } - - /** - * Get text from the current position until the end of the line. After return, the current position is the start of the next line. - * - * @return the text until end of the line - */ - public String line() { - return line(false); - } - - /** - * Get text from the current position until the end of the line. After return, the current position is the start of the next line. - * - * @param includeNewLine include new line characters into included string - * @return the text until end of the line - */ - public String line(boolean includeNewLine) { - int start = myPosition; - while (!isEol()) { - myPosition++; - } - int end; - if (includeNewLine) { - nextLine(); - end = myPosition; - } - else { - end = myPosition; - nextLine(); - } - return myText.substring(start, end); - } - - /** - * Skip specified amount of characters - * - * @param n characters to skip - */ - public void skipChars(final int n) { - if (n < 0) { - throw new IllegalArgumentException("Amount of chars to skip must be non neagitve: " + n); - } - if (myPosition + n >= myText.length()) { - throw new IllegalArgumentException("Skipping beyond end of the text (" + myPosition + " + " + n + " >= " + myText.length() + ")"); - } - myPosition += n; - } - - /** - * Try string and consume it if it matches - * - * @param c a character to try - * @return true if the string was consumed. - */ - public boolean tryConsume(final char c) { - if (startsWith(c)) { - skipChars(1); - return true; - } - return false; - } - - /** - * Try consuming a sequence of characters - * - * @param chars a sequence of characters - * @return true if consumed successfully - */ - public boolean tryConsume(String chars) { - if (startsWith(chars)) { - skipChars(chars.length()); - return true; - } - return false; - } - - /** - * @return the next character to be consumed - */ - public char peek() { - if (!hasMoreData()) { - throw new IllegalStateException("There is no next character"); - } - return myText.charAt(myPosition); - } - - public String getAllText() { - return myText; - } + return false; + } + + /** + * @return the next character to be consumed + */ + public char peek() { + if (!hasMoreData()) { + throw new IllegalStateException("There is no next character"); + } + return myText.charAt(myPosition); + } + + public String getAllText() { + return myText; + } } From 726d15faf5a421ca5a7e1b6d143de88e259e34a0 Mon Sep 17 00:00:00 2001 From: UNV Date: Tue, 18 Nov 2025 17:35:55 +0300 Subject: [PATCH 2/2] GitCloneDialog form removal. Form Bundle usage elimination. Some fixes. --- .../java/git4idea/branch/DeepComparator.java | 8 +- .../branch/GitDeleteBranchOperation.java | 2 +- .../git4idea/checkout/GitCloneDialog.form | 98 ------------------- .../git4idea/checkout/GitCloneDialog.java | 11 +-- .../git4idea/roots/GitIntegrationEnabler.java | 4 +- .../main/java/git4idea/update/GitFetcher.java | 4 +- .../GitUpdateLocallyModifiedDialog.java | 4 +- 7 files changed, 18 insertions(+), 113 deletions(-) delete mode 100644 plugin/src/main/java/git4idea/checkout/GitCloneDialog.form diff --git a/plugin/src/main/java/git4idea/branch/DeepComparator.java b/plugin/src/main/java/git4idea/branch/DeepComparator.java index 094736a..886a279 100644 --- a/plugin/src/main/java/git4idea/branch/DeepComparator.java +++ b/plugin/src/main/java/git4idea/branch/DeepComparator.java @@ -19,8 +19,10 @@ import consulo.application.progress.Task; import consulo.disposer.Disposable; import consulo.disposer.Disposer; +import consulo.localize.LocalizeValue; import consulo.logging.Logger; import consulo.project.Project; +import consulo.project.ui.notification.NotificationService; import consulo.ui.annotation.RequiredUIAccess; import consulo.util.dataholder.Key; import consulo.versionControlSystem.VcsException; @@ -270,8 +272,10 @@ public void onSuccess() { removeHighlighting(); if (myException != null) { - VcsNotifier.getInstance(myProject) - .notifyError("Couldn't compare with branch " + myComparedBranch, myException.getMessage()); + NotificationService.getInstance().newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) + .title(LocalizeValue.localizeTODO("Couldn't compare with branch " + myComparedBranch)) + .content(LocalizeValue.of(myException.getMessage())) + .notify(myProject); return; } myNonPickedCommits = myCollectedNonPickedCommits; diff --git a/plugin/src/main/java/git4idea/branch/GitDeleteBranchOperation.java b/plugin/src/main/java/git4idea/branch/GitDeleteBranchOperation.java index 0e24b7e..6d0e6ee 100644 --- a/plugin/src/main/java/git4idea/branch/GitDeleteBranchOperation.java +++ b/plugin/src/main/java/git4idea/branch/GitDeleteBranchOperation.java @@ -406,7 +406,7 @@ private void rollbackBranchDeletion(@Nonnull Notification notification) { myNotificationService.newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) .title(LocalizeValue.localizeTODO("Couldn't Restore " + formatBranchName(myBranchName))) .content(result.getErrorOutputWithReposIndication()) - .notifyAndGet(myProject); + .notify(myProject); } } diff --git a/plugin/src/main/java/git4idea/checkout/GitCloneDialog.form b/plugin/src/main/java/git4idea/checkout/GitCloneDialog.form deleted file mode 100644 index 8578744..0000000 --- a/plugin/src/main/java/git4idea/checkout/GitCloneDialog.form +++ /dev/null @@ -1,98 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/plugin/src/main/java/git4idea/checkout/GitCloneDialog.java b/plugin/src/main/java/git4idea/checkout/GitCloneDialog.java index 3ae130d..15deec7 100644 --- a/plugin/src/main/java/git4idea/checkout/GitCloneDialog.java +++ b/plugin/src/main/java/git4idea/checkout/GitCloneDialog.java @@ -45,7 +45,6 @@ import java.io.File; import java.net.URI; import java.net.URISyntaxException; -import java.util.ResourceBundle; import java.util.regex.Pattern; /** @@ -394,7 +393,7 @@ protected String getHelpId() { myRootPanel = new JPanel(); myRootPanel.setLayout(new GridLayoutManager(5, 4, JBUI.emptyInsets(), -1, -1)); JLabel label1 = new JLabel(); - this.$$$loadLabelText$$$(label1, ResourceBundle.getBundle("git4idea/i18n/GitBundle").getString("clone.repository.url")); + this.$$$loadLabelText$$$(label1, GitLocalize.cloneRepositoryUrl().get()); myRootPanel.add( label1, new GridConstraints( @@ -470,7 +469,7 @@ protected String getHelpId() { ) ); JLabel label2 = new JLabel(); - this.$$$loadLabelText$$$(label2, ResourceBundle.getBundle("git4idea/i18n/GitBundle").getString("clone.parent.dir")); + this.$$$loadLabelText$$$(label2, GitLocalize.cloneParentDir().get()); myRootPanel.add( label2, new GridConstraints( @@ -509,7 +508,7 @@ protected String getHelpId() { ) ); JLabel label3 = new JLabel(); - this.$$$loadLabelText$$$(label3, ResourceBundle.getBundle("git4idea/i18n/GitBundle").getString("clone.dir.name")); + this.$$$loadLabelText$$$(label3, GitLocalize.cloneDirName().get()); myRootPanel.add( label3, new GridConstraints( @@ -529,7 +528,7 @@ protected String getHelpId() { ) ); myTestButton = new JButton(); - this.$$$loadButtonText$$$(myTestButton, ResourceBundle.getBundle("git4idea/i18n/GitBundle").getString("clone.test")); + this.$$$loadButtonText$$$(myTestButton, GitLocalize.cloneTest().get()); myRootPanel.add( myTestButton, new GridConstraints( @@ -587,7 +586,7 @@ protected String getHelpId() { ) ); myPuttyLabel = new JLabel(); - this.$$$loadLabelText$$$(myPuttyLabel, ResourceBundle.getBundle("git4idea/i18n/GitBundle").getString("clone.repository.putty.key")); + this.$$$loadLabelText$$$(myPuttyLabel, GitLocalize.cloneRepositoryPuttyKey().get()); myRootPanel.add( myPuttyLabel, new GridConstraints( diff --git a/plugin/src/main/java/git4idea/roots/GitIntegrationEnabler.java b/plugin/src/main/java/git4idea/roots/GitIntegrationEnabler.java index 25ee363..b3c2bf5 100644 --- a/plugin/src/main/java/git4idea/roots/GitIntegrationEnabler.java +++ b/plugin/src/main/java/git4idea/roots/GitIntegrationEnabler.java @@ -47,14 +47,14 @@ protected boolean initOrNotifyError(@Nonnull VirtualFile projectDir) { refreshVcsDir(projectDir, GitUtil.DOT_GIT); notificationService.newInfo(VcsNotifier.NOTIFICATION_GROUP_ID) .content(LocalizeValue.localizeTODO("Created Git repository in " + projectDir.getPresentableUrl())) - .notifyAndGet(myProject); + .notify(myProject); return true; } else if (myVcs.getExecutableValidator().checkExecutableAndNotifyIfNeeded()) { notificationService.newError(VcsNotifier.IMPORTANT_ERROR_NOTIFICATION) .title(LocalizeValue.localizeTODO("Couldn't git init " + projectDir.getPresentableUrl())) .content(result.getErrorOutputAsHtmlValue()) - .notifyAndGet(myProject); + .notify(myProject); LOG.info(result.getErrorOutputAsHtmlString()); } return false; diff --git a/plugin/src/main/java/git4idea/update/GitFetcher.java b/plugin/src/main/java/git4idea/update/GitFetcher.java index e5fb64e..89d4c72 100644 --- a/plugin/src/main/java/git4idea/update/GitFetcher.java +++ b/plugin/src/main/java/git4idea/update/GitFetcher.java @@ -294,12 +294,12 @@ public static void displayFetchResult( if (result.isSuccess()) { NotificationService.getInstance().newInfo(VcsNotifier.NOTIFICATION_GROUP_ID) .content(LocalizeValue.localizeTODO("Fetched successfully" + result.getAdditionalInfo())) - .notifyAndGet(project); + .notify(project); } else if (result.isCancelled()) { NotificationService.getInstance().newWarn(VcsNotifier.STANDARD_NOTIFICATION) .content(LocalizeValue.localizeTODO("Fetch cancelled by user" + result.getAdditionalInfo())) - .notifyAndGet(project); + .notify(project); } else if (result.isNotAuthorized()) { LocalizeValue title; diff --git a/plugin/src/main/java/git4idea/update/GitUpdateLocallyModifiedDialog.java b/plugin/src/main/java/git4idea/update/GitUpdateLocallyModifiedDialog.java index 44abe79..cae9b43 100644 --- a/plugin/src/main/java/git4idea/update/GitUpdateLocallyModifiedDialog.java +++ b/plugin/src/main/java/git4idea/update/GitUpdateLocallyModifiedDialog.java @@ -216,7 +216,7 @@ private static void revertFiles(Project project, VirtualFile root, List myRootPanel = new JPanel(); myRootPanel.setLayout(new GridLayoutManager(3, 3, JBUI.emptyInsets(), -1, -1)); JLabel label1 = new JLabel(); - this.$$$loadLabelText$$$(label1, ResourceBundle.getBundle("git4idea/i18n/GitBundle").getString("update.locally.modified.git.root")); + this.$$$loadLabelText$$$(label1, GitLocalize.updateLocallyModifiedGitRoot().get()); myRootPanel.add( label1, new GridConstraints( @@ -295,7 +295,7 @@ private static void revertFiles(Project project, VirtualFile root, List ) ); myFilesList = new JBList<>(); - myFilesList.setToolTipText(ResourceBundle.getBundle("git4idea/i18n/GitBundle").getString("update.locally.modified.files.tooltip")); + myFilesList.setToolTipText(GitLocalize.updateLocallyModifiedFilesTooltip().get()); jBScrollPane1.setViewportView(myFilesList); myRescanButton = new JButton(); this.$$$loadButtonText$$$(myRescanButton, GitLocalize.updateLocallyModifiedRescan().get());