diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/impl/LocalChangesUnderRoots.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/impl/LocalChangesUnderRoots.java index e20dbd7d3860..c897a06b7b9a 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/impl/LocalChangesUnderRoots.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/impl/LocalChangesUnderRoots.java @@ -15,7 +15,6 @@ */ package com.intellij.openapi.vcs.impl; -import com.intellij.openapi.project.Project; import com.intellij.openapi.vcs.FilePath; import com.intellij.openapi.vcs.ProjectLevelVcsManager; import com.intellij.openapi.vcs.VcsRoot; @@ -35,15 +34,13 @@ import java.util.*; * @author Kirill Likhodedov */ public class LocalChangesUnderRoots { - private final Project myProject; private final ChangeListManager myChangeManager; private final ProjectLevelVcsManager myVcsManager; private VcsRoot[] myRoots; - public LocalChangesUnderRoots(Project project) { - myProject = project; - myChangeManager = ChangeListManager.getInstance(myProject); - myVcsManager = ProjectLevelVcsManager.getInstance(myProject); + public LocalChangesUnderRoots(@NotNull ChangeListManager changeListManager, @NotNull ProjectLevelVcsManager projectLevelVcsManager) { + myChangeManager = changeListManager; + myVcsManager = projectLevelVcsManager; } public Map>> getChangesByLists(@NotNull Collection rootsToSave) { diff --git a/plugins/git4idea/src/git4idea/GitUtil.java b/plugins/git4idea/src/git4idea/GitUtil.java index 5b10f51cf0c2..bb3fee239efd 100644 --- a/plugins/git4idea/src/git4idea/GitUtil.java +++ b/plugins/git4idea/src/git4idea/GitUtil.java @@ -27,8 +27,6 @@ import com.intellij.openapi.vcs.AbstractVcsHelper; import com.intellij.openapi.vcs.FilePath; import com.intellij.openapi.vcs.ProjectLevelVcsManager; import com.intellij.openapi.vcs.VcsException; -import com.intellij.openapi.vcs.changes.Change; -import com.intellij.openapi.vcs.changes.ChangeListManager; import com.intellij.openapi.vcs.changes.FilePathsHelper; import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList; import com.intellij.openapi.vcs.vfs.AbstractVcsVirtualFile; @@ -42,9 +40,7 @@ import com.intellij.vcsUtil.VcsUtil; import git4idea.branch.GitBranchUtil; import git4idea.changes.GitChangeUtils; import git4idea.changes.GitCommittedChangeList; -import git4idea.commands.GitCommand; -import git4idea.commands.GitHandler; -import git4idea.commands.GitSimpleHandler; +import git4idea.commands.*; import git4idea.config.GitConfigUtil; import git4idea.i18n.GitBundle; import git4idea.repo.GitBranchTrackInfo; @@ -785,59 +781,28 @@ public class GitUtil { * git diff --name-only master..origin/master */ @NotNull - public static Collection getPathsDiffBetweenRefs(@NotNull String beforeRef, @NotNull String afterRef, @NotNull Project project, - @NotNull VirtualFile root) throws VcsException { - final GitSimpleHandler diff = new GitSimpleHandler(project, root, GitCommand.DIFF); - diff.addParameters("--name-only", "--pretty=format:"); - diff.addParameters(beforeRef + ".." + afterRef); - diff.setNoSSH(true); - diff.setStdoutSuppressed(true); - diff.setStderrSuppressed(true); - diff.setSilent(true); - final String output = diff.run(); + public static Collection getPathsDiffBetweenRefs(@NotNull Git git, @NotNull GitRepository repository, + @NotNull String beforeRef, @NotNull String afterRef) throws VcsException { + List parameters = Arrays.asList("--name-only", "--pretty=format:"); + String range = beforeRef + ".." + afterRef; + GitCommandResult result = git.diff(repository, parameters, range); + if (!result.success()) { + LOG.info(String.format("Couldn't get diff in range [%s] for repository [%s]", range, repository.toLogString())); + return Collections.emptyList(); + } final Collection remoteChanges = new HashSet(); - for (StringScanner s = new StringScanner(output); s.hasMoreData();) { + for (StringScanner s = new StringScanner(result.getOutputAsJoinedString()); s.hasMoreData(); ) { final String relative = s.line(); if (StringUtil.isEmptyOrSpaces(relative)) { continue; } - final String path = root.getPath() + "/" + unescapePath(relative); + final String path = repository.getRoot().getPath() + "/" + unescapePath(relative); remoteChanges.add(FilePathsHelper.convertPath(path)); } return remoteChanges; } - /** - * Given the list of paths converts them to the list of {@link Change Changes} found in the {@link ChangeListManager}, - * i.e. this works only for local changes. - * Paths can be absolute or relative to the repository. - * If a path is not in the local changes, it is ignored. - */ - @NotNull - public static List convertPathsToChanges(@NotNull GitRepository repository, - @NotNull Collection affectedPaths, boolean relativePaths) { - ChangeListManager changeListManager = ChangeListManager.getInstance(repository.getProject()); - List affectedChanges = new ArrayList(); - for (String path : affectedPaths) { - VirtualFile file; - if (relativePaths) { - file = repository.getRoot().findFileByRelativePath(FileUtil.toSystemIndependentName(path)); - } - else { - file = VcsUtil.getVirtualFile(path); - } - - if (file != null) { - Change change = changeListManager.getChange(file); - if (change != null) { - affectedChanges.add(change); - } - } - } - return affectedChanges; - } - @NotNull public static GitRepositoryManager getRepositoryManager(@NotNull Project project) { return ServiceManager.getService(project, GitRepositoryManager.class); diff --git a/plugins/git4idea/src/git4idea/PlatformFacade.java b/plugins/git4idea/src/git4idea/PlatformFacade.java index 03d40d54c54a..fb41280fbc45 100644 --- a/plugins/git4idea/src/git4idea/PlatformFacade.java +++ b/plugins/git4idea/src/git4idea/PlatformFacade.java @@ -15,18 +15,21 @@ */ package git4idea; +import com.intellij.ide.SaveAndSyncHandler; import com.intellij.ide.plugins.IdeaPluginDescriptor; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ex.ProjectManagerEx; import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.util.Computable; import com.intellij.openapi.vcs.AbstractVcs; import com.intellij.openapi.vcs.AbstractVcsHelper; import com.intellij.openapi.vcs.ProjectLevelVcsManager; -import com.intellij.openapi.vcs.changes.ChangeListManager; +import com.intellij.openapi.vcs.changes.ChangeListManagerEx; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; +import git4idea.config.GitVcsSettings; import git4idea.repo.GitRepositoryManager; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -74,7 +77,7 @@ public interface PlatformFacade { void invokeAndWait(@NotNull Runnable runnable, @NotNull ModalityState modalityState); - ChangeListManager getChangeListManager(@NotNull Project project); + ChangeListManagerEx getChangeListManager(@NotNull Project project); LocalFileSystem getLocalFileSystem(); @@ -93,4 +96,18 @@ public interface PlatformFacade { */ @Nullable String getLineSeparator(@NotNull VirtualFile file, boolean detect); + + @NotNull + GitVcsSettings getSettings(Project project); + + void saveAllDocuments(); + + @Nullable + VirtualFile getVirtualFileByPath(@NotNull String path); + + @NotNull + ProjectManagerEx getProjectManager(); + + @NotNull + SaveAndSyncHandler getSaveAndSyncHandler(); } diff --git a/plugins/git4idea/src/git4idea/PlatformFacadeImpl.java b/plugins/git4idea/src/git4idea/PlatformFacadeImpl.java index f5f20dccb7da..012cf62394a0 100644 --- a/plugins/git4idea/src/git4idea/PlatformFacadeImpl.java +++ b/plugins/git4idea/src/git4idea/PlatformFacadeImpl.java @@ -15,13 +15,17 @@ */ package git4idea; +import com.intellij.ide.SaveAndSyncHandler; +import com.intellij.ide.SaveAndSyncHandlerImpl; import com.intellij.ide.plugins.IdeaPluginDescriptor; import com.intellij.ide.plugins.PluginManager; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.fileEditor.impl.LoadTextUtil; import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ex.ProjectManagerEx; import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.util.Computable; @@ -29,8 +33,12 @@ import com.intellij.openapi.vcs.AbstractVcs; import com.intellij.openapi.vcs.AbstractVcsHelper; import com.intellij.openapi.vcs.ProjectLevelVcsManager; import com.intellij.openapi.vcs.changes.ChangeListManager; +import com.intellij.openapi.vcs.changes.ChangeListManagerEx; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.ui.UIUtil; +import com.intellij.vcsUtil.VcsUtil; +import git4idea.config.GitVcsSettings; import git4idea.repo.GitRepositoryManager; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -84,8 +92,8 @@ public class PlatformFacadeImpl implements PlatformFacade { } @Override - public ChangeListManager getChangeListManager(@NotNull Project project) { - return ChangeListManager.getInstance(project); + public ChangeListManagerEx getChangeListManager(@NotNull Project project) { + return (ChangeListManagerEx)ChangeListManager.getInstance(project); } @Override @@ -117,6 +125,40 @@ public class PlatformFacadeImpl implements PlatformFacade { return LoadTextUtil.detectLineSeparator(file, detect); } + @NotNull + @Override + public GitVcsSettings getSettings(@NotNull Project project) { + return GitVcsSettings.getInstance(project); + } + + @Override + public void saveAllDocuments() { + UIUtil.invokeAndWaitIfNeeded(new Runnable() { + @Override + public void run() { + FileDocumentManager.getInstance().saveAllDocuments(); + } + }); + } + + @Nullable + @Override + public VirtualFile getVirtualFileByPath(@NotNull String path) { + return VcsUtil.getVirtualFile(path); + } + + @NotNull + @Override + public ProjectManagerEx getProjectManager() { + return ProjectManagerEx.getInstanceEx(); + } + + @NotNull + @Override + public SaveAndSyncHandler getSaveAndSyncHandler() { + return SaveAndSyncHandlerImpl.getInstance(); + } + @NotNull @Override public AbstractVcs getVcs(@NotNull Project project) { diff --git a/plugins/git4idea/src/git4idea/actions/GitPushAction.java b/plugins/git4idea/src/git4idea/actions/GitPushAction.java index f9df147ba73d..8ead4439b12b 100644 --- a/plugins/git4idea/src/git4idea/actions/GitPushAction.java +++ b/plugins/git4idea/src/git4idea/actions/GitPushAction.java @@ -15,9 +15,11 @@ */ package git4idea.actions; +import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vfs.VirtualFile; +import git4idea.PlatformFacade; import git4idea.push.GitPusher; import org.jetbrains.annotations.NotNull; @@ -40,6 +42,6 @@ public class GitPushAction extends GitRepositoryAction { @NotNull final VirtualFile defaultRoot, final Set affectedRoots, final List exceptions) throws VcsException { - GitPusher.showPushDialogAndPerformPush(project); + GitPusher.showPushDialogAndPerformPush(project, ServiceManager.getService(project, PlatformFacade.class)); } } diff --git a/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java b/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java index fb85ca9e1541..ee8c04e14242 100644 --- a/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java @@ -15,41 +15,27 @@ */ package git4idea.branch; -import com.intellij.notification.Notification; -import com.intellij.notification.NotificationListener; -import com.intellij.notification.NotificationType; -import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.ui.VerticalFlowLayout; import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.Change; -import com.intellij.openapi.vcs.changes.ui.SelectFilesDialog; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.ui.components.JBLabel; import com.intellij.util.Function; -import com.intellij.util.ui.UIUtil; -import git4idea.*; +import git4idea.GitUtil; +import git4idea.PlatformFacade; import git4idea.commands.Git; import git4idea.commands.GitMessageWithFilesDetector; import git4idea.config.GitVcsSettings; -import git4idea.merge.GitConflictResolver; import git4idea.repo.GitRepository; -import git4idea.util.UntrackedFilesNotifier; import org.jetbrains.annotations.NotNull; -import javax.swing.*; -import javax.swing.event.HyperlinkEvent; import java.util.*; -import java.util.concurrent.atomic.AtomicBoolean; import static com.intellij.openapi.util.text.StringUtil.pluralize; -import static com.intellij.openapi.util.text.StringUtil.stripHtml; /** * Common class for Git operations with branches aware of multi-root configuration, @@ -62,25 +48,27 @@ abstract class GitBranchOperation { private static final Logger LOG = Logger.getInstance(GitBranchOperation.class); @NotNull protected final Project myProject; + @NotNull protected final PlatformFacade myFacade; @NotNull protected final Git myGit; + @NotNull protected final GitBranchUiHandler myUiHandler; @NotNull private final Collection myRepositories; - @NotNull private final String myCurrentBranchOrRev; - @NotNull private final ProgressIndicator myIndicator; + @NotNull protected final String myCurrentBranchOrRev; private final GitVcsSettings mySettings; @NotNull private final Collection mySuccessfulRepositories; @NotNull private final Collection myRemainingRepositories; - protected GitBranchOperation(@NotNull Project project, @NotNull Git git, @NotNull Collection repositories, - @NotNull String currentBranchOrRev, @NotNull ProgressIndicator indicator) { + protected GitBranchOperation(@NotNull Project project, @NotNull PlatformFacade facade, @NotNull Git git, + @NotNull GitBranchUiHandler uiHandler, @NotNull Collection repositories) { myProject = project; + myFacade = facade; myGit = git; + myUiHandler = uiHandler; myRepositories = repositories; - myCurrentBranchOrRev = currentBranchOrRev; - myIndicator = indicator; + myCurrentBranchOrRev = GitBranchUtil.getCurrentBranchOrRev(repositories); mySuccessfulRepositories = new ArrayList(); myRemainingRepositories = new ArrayList(myRepositories); - mySettings = GitVcsSettings.getInstance(myProject); + mySettings = myFacade.getSettings(myProject); } protected abstract void execute(); @@ -166,20 +154,15 @@ abstract class GitBranchOperation { } protected void notifySuccess(@NotNull String message) { - Notificator.getInstance(myProject).notify(GitVcs.NOTIFICATION_GROUP_ID, "", message, NotificationType.INFORMATION); + myUiHandler.notifySuccess(message); } protected final void notifySuccess() { notifySuccess(getSuccessMessage()); } - protected static void saveAllDocuments() { - UIUtil.invokeAndWaitIfNeeded(new Runnable() { - @Override - public void run() { - FileDocumentManager.getInstance().saveAllDocuments(); - } - }); + protected final void saveAllDocuments() { + myFacade.saveAllDocuments(); } /** @@ -195,56 +178,23 @@ abstract class GitBranchOperation { } protected void showFatalErrorDialogWithRollback(@NotNull final String title, @NotNull final String message) { - 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(getRollbackProposal()).append(""); - ok.set(Messages.OK == - MessageManager.showYesNoDialog(myProject, description.toString(), title, "Rollback", "Don't rollback", Messages.getErrorIcon())); - } - }); - if (ok.get()) { + boolean rollback = myUiHandler.notifyErrorWithRollbackProposal(title, message, getRollbackProposal()); + if (rollback) { rollback(); } } - @NotNull - private String unmergedFilesErrorTitle() { - return unmergedFilesErrorTitle(getOperationName()); - } - - @NotNull - private static String unmergedFilesErrorTitle(String operationName) { - return "Can't " + operationName + " because of unmerged files"; - } - - @NotNull - private String unmergedFilesErrorNotificationDescription() { - return unmergedFilesErrorNotificationDescription(getOperationName()); - } - - @NotNull - 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."; - } - protected void showFatalNotification(@NotNull String title, @NotNull String message) { notifyError(title, message); } protected void notifyError(@NotNull String title, @NotNull String message) { - Notificator.getInstance(myProject).notify(GitVcs.IMPORTANT_ERROR_NOTIFICATION, title, message, NotificationType.ERROR); + myUiHandler.notifyError(title, message); } @NotNull protected ProgressIndicator getIndicator() { - return myIndicator; + return myUiHandler.getProgressIndicator(); } /** @@ -280,38 +230,14 @@ abstract class GitBranchOperation { } private void showUnmergedFilesDialogWithRollback() { - final AtomicBoolean ok = new AtomicBoolean(); - UIUtil.invokeAndWaitIfNeeded(new Runnable() { - @Override public void run() { - String description = "You have to resolve all merge conflicts before " + getOperationName() + ".
" + - getRollbackProposal() + ""; - // suppressing: this message looks ugly if capitalized by words - //noinspection DialogTitleCapitalization - ok.set(Messages.OK == MessageManager.showYesNoDialog(myProject, description, unmergedFilesErrorTitle(), - "Rollback", "Don't rollback", Messages.getErrorIcon())); - } - }); - if (ok.get()) { + boolean ok = myUiHandler.showUnmergedFilesMessageWithRollback(getOperationName(), getRollbackProposal()); + if (ok) { rollback(); } } private void showUnmergedFilesNotification() { - String title = unmergedFilesErrorTitle(); - String description = unmergedFilesErrorNotificationDescription(); - Notificator.getInstance(myProject).notify(GitVcs.IMPORTANT_ERROR_NOTIFICATION, title, description, NotificationType.ERROR, - new NotificationListener() { - @Override public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) { - if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED && event.getDescription().equals("resolve")) { - GitConflictResolver.Params params = new GitConflictResolver.Params(). - setMergeDescription("The following files have unresolved conflicts. You need to resolve them before " + - getOperationName() + "."). - setErrorNotificationTitle("Unresolved files remain."); - new GitConflictResolver(myProject, myGit, ServiceManager.getService(PlatformFacade.class), GitUtil.getRootsFromRepositories( - getRepositories()), params).merge(); - } - } - }); + myUiHandler.showUnmergedFilesNotification(getOperationName(), getRepositories()); } /** @@ -344,25 +270,12 @@ abstract class GitBranchOperation { } private void showUntrackedFilesNotification(@NotNull Collection untrackedFiles) { - UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject, ServiceManager.getService(myProject, PlatformFacade.class), - untrackedFiles, getOperationName(), null); + myUiHandler.showUntrackedFilesNotification(getOperationName(), untrackedFiles); } private void showUntrackedFilesDialogWithRollback(@NotNull Collection untrackedFiles) { - String title = "Couldn't " + getOperationName(); - String description = UntrackedFilesNotifier.createUntrackedFilesOverwrittenDescription(getOperationName(), false); - - final SelectFilesDialog dialog = new UntrackedFilesDialog(myProject, new ArrayList(untrackedFiles), - stripHtml(description, true)); - dialog.setTitle(title); - UIUtil.invokeAndWaitIfNeeded(new Runnable() { - @Override - public void run() { - ServiceManager.getService(myProject, PlatformFacade.class).showDialog(dialog); - } - }); - - if (dialog.isOK()) { + boolean ok = myUiHandler.showUntrackedFilesDialogWithRollback(getOperationName(), getRollbackProposal(), untrackedFiles); + if (ok) { rollback(); } } @@ -373,15 +286,13 @@ abstract class GitBranchOperation { * local changes. */ @NotNull - static Map> collectLocalChangesConflictingWithBranch(@NotNull Project project, - @NotNull Collection repositories, - @NotNull String currentBranch, - @NotNull String otherBranch) { + Map> collectLocalChangesConflictingWithBranch(@NotNull Collection repositories, + @NotNull String currentBranch, @NotNull String otherBranch) { Map> changes = new HashMap>(); for (GitRepository repository : repositories) { try { - Collection diff = GitUtil.getPathsDiffBetweenRefs(currentBranch, otherBranch, project, repository.getRoot()); - List changesInRepo = GitUtil.convertPathsToChanges(repository, diff, false); + Collection diff = GitUtil.getPathsDiffBetweenRefs(myGit, repository, currentBranch, otherBranch); + List changesInRepo = convertPathsToChanges(repository, diff, false); if (!changesInRepo.isEmpty()) { changes.put(repository, changesInRepo); } @@ -395,24 +306,6 @@ abstract class GitBranchOperation { return changes; } - private class UntrackedFilesDialog extends SelectFilesDialog { - - public UntrackedFilesDialog(@NotNull Project project, @NotNull List originalFiles, @NotNull String prompt) { - super(project, originalFiles, prompt, null, false, false, false); - setOKButtonText("Rollback"); - setCancelButtonText("Don't rollback"); - } - - @Override - protected JComponent createSouthPanel() { - JComponent buttons = super.createSouthPanel(); - JPanel panel = new JPanel(new VerticalFlowLayout()); - panel.add(new JBLabel("" + getRollbackProposal() + "")); - panel.add(buttons); - return panel; - } - } - /** * When checkout or merge operation on a repository fails with the error "local changes would be overwritten by...", * affected local files are captured by the {@link git4idea.commands.GitMessageWithFilesDetector detector}. @@ -430,11 +323,11 @@ abstract class GitBranchOperation { String currentBranch, String nextBranch) { // get changes overwritten by checkout from the error message captured from Git - List affectedChanges = GitUtil.convertPathsToChanges(currentRepository, localChangesOverwrittenBy.getRelativeFilePaths(), true); + List affectedChanges = convertPathsToChanges(currentRepository, 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(myProject, getRemainingRepositoriesExceptGiven(currentRepository), currentBranch, nextBranch); + collectLocalChangesConflictingWithBranch(getRemainingRepositoriesExceptGiven(currentRepository), currentBranch, nextBranch); Set otherProblematicRepositories = conflictingChangesInRepositories.keySet(); List allConflictingRepositories = new ArrayList(otherProblematicRepositories); @@ -446,4 +339,33 @@ abstract class GitBranchOperation { return Pair.create(allConflictingRepositories, affectedChanges); } + /** + * Given the list of paths converts them to the list of {@link com.intellij.openapi.vcs.changes.Change Changes} found in the {@link com.intellij.openapi.vcs.changes.ChangeListManager}, + * i.e. this works only for local changes. + * Paths can be absolute or relative to the repository. + * If a path is not in the local changes, it is ignored. + */ + @NotNull + private List convertPathsToChanges(@NotNull GitRepository repository, + @NotNull Collection affectedPaths, boolean relativePaths) { + List affectedChanges = new ArrayList(); + for (String path : affectedPaths) { + VirtualFile file; + if (relativePaths) { + file = repository.getRoot().findFileByRelativePath(FileUtil.toSystemIndependentName(path)); + } + else { + file = myFacade.getVirtualFileByPath(path); + } + + if (file != null) { + Change change = myFacade.getChangeListManager(myProject).getChange(file); + if (change != null) { + affectedChanges.add(change); + } + } + } + return affectedChanges; + } + } diff --git a/plugins/git4idea/src/git4idea/branch/GitBranchUiHandler.java b/plugins/git4idea/src/git4idea/branch/GitBranchUiHandler.java new file mode 100644 index 000000000000..80908c8fb4ef --- /dev/null +++ b/plugins/git4idea/src/git4idea/branch/GitBranchUiHandler.java @@ -0,0 +1,97 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package git4idea.branch; + +import com.intellij.notification.NotificationListener; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vfs.VirtualFile; +import git4idea.history.browser.GitCommit; +import git4idea.repo.GitRepository; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + *

Handles UI interaction during various operations on branches: shows notifications, proposes to rollback, shows dialogs, messages, etc. + * Some methods return the choice selected by user to the calling code, if it is needed.

+ *

The purpose of this class is to separate UI interaction from the main code, which would in particular simplify testing.

+ * + * @author Kirill Likhodedov + */ +interface GitBranchUiHandler { + + @NotNull + ProgressIndicator getProgressIndicator(); + + /** + * Shows a notification about successful branch operation. The title is empty. + */ + void notifySuccess(@NotNull String message); + + void notifySuccess(@NotNull String title, @NotNull String message); + + void notifySuccess(@NotNull String title, @NotNull String description, @Nullable NotificationListener listener); + + void notifyError(@NotNull String title, @NotNull String message); + + boolean notifyErrorWithRollbackProposal(@NotNull String title, @NotNull String message, @NotNull String rollbackProposal); + + /** + * Shows notification about unmerged files preventing checkout, merge, etc. + * @param operationName + * @param repositories + */ + void showUnmergedFilesNotification(@NotNull String operationName, @NotNull Collection repositories); + + /** + * Shows a modal notification about unmerged files preventing an operation, with "Rollback" button. + * Pressing "Rollback" would should the operation which has already successfully executed on other repositories. + * + * @return true if user has agreed to rollback, false if user denied the rollback proposal. + * @param operationName + * @param rollbackProposal + */ + boolean showUnmergedFilesMessageWithRollback(@NotNull String operationName, @NotNull String rollbackProposal); + + /** + * Show notification about "untracked files would be overwritten by merge/checkout". + * @param untrackedFiles + */ + void showUntrackedFilesNotification(@NotNull String operationName, @NotNull Collection untrackedFiles); + + boolean showUntrackedFilesDialogWithRollback(@NotNull String operationName, @NotNull String rollbackProposal, + @NotNull Collection untrackedFiles); + + /** + * Shows the dialog proposing to execute the operation (checkout or merge) smartly, i.e. stash-execute-unstash. + * @param project + * @param changes local changes that would be overwritten by checkout or merge. + * @param operation operation name + * @param force can the operation be executed force (force checkout is possible, force merge - not). + * @return the code of the decision. + */ + int showSmartOperationDialog(@NotNull Project project, @NotNull List changes, @NotNull String operation, boolean force); + + boolean showBranchIsNotFullyMergedDialog(@NotNull Project project, @NotNull Map> history, + @NotNull String unmergedBranch, @NotNull List mergedToBranches, + @NotNull String baseBranch); + +} diff --git a/plugins/git4idea/src/git4idea/branch/GitBranchUiHandlerImpl.java b/plugins/git4idea/src/git4idea/branch/GitBranchUiHandlerImpl.java new file mode 100644 index 000000000000..40bfc30fb050 --- /dev/null +++ b/plugins/git4idea/src/git4idea/branch/GitBranchUiHandlerImpl.java @@ -0,0 +1,225 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package git4idea.branch; + +import com.intellij.notification.Notification; +import com.intellij.notification.NotificationListener; +import com.intellij.notification.NotificationType; +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.ui.VerticalFlowLayout; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vcs.changes.ui.SelectFilesDialog; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.components.JBLabel; +import com.intellij.util.ui.UIUtil; +import git4idea.*; +import git4idea.commands.Git; +import git4idea.history.browser.GitCommit; +import git4idea.merge.GitConflictResolver; +import git4idea.repo.GitRepository; +import git4idea.util.UntrackedFilesNotifier; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import javax.swing.event.HyperlinkEvent; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.intellij.openapi.util.text.StringUtil.stripHtml; + +/** + * @author Kirill Likhodedov + */ +class GitBranchUiHandlerImpl implements GitBranchUiHandler { + + @NotNull private final Project myProject; + @NotNull private final Git myGit; + @NotNull private final PlatformFacade myFacade; + @NotNull private final ProgressIndicator myProgressIndicator; + + GitBranchUiHandlerImpl(@NotNull Project project, @NotNull PlatformFacade facade, @NotNull Git git, @NotNull ProgressIndicator indicator) { + myProject = project; + myGit = git; + myFacade = facade; + myProgressIndicator = indicator; + } + + @Override + public void notifySuccess(@NotNull String message) { + notifySuccess("", message); + } + + @Override + public void notifySuccess(@NotNull String title, @NotNull String message) { + notifySuccess(title, message, null); + } + + @Override + public void notifySuccess(@NotNull String title, @NotNull String description, @Nullable NotificationListener listener) { + Notificator.getInstance(myProject).notify(GitVcs.NOTIFICATION_GROUP_ID, title, description, NotificationType.INFORMATION, listener); + } + + @Override + public void notifyError(@NotNull String title, @NotNull String message) { + Notificator.getInstance(myProject).notify(GitVcs.IMPORTANT_ERROR_NOTIFICATION, title, message, NotificationType.ERROR); + } + + @Override + public boolean notifyErrorWithRollbackProposal(@NotNull final String title, @NotNull final String message, + @NotNull 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).append(""); + ok.set(Messages.OK == MessageManager.showYesNoDialog(myProject, description.toString(), title, + "Rollback", "Don't rollback", Messages.getErrorIcon())); + } + }); + return ok.get(); + } + + @Override + public void showUnmergedFilesNotification(@NotNull final String operationName, @NotNull final Collection repositories) { + String title = unmergedFilesErrorTitle(operationName); + String description = unmergedFilesErrorNotificationDescription(operationName); + Notificator.getInstance(myProject).notify(GitVcs.IMPORTANT_ERROR_NOTIFICATION, title, description, NotificationType.ERROR, + new NotificationListener() { + @Override + public void hyperlinkUpdate(@NotNull Notification notification, + @NotNull HyperlinkEvent event) { + if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED && event.getDescription().equals("resolve")) { + 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."); + new GitConflictResolver(myProject, myGit, myFacade, GitUtil.getRootsFromRepositories(repositories), params).merge(); + } + } + } + ); + } + + @Override + public boolean showUnmergedFilesMessageWithRollback(@NotNull final String operationName, @NotNull 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.OK == MessageManager.showYesNoDialog(myProject, description, unmergedFilesErrorTitle(operationName), + "Rollback", "Don't rollback", Messages.getErrorIcon())); + } + }); + return ok.get(); + } + + @Override + public void showUntrackedFilesNotification(@NotNull String operationName, @NotNull Collection untrackedFiles) { + UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject, ServiceManager.getService(myProject, PlatformFacade.class), + untrackedFiles, operationName, null); + } + + @Override + public boolean showUntrackedFilesDialogWithRollback(@NotNull String operationName, @NotNull String rollbackProposal, + @NotNull Collection untrackedFiles) { + String title = "Couldn't " + operationName; + String description = UntrackedFilesNotifier.createUntrackedFilesOverwrittenDescription(operationName, false); + + final SelectFilesDialog dialog = new UntrackedFilesDialog(myProject, untrackedFiles, stripHtml(description, true), rollbackProposal); + dialog.setTitle(title); + UIUtil.invokeAndWaitIfNeeded(new Runnable() { + @Override + public void run() { + myFacade.showDialog(dialog); + } + }); + return dialog.isOK(); + } + + @NotNull + @Override + public ProgressIndicator getProgressIndicator() { + return myProgressIndicator; + } + + @Override + public int showSmartOperationDialog(@NotNull Project project, @NotNull List changes, @NotNull String operation, boolean force) { + return GitSmartOperationDialog.showAndGetAnswer(myProject, changes, "checkout", true); + } + + @Override + public boolean showBranchIsNotFullyMergedDialog(@NotNull Project project, @NotNull final Map> history, + @NotNull final String unmergedBranch, @NotNull final List mergedToBranches, + @NotNull final String baseBranch) { + final AtomicBoolean forceDelete = new AtomicBoolean(); + UIUtil.invokeAndWaitIfNeeded(new Runnable() { + @Override + public void run() { + forceDelete.set(GitBranchIsNotFullyMergedDialog.showAndGetAnswer(myProject, history, unmergedBranch, mergedToBranches, baseBranch)); + } + }); + return forceDelete.get(); + } + + @NotNull + private static String unmergedFilesErrorTitle(@NotNull String operationName) { + return "Can't " + operationName + " because of unmerged files"; + } + + @NotNull + 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 class UntrackedFilesDialog extends SelectFilesDialog { + + @NotNull private final String myRollbackProposal; + + public UntrackedFilesDialog(@NotNull Project project, @NotNull Collection originalFiles, @NotNull String prompt, + @NotNull String rollbackProposal) { + super(project, new ArrayList(originalFiles), prompt, null, false, false, false); + myRollbackProposal = rollbackProposal; + setOKButtonText("Rollback"); + setCancelButtonText("Don't rollback"); + } + + @Override + protected JComponent createSouthPanel() { + JComponent buttons = super.createSouthPanel(); + JPanel panel = new JPanel(new VerticalFlowLayout()); + panel.add(new JBLabel("" + myRollbackProposal + "")); + panel.add(buttons); + return panel; + } + } +} diff --git a/plugins/git4idea/src/git4idea/branch/GitBranchUtil.java b/plugins/git4idea/src/git4idea/branch/GitBranchUtil.java index c53303f318a8..43e46e7120b5 100644 --- a/plugins/git4idea/src/git4idea/branch/GitBranchUtil.java +++ b/plugins/git4idea/src/git4idea/branch/GitBranchUtil.java @@ -15,18 +15,25 @@ */ package git4idea.branch; +import com.intellij.openapi.diagnostic.Logger; import git4idea.GitBranch; import git4idea.repo.GitBranchTrackInfo; import git4idea.repo.GitRemote; import git4idea.repo.GitRepository; +import git4idea.ui.branch.GitBranchUiUtil; +import git4idea.ui.branch.GitMultiRootBranchConfig; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Collection; + /** * @author Kirill Likhodedov */ public class GitBranchUtil { + private static final Logger LOG = Logger.getInstance(GitBranchUtil.class); + private GitBranchUtil() {} /** @@ -65,4 +72,19 @@ public class GitBranchUtil { } return null; } + + @NotNull + static String getCurrentBranchOrRev(@NotNull Collection repositories) { + if (repositories.size() > 1) { + GitMultiRootBranchConfig multiRootBranchConfig = new GitMultiRootBranchConfig(repositories); + String currentBranch = multiRootBranchConfig.getCurrentBranch(); + LOG.assertTrue(currentBranch != null, "Repositories have unexpectedly diverged. " + multiRootBranchConfig); + return currentBranch; + } + else { + assert !repositories.isEmpty() : "No repositories passed to GitBranchOperationsProcessor."; + GitRepository repository = repositories.iterator().next(); + return GitBranchUiUtil.getBranchNameOrRev(repository); + } + } } diff --git a/plugins/git4idea/src/git4idea/branch/GitBranchWorker.java b/plugins/git4idea/src/git4idea/branch/GitBranchWorker.java new file mode 100644 index 000000000000..6753bf1ae202 --- /dev/null +++ b/plugins/git4idea/src/git4idea/branch/GitBranchWorker.java @@ -0,0 +1,175 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package git4idea.branch; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.vcs.VcsException; +import com.intellij.openapi.vcs.changes.Change; +import git4idea.GitExecutionException; +import git4idea.PlatformFacade; +import git4idea.changes.GitChangeUtils; +import git4idea.commands.Git; +import git4idea.history.GitHistoryUtils; +import git4idea.history.browser.GitCommit; +import git4idea.repo.GitRepository; +import git4idea.ui.branch.GitCompareBranchesDialog; +import git4idea.util.GitCommitCompareInfo; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Executes the logic of git branch operations. + * All operations are run in the current thread. + * All UI interaction is done via the {@link GitBranchUiHandler} passed to the constructor. + * + * @author Kirill Likhodedov + */ +final class GitBranchWorker { + + private static final Logger LOG = Logger.getInstance(GitBranchWorker.class); + + @NotNull private final Project myProject; + @NotNull private final PlatformFacade myFacade; + @NotNull private final Git myGit; + @NotNull private final GitBranchUiHandler myUiHandler; + + GitBranchWorker(@NotNull Project project, @NotNull PlatformFacade facade, @NotNull Git git, @NotNull GitBranchUiHandler uiHandler) { + myProject = project; + myFacade = facade; + myGit = git; + myUiHandler = uiHandler; + } + + public void checkoutNewBranch(@NotNull final String name, @NotNull final List repositories) { + updateInfo(repositories); + new GitCheckoutNewBranchOperation(myProject, myFacade, myGit, myUiHandler, repositories, name).execute(); + } + + public void createNewTag(@NotNull final String name, @NotNull final String reference, @NotNull final List repositories) { + updateInfo(repositories); + for (GitRepository repository : repositories) { + myGit.createNewTag(repository, name, null, reference); + } + } + + public void checkoutNewBranchStartingFrom(@NotNull String newBranchName, @NotNull String startPoint, + @NotNull List repositories) { + updateInfo(repositories); + new GitCheckoutOperation(myProject, myFacade, myGit, myUiHandler, repositories, startPoint, newBranchName).execute(); + } + + public void checkout(@NotNull final String reference, @NotNull List repositories) { + updateInfo(repositories); + new GitCheckoutOperation(myProject, myFacade, myGit, myUiHandler, repositories, reference, null).execute(); + } + + + public void deleteBranch(@NotNull final String branchName, @NotNull final List repositories) { + updateInfo(repositories); + new GitDeleteBranchOperation(myProject, myFacade, myGit, myUiHandler, repositories, branchName).execute(); + } + + public void deleteRemoteBranch(@NotNull final String branchName, @NotNull final List repositories) { + updateInfo(repositories); + new GitDeleteRemoteBranchOperation(myProject, myFacade, myGit, myUiHandler, repositories, branchName).execute(); + } + + public void merge(@NotNull final String branchName, @NotNull final GitBrancher.DeleteOnMergeOption deleteOnMerge, + @NotNull final List repositories) { + updateInfo(repositories); + Map revisions = new HashMap(); + for (GitRepository repository : repositories) { + revisions.put(repository, repository.getCurrentRevision()); + } + new GitMergeOperation(myProject, myFacade, myGit, myUiHandler, repositories, branchName, deleteOnMerge, revisions).execute(); + } + + public void compare(@NotNull final String branchName, @NotNull final List repositories, + @NotNull final GitRepository selectedRepository) { + final GitCommitCompareInfo myCompareInfo = loadCommitsToCompare(repositories, branchName); + if (myCompareInfo == null) { + LOG.error("The task to get compare info didn't finish. Repositories: \n" + repositories + "\nbranch name: " + branchName); + return; + } + ApplicationManager.getApplication().invokeLater(new Runnable() { + @Override + public void run() { + displayCompareDialog(branchName, GitBranchUtil.getCurrentBranchOrRev(repositories), myCompareInfo, selectedRepository); + } + }); + } + + private GitCommitCompareInfo loadCommitsToCompare(List repositories, String branchName) { + GitCommitCompareInfo compareInfo = new GitCommitCompareInfo(); + for (GitRepository repository : repositories) { + compareInfo.put(repository, loadCommitsToCompare(repository, branchName)); + compareInfo.put(repository, loadTotalDiff(repository, branchName)); + } + return compareInfo; + } + + @NotNull + private static Collection loadTotalDiff(@NotNull GitRepository repository, @NotNull String branchName) { + try { + return GitChangeUtils.getDiff(repository.getProject(), repository.getRoot(), "HEAD", branchName, null); + } + catch (VcsException e) { + // we treat it as critical and report an error + throw new GitExecutionException("Couldn't get [git diff " + branchName + "] on repository [" + repository.getRoot() + "]", e); + } + } + + @NotNull + private Pair, List> loadCommitsToCompare(@NotNull GitRepository repository, @NotNull final String branchName) { + final List headToBranch; + final List branchToHead; + try { + headToBranch = GitHistoryUtils.history(myProject, repository.getRoot(), ".." + branchName); + branchToHead = GitHistoryUtils.history(myProject, repository.getRoot(), branchName + ".."); + } + catch (VcsException e) { + // we treat it as critical and report an error + throw new GitExecutionException("Couldn't get [git log .." + branchName + "] on repository [" + repository.getRoot() + "]", e); + } + return Pair.create(headToBranch, branchToHead); + } + + private void displayCompareDialog(@NotNull String branchName, @NotNull String currentBranch, @NotNull GitCommitCompareInfo compareInfo, + @NotNull GitRepository selectedRepository) { + if (compareInfo.isEmpty()) { + Messages.showInfoMessage(myProject, String.format("There are no changes between %s and %s", + currentBranch, branchName), "No Changes Detected"); + } + else { + new GitCompareBranchesDialog(myProject, branchName, currentBranch, compareInfo, selectedRepository).show(); + } + } + + private static void updateInfo(@NotNull Collection repositories) { + for (GitRepository repository : repositories) { + repository.update(GitRepository.TrackedTopic.ALL); + } + } + +} diff --git a/plugins/git4idea/src/git4idea/branch/GitBrancher.java b/plugins/git4idea/src/git4idea/branch/GitBrancher.java index 09169e509855..922b643fad4d 100644 --- a/plugins/git4idea/src/git4idea/branch/GitBrancher.java +++ b/plugins/git4idea/src/git4idea/branch/GitBrancher.java @@ -15,7 +15,6 @@ */ package git4idea.branch; -import com.intellij.util.Consumer; import git4idea.repo.GitRepository; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -41,11 +40,8 @@ public interface GitBrancher { * * @param name name of the new branch to check out. * @param repositories repositories to operate on. - * @param resultHandler This is called after the operation is executed. - * The boolean parameter passed to the consumer indicates whether checkout was successful - * or failed with an error (or user cancelled it). The consumer is called from the background thread. */ - void checkoutNewBranch(@NotNull String name, @NotNull List repositories, @Nullable Consumer resultHandler); + void checkoutNewBranch(@NotNull String name, @NotNull List repositories); /** *

Creates new tag on the selected reference.

@@ -75,7 +71,7 @@ public interface GitBrancher { /** * Creates and checks out a new local branch starting from the given reference: * {@code git checkout -b }.
- * Provides the "smart checkout" procedure the same as in {@link #checkout(String, Runnable)}. + * Provides the "smart checkout" procedure the same as in {@link #checkout(String, java.util.List, Runnable)}. * * @param newBranchName the name of the new local branch. * @param startPoint the reference to checkout. @@ -127,12 +123,8 @@ public interface GitBrancher { * @param branchName the branch to be merged into HEAD. * @param deleteOnMerge specify whether the branch should be automatically deleted or proposed to be deleted after merge. * @param repositories repositories to operate on. - * @param resultHandler This is called after the operation is executed. - * The boolean parameter passed to the consumer indicates whether checkout was successful - * or failed with an error (or user cancelled it). The consumer is called from the background thread. */ - void merge(@NotNull String branchName, @NotNull DeleteOnMergeOption deleteOnMerge, @NotNull List repositories, - @Nullable Consumer resultHandler); + void merge(@NotNull String branchName, @NotNull DeleteOnMergeOption deleteOnMerge, @NotNull List repositories); /** * What should be done after successful merging a branch: delete the merged branch, propose to delete or do nothing. diff --git a/plugins/git4idea/src/git4idea/branch/GitBrancherImpl.java b/plugins/git4idea/src/git4idea/branch/GitBrancherImpl.java index e7d2eebe5730..27759838fa45 100644 --- a/plugins/git4idea/src/git4idea/branch/GitBrancherImpl.java +++ b/plugins/git4idea/src/git4idea/branch/GitBrancherImpl.java @@ -15,412 +15,120 @@ */ package git4idea.branch; -import com.intellij.notification.NotificationType; -import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.application.Application; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.DialogWrapper; -import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.util.Pair; -import com.intellij.openapi.util.text.StringUtil; -import com.intellij.openapi.vcs.VcsException; -import com.intellij.openapi.vcs.changes.Change; -import com.intellij.util.Consumer; -import git4idea.*; -import git4idea.changes.GitChangeUtils; +import git4idea.GitVcs; +import git4idea.PlatformFacade; import git4idea.commands.Git; -import git4idea.commands.GitCommandResult; -import git4idea.commands.GitCompoundResult; -import git4idea.history.GitHistoryUtils; -import git4idea.history.browser.GitCommit; -import git4idea.jgit.GitHttpAdapter; -import git4idea.push.GitSimplePushResult; -import git4idea.repo.GitRemote; import git4idea.repo.GitRepository; -import git4idea.ui.branch.GitBranchUiUtil; -import git4idea.ui.branch.GitCompareBranchesDialog; -import git4idea.ui.branch.GitMultiRootBranchConfig; -import git4idea.util.GitCommitCompareInfo; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import javax.swing.*; -import java.util.*; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.List; /** * @author Kirill Likhodedov */ -final class GitBrancherImpl implements GitBrancher { +class GitBrancherImpl implements GitBrancher { - private static final Logger LOG = Logger.getInstance(GitBrancherImpl.class); @NotNull private final Project myProject; @NotNull private final PlatformFacade myFacade; @NotNull private final Git myGit; - GitBrancherImpl(@NotNull Project project, @NotNull PlatformFacade platformFacade, @NotNull Git git) { + GitBrancherImpl(@NotNull Project project, @NotNull PlatformFacade facade, @NotNull Git git) { myProject = project; - myFacade = platformFacade; + myFacade = facade; myGit = git; } - - @NotNull - private static String getCurrentBranchOrRev(@NotNull List repositories) { - if (repositories.size() > 1) { - GitMultiRootBranchConfig multiRootBranchConfig = new GitMultiRootBranchConfig(repositories); - String currentBranch = multiRootBranchConfig.getCurrentBranch(); - LOG.assertTrue(currentBranch != null, "Repositories have unexpectedly diverged. " + multiRootBranchConfig); - return currentBranch; - } - else { - assert !repositories.isEmpty() : "No repositories passed to GitBranchOperationsProcessor."; - GitRepository repository = repositories.iterator().next(); - return GitBranchUiUtil.getBranchNameOrRev(repository); - } - } @Override - public void checkoutNewBranch(@NotNull final String name, @NotNull final List repositories, - @Nullable final Consumer resultHandler) { + public void checkoutNewBranch(@NotNull final String name, @NotNull final List repositories) { new CommonBackgroundTask(myProject, "Checking out new branch " + name, null) { @Override public void execute(@NotNull ProgressIndicator indicator) { - doCheckoutNewBranch(name, repositories, resultHandler, indicator); + newWorker(indicator).checkoutNewBranch(name, repositories); } }.runInBackground(); } + private GitBranchWorker newWorker(ProgressIndicator indicator) { + return new GitBranchWorker(myProject, myFacade, myGit, new GitBranchUiHandlerImpl(myProject, myFacade, myGit, indicator)); + } + @Override public void createNewTag(@NotNull final String name, @NotNull final String reference, @NotNull final List repositories, @Nullable Runnable callInAwtLater) { new CommonBackgroundTask(myProject, "Checking out new branch " + name, callInAwtLater) { @Override public void execute(@NotNull ProgressIndicator indicator) { - for (GitRepository repository : repositories) { - myGit.createNewTag(repository, name, null, reference); - } + newWorker(indicator).createNewTag(name, reference, repositories); } }.runInBackground(); } - private void doCheckoutNewBranch(@NotNull final String name, @NotNull List repositories, - @Nullable Consumer resultHandler, @NotNull ProgressIndicator indicator) { - new GitCheckoutNewBranchOperation(myProject, myGit, repositories, name, getCurrentBranchOrRev(repositories), resultHandler, indicator) - .execute(); - } - @Override - public void checkoutNewBranchStartingFrom(@NotNull String newBranchName, @NotNull String startPoint, - @NotNull List repositories, @Nullable Runnable callInAwtLater) { - commonCheckout(startPoint, newBranchName, repositories, callInAwtLater); - } - - @Override - public void checkout(@NotNull final String reference, @NotNull List repositories, @Nullable Runnable callInAwtLater) { - commonCheckout(reference, null, repositories, callInAwtLater); - } - - private void commonCheckout(@NotNull final String reference, @Nullable final String newBranch, @NotNull final List repositories, - @Nullable Runnable callInAwtLater) { + public void checkout(@NotNull final String reference, @NotNull final List repositories, + @Nullable Runnable callInAwtLater) { new CommonBackgroundTask(myProject, "Checking out " + reference, callInAwtLater) { @Override public void execute(@NotNull ProgressIndicator indicator) { - doCheckout(indicator, reference, repositories, newBranch); + newWorker(indicator).checkout(reference, repositories); } }.runInBackground(); } - private void doCheckout(@NotNull ProgressIndicator indicator, @NotNull String reference, @NotNull List repositories, - @Nullable String newBranch) { - new GitCheckoutOperation(myProject, myGit, repositories, reference, newBranch, getCurrentBranchOrRev(repositories), indicator) - .execute(); + @Override + public void checkoutNewBranchStartingFrom(@NotNull final String newBranchName, @NotNull final String startPoint, + @NotNull final List repositories, @Nullable Runnable callInAwtLater) { + new CommonBackgroundTask(myProject, String.format("Checking out %s from %s", newBranchName, startPoint), callInAwtLater) { + @Override + public void execute(@NotNull ProgressIndicator indicator) { + newWorker(indicator).checkoutNewBranchStartingFrom(newBranchName, startPoint, repositories); + } + }.runInBackground(); } @Override public void deleteBranch(@NotNull final String branchName, @NotNull final List repositories) { new CommonBackgroundTask(myProject, "Deleting " + branchName, null) { @Override public void execute(@NotNull ProgressIndicator indicator) { - doDelete(branchName, repositories, indicator); + newWorker(indicator).deleteBranch(branchName, repositories); } }.runInBackground(); } - private void doDelete(@NotNull String branchName, @NotNull List repositories, @NotNull ProgressIndicator indicator) { - new GitDeleteBranchOperation(myProject, myGit, repositories, branchName, getCurrentBranchOrRev(repositories), indicator).execute(); - } - @Override public void deleteRemoteBranch(@NotNull final String branchName, @NotNull final List repositories) { - final Collection trackingBranches = findTrackingBranches(branchName, repositories); - String currentBranch = getCurrentBranchOrRev(repositories); - boolean currentBranchTracksBranchToDelete = false; - if (trackingBranches.contains(currentBranch)) { - currentBranchTracksBranchToDelete = true; - trackingBranches.remove(currentBranch); - } - - final DeleteRemoteBranchDecision decision = confirmBranchDeletion(branchName, trackingBranches, currentBranchTracksBranchToDelete, - repositories); - - if (decision.delete()) { - new CommonBackgroundTask(myProject, "Deleting " + branchName, null) { - @Override public void execute(@NotNull ProgressIndicator indicator) { - boolean deletedSuccessfully = doDeleteRemote(branchName, repositories); - if (deletedSuccessfully) { - final Collection successfullyDeletedLocalBranches = new ArrayList(1); - if (decision.deleteTracking()) { - for (final String branch : trackingBranches) { - indicator.setText("Deleting " + branch); - new GitDeleteBranchOperation(myProject, myGit, repositories, branch, getCurrentBranchOrRev(repositories), indicator) { - @Override - protected void notifySuccess(@NotNull String message) { - // do nothing - will display a combo notification for all deleted branches below - successfullyDeletedLocalBranches.add(branch); - } - }.execute(); - } - } - notifySuccessfulDeletion(branchName, successfullyDeletedLocalBranches); - } - } - }.runInBackground(); - } - } - - @NotNull - private static Collection findTrackingBranches(@NotNull String remoteBranch, @NotNull List repositories) { - return new GitMultiRootBranchConfig(repositories).getTrackingBranches(remoteBranch); - } - - private boolean doDeleteRemote(@NotNull String branchName, @NotNull List repositories) { - GitCompoundResult result = new GitCompoundResult(myProject); - for (GitRepository repository : repositories) { - Pair pair = GitBranch.splitNameOfRemoteBranch(branchName); - String remote = pair.getFirst(); - String branch = pair.getSecond(); - GitCommandResult res = pushDeletion(repository, remote, branch); - result.append(repository, res); - repository.update(GitRepository.TrackedTopic.BRANCHES); - } - if (!result.totalSuccess()) { - Notificator.getInstance(myProject).notifyError("Failed to delete remote branch " + branchName, - result.getErrorOutputWithReposIndication()); - } - return result.totalSuccess(); - } - - @NotNull - private GitCommandResult pushDeletion(@NotNull GitRepository repository, @NotNull String remoteName, @NotNull String branchName) { - GitRemote remote = getRemoteByName(repository, remoteName); - if (remote == null) { - return pushDeletionNatively(repository, remoteName, branchName); - } - - String remoteUrl = remote.getFirstUrl(); - if (remoteUrl != null && GitHttpAdapter.shouldUseJGit(remoteUrl)) { - String fullBranchName = branchName.startsWith(GitBranch.REFS_HEADS_PREFIX) ? branchName : GitBranch.REFS_HEADS_PREFIX + branchName; - String spec = ":" + fullBranchName; - GitSimplePushResult simplePushResult = GitHttpAdapter.push(repository, remote.getName(), remoteUrl, spec); - return convertSimplePushResultToCommandResult(simplePushResult); - } - else { - return pushDeletionNatively(repository, remoteName, branchName); - } - } - - @NotNull - private GitCommandResult pushDeletionNatively(@NotNull GitRepository repository, @NotNull String remoteName, @NotNull String branchName) { - return myGit.push(repository, remoteName, ":" + branchName); - } - - @NotNull - private static GitCommandResult convertSimplePushResultToCommandResult(@NotNull GitSimplePushResult result) { - boolean success = result.getType() == GitSimplePushResult.Type.SUCCESS; - return new GitCommandResult(success, -1, success ? Collections.emptyList() : Collections.singletonList(result.getOutput()), - success ? Collections.singletonList(result.getOutput()) : Collections.emptyList()); - } - - @Nullable - private static GitRemote getRemoteByName(@NotNull GitRepository repository, @NotNull String remoteName) { - for (GitRemote remote : repository.getRemotes()) { - if (remote.getName().equals(remoteName)) { - return remote; + new CommonBackgroundTask(myProject, "Deleting " + branchName, null) { + @Override public void execute(@NotNull ProgressIndicator indicator) { + newWorker(indicator).deleteRemoteBranch(branchName, repositories); } - } - return null; - } - - private void notifySuccessfulDeletion(@NotNull String remoteBranchName, @NotNull Collection localBranches) { - String message = ""; - if (!localBranches.isEmpty()) { - message = "Also deleted local " + StringUtil.pluralize("branch", localBranches.size()) + ": " + StringUtil.join(localBranches, ", "); - } - Notificator.getInstance(myProject).notify(GitVcs.NOTIFICATION_GROUP_ID, "Deleted remote branch " + remoteBranchName, - message, NotificationType.INFORMATION); - } - - private DeleteRemoteBranchDecision confirmBranchDeletion(@NotNull String branchName, @NotNull Collection trackingBranches, - boolean currentBranchTracksBranchToDelete, - @NotNull List 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.OK; - deleteTracking = false; - } - else { - if (currentBranchTracksBranchToDelete) { - message += "\n\nCurrent branch " + getCurrentBranchOrRev(repositories) + " tracks " + branchName + " but won't be deleted."; - } - final String checkboxMessage; - if (trackingBranches.size() == 1) { - checkboxMessage = "Delete tracking local branch " + trackingBranches.iterator().next() + " as well"; - } - else { - checkboxMessage = "Delete tracking local branches " + StringUtil.join(trackingBranches, ", "); - } - - final AtomicBoolean deleteChoice = new AtomicBoolean(); - delete = Messages.OK == Messages.showYesNoDialog(message, title, "Delete", "Cancel", Messages.getQuestionIcon(), new DialogWrapper.DoNotAskOption() { - @Override - public boolean isToBeShown() { - return true; - } - - @Override - public void setToBeShown(boolean value, int exitCode) { - deleteChoice.set(!value); - } - - @Override - public boolean canBeHidden() { - return true; - } - - @Override - public boolean shouldSaveOptionsOnCancel() { - return false; - } - - @Override - public String getDoNotShowMessage() { - return checkboxMessage; - } - }); - deleteTracking = deleteChoice.get(); - } - return new DeleteRemoteBranchDecision(delete, 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; - } - - public boolean delete() { - return delete; - } - - public boolean deleteTracking() { - return deleteTracking; - } + }.runInBackground(); } @Override public void compare(@NotNull final String branchName, @NotNull final List repositories, @NotNull final GitRepository selectedRepository) { new CommonBackgroundTask(myProject, "Comparing with " + branchName, null) { - - private GitCommitCompareInfo myCompareInfo; - @Override public void execute(@NotNull ProgressIndicator indicator) { - myCompareInfo = loadCommitsToCompare(repositories, branchName); - } - - @Override - public void onSuccess() { - if (myCompareInfo == null) { - LOG.error("The task to get compare info didn't finish. Repositories: \n" + repositories + "\nbranch name: " + branchName); - return; - } - displayCompareDialog(branchName, getCurrentBranchOrRev(repositories), myCompareInfo, selectedRepository); + newWorker(indicator).compare(branchName, repositories, selectedRepository); } }.runInBackground(); - } - private GitCommitCompareInfo loadCommitsToCompare(List repositories, String branchName) { - GitCommitCompareInfo compareInfo = new GitCommitCompareInfo(); - for (GitRepository repository : repositories) { - compareInfo.put(repository, loadCommitsToCompare(repository, branchName)); - compareInfo.put(repository, loadTotalDiff(repository, branchName)); - } - return compareInfo; - } - - @NotNull - private static Collection loadTotalDiff(@NotNull GitRepository repository, @NotNull String branchName) { - try { - return GitChangeUtils.getDiff(repository.getProject(), repository.getRoot(), "HEAD", branchName, null); - } - catch (VcsException e) { - // we treat it as critical and report an error - throw new GitExecutionException("Couldn't get [git diff " + branchName + "] on repository [" + repository.getRoot() + "]", e); - } - } - - @NotNull - private Pair, List> loadCommitsToCompare(@NotNull GitRepository repository, @NotNull final String branchName) { - final List headToBranch; - final List branchToHead; - try { - headToBranch = GitHistoryUtils.history(myProject, repository.getRoot(), ".." + branchName); - branchToHead = GitHistoryUtils.history(myProject, repository.getRoot(), branchName + ".."); - } - catch (VcsException e) { - // we treat it as critical and report an error - throw new GitExecutionException("Couldn't get [git log .." + branchName + "] on repository [" + repository.getRoot() + "]", e); - } - return Pair.create(headToBranch, branchToHead); - } - - private void displayCompareDialog(@NotNull String branchName, @NotNull String currentBranch, @NotNull GitCommitCompareInfo compareInfo, - @NotNull GitRepository selectedRepository) { - if (compareInfo.isEmpty()) { - Messages.showInfoMessage(myProject, String.format("There are no changes between %s and %s", - currentBranch, branchName), "No Changes Detected"); - } - else { - new GitCompareBranchesDialog(myProject, branchName, currentBranch, compareInfo, selectedRepository).show(); - } } @Override public void merge(@NotNull final String branchName, @NotNull final DeleteOnMergeOption deleteOnMerge, - @NotNull final List repositories, @Nullable final Consumer resultHandler) { + @NotNull final List repositories) { new CommonBackgroundTask(myProject, "Merging " + branchName, null) { @Override public void execute(@NotNull ProgressIndicator indicator) { - doMerge(branchName, deleteOnMerge, repositories, resultHandler, indicator); + newWorker(indicator).merge(branchName, deleteOnMerge, repositories); } }.runInBackground(); } - private void doMerge(@NotNull String branchName, DeleteOnMergeOption deleteOnMerge, @NotNull List repositories, - @Nullable Consumer resultHandler, @NotNull ProgressIndicator indicator) { - Map revisions = new HashMap(); - for (GitRepository repository : repositories) { - revisions.put(repository, repository.getCurrentRevision()); - } - new GitMergeOperation(myProject, myGit, repositories, branchName, deleteOnMerge, getCurrentBranchOrRev(repositories), - revisions, resultHandler, indicator).execute(); - } - /** * Executes common operations before/after executing the actual branch operation. */ @@ -437,7 +145,8 @@ final class GitBrancherImpl implements GitBrancher { public final void run(@NotNull ProgressIndicator indicator) { execute(indicator); if (myCallInAwtAfterExecution != null) { - SwingUtilities.invokeLater(myCallInAwtAfterExecution); + Application application = ApplicationManager.getApplication(); + application.invokeLater(myCallInAwtAfterExecution, application.getDefaultModalityState()); } } diff --git a/plugins/git4idea/src/git4idea/branch/GitCheckoutNewBranchOperation.java b/plugins/git4idea/src/git4idea/branch/GitCheckoutNewBranchOperation.java index d674ba616d43..fb784ee96338 100644 --- a/plugins/git4idea/src/git4idea/branch/GitCheckoutNewBranchOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitCheckoutNewBranchOperation.java @@ -15,20 +15,15 @@ */ package git4idea.branch; -import com.intellij.notification.NotificationType; -import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.text.StringUtil; -import com.intellij.util.Consumer; -import git4idea.GitVcs; +import git4idea.PlatformFacade; import git4idea.commands.Git; import git4idea.commands.GitCommandResult; import git4idea.commands.GitCompoundResult; import git4idea.commands.GitSimpleEventDetector; import git4idea.repo.GitRepository; -import git4idea.util.GitUIUtil; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.util.Collection; @@ -43,17 +38,12 @@ class GitCheckoutNewBranchOperation extends GitBranchOperation { @NotNull private final Project myProject; @NotNull private final String myNewBranchName; - @NotNull private final String myPreviousBranch; - @Nullable private final Consumer myResultHandler; - GitCheckoutNewBranchOperation(@NotNull Project project, @NotNull Git git, @NotNull Collection repositories, - @NotNull String newBranchName, @NotNull String previousBranch, - @Nullable Consumer resultHandler, @NotNull ProgressIndicator indicator) { - super(project, git, repositories, previousBranch, indicator); + GitCheckoutNewBranchOperation(@NotNull Project project, PlatformFacade facade, @NotNull Git git, @NotNull GitBranchUiHandler uiHandler, + @NotNull Collection repositories, @NotNull String newBranchName) { + super(project, facade, git, uiHandler, repositories); myNewBranchName = newBranchName; myProject = project; - myPreviousBranch = previousBranch; - myResultHandler = resultHandler; } @Override @@ -82,16 +72,6 @@ class GitCheckoutNewBranchOperation extends GitBranchOperation { if (!fatalErrorHappened) { notifySuccess(); updateRecentBranch(); - handleResult(true); - } - else { - handleResult(false); - } - } - - private void handleResult(boolean success) { - if (myResultHandler != null) { - myResultHandler.consume(success); } } @@ -110,7 +90,7 @@ class GitCheckoutNewBranchOperation extends GitBranchOperation { protected String getRollbackProposal() { return "However checkout has succeeded for the following " + repositories() + ":
" + successfulRepositoriesJoined() + - "
You may rollback (checkout back to " + myPreviousBranch + " and delete " + myNewBranchName + ") not to let branches diverge."; + "
You may rollback (checkout back to " + myCurrentBranchOrRev + " and delete " + myNewBranchName + ") not to let branches diverge."; } @NotNull @@ -125,7 +105,7 @@ class GitCheckoutNewBranchOperation extends GitBranchOperation { GitCompoundResult deleteResult = new GitCompoundResult(myProject); Collection repositories = getSuccessfulRepositories(); for (GitRepository repository : repositories) { - GitCommandResult result = myGit.checkout(repository, myPreviousBranch, null, true); + GitCommandResult result = myGit.checkout(repository, myCurrentBranchOrRev, null, true); checkoutResult.append(repository, result); if (result.success()) { deleteResult.append(repository, myGit.branchDelete(repository, myNewBranchName, false)); @@ -133,10 +113,8 @@ class GitCheckoutNewBranchOperation extends GitBranchOperation { refresh(repository); } if (checkoutResult.totalSuccess() && deleteResult.totalSuccess()) { - GitUIUtil.notify(GitVcs.NOTIFICATION_GROUP_ID, myProject, "Rollback successful", - String.format("Checked out %s and deleted %s on %s %s", code(myPreviousBranch), code(myNewBranchName), - StringUtil.pluralize("root", repositories.size()), successfulRepositoriesJoined()), - NotificationType.INFORMATION, null); + myUiHandler.notifySuccess("Rollback successful", String.format("Checked out %s and deleted %s on %s %s", code(myCurrentBranchOrRev), code(myNewBranchName), + StringUtil.pluralize("root", repositories.size()), successfulRepositoriesJoined())); } else { StringBuilder message = new StringBuilder(); @@ -148,7 +126,7 @@ class GitCheckoutNewBranchOperation extends GitBranchOperation { message.append("Errors during deleting ").append(code(myNewBranchName)); message.append(deleteResult.getErrorOutputWithReposIndication()); } - GitUIUtil.notify(GitVcs.IMPORTANT_ERROR_NOTIFICATION, myProject, "Error during rollback", message.toString(), NotificationType.ERROR, null); + myUiHandler.notifyError("Error during rollback", message.toString()); } } diff --git a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java index e610e59ba28a..92a5fd8a7e36 100644 --- a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java @@ -22,6 +22,7 @@ import com.intellij.openapi.util.Pair; import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vfs.VirtualFile; import git4idea.GitVcs; +import git4idea.PlatformFacade; import git4idea.commands.*; import git4idea.repo.GitRepository; import git4idea.util.GitPreservingProcess; @@ -52,15 +53,13 @@ class GitCheckoutOperation extends GitBranchOperation { @NotNull private final String myStartPointReference; @Nullable private final String myNewBranch; - @NotNull private final String myPreviousBranch; - GitCheckoutOperation(@NotNull Project project, @NotNull Git git, @NotNull Collection repositories, - @NotNull String startPointReference, @Nullable String newBranch, @NotNull String previousBranch, - @NotNull ProgressIndicator indicator) { - super(project, git, repositories, previousBranch, indicator); + GitCheckoutOperation(@NotNull Project project, PlatformFacade facade, @NotNull Git git, @NotNull GitBranchUiHandler uiHandler, + @NotNull Collection repositories, + @NotNull String startPointReference, @Nullable String newBranch) { + super(project, facade, git, uiHandler, repositories); myStartPointReference = startPointReference; myNewBranch = newBranch; - myPreviousBranch = previousBranch; } @Override @@ -110,11 +109,11 @@ class GitCheckoutOperation extends GitBranchOperation { private boolean smartCheckoutOrNotify(@NotNull GitRepository repository, @NotNull GitMessageWithFilesDetector localChangesOverwrittenByCheckout) { Pair, List> conflictingRepositoriesAndAffectedChanges = - getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByCheckout, myPreviousBranch, myStartPointReference); + getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByCheckout, myCurrentBranchOrRev, myStartPointReference); List allConflictingRepositories = conflictingRepositoriesAndAffectedChanges.getFirst(); List affectedChanges = conflictingRepositoriesAndAffectedChanges.getSecond(); - int smartCheckoutDecision = GitSmartOperationDialog.showAndGetAnswer(myProject, affectedChanges, "checkout", true); + int smartCheckoutDecision = myUiHandler.showSmartOperationDialog(myProject, affectedChanges, "checkout", true); if (smartCheckoutDecision == GitSmartOperationDialog.SMART_EXIT_CODE) { boolean smartCheckedOutSuccessfully = smartCheckout(allConflictingRepositories, myStartPointReference, myNewBranch, getIndicator()); if (smartCheckedOutSuccessfully) { @@ -143,7 +142,7 @@ class GitCheckoutOperation extends GitBranchOperation { protected String getRollbackProposal() { return "However checkout has succeeded for the following " + repositories() + ":
" + successfulRepositoriesJoined() + - "
" + String.format(ROLLBACK_PROPOSAL_FORMAT, myPreviousBranch); + "
" + String.format(ROLLBACK_PROPOSAL_FORMAT, myCurrentBranchOrRev); } @NotNull @@ -157,7 +156,7 @@ class GitCheckoutOperation extends GitBranchOperation { GitCompoundResult checkoutResult = new GitCompoundResult(myProject); GitCompoundResult deleteResult = new GitCompoundResult(myProject); for (GitRepository repository : getSuccessfulRepositories()) { - GitCommandResult result = myGit.checkout(repository, myPreviousBranch, null, true); + GitCommandResult result = myGit.checkout(repository, myCurrentBranchOrRev, null, true); checkoutResult.append(repository, result); if (result.success() && myNewBranch != null) { /* @@ -172,7 +171,7 @@ class GitCheckoutOperation extends GitBranchOperation { if (!checkoutResult.totalSuccess() || !deleteResult.totalSuccess()) { StringBuilder message = new StringBuilder(); if (!checkoutResult.totalSuccess()) { - message.append("Errors during checking out ").append(myPreviousBranch).append(": "); + message.append("Errors during checking out ").append(myCurrentBranchOrRev).append(": "); message.append(checkoutResult.getErrorOutputWithReposIndication()); } if (!deleteResult.totalSuccess()) { @@ -199,10 +198,11 @@ class GitCheckoutOperation extends GitBranchOperation { } // stash - checkout - unstash - private boolean smartCheckout(@NotNull final List repositories, @NotNull final String reference, @Nullable final String newBranch, @NotNull ProgressIndicator indicator) { - + private boolean smartCheckout(@NotNull final List repositories, @NotNull final String reference, + @Nullable final String newBranch, @NotNull ProgressIndicator indicator) { final AtomicBoolean result = new AtomicBoolean(); - GitPreservingProcess preservingProcess = new GitPreservingProcess(myProject, myGit, repositories, "checkout", reference, indicator, + GitPreservingProcess preservingProcess = new GitPreservingProcess(myProject, myFacade, myGit, + repositories, "checkout", reference, indicator, new Runnable() { @Override public void run() { diff --git a/plugins/git4idea/src/git4idea/branch/GitDeleteBranchOperation.java b/plugins/git4idea/src/git4idea/branch/GitDeleteBranchOperation.java index b1e9ec5c0fc6..4c9ef88197dd 100644 --- a/plugins/git4idea/src/git4idea/branch/GitDeleteBranchOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitDeleteBranchOperation.java @@ -17,16 +17,12 @@ package git4idea.branch; import com.intellij.notification.NotificationType; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Key; -import com.intellij.openapi.vcs.VcsException; import com.intellij.util.ArrayUtil; -import com.intellij.util.ui.UIUtil; -import git4idea.GitExecutionException; import git4idea.GitVcs; +import git4idea.PlatformFacade; import git4idea.commands.*; -import git4idea.history.GitHistoryUtils; import git4idea.history.browser.GitCommit; import git4idea.repo.GitRepository; import git4idea.util.GitUIUtil; @@ -34,7 +30,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -50,13 +45,11 @@ class GitDeleteBranchOperation extends GitBranchOperation { private static final Logger LOG = Logger.getInstance(GitDeleteBranchOperation.class); private final String myBranchName; - private final String myCurrentBranch; - GitDeleteBranchOperation(@NotNull Project project, @NotNull Git git, @NotNull Collection repositories, - @NotNull String branchName, @NotNull String currentBranch, @NotNull ProgressIndicator indicator) { - super(project, git, repositories, currentBranch, indicator); + GitDeleteBranchOperation(@NotNull Project project, PlatformFacade facade, @NotNull Git git, @NotNull GitBranchUiHandler uiHandler, + @NotNull Collection repositories, @NotNull String branchName) { + super(project, facade, git, uiHandler, repositories); myBranchName = branchName; - myCurrentBranch = currentBranch; } @Override @@ -76,7 +69,7 @@ class GitDeleteBranchOperation extends GitBranchOperation { else if (notFullyMergedDetector.hasHappened()) { String baseBranch = notMergedToUpstreamDetector.getBaseBranch(); if (baseBranch == null) { // GitBranchNotMergedToUpstreamDetector didn't happen - baseBranch = myCurrentBranch; + baseBranch = myCurrentBranchOrRev; } Collection remainingRepositories = getRemainingRepositories(); @@ -94,7 +87,9 @@ class GitDeleteBranchOperation extends GitBranchOperation { } } else { - fatalError(getErrorTitle(), "This branch is not fully merged to " + baseBranch + "."); + if (wereSuccessful()) { + showFatalErrorDialogWithRollback(getErrorTitle(), "This branch is not fully merged to " + baseBranch + "."); + } fatalErrorHappened = true; } } @@ -175,8 +170,10 @@ class GitDeleteBranchOperation extends GitBranchOperation { final List mergedToBranches = getMergedToBranches(unmergedBranch); final Map> history = new HashMap>(); + + // note getRepositories() instead of getRemainingRepositories() here: + // we don't confuse user with the absence of repositories that have succeeded, just show no commits for them (and don't query for log) for (GitRepository repository : getRepositories()) { - // we don't confuse user with the absence of repositories that have succeeded, just show no commits for them (and don't query for log) if (repositories.contains(repository)) { history.put(repository, getUnmergedCommits(repository, unmergedBranch, baseBranch)); } @@ -185,32 +182,18 @@ class GitDeleteBranchOperation extends GitBranchOperation { } } - final AtomicBoolean forceDelete = new AtomicBoolean(); - UIUtil.invokeAndWaitIfNeeded(new Runnable() { - @Override - public void run() { - forceDelete.set(GitBranchIsNotFullyMergedDialog.showAndGetAnswer(myProject, history, unmergedBranch, mergedToBranches, baseBranch)); - } - }); - return forceDelete.get(); + return myUiHandler.showBranchIsNotFullyMergedDialog(myProject, history, unmergedBranch, mergedToBranches, baseBranch); } @NotNull private List getUnmergedCommits(@NotNull GitRepository repository, @NotNull String branchName, @NotNull String baseBranch) { - List history; - try { - history = GitHistoryUtils.history(myProject, repository.getRoot(), baseBranch + ".." + branchName); - } catch (VcsException e) { - // this is critical, because we need to show the list of unmerged commits, and it shouldn't happen => inform user and developer - throw new GitExecutionException("Couldn't get [git log .." + branchName + "] on repository [" + repository.getRoot() + "]", e); - } - return history; + return myGit.history(repository, baseBranch + ".." + branchName); } @NotNull private List getMergedToBranches(String branchName) { List mergedToBranches = null; - for (GitRepository repository : getRepositories()) { + for (GitRepository repository : getRemainingRepositories()) { List branches = getMergedToBranches(repository, branchName); if (mergedToBranches == null) { mergedToBranches = branches; diff --git a/plugins/git4idea/src/git4idea/branch/GitDeleteRemoteBranchOperation.java b/plugins/git4idea/src/git4idea/branch/GitDeleteRemoteBranchOperation.java new file mode 100644 index 000000000000..f128df1f25fe --- /dev/null +++ b/plugins/git4idea/src/git4idea/branch/GitDeleteRemoteBranchOperation.java @@ -0,0 +1,264 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance permissions and + * limitations under the License. + */ +package git4idea.branch; + +import com.intellij.notification.NotificationType; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.util.ui.UIUtil; +import git4idea.GitBranch; +import git4idea.GitVcs; +import git4idea.Notificator; +import git4idea.PlatformFacade; +import git4idea.commands.Git; +import git4idea.commands.GitCommandResult; +import git4idea.commands.GitCompoundResult; +import git4idea.jgit.GitHttpAdapter; +import git4idea.push.GitSimplePushResult; +import git4idea.repo.GitRemote; +import git4idea.repo.GitRepository; +import git4idea.ui.branch.GitMultiRootBranchConfig; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author Kirill Likhodedov + */ +class GitDeleteRemoteBranchOperation extends GitBranchOperation { + private final String myBranchName; + + public GitDeleteRemoteBranchOperation(@NotNull Project project, @NotNull PlatformFacade facade, @NotNull Git git, + @NotNull GitBranchUiHandler handler, @NotNull List repositories, + @NotNull String name) { + super(project, facade, 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); + } + + final AtomicReference decision = new AtomicReference(); + final boolean finalCurrentBranchTracksBranchToDelete = currentBranchTracksBranchToDelete; + UIUtil.invokeAndWaitIfNeeded(new Runnable() { + @Override + public void run() { + 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, myFacade, myGit, myUiHandler, repositories, branch) { + @Override + protected void notifySuccess(@NotNull String message) { + // do nothing - will display a combo notification for all deleted branches below + successfullyDeletedLocalBranches.add(branch); + } + }.execute(); + } + } + notifySuccessfulDeletion(myBranchName, successfullyDeletedLocalBranches); + } + } + + } + + @Override + protected void rollback() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public String getSuccessMessage() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + protected String getRollbackProposal() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + protected String getOperationName() { + throw new UnsupportedOperationException(); + } + + @NotNull + private static Collection findTrackingBranches(@NotNull String remoteBranch, @NotNull Collection repositories) { + return new GitMultiRootBranchConfig(repositories).getTrackingBranches(remoteBranch); + } + + private boolean doDeleteRemote(@NotNull String branchName, @NotNull Collection repositories) { + GitCompoundResult result = new GitCompoundResult(myProject); + for (GitRepository repository : repositories) { + Pair pair = GitBranch.splitNameOfRemoteBranch(branchName); + String remote = pair.getFirst(); + String branch = pair.getSecond(); + GitCommandResult res = pushDeletion(repository, remote, branch); + result.append(repository, res); + repository.update(GitRepository.TrackedTopic.BRANCHES); + } + if (!result.totalSuccess()) { + Notificator.getInstance(myProject).notifyError("Failed to delete remote branch " + branchName, + result.getErrorOutputWithReposIndication()); + } + return result.totalSuccess(); + } + + @NotNull + private GitCommandResult pushDeletion(@NotNull GitRepository repository, @NotNull String remoteName, @NotNull String branchName) { + GitRemote remote = getRemoteByName(repository, remoteName); + if (remote == null) { + return pushDeletionNatively(repository, remoteName, branchName); + } + + String remoteUrl = remote.getFirstUrl(); + if (remoteUrl != null && GitHttpAdapter.shouldUseJGit(remoteUrl)) { + String fullBranchName = branchName.startsWith(GitBranch.REFS_HEADS_PREFIX) ? branchName : GitBranch.REFS_HEADS_PREFIX + branchName; + String spec = ":" + fullBranchName; + GitSimplePushResult simplePushResult = GitHttpAdapter.push(repository, remote.getName(), remoteUrl, spec); + return convertSimplePushResultToCommandResult(simplePushResult); + } + else { + return pushDeletionNatively(repository, remoteName, branchName); + } + } + + @NotNull + private GitCommandResult pushDeletionNatively(@NotNull GitRepository repository, @NotNull String remoteName, @NotNull String branchName) { + return myGit.push(repository, remoteName, ":" + branchName); + } + + @NotNull + private static GitCommandResult convertSimplePushResultToCommandResult(@NotNull GitSimplePushResult result) { + boolean success = result.getType() == GitSimplePushResult.Type.SUCCESS; + return new GitCommandResult(success, -1, success ? Collections.emptyList() : Collections.singletonList(result.getOutput()), + success ? Collections.singletonList(result.getOutput()) : Collections.emptyList()); + } + + @Nullable + private static GitRemote getRemoteByName(@NotNull GitRepository repository, @NotNull String remoteName) { + for (GitRemote remote : repository.getRemotes()) { + if (remote.getName().equals(remoteName)) { + return remote; + } + } + return null; + } + + private void notifySuccessfulDeletion(@NotNull String remoteBranchName, @NotNull Collection localBranches) { + String message = ""; + if (!localBranches.isEmpty()) { + message = "Also deleted local " + StringUtil.pluralize("branch", localBranches.size()) + ": " + StringUtil.join(localBranches, ", "); + } + Notificator.getInstance(myProject).notify(GitVcs.NOTIFICATION_GROUP_ID, "Deleted remote branch " + remoteBranchName, + message, NotificationType.INFORMATION); + } + + private DeleteRemoteBranchDecision confirmBranchDeletion(@NotNull String branchName, @NotNull Collection trackingBranches, + boolean currentBranchTracksBranchToDelete, + @NotNull 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.OK; + deleteTracking = false; + } + else { + if (currentBranchTracksBranchToDelete) { + message += "\n\nCurrent branch " + GitBranchUtil.getCurrentBranchOrRev(repositories) + " tracks " + branchName + " but won't be deleted."; + } + final String checkboxMessage; + if (trackingBranches.size() == 1) { + checkboxMessage = "Delete tracking local branch " + trackingBranches.iterator().next() + " as well"; + } + else { + checkboxMessage = "Delete tracking local branches " + StringUtil.join(trackingBranches, ", "); + } + + final AtomicBoolean deleteChoice = new AtomicBoolean(); + delete = Messages.OK == Messages.showYesNoDialog(message, title, "Delete", "Cancel", Messages.getQuestionIcon(), new DialogWrapper.DoNotAskOption() { + @Override + public boolean isToBeShown() { + return true; + } + + @Override + public void setToBeShown(boolean value, int exitCode) { + deleteChoice.set(!value); + } + + @Override + public boolean canBeHidden() { + return true; + } + + @Override + public boolean shouldSaveOptionsOnCancel() { + return false; + } + + @Override + public String getDoNotShowMessage() { + return checkboxMessage; + } + }); + deleteTracking = deleteChoice.get(); + } + return new DeleteRemoteBranchDecision(delete, 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; + } + + public boolean delete() { + return delete; + } + + public boolean deleteTracking() { + return deleteTracking; + } + } + +} \ No newline at end of file diff --git a/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java b/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java index e21e6d6d08a5..96b450f00ac9 100644 --- a/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java @@ -20,17 +20,16 @@ import com.intellij.notification.NotificationListener; import com.intellij.notification.NotificationType; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Computable; import com.intellij.openapi.util.Pair; import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.changes.ChangeListManager; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.util.Consumer; import git4idea.GitUtil; import git4idea.GitVcs; import git4idea.Notificator; +import git4idea.PlatformFacade; import git4idea.commands.*; import git4idea.merge.GitMergeCommittingConflictResolver; import git4idea.merge.GitMerger; @@ -38,7 +37,6 @@ import git4idea.repo.GitRepository; import git4idea.util.GitPreservingProcess; import git4idea.util.GitUIUtil; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import javax.swing.event.HyperlinkEvent; import java.util.*; @@ -58,25 +56,21 @@ class GitMergeOperation extends GitBranchOperation { @NotNull private final ChangeListManager myChangeListManager; @NotNull private final String myBranchToMerge; private final GitBrancher.DeleteOnMergeOption myDeleteOnMerge; - @NotNull private final String myCurrentBranch; @NotNull private final Map myCurrentRevisionsBeforeMerge; - @Nullable private final Consumer myResultHandler; // true in value, if we've stashed local changes before merge and will need to unstash after resolving conflicts. @NotNull private final Map myConflictedRepositories = new HashMap(); private GitPreservingProcess myPreservingProcess; - GitMergeOperation(@NotNull Project project, @NotNull Git git, @NotNull Collection repositories, - @NotNull String branchToMerge, GitBrancher.DeleteOnMergeOption deleteOnMerge, @NotNull String currentBranch, - @NotNull Map currentRevisionsBeforeMerge, - @Nullable Consumer resultHandler, @NotNull ProgressIndicator indicator) { - super(project, git, repositories, currentBranch, indicator); + GitMergeOperation(@NotNull Project project, PlatformFacade facade, @NotNull Git git, @NotNull GitBranchUiHandler uiHandler, + @NotNull Collection repositories, + @NotNull String branchToMerge, GitBrancher.DeleteOnMergeOption deleteOnMerge, + @NotNull Map currentRevisionsBeforeMerge) { + super(project, facade, git, uiHandler, repositories); myBranchToMerge = branchToMerge; myDeleteOnMerge = deleteOnMerge; - myCurrentBranch = currentBranch; myCurrentRevisionsBeforeMerge = currentRevisionsBeforeMerge; - myResultHandler = resultHandler; - myChangeListManager = ChangeListManager.getInstance(myProject); + myChangeListManager = myFacade.getChangeListManager(myProject); } @Override @@ -139,7 +133,6 @@ class GitMergeOperation extends GitBranchOperation { if (fatalErrorHappened) { notifyAboutRemainingConflicts(); - handleResult(false); } else { boolean allConflictsResolved = resolveConflicts(); @@ -150,22 +143,12 @@ class GitMergeOperation extends GitBranchOperation { else { notifySuccess("Already up-to-date"); } - handleResult(true); - } - else { - handleResult(false); } } restoreLocalChanges(); } - private void handleResult(boolean success) { - if (myResultHandler != null) { - myResultHandler.consume(success); - } - } - private void notifyAboutRemainingConflicts() { if (!myConflictedRepositories.isEmpty()) { new MyMergeConflictResolver().notifyUnresolvedRemain(); @@ -182,8 +165,7 @@ class GitMergeOperation extends GitBranchOperation { break; case PROPOSE: String description = message + "
Delete " + myBranchToMerge + ""; - Notificator.getInstance(myProject).notify(GitVcs.NOTIFICATION_GROUP_ID, "", description, NotificationType.INFORMATION, - new DeleteMergedLocalBranchNotificationListener()); + myUiHandler.notifySuccess("", description, new DeleteMergedLocalBranchNotificationListener()); break; case NOTHING: super.notifySuccess(message); @@ -191,10 +173,6 @@ class GitMergeOperation extends GitBranchOperation { } } - private boolean isMasterBranch() { - return myBranchToMerge.equals("master"); - } - private boolean resolveConflicts() { if (!myConflictedRepositories.isEmpty()) { return new MyMergeConflictResolver().merge(); @@ -205,11 +183,11 @@ class GitMergeOperation extends GitBranchOperation { private boolean proposeSmartMergePerformAndNotify(@NotNull GitRepository repository, @NotNull GitMessageWithFilesDetector localChangesOverwrittenByMerge) { Pair, List> conflictingRepositoriesAndAffectedChanges = - getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByMerge, myCurrentBranch, myBranchToMerge); + getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByMerge, myCurrentBranchOrRev, myBranchToMerge); List allConflictingRepositories = conflictingRepositoriesAndAffectedChanges.getFirst(); List affectedChanges = conflictingRepositoriesAndAffectedChanges.getSecond(); - int smartCheckoutDecision = GitSmartOperationDialog.showAndGetAnswer(myProject, affectedChanges, "merge", false); + int smartCheckoutDecision = myUiHandler.showSmartOperationDialog(myProject, affectedChanges, "merge", false); if (smartCheckoutDecision == GitSmartOperationDialog.SMART_EXIT_CODE) { return doSmartMerge(allConflictingRepositories); } @@ -227,7 +205,7 @@ class GitMergeOperation extends GitBranchOperation { private boolean doSmartMerge(@NotNull final Collection repositories) { final AtomicBoolean success = new AtomicBoolean(); - myPreservingProcess = new GitPreservingProcess(myProject, myGit, repositories, "merge", myBranchToMerge, getIndicator(), + myPreservingProcess = new GitPreservingProcess(myProject, myFacade, myGit, repositories, "merge", myBranchToMerge, getIndicator(), new Runnable() { @Override public void run() { @@ -321,7 +299,7 @@ class GitMergeOperation extends GitBranchOperation { private GitCompoundResult smartRollback(@NotNull final Collection repositories) { LOG.info("Starting smart rollback..."); final GitCompoundResult result = new GitCompoundResult(myProject); - GitPreservingProcess preservingProcess = new GitPreservingProcess(myProject, myGit, repositories, "merge", myBranchToMerge, + GitPreservingProcess preservingProcess = new GitPreservingProcess(myProject, myFacade, myGit, repositories, "merge", myBranchToMerge, getIndicator(), new Runnable() { @Override public void run() { @@ -354,7 +332,7 @@ class GitMergeOperation extends GitBranchOperation { @NotNull @Override public String getSuccessMessage() { - return String.format("Merged %s to %s", myBranchToMerge, myCurrentBranch); + return String.format("Merged %s to %s", myBranchToMerge, myCurrentBranchOrRev); } @NotNull diff --git a/plugins/git4idea/src/git4idea/checkin/GitCheckinEnvironment.java b/plugins/git4idea/src/git4idea/checkin/GitCheckinEnvironment.java index 4571e6dd8e63..d8c59a7b56b5 100644 --- a/plugins/git4idea/src/git4idea/checkin/GitCheckinEnvironment.java +++ b/plugins/git4idea/src/git4idea/checkin/GitCheckinEnvironment.java @@ -15,6 +15,7 @@ */ package git4idea.checkin; +import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; @@ -42,6 +43,7 @@ import com.intellij.util.ui.UIUtil; import com.intellij.vcsUtil.VcsFileUtil; import com.intellij.vcsUtil.VcsUtil; import git4idea.GitUtil; +import git4idea.PlatformFacade; import git4idea.commands.GitCommand; import git4idea.commands.GitSimpleHandler; import git4idea.config.GitConfigUtil; @@ -209,7 +211,7 @@ public class GitCheckinEnvironment implements CheckinEnvironment { // push UIUtil.invokeLaterIfNeeded(new Runnable() { public void run() { - GitPusher.showPushDialogAndPerformPush(myProject); + GitPusher.showPushDialogAndPerformPush(myProject, ServiceManager.getService(myProject, PlatformFacade.class)); } }); } diff --git a/plugins/git4idea/src/git4idea/commands/Git.java b/plugins/git4idea/src/git4idea/commands/Git.java index bfd3cefcc984..c9fc7f0555c7 100644 --- a/plugins/git4idea/src/git4idea/commands/Git.java +++ b/plugins/git4idea/src/git4idea/commands/Git.java @@ -18,6 +18,7 @@ package git4idea.commands; import com.intellij.openapi.project.Project; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vfs.VirtualFile; +import git4idea.history.browser.GitCommit; import git4idea.push.GitPushSpec; import git4idea.repo.GitRepository; import org.jetbrains.annotations.NotNull; @@ -51,6 +52,9 @@ public interface Git { @NotNull GitCommandResult config(@NotNull GitRepository repository, String... params); + @NotNull + GitCommandResult diff(@NotNull GitRepository repository, @NotNull List parameters, @NotNull String range); + @NotNull GitCommandResult merge(@NotNull GitRepository repository, @NotNull String branchToMerge, @Nullable List additionalParams, @NotNull GitLineHandlerListener... listeners); @@ -107,4 +111,13 @@ public interface Git { @NotNull GitCommandResult checkAttr(@NotNull GitRepository repository, @NotNull Collection attributes, @NotNull Collection files); + + @NotNull + GitCommandResult stashSave(@NotNull GitRepository repository, @NotNull String message); + + @NotNull + GitCommandResult stashPop(@NotNull GitRepository repository, @NotNull GitLineHandlerListener... listeners); + + @NotNull + List history(@NotNull GitRepository repository, @NotNull String range); } diff --git a/plugins/git4idea/src/git4idea/commands/GitImpl.java b/plugins/git4idea/src/git4idea/commands/GitImpl.java index 572a6b50ac45..557869ee51b5 100644 --- a/plugins/git4idea/src/git4idea/commands/GitImpl.java +++ b/plugins/git4idea/src/git4idea/commands/GitImpl.java @@ -24,6 +24,9 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ExceptionUtil; import com.intellij.vcsUtil.VcsFileUtil; import git4idea.GitBranch; +import git4idea.GitExecutionException; +import git4idea.history.GitHistoryUtils; +import git4idea.history.browser.GitCommit; import git4idea.push.GitPushSpec; import git4idea.repo.GitRemote; import git4idea.repo.GitRepository; @@ -143,6 +146,19 @@ public class GitImpl implements Git { return run(h); } + @NotNull + @Override + public GitCommandResult diff(@NotNull GitRepository repository, @NotNull List parameters, @NotNull String range) { + final GitLineHandler diff = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.DIFF); + diff.addParameters(parameters); + diff.addParameters(range); + diff.setNoSSH(true); + diff.setStdoutSuppressed(true); + diff.setStderrSuppressed(true); + diff.setSilent(true); + return run(diff); + } + @NotNull @Override public GitCommandResult checkAttr(@NotNull GitRepository repository, @NotNull Collection attributes, @@ -154,6 +170,35 @@ public class GitImpl implements Git { return run(h); } + @NotNull + @Override + public GitCommandResult stashSave(@NotNull GitRepository repository, @NotNull String message) { + final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.STASH); + h.addParameters("save"); + h.addParameters(message); + return run(h); + } + + @NotNull + @Override + public GitCommandResult stashPop(@NotNull GitRepository repository, @NotNull GitLineHandlerListener... listeners) { + final GitLineHandler handler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.STASH); + handler.addParameters("pop"); + return run(handler); + } + + @NotNull + @Override + public List history(@NotNull GitRepository repository, @NotNull String range) { + try { + return GitHistoryUtils.history(repository.getProject(), repository.getRoot(), range); + } + catch (VcsException e) { + // this is critical, because we need to show the list of unmerged commits, and it shouldn't happen => inform user and developer + throw new GitExecutionException("Couldn't get [git log " + range + "] on repository [" + repository.getRoot() + "]", e); + } + } + @Override @NotNull public GitCommandResult merge(@NotNull GitRepository repository, @NotNull String branchToMerge, diff --git a/plugins/git4idea/src/git4idea/commands/GitSimpleEventDetector.java b/plugins/git4idea/src/git4idea/commands/GitSimpleEventDetector.java index b057ad1aa2ca..7fd7d04ce59d 100644 --- a/plugins/git4idea/src/git4idea/commands/GitSimpleEventDetector.java +++ b/plugins/git4idea/src/git4idea/commands/GitSimpleEventDetector.java @@ -32,6 +32,7 @@ public class GitSimpleEventDetector implements GitLineHandlerListener { UNMERGED_PREVENTING_MERGE("is not possible because you have unmerged files"), BRANCH_NOT_FULLY_MERGED("is not fully merged"), MERGE_CONFLICT("Automatic merge failed; fix conflicts and then commit the result"), + MERGE_CONFLICT_ON_UNSTASH("Merge conflict"), ALREADY_UP_TO_DATE("Already up-to-date"); private final String myDetectionString; diff --git a/plugins/git4idea/src/git4idea/push/GitPushDialog.java b/plugins/git4idea/src/git4idea/push/GitPushDialog.java index 53fd94158637..2e26b537a79a 100644 --- a/plugins/git4idea/src/git4idea/push/GitPushDialog.java +++ b/plugins/git4idea/src/git4idea/push/GitPushDialog.java @@ -16,6 +16,7 @@ package git4idea.push; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.EmptyProgressIndicator; import com.intellij.openapi.project.Project; @@ -27,6 +28,7 @@ import com.intellij.util.Consumer; import com.intellij.util.ui.UIUtil; import git4idea.GitBranch; import git4idea.GitUtil; +import git4idea.PlatformFacade; import git4idea.branch.GitBranchUtil; import git4idea.history.browser.GitCommit; import git4idea.repo.GitRemote; @@ -66,7 +68,7 @@ public class GitPushDialog extends DialogWrapper { public GitPushDialog(@NotNull Project project) { super(project); myProject = project; - myPusher = new GitPusher(myProject, new EmptyProgressIndicator()); + myPusher = new GitPusher(myProject, ServiceManager.getService(project, PlatformFacade.class), new EmptyProgressIndicator()); myRepositoryManager = GitUtil.getRepositoryManager(myProject); myRepositories = getRepositoriesWithRemotes(); diff --git a/plugins/git4idea/src/git4idea/push/GitPusher.java b/plugins/git4idea/src/git4idea/push/GitPusher.java index 9968fedb790a..25a269cca29b 100644 --- a/plugins/git4idea/src/git4idea/push/GitPusher.java +++ b/plugins/git4idea/src/git4idea/push/GitPusher.java @@ -27,10 +27,7 @@ import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.update.UpdatedFiles; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ui.UIUtil; -import git4idea.GitBranch; -import git4idea.GitUtil; -import git4idea.GitVcs; -import git4idea.Notificator; +import git4idea.*; import git4idea.branch.GitBranchPair; import git4idea.commands.Git; import git4idea.commands.GitCommandResult; @@ -78,15 +75,16 @@ public final class GitPusher { private final @NotNull GitVcsSettings mySettings; private final @NotNull GitPushSettings myPushSettings; private final @NotNull Git myGit; + @NotNull private final PlatformFacade myPlatformFacade; - public static void showPushDialogAndPerformPush(@NotNull final Project project) { + public static void showPushDialogAndPerformPush(@NotNull final Project project, @NotNull final PlatformFacade facade) { final GitPushDialog dialog = new GitPushDialog(project); dialog.show(); if (dialog.isOK()) { Task.Backgroundable task = new Task.Backgroundable(project, INDICATOR_TEXT, false) { @Override public void run(@NotNull ProgressIndicator indicator) { - new GitPusher(project, indicator).push(dialog.getPushInfo()); + new GitPusher(project, facade, indicator).push(dialog.getPushInfo()); } }; GitVcs.runInBackground(task); @@ -121,8 +119,9 @@ public final class GitPusher { } } - public GitPusher(@NotNull Project project, @NotNull ProgressIndicator indicator) { + public GitPusher(@NotNull Project project, @NotNull PlatformFacade facade, @NotNull ProgressIndicator indicator) { myProject = project; + myPlatformFacade = facade; myProgressIndicator = indicator; myRepositoryManager = GitUtil.getRepositoryManager(myProject); myRepositories = myRepositoryManager.getRepositories(); @@ -551,8 +550,8 @@ public final class GitPusher { private boolean update(@NotNull Collection rootsToUpdate, @NotNull UpdateMethod updateMethod) { GitUpdateProcess.UpdateMethod um = updateMethod == UpdateMethod.MERGE ? GitUpdateProcess.UpdateMethod.MERGE : GitUpdateProcess.UpdateMethod.REBASE; - GitUpdateResult updateResult = new GitUpdateProcess(myProject, myProgressIndicator, new HashSet(rootsToUpdate), - UpdatedFiles.create()).update(um); + GitUpdateResult updateResult = new GitUpdateProcess(myProject, myPlatformFacade, myProgressIndicator, + new HashSet(rootsToUpdate), UpdatedFiles.create()).update(um); for (GitRepository repository : rootsToUpdate) { repository.getRoot().refresh(true, true); } diff --git a/plugins/git4idea/src/git4idea/repo/GitRepositoryImpl.java b/plugins/git4idea/src/git4idea/repo/GitRepositoryImpl.java index 0e95f68a22da..b55c16ca1b84 100644 --- a/plugins/git4idea/src/git4idea/repo/GitRepositoryImpl.java +++ b/plugins/git4idea/src/git4idea/repo/GitRepositoryImpl.java @@ -61,7 +61,7 @@ public class GitRepositoryImpl implements GitRepository, Disposable { * If you need to have an instance of GitRepository for a repository outside the project, use * {@link #getLightInstance(VirtualFile, Project, PlatformFacade, Disposable)}. */ - private GitRepositoryImpl(@NotNull VirtualFile rootDir, @NotNull PlatformFacade facade, @NotNull Project project, + protected GitRepositoryImpl(@NotNull VirtualFile rootDir, @NotNull PlatformFacade facade, @NotNull Project project, @NotNull Disposable parentDisposable, final boolean light) { myRootDir = rootDir; myPlatformFacade = facade; @@ -271,7 +271,7 @@ public class GitRepositoryImpl implements GitRepository, Disposable { myBranches = myReader.readBranches(); } - private void notifyListeners() { + protected void notifyListeners() { myNotifier.add(STUB_OBJECT); // we don't have parameters for listeners } diff --git a/plugins/git4idea/src/git4idea/stash/GitChangesSaver.java b/plugins/git4idea/src/git4idea/stash/GitChangesSaver.java index fddc877d85fe..0972cd3a59b1 100644 --- a/plugins/git4idea/src/git4idea/stash/GitChangesSaver.java +++ b/plugins/git4idea/src/git4idea/stash/GitChangesSaver.java @@ -26,6 +26,7 @@ import com.intellij.openapi.vcs.changes.ChangeListManagerEx; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.continuation.ContinuationContext; import git4idea.GitVcs; +import git4idea.PlatformFacade; import git4idea.commands.Git; import git4idea.config.GitVcsSettings; import git4idea.merge.GitConflictResolver; @@ -45,11 +46,12 @@ public abstract class GitChangesSaver { private static final Logger LOG = Logger.getInstance(GitChangesSaver.class); - protected final Project myProject; - protected final ChangeListManagerEx myChangeManager; + @NotNull protected final Project myProject; + @NotNull protected final PlatformFacade myPlatformFacade; + @NotNull protected final ChangeListManagerEx myChangeManager; @NotNull protected final Git myGit; - protected final ProgressIndicator myProgressIndicator; - protected final String myStashMessage; + @NotNull protected final ProgressIndicator myProgressIndicator; + @NotNull protected final String myStashMessage; protected GitConflictResolver.Params myParams; @@ -62,30 +64,33 @@ public abstract class GitChangesSaver { * Returns an instance of the proper GitChangesSaver depending on the chosen save changes policy. * @return {@link GitStashChangesSaver}, {@link GitShelveChangesSaver} or {@link GitDumbChangesSaver} */ - public static GitChangesSaver getSaver(Project project, @NotNull Git git, ProgressIndicator progressIndicator, String stashMessage) { + public static GitChangesSaver getSaver(@NotNull Project project, @NotNull PlatformFacade platformFacade, @NotNull Git git, + @NotNull ProgressIndicator progressIndicator, @NotNull String stashMessage) { final GitVcsSettings settings = GitVcsSettings.getInstance(project); if (settings == null) { - return getDefaultSaver(project, git, progressIndicator, stashMessage); + return getDefaultSaver(project, platformFacade, git, progressIndicator, stashMessage); } switch (settings.updateChangesPolicy()) { - case STASH: return new GitStashChangesSaver(project, git, progressIndicator, stashMessage); - case SHELVE: return new GitShelveChangesSaver(project, git, progressIndicator, stashMessage); + case STASH: return new GitStashChangesSaver(project, platformFacade, git, progressIndicator, stashMessage); + case SHELVE: return new GitShelveChangesSaver(project, platformFacade, git, progressIndicator, stashMessage); } - return getDefaultSaver(project, git, progressIndicator, stashMessage); + return getDefaultSaver(project, platformFacade, git, progressIndicator, stashMessage); } // In the case of illegal value in the settings or impossibility to get the settings. - private static GitChangesSaver getDefaultSaver(Project project, @NotNull Git git, ProgressIndicator progressIndicator, - String stashMessage) { - return new GitStashChangesSaver(project, git, progressIndicator, stashMessage); + private static GitChangesSaver getDefaultSaver(@NotNull Project project, @NotNull PlatformFacade platformFacade, @NotNull Git git, + @NotNull ProgressIndicator progressIndicator, @NotNull String stashMessage) { + return new GitStashChangesSaver(project, platformFacade, git, progressIndicator, stashMessage); } - protected GitChangesSaver(Project project, @NotNull Git git, ProgressIndicator indicator, String stashMessage) { + protected GitChangesSaver(@NotNull Project project, @NotNull PlatformFacade platformFacade, @NotNull Git git, + @NotNull ProgressIndicator indicator, @NotNull String stashMessage) { myProject = project; + myPlatformFacade = platformFacade; myGit = git; myProgressIndicator = indicator; myStashMessage = stashMessage; - myChangeManager = (ChangeListManagerEx)ChangeListManagerEx.getInstance(myProject); + myChangeManager = platformFacade.getChangeListManager(project); } /** diff --git a/plugins/git4idea/src/git4idea/stash/GitShelveChangesSaver.java b/plugins/git4idea/src/git4idea/stash/GitShelveChangesSaver.java index 467bf02936c8..cb64368ceb25 100644 --- a/plugins/git4idea/src/git4idea/stash/GitShelveChangesSaver.java +++ b/plugins/git4idea/src/git4idea/stash/GitShelveChangesSaver.java @@ -28,6 +28,7 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.continuation.ContinuationContext; import com.intellij.util.continuation.TaskDescriptor; import com.intellij.util.continuation.Where; +import git4idea.PlatformFacade; import git4idea.commands.Git; import git4idea.i18n.GitBundle; import git4idea.rollback.GitRollbackEnvironment; @@ -44,8 +45,9 @@ public class GitShelveChangesSaver extends GitChangesSaver { private final ShelvedChangesViewManager myShelveViewManager; private Map myShelvedLists; - public GitShelveChangesSaver(Project project, Git git, ProgressIndicator indicator, String stashMessage) { - super(project, git, indicator, stashMessage); + public GitShelveChangesSaver(@NotNull Project project, PlatformFacade platformFacade, @NotNull Git git, + @NotNull ProgressIndicator indicator, String stashMessage) { + super(project, platformFacade, git, indicator, stashMessage); myShelveManager = ShelveChangesManager.getInstance(myProject); myShelveViewManager = ShelvedChangesViewManager.getInstance(myProject); } @@ -53,7 +55,8 @@ public class GitShelveChangesSaver extends GitChangesSaver { @Override protected void save(@NotNull Collection rootsToSave) throws VcsException { LOG.info("save " + rootsToSave); - final Map>> lists = new LocalChangesUnderRoots(myProject).getChangesByLists(rootsToSave); + final Map>> lists = + new LocalChangesUnderRoots(myChangeManager, myPlatformFacade.getVcsManager(myProject)).getChangesByLists(rootsToSave); String oldProgressTitle = myProgressIndicator.getText(); myProgressIndicator.setText(GitBundle.getString("update.shelving.changes")); diff --git a/plugins/git4idea/src/git4idea/stash/GitStashChangesSaver.java b/plugins/git4idea/src/git4idea/stash/GitStashChangesSaver.java index ecd86ea2bf53..1a1f73a9d830 100644 --- a/plugins/git4idea/src/git4idea/stash/GitStashChangesSaver.java +++ b/plugins/git4idea/src/git4idea/stash/GitStashChangesSaver.java @@ -18,11 +18,9 @@ package git4idea.stash; import com.intellij.notification.Notification; import com.intellij.notification.NotificationListener; import com.intellij.notification.NotificationType; -import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.Key; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.history.VcsRevisionNumber; @@ -33,8 +31,13 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.continuation.ContinuationContext; import git4idea.GitVcs; import git4idea.PlatformFacade; -import git4idea.commands.*; +import git4idea.commands.Git; +import git4idea.commands.GitCommandResult; +import git4idea.commands.GitHandlerUtil; +import git4idea.commands.GitSimpleEventDetector; import git4idea.merge.GitConflictResolver; +import git4idea.repo.GitRepository; +import git4idea.repo.GitRepositoryManager; import git4idea.ui.GitUnstashDialog; import git4idea.util.GitUIUtil; import org.jetbrains.annotations.NotNull; @@ -42,9 +45,6 @@ import org.jetbrains.annotations.Nullable; import javax.swing.event.HyperlinkEvent; import java.util.*; -import java.util.concurrent.atomic.AtomicBoolean; - -import static com.intellij.notification.NotificationType.WARNING; /** * @author Kirill Likhodedov @@ -53,15 +53,19 @@ public class GitStashChangesSaver extends GitChangesSaver { private static final Logger LOG = Logger.getInstance(GitStashChangesSaver.class); private final Set myStashedRoots = new HashSet(); // save stashed roots to unstash only them + @NotNull private final GitRepositoryManager myRepositoryManager; - public GitStashChangesSaver(Project project, @NotNull Git git, ProgressIndicator progressIndicator, String stashMessage) { - super(project, git, progressIndicator, stashMessage); + public GitStashChangesSaver(@NotNull Project project, @NotNull PlatformFacade platformFacade, @NotNull Git git, + @NotNull ProgressIndicator progressIndicator, @NotNull String stashMessage) { + super(project, platformFacade, git, progressIndicator, stashMessage); + myRepositoryManager = platformFacade.getRepositoryManager(project); } @Override protected void save(Collection rootsToSave) throws VcsException { LOG.info("save " + rootsToSave); - final Map> changes = new LocalChangesUnderRoots(myProject).getChangesUnderRoots(rootsToSave); + final Map> changes = + new LocalChangesUnderRoots(myChangeManager, myPlatformFacade.getVcsManager(myProject)).getChangesUnderRoots(rootsToSave); stash(changes.keySet()); } @@ -80,7 +84,7 @@ public class GitStashChangesSaver extends GitChangesSaver { loadRoot(root); } - boolean conflictsResolved = new UnstashConflictResolver(myProject, myGit, myStashedRoots, myParams).merge(); + boolean conflictsResolved = new UnstashConflictResolver(myProject, myPlatformFacade, myGit, myStashedRoots, myParams).merge(); LOG.info("load: conflicts resolved status is " + conflictsResolved + " in roots " + myStashedRoots); } @@ -111,8 +115,12 @@ public class GitStashChangesSaver extends GitChangesSaver { LOG.info(message); final String oldProgressTitle = myProgressIndicator.getText(); myProgressIndicator.setText(message); - if (GitStashUtils.saveStash(myProject, root, myStashMessage)) { - myStashedRoots.add(root); + GitRepository repository = myRepositoryManager.getRepositoryForRoot(root); + if (repository == null) { + LOG.error("Repository is null for root " + root); + } + else if (GitStashUtils.saveStash(myGit, repository, myStashMessage)) { + myStashedRoots.add(root); } myProgressIndicator.setText(oldProgressTitle); } @@ -125,55 +133,35 @@ public class GitStashChangesSaver extends GitChangesSaver { private boolean loadRoot(final VirtualFile root) throws VcsException { LOG.info("loadRoot " + root); myProgressIndicator.setText(GitHandlerUtil.formatOperationName("Unstashing changes to", root)); - final GitLineHandler handler = new GitLineHandler(myProject, root, GitCommand.STASH); - handler.setNoSSH(true); - handler.addParameters("pop"); - final AtomicBoolean conflict = new AtomicBoolean(); - handler.addLineListener(new GitLineHandlerAdapter() { - @Override - public void onLineAvailable(String line, Key outputType) { - if (line.contains("Merge conflict")) { - conflict.set(true); - } - } - }); + GitRepository repository = myRepositoryManager.getRepositoryForRoot(root); + if (repository == null) { + LOG.error("Repository is null for root " + root); + return false; + } - final GitTask task = new GitTask(myProject, handler, "Unstashing uncommitted changes"); - task.setProgressIndicator(myProgressIndicator); - final AtomicBoolean failure = new AtomicBoolean(); - task.executeInBackground(true, new GitTaskResultHandlerAdapter() { - @Override protected void onSuccess() { - } - - @Override protected void onCancel() { - GitVcs.NOTIFICATION_GROUP_ID.createNotification("Unstash cancelled", - "You may view the stashed changes here", WARNING, - new ShowSavedChangesNotificationListener()).notify(myProject); - } - - @Override protected void onFailure() { - failure.set(true); - } - }); - - if (failure.get()) { - if (conflict.get()) { - return true; - } else { - LOG.info("unstash failed " + handler.errors()); - GitUIUtil.notifyImportantError(myProject, "Couldn't unstash", "
" + GitUIUtil.stringifyErrors(handler.errors())); - } + GitSimpleEventDetector conflictDetector = new GitSimpleEventDetector(GitSimpleEventDetector.Event.MERGE_CONFLICT_ON_UNSTASH); + GitCommandResult result = myGit.stashPop(repository, conflictDetector); + 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; } - return false; } private static class UnstashConflictResolver extends GitConflictResolver { private final Set myStashedRoots; - public UnstashConflictResolver(@NotNull Project project, @NotNull Git git, @NotNull Set stashedRoots, @Nullable Params params) { - super(project, git, ServiceManager.getService(PlatformFacade.class), stashedRoots, makeParamsOrUse(params)); + public UnstashConflictResolver(@NotNull Project project, PlatformFacade platformFacade, @NotNull Git git, + @NotNull Set stashedRoots, @Nullable Params params) { + super(project, git, platformFacade, stashedRoots, makeParamsOrUse(params)); myStashedRoots = stashedRoots; } diff --git a/plugins/git4idea/src/git4idea/stash/GitStashUtils.java b/plugins/git4idea/src/git4idea/stash/GitStashUtils.java index 685164150f63..90cfac4153c6 100644 --- a/plugins/git4idea/src/git4idea/stash/GitStashUtils.java +++ b/plugins/git4idea/src/git4idea/stash/GitStashUtils.java @@ -20,18 +20,19 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.Consumer; +import git4idea.commands.Git; import git4idea.commands.GitCommand; +import git4idea.commands.GitCommandResult; import git4idea.commands.GitSimpleHandler; -import git4idea.util.StringScanner; import git4idea.config.GitConfigUtil; -import git4idea.util.GitUIUtil; +import git4idea.repo.GitRepository; import git4idea.ui.StashInfo; +import git4idea.util.GitUIUtil; +import git4idea.util.StringScanner; import org.jetbrains.annotations.NotNull; import java.nio.charset.Charset; -import static com.intellij.notification.NotificationType.WARNING; - /** * The class contains utilities for creating and removing stashes. */ @@ -42,20 +43,9 @@ public class GitStashUtils { private GitStashUtils() { } - /** - * Create stash for later use - * - * @param project the project to use - * @param root the root - * @param message the message for the stash - * @return true if the stash was created, false otherwise - */ - public static boolean saveStash(@NotNull Project project, @NotNull VirtualFile root, final String message) throws VcsException { - GitSimpleHandler handler = new GitSimpleHandler(project, root, GitCommand.STASH); - handler.setNoSSH(true); - handler.addParameters("save", message); - String output = handler.run(); - return !output.startsWith("No local changes to save"); + public static boolean saveStash(@NotNull Git git, @NotNull GitRepository repository, final String message) { + GitCommandResult result = git.stashSave(repository, message); + return result.success() && !result.getErrorOutputAsJoinedString().contains("No local changes to save"); } public static void loadStashStack(@NotNull Project project, @NotNull VirtualFile root, Consumer consumer) { diff --git a/plugins/git4idea/src/git4idea/ui/branch/GitBranchPopupActions.java b/plugins/git4idea/src/git4idea/ui/branch/GitBranchPopupActions.java index 58441d8bb3dd..00c3a4378964 100644 --- a/plugins/git4idea/src/git4idea/ui/branch/GitBranchPopupActions.java +++ b/plugins/git4idea/src/git4idea/ui/branch/GitBranchPopupActions.java @@ -95,7 +95,7 @@ class GitBranchPopupActions { final String name = GitBranchUiUtil.getNewBranchNameFromUser(myProject, myRepositories, "Create New Branch"); if (name != null) { GitBrancher brancher = ServiceManager.getService(myProject, GitBrancher.class); - brancher.checkoutNewBranch(name, myRepositories, null); + brancher.checkoutNewBranch(name, myRepositories); } } @@ -394,7 +394,7 @@ class GitBranchPopupActions { @Override public void actionPerformed(AnActionEvent e) { GitBrancher brancher = ServiceManager.getService(myProject, GitBrancher.class); - brancher.merge(myBranchName, deleteOnMerge(), myRepositories, null); + brancher.merge(myBranchName, deleteOnMerge(), myRepositories); } private GitBrancher.DeleteOnMergeOption deleteOnMerge() { diff --git a/plugins/git4idea/src/git4idea/ui/branch/GitMultiRootBranchConfig.java b/plugins/git4idea/src/git4idea/ui/branch/GitMultiRootBranchConfig.java index 2072bc8c7fa1..51919ebf9b73 100644 --- a/plugins/git4idea/src/git4idea/ui/branch/GitMultiRootBranchConfig.java +++ b/plugins/git4idea/src/git4idea/ui/branch/GitMultiRootBranchConfig.java @@ -28,16 +28,15 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.List; /** * @author Kirill Likhodedov */ public class GitMultiRootBranchConfig { - private final List myRepositories; + private final Collection myRepositories; - public GitMultiRootBranchConfig(@NotNull List repositories) { + public GitMultiRootBranchConfig(@NotNull Collection repositories) { myRepositories = repositories; } diff --git a/plugins/git4idea/src/git4idea/update/GitComplexProcess.java b/plugins/git4idea/src/git4idea/update/GitComplexProcess.java index ca6e480afc99..2f4c96432592 100644 --- a/plugins/git4idea/src/git4idea/update/GitComplexProcess.java +++ b/plugins/git4idea/src/git4idea/update/GitComplexProcess.java @@ -15,6 +15,7 @@ */ package git4idea.update; +import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.vcs.changes.ChangeListManager; import com.intellij.util.continuation.Continuation; @@ -22,6 +23,7 @@ import com.intellij.util.continuation.ContinuationContext; import com.intellij.util.continuation.TaskDescriptor; import com.intellij.util.continuation.Where; import git4idea.GitUtil; +import git4idea.PlatformFacade; import git4idea.repo.GitRepository; import git4idea.repo.GitRepositoryManager; import git4idea.util.GitFreezingProcess; @@ -57,7 +59,7 @@ public class GitComplexProcess { private final TaskDescriptor BLOCK = new TaskDescriptor("", Where.AWT) { @Override public void run(ContinuationContext context) { - GitFreezingProcess.saveAndBlock(); + GitFreezingProcess.saveAndBlock(ServiceManager.getService(myProject, PlatformFacade.class)); } }; @@ -75,7 +77,7 @@ public class GitComplexProcess { private final TaskDescriptor UNBLOCK = new TaskDescriptor("", Where.AWT) { @Override public void run(ContinuationContext context) { - GitFreezingProcess.unblock(); + GitFreezingProcess.unblock(ServiceManager.getService(myProject, PlatformFacade.class)); } @Override public boolean isHaveMagicCure() { diff --git a/plugins/git4idea/src/git4idea/update/GitMergeUpdater.java b/plugins/git4idea/src/git4idea/update/GitMergeUpdater.java index 6d4c72597fac..529b6f930784 100644 --- a/plugins/git4idea/src/git4idea/update/GitMergeUpdater.java +++ b/plugins/git4idea/src/git4idea/update/GitMergeUpdater.java @@ -35,6 +35,7 @@ import git4idea.branch.GitBranchPair; import git4idea.commands.*; import git4idea.merge.GitConflictResolver; import git4idea.merge.GitMerger; +import git4idea.repo.GitRepository; import git4idea.util.GitUIUtil; import git4idea.util.UntrackedFilesNotifier; import org.jetbrains.annotations.NotNull; @@ -164,7 +165,13 @@ public class GitMergeUpdater extends GitUpdater { String currentBranch = gitBranchPair.getBranch().getName(); String remoteBranch = gitBranchPair.getDest().getName(); try { - final Collection remotelyChanged = GitUtil.getPathsDiffBetweenRefs(currentBranch, remoteBranch, myProject, myRoot); + GitRepository repository = GitUtil.getRepositoryManager(myProject).getRepositoryForRoot(myRoot); + if (repository == null) { + LOG.error("Repository is null for root " + myRoot); + return true; // fail safe + } + final Collection remotelyChanged = GitUtil.getPathsDiffBetweenRefs(ServiceManager.getService(Git.class), repository, + currentBranch, remoteBranch); final List locallyChanged = myChangeListManager.getAffectedPaths(); for (File localPath : locallyChanged) { if (remotelyChanged.contains(FilePathsHelper.convertPath(localPath.getPath()))) { diff --git a/plugins/git4idea/src/git4idea/update/GitUpdateEnvironment.java b/plugins/git4idea/src/git4idea/update/GitUpdateEnvironment.java index 0fa3515fdf7c..acad52589f94 100644 --- a/plugins/git4idea/src/git4idea/update/GitUpdateEnvironment.java +++ b/plugins/git4idea/src/git4idea/update/GitUpdateEnvironment.java @@ -15,6 +15,7 @@ */ package git4idea.update; +import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.options.Configurable; import com.intellij.openapi.progress.ProcessCanceledException; @@ -28,6 +29,7 @@ import com.intellij.openapi.vcs.update.UpdateSession; import com.intellij.openapi.vcs.update.UpdatedFiles; import com.intellij.openapi.vfs.VirtualFile; import git4idea.GitVcs; +import git4idea.PlatformFacade; import git4idea.config.GitVcsSettings; import git4idea.repo.GitRepositoryManager; import org.jetbrains.annotations.NotNull; @@ -48,12 +50,15 @@ public class GitUpdateEnvironment implements UpdateEnvironment { private final GitVcs myVcs; private final Project myProject; private final GitVcsSettings mySettings; + @NotNull private final PlatformFacade myPlatformFacade; + private static final Logger LOG = Logger.getInstance(GitUpdateEnvironment.class); public GitUpdateEnvironment(@NotNull Project project, @NotNull GitVcs vcs, GitVcsSettings settings) { myVcs = vcs; myProject = project; mySettings = settings; + myPlatformFacade = ServiceManager.getService(project, PlatformFacade.class); } public void fillGroups(UpdatedFiles updatedFiles) { @@ -64,8 +69,9 @@ public class GitUpdateEnvironment implements UpdateEnvironment { public UpdateSession updateDirectories(@NotNull FilePath[] filePaths, UpdatedFiles updatedFiles, ProgressIndicator progressIndicator, @NotNull 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); + final GitUpdateProcess gitUpdateProcess = new GitUpdateProcess(myProject, myPlatformFacade, + progressIndicator, getRepositoriesFromRoots(repositoryManager, roots), + updatedFiles); boolean result = gitUpdateProcess.update(GitUpdateProcess.UpdateMethod.READ_FROM_SETTINGS).isSuccess(); return new GitUpdateSession(result); } diff --git a/plugins/git4idea/src/git4idea/update/GitUpdateProcess.java b/plugins/git4idea/src/git4idea/update/GitUpdateProcess.java index 2de2a91e3dec..73b8105e45d4 100644 --- a/plugins/git4idea/src/git4idea/update/GitUpdateProcess.java +++ b/plugins/git4idea/src/git4idea/update/GitUpdateProcess.java @@ -21,8 +21,10 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Clock; +import com.intellij.openapi.vcs.ProjectLevelVcsManager; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vcs.changes.ChangeListManager; import com.intellij.openapi.vcs.impl.LocalChangesUnderRoots; import com.intellij.openapi.vcs.update.UpdatedFiles; import com.intellij.openapi.vfs.VirtualFile; @@ -81,7 +83,7 @@ public class GitUpdateProcess { READ_FROM_SETTINGS } - public GitUpdateProcess(@NotNull Project project, @NotNull ProgressIndicator progressIndicator, + public GitUpdateProcess(@NotNull Project project, @NotNull PlatformFacade platformFacade, @NotNull ProgressIndicator progressIndicator, @NotNull Collection repositories, @NotNull UpdatedFiles updatedFiles) { myProject = project; myRepositories = repositories; @@ -89,8 +91,9 @@ public class GitUpdateProcess { myUpdatedFiles = updatedFiles; myProgressIndicator = progressIndicator; myMerger = new GitMerger(myProject); - mySaver = GitChangesSaver.getSaver(myProject, myGit, myProgressIndicator, - "Uncommitted changes before update operation at " + DateFormatUtil.formatDateTime(Clock.getTime())); + mySaver = GitChangesSaver.getSaver(myProject, platformFacade, myGit, + myProgressIndicator, + "Uncommitted changes before update operation at " + DateFormatUtil.formatDateTime(Clock.getTime())); myRootsToSave = new HashSet(1); } @@ -218,7 +221,9 @@ public class GitUpdateProcess { @NotNull private Map tryFastForwardMergeForRebaseUpdaters(@NotNull Map updaters) { Map modifiedUpdaters = new HashMap(); - Map> changesUnderRoots = new LocalChangesUnderRoots(myProject).getChangesUnderRoots(updaters.keySet()); + Map> changesUnderRoots = + new LocalChangesUnderRoots(ChangeListManager.getInstance(myProject), ProjectLevelVcsManager.getInstance(myProject)). + getChangesUnderRoots(updaters.keySet()); for (Map.Entry updaterEntry : updaters.entrySet()) { VirtualFile root = updaterEntry.getKey(); GitUpdater updater = updaterEntry.getValue(); diff --git a/plugins/git4idea/src/git4idea/util/GitFreezingProcess.java b/plugins/git4idea/src/git4idea/util/GitFreezingProcess.java index 5d4975498c30..d0027984164d 100644 --- a/plugins/git4idea/src/git4idea/util/GitFreezingProcess.java +++ b/plugins/git4idea/src/git4idea/util/GitFreezingProcess.java @@ -15,13 +15,11 @@ */ package git4idea.util; -import com.intellij.ide.SaveAndSyncHandler; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.project.Project; -import com.intellij.openapi.project.ex.ProjectManagerEx; -import com.intellij.openapi.vcs.changes.ChangeListManagerImpl; +import com.intellij.openapi.vcs.changes.ChangeListManagerEx; import com.intellij.util.ui.UIUtil; +import git4idea.PlatformFacade; import org.jetbrains.annotations.NotNull; /** @@ -35,14 +33,17 @@ public class GitFreezingProcess { private static final Logger LOG = Logger.getInstance(GitFreezingProcess.class); + @NotNull private final PlatformFacade myFacade; @NotNull private final String myOperationTitle; @NotNull private final Runnable myRunnable; - @NotNull private final ChangeListManagerImpl myChangeListManager; + @NotNull private final ChangeListManagerEx myChangeListManager; - public GitFreezingProcess(@NotNull Project project, @NotNull String operationTitle, @NotNull Runnable runnable) { + public GitFreezingProcess(@NotNull Project project, @NotNull PlatformFacade facade, + @NotNull String operationTitle, @NotNull Runnable runnable) { + myFacade = facade; myOperationTitle = operationTitle; myRunnable = runnable; - myChangeListManager = ChangeListManagerImpl.getInstanceImpl(project); + myChangeListManager = myFacade.getChangeListManager(project); } public void execute() { @@ -69,37 +70,37 @@ public class GitFreezingProcess { LOG.debug("finished."); } - public static void saveAndBlock() { - ProjectManagerEx.getInstanceEx().blockReloadingProjectOnExternalChanges(); - FileDocumentManager.getInstance().saveAllDocuments(); - SaveAndSyncHandler.getInstance().blockSaveOnFrameDeactivation(); - SaveAndSyncHandler.getInstance().blockSyncOnFrameActivation(); + public static void saveAndBlock(@NotNull PlatformFacade platformFacade) { + platformFacade.getProjectManager().blockReloadingProjectOnExternalChanges(); + platformFacade.saveAllDocuments(); + platformFacade.getSaveAndSyncHandler().blockSaveOnFrameDeactivation(); + platformFacade.getSaveAndSyncHandler().blockSyncOnFrameActivation(); } - private static void saveAndBlockInAwt() { + private void saveAndBlockInAwt() { RethrowingRunnable rethrowingRunnable = new RethrowingRunnable(new Runnable() { @Override public void run() { - saveAndBlock(); + saveAndBlock(myFacade); } }); UIUtil.invokeAndWaitIfNeeded(rethrowingRunnable); rethrowingRunnable.rethrowIfHappened(); } - private static void unblockInAwt() { + private void unblockInAwt() { RethrowingRunnable rethrowingRunnable = new RethrowingRunnable(new Runnable() { @Override public void run() { - unblock(); + unblock(myFacade); } }); UIUtil.invokeAndWaitIfNeeded(rethrowingRunnable); rethrowingRunnable.rethrowIfHappened(); } - public static void unblock() { - ProjectManagerEx.getInstanceEx().unblockReloadingProjectOnExternalChanges(); - SaveAndSyncHandler.getInstance().unblockSaveOnFrameDeactivation(); - SaveAndSyncHandler.getInstance().unblockSyncOnFrameActivation(); + public static void unblock(@NotNull PlatformFacade platformFacade) { + platformFacade.getProjectManager().unblockReloadingProjectOnExternalChanges(); + platformFacade.getSaveAndSyncHandler().unblockSaveOnFrameDeactivation(); + platformFacade.getSaveAndSyncHandler().unblockSyncOnFrameActivation(); } private void freeze() { diff --git a/plugins/git4idea/src/git4idea/util/GitPreservingProcess.java b/plugins/git4idea/src/git4idea/util/GitPreservingProcess.java index 2f0463065066..a1fd4fe5e385 100644 --- a/plugins/git4idea/src/git4idea/util/GitPreservingProcess.java +++ b/plugins/git4idea/src/git4idea/util/GitPreservingProcess.java @@ -28,6 +28,7 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.text.DateFormatUtil; import git4idea.GitUtil; import git4idea.Notificator; +import git4idea.PlatformFacade; import git4idea.commands.Git; import git4idea.merge.GitConflictResolver; import git4idea.repo.GitRepository; @@ -50,6 +51,7 @@ public class GitPreservingProcess { private static final Logger LOG = Logger.getInstance(GitPreservingProcess.class); @NotNull private final Project myProject; + @NotNull private final PlatformFacade myFacade; @NotNull private final Git myGit; @NotNull private final Collection myRepositories; @NotNull private final String myOperationTitle; @@ -63,10 +65,12 @@ public class GitPreservingProcess { private boolean myLoaded; private final Object LOAD_LOCK = new Object(); - public GitPreservingProcess(@NotNull Project project, @NotNull Git git, @NotNull Collection repositories, + public GitPreservingProcess(@NotNull Project project, @NotNull PlatformFacade facade, @NotNull Git git, + @NotNull Collection repositories, @NotNull String operationTitle, @NotNull String destinationName, @NotNull ProgressIndicator indicator, @NotNull Runnable operation) { myProject = project; + myFacade = facade; myGit = git; myRepositories = repositories; myOperationTitle = operationTitle; @@ -106,14 +110,14 @@ public class GitPreservingProcess { } }; - new GitFreezingProcess(myProject, myOperationTitle, operation).execute(); + new GitFreezingProcess(myProject, myFacade, myOperationTitle, operation).execute(); } /** * Configures the saver, actually notifications and texts in the GitConflictResolver used inside. */ private GitStashChangesSaver configureSaver() { - GitStashChangesSaver saver = new GitStashChangesSaver(myProject, myGit, myProgressIndicator, myStashMessage); + GitStashChangesSaver saver = new GitStashChangesSaver(myProject, myFacade, myGit, myProgressIndicator, myStashMessage); MergeDialogCustomizer mergeDialogCustomizer = new MergeDialogCustomizer() { @Override public String getMultipleFileMergeDescription(Collection files) { diff --git a/plugins/git4idea/tests/git4idea/branch/GitBranchOperationsTest.java b/plugins/git4idea/tests/git4idea/branch/GitBranchOperationsTest.java deleted file mode 100644 index ac2072b2e5a2..000000000000 --- a/plugins/git4idea/tests/git4idea/branch/GitBranchOperationsTest.java +++ /dev/null @@ -1,780 +0,0 @@ -package git4idea.branch; - -import com.intellij.notification.Notification; -import com.intellij.notification.NotificationType; -import com.intellij.openapi.components.ServiceManager; -import com.intellij.openapi.progress.EmptyProgressIndicator; -import com.intellij.openapi.progress.ProgressIndicator; -import com.intellij.openapi.ui.DialogWrapper; -import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.util.text.StringUtil; -import com.intellij.openapi.vcs.ProjectLevelVcsManager; -import com.intellij.openapi.vcs.VcsConfiguration; -import com.intellij.openapi.vcs.VcsShowConfirmationOption; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.testFramework.vcs.AbstractVcsTestCase; -import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory; -import com.intellij.testFramework.fixtures.TempDirTestFixture; -import com.intellij.util.ui.UIUtil; -import com.intellij.vcsUtil.VcsUtil; -import git4idea.GitVcs; -import git4idea.repo.GitRepository; -import git4idea.test.*; -import git4idea.tests.TestDialogHandler; -import git4idea.tests.TestDialogManager; -import git4idea.util.UntrackedFilesNotifier; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.Test; - -import java.io.File; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; - -import static git4idea.test.GitExec.*; -import static git4idea.util.GitUIUtil.getShortRepositoryName; -import static java.util.Collections.singletonList; -import static org.testng.Assert.*; - -/** - * @author Kirill Likhodedov - */ -public class GitBranchOperationsTest extends AbstractVcsTestCase { - - private static final String NEW_BRANCH = "new_branch"; - private static final String MASTER = "master"; - - private List myRepositories; - private GitRepository myUltimate; - private GitRepository myCommunity; - private GitRepository myContrib; - - private TestMessageManager myMessageManager; - private TestNotificator myNotificationManager; - private TestDialogManager myDialogManager; - - private TempDirTestFixture myTempDirFixture; - - @BeforeMethod - public void setup(final Method testMethod) throws Exception { - final IdeaTestFixtureFactory fixtureFactory = IdeaTestFixtureFactory.getFixtureFactory(); - myTempDirFixture = fixtureFactory.createTempDirTestFixture(); - myTempDirFixture.setUp(); - - final File projectDir = new File(myTempDirFixture.getTempDirPath(), "ultimate"); - assertTrue(projectDir.mkdir()); - - UIUtil.invokeAndWaitIfNeeded(new Runnable() { - @Override - public void run() { - try { - initProject(projectDir, testMethod.getName()); - initRepositories(VcsUtil.getVirtualFile(projectDir)); - } - catch (Exception e) { - throw new RuntimeException("Exception initializing the test", e); - } - } - }); - - GitVcs vcs = GitVcs.getInstance(myProject); - assertNotNull(vcs); - myTraceClient = true; - doActionSilently(VcsConfiguration.StandardConfirmation.ADD); - doActionSilently(VcsConfiguration.StandardConfirmation.REMOVE); - - myNotificationManager = GitTestUtil.registerNotificationManager(myProject); - myMessageManager = GitTestUtil.registerMessageManager(myProject); - GitTestPlatformFacade platformFacade = GitTestUtil.registerPlatformFacade(myProject); - myDialogManager = platformFacade.getDialogManager(); - - createAddCommit(myUltimate, "a"); - createAddCommit(myCommunity, "a"); - createAddCommit(myContrib, "a"); - - myUltimate.getRoot().refresh(false, true); - updateRepositories(); - } - - protected void doActionSilently(final VcsConfiguration.StandardConfirmation op) { - setStandardConfirmation(GitVcs.NAME, op, VcsShowConfirmationOption.Value.DO_ACTION_SILENTLY); - } - - @AfterMethod - public void tearDown() throws Exception { - if (myTempDirFixture != null) { - myTempDirFixture.tearDown(); - myTempDirFixture = null; - } - } - - private void initRepositories(VirtualFile projectDir) throws IOException { - myUltimate = init(myProject, projectDir); - VirtualFile communityDir = createDirInCommand(projectDir, "community"); - VirtualFile contribDir = createDirInCommand(projectDir, "contrib"); - myCommunity = init(myProject, communityDir); - myContrib = init(myProject, contribDir); - - addProjectRoots(); - myRepositories = Arrays.asList(myUltimate, myCommunity, myContrib); - } - - private void addProjectRoots() { - ProjectLevelVcsManager vcsManager = ProjectLevelVcsManager.getInstance(myProject); - vcsManager.setDirectoryMapping(myUltimate.getRoot().getPath(), GitVcs.NAME); - vcsManager.setDirectoryMapping(myCommunity.getRoot().getPath(), GitVcs.NAME); - vcsManager.setDirectoryMapping(myContrib.getRoot().getPath(), GitVcs.NAME); - } - - @Test - public void create_new_branch_without_problems() throws Exception { - doCheckoutNewBranch(); - assertNotify(NotificationType.INFORMATION, "Branch new_branch was created"); - } - - @Test - public void create_new_branch_with_unmerged_files_in_first_repo_should_show_notification() throws Exception { - GitTestScenarioGenerator.prepareUnmergedFiles(myUltimate); - doCheckoutNewBranch(); - assertNotify(NotificationType.ERROR, unmergedFilesErrorNotificationDescription("checkout")); - } - - @Test - public void create_new_branch_with_unmerged_files_in_second_repo_should_propose_to_rollback() throws Exception { - GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity, myContrib); - doCheckoutNewBranch(); - assertMessage(unmergedFilesErrorTitle("checkout")); - } - - @Test - public void rollback_create_new_branch_should_delete_branch() throws Exception { - GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity, myContrib); - myMessageManager.nextAnswer(Messages.OK); - doCheckoutNewBranch(); - assertMessage(unmergedFilesErrorTitle("checkout")); - assertBranch("master"); - assertTrue(!branch(myUltimate).contains(NEW_BRANCH)); - } - - @Test - public void deny_rollback_create_new_branch() throws Exception { - GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity, myContrib); - myMessageManager.nextAnswer(Messages.CANCEL); - doCheckoutNewBranch(); - assertMessage(unmergedFilesErrorTitle("checkout")); - - assertBranch(myUltimate, NEW_BRANCH); - assertBranch(myCommunity, MASTER); - assertBranch(myContrib, MASTER); - } - - @Test - public void checkout_without_problems() throws Exception { - prepareBranchWithCommit("feature"); - doCheckout("feature", null); - assertNotify(NotificationType.INFORMATION, "Checked out feature"); - } - - @Test - public void checkout_with_unmerged_files_in_first_repo_should_show_notification() throws Exception { - prepareBranchWithCommit("feature"); - GitTestScenarioGenerator.prepareUnmergedFiles(myUltimate); - doCheckout("feature", null); - assertNotify(NotificationType.ERROR, unmergedFilesErrorNotificationDescription("checkout")); - } - - @Test - public void checkout_with_unmerged_file_in_second_repo_should_propose_to_rollback() throws Exception { - prepareBranchWithCommit("feature"); - GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity); - doCheckout("feature", null); - assertMessage(unmergedFilesErrorTitle("checkout")); - } - - @Test - public void rollback_checkout_should_return_to_previous_branch() throws Exception { - prepareBranchWithCommit("feature"); - GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity); - myMessageManager.nextAnswer(Messages.OK); - doCheckout("feature", null); - assertMessage(unmergedFilesErrorTitle("checkout")); - assertBranch("master"); - } - - @Test - public void deny_rollback_checkout_should_do_nothing() throws Exception { - prepareBranchWithCommit("feature"); - GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity); - myMessageManager.nextAnswer(Messages.CANCEL); - doCheckout("feature", null); - assertMessage(unmergedFilesErrorTitle("checkout")); - assertBranch(myUltimate, "feature"); - assertBranch(myCommunity, "master"); - assertBranch(myContrib, "master"); - } - - @Test - public void checkout_with_untracked_files_overwritten_by_checkout_in_first_repo_should_show_notification() throws Exception { - test_untracked_files_overwritten_by_in_first_repo(true); - } - - private void test_untracked_files_overwritten_by_in_first_repo(boolean checkout) throws Exception { - prepareUntrackedFilesAndBranchWithSameTrackedFiles(myUltimate); - branch(myCommunity, "feature"); - branch(myContrib, "feature"); - - doCheckoutOrMerge(checkout, "feature"); - String operation = checkout ? "checkout" : "merge"; - assertNotify(NotificationType.ERROR, UntrackedFilesNotifier.createUntrackedFilesOverwrittenDescription(operation, true)); - } - - @Test - public void checkout_with_untracked_files_overwritten_by_checkout_in_second_repo_should_show_rollback_proposal_with_file_list() throws Exception { - test_checkout_with_untracked_files_overwritten_by_in_second_repo(true); - } - - public void test_checkout_with_untracked_files_overwritten_by_in_second_repo(boolean checkout) throws Exception { - prepareUntrackedFilesAndBranchWithSameTrackedFiles(myCommunity); - branch(myUltimate, "feature"); - branch(myContrib, "feature"); - - Class gitCheckoutOperationClass = Class.forName("git4idea.branch.GitBranchOperation"); - Class[] classes = gitCheckoutOperationClass.getDeclaredClasses(); - Class untrackedFilesDialogClass = null; - for (Class aClass : classes) { - if (aClass.getName().endsWith("UntrackedFilesDialog")) { - untrackedFilesDialogClass = aClass; - } - } - assertNotNull(untrackedFilesDialogClass); - - final AtomicBoolean dialogShown = new AtomicBoolean(); - final Class finalUntrackedFilesDialogClass = untrackedFilesDialogClass; - myDialogManager.registerDialogHandler(untrackedFilesDialogClass, new TestDialogHandler() { - @Override - public int handleDialog(Object dialog) { - if (dialog.getClass().equals(finalUntrackedFilesDialogClass)) { - dialogShown.set(true); - } - return DialogWrapper.CANCEL_EXIT_CODE; - } - }); - - doCheckoutOrMerge(checkout, "feature"); - assertTrue(dialogShown.get()); - } - - @Test - public void checkout_with_local_changes_overwritten_by_checkout_should_show_smart_checkout_dialog() throws Exception { - test_operation_with_local_changes_overwritten_by_should_show_smart_checkout_dialog(true); - } - - public void test_operation_with_local_changes_overwritten_by_should_show_smart_checkout_dialog(boolean checkout) throws Exception { - prepareLocalChangesAndBranchWithSameModifiedFilesWithoutConflicts(myUltimate); - branch(myCommunity, "feature"); - branch(myContrib, "feature"); - - final AtomicBoolean dialogShown = new AtomicBoolean(); - myDialogManager.registerDialogHandler(GitSmartOperationDialog.class, new TestDialogHandler() { - @Override - public int handleDialog(GitSmartOperationDialog dialog) { - dialogShown.set(true); - return DialogWrapper.CANCEL_EXIT_CODE; - } - }); - - doCheckoutOrMerge(checkout, "feature"); - assertTrue(dialogShown.get()); - } - - @Test - public void agree_to_smart_checkout_should_smart_checkout() throws Exception { - prepare_agree_to_smart_operation(true); - assertBranch("feature"); - for (GitRepository repository : myRepositories) { - refresh(repository); - assertBranch(repository, "feature"); - assertEquals(read(repository, "local.txt"), "master\ninitial content\nfeature content\n"); - } - } - - public void prepare_agree_to_smart_operation(boolean checkout) throws Exception { - prepareLocalChangesAndBranchWithSameModifiedFilesWithoutConflicts(myUltimate); - prepareLocalChangesAndBranchWithSameModifiedFilesWithoutConflicts(myCommunity); - prepareLocalChangesAndBranchWithSameModifiedFilesWithoutConflicts(myContrib); - myDialogManager.registerDialogHandler(GitSmartOperationDialog.class, - new TestDialogHandler() { - @Override - public int handleDialog(GitSmartOperationDialog dialog) { - return DialogWrapper.OK_EXIT_CODE; - } - }); - - doCheckoutOrMerge(checkout, "feature"); - updateRepositories(); - } - - private void updateRepositories() { - myUltimate.update(GitRepository.TrackedTopic.ALL); - myCommunity.update(GitRepository.TrackedTopic.ALL); - myContrib.update(GitRepository.TrackedTopic.ALL); - } - - @Test - public void deny_to_smart_checkout_in_first_repo_should_show_notification() throws Exception { - test_deny_to_smart_operation_in_first_repo_should_show_notification(true); - } - - public void test_deny_to_smart_operation_in_first_repo_should_show_notification(boolean checkout) throws Exception { - prepareLocalChangesAndBranchWithSameModifiedFilesWithoutConflicts(myUltimate); - branch(myCommunity, "feature"); - branch(myContrib, "feature"); - - myDialogManager.registerDialogHandler(GitSmartOperationDialog.class, new TestDialogHandler() { - @Override - public int handleDialog(GitSmartOperationDialog dialog) { - return DialogWrapper.CANCEL_EXIT_CODE; - } - }); - - doCheckoutOrMerge(checkout, "feature"); - String operation = checkout ? "checkout" : "merge"; - assertBranch("master"); - } - - @Test - public void deny_to_smart_checkout_in_second_repo_should_show_rollback_proposal() throws Exception { - test_deny_to_smart_operation_in_second_repo_should_show_rollback_proposal(true); - } - - public void test_deny_to_smart_operation_in_second_repo_should_show_rollback_proposal(boolean checkout) throws Exception { - prepareLocalChangesAndBranchWithSameModifiedFilesWithoutConflicts(myCommunity); - branch(myUltimate, "feature"); - branch(myContrib, "feature"); - myDialogManager.registerDialogHandler(GitSmartOperationDialog.class, new TestDialogHandler() { - @Override - public int handleDialog(GitSmartOperationDialog dialog) { - return DialogWrapper.CANCEL_EXIT_CODE; - } - }); - - doCheckoutOrMerge(checkout, "feature"); - String operationName = checkout ? "checkout" : "merge"; - String rollbackProposal = checkout ? - String.format(GitCheckoutOperation.ROLLBACK_PROPOSAL_FORMAT, "master") : - GitMergeOperation.ROLLBACK_PROPOSAL; - assertMessage("Couldn't " + operationName + " feature", - "However " + operationName + " has succeeded for the following repository:
" + - myUltimate.getPresentableUrl() + - "
" + rollbackProposal, - "Rollback", "Don't rollback"); - } - - @Test - public void rollback_checkout_branch_as_new_branch_should_delete_branches() throws Exception { - prepareBranchWithCommit("feature"); - GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity); - myMessageManager.nextAnswer(Messages.OK); - doCheckout("feature", "newBranch"); - assertMessage(unmergedFilesErrorTitle("checkout")); - assertBranch("master"); - for (GitRepository repository : myRepositories) { - assertFalse(branch(repository).contains("newBranch"), "Branch newBranch wasn't deleted from repository " + getShortRepositoryName( - repository)); - } - } - - private static void prepareLocalChangesAndBranchWithSameModifiedFilesWithoutConflicts(GitRepository repository) throws IOException { - create(repository, "local.txt", "initial content\n"); - addCommit(repository); - checkout(repository, "-b", "feature"); - edit(repository, "local.txt", "initial content\nfeature content\n"); - addCommit(repository); - checkout(repository, "master"); - edit(repository, "local.txt", "master\ninitial content\n"); - } - - private static void prepareUntrackedFilesAndBranchWithSameTrackedFiles(GitRepository repository) throws IOException { - checkout(repository, "-b", "feature"); - createAddCommit(repository, "untracked.txt"); - checkout(repository, "master"); - create(repository, "untracked.txt"); - } - - @Test - public void delete_branch_without_problems() throws Exception { - for (GitRepository repository : myRepositories) { - branch(repository, "master1"); - refresh(repository); - } - doDeleteBranch("master1"); - assertNotify(NotificationType.INFORMATION, "Deleted branch master1"); - } - - @Test - public void delete_unmerged_branch_should_show_dialog() throws Exception { - prepareBranchWithCommit("unmerged_branch", myUltimate, myCommunity, myContrib); - - final AtomicBoolean dialogShown = new AtomicBoolean(); - myDialogManager.registerDialogHandler(GitBranchIsNotFullyMergedDialog.class, new TestDialogHandler() { - @Override public int handleDialog(GitBranchIsNotFullyMergedDialog dialog) { - dialogShown.set(true); - return DialogWrapper.CANCEL_EXIT_CODE; - } - }); - - doDeleteBranch("unmerged_branch"); - assertTrue(dialogShown.get()); - } - - @Test - public void delete_branch_merged_to_head_but_unmerged_to_upstream_should_show_dialog() throws Exception { - // inspired by IDEA-83604 - // dealing with a single myCommunity repository here - - // prepare parent repository - final File parentDir = new File(myTempDirFixture.getTempDirPath(), "parent.git"); - GitExec.clone(myProject, myCommunity.getRoot().getPath(), parentDir.getPath(), true); - - // initialize feature branch and push to make origin/feature, set up tracking - checkout(myCommunity, "-b", "feature"); - remoteAdd(myCommunity, "origin", parentDir.getPath()); - push(myCommunity, "-u", "origin", "feature"); - - // create a commit and merge it to master, but not to feature's upstream - createAddCommit(myCommunity, "file"); - checkout(myCommunity, "master"); - merge(myCommunity, "feature"); - refresh(myCommunity); - - // delete feature fully merged to current HEAD, but not to the upstream - final AtomicBoolean dialogShown = new AtomicBoolean(); - myDialogManager.registerDialogHandler(GitBranchIsNotFullyMergedDialog.class, new TestDialogHandler() { - @Override - public int handleDialog(GitBranchIsNotFullyMergedDialog dialog) { - dialogShown.set(true); - return DialogWrapper.CANCEL_EXIT_CODE; - } - }); - GitBrancher processor = ServiceManager.getService(myProject, GitBrancher.class); - Method method = GitBrancherImpl.class.getDeclaredMethod("doDelete", String.class, ProgressIndicator.class); - method.setAccessible(true); - method.invoke(processor, "feature", singletonList(myCommunity), new EmptyProgressIndicator()); - - assertTrue(dialogShown.get()); - } - - @Test - public void ok_in_unmerged_branch_dialog_should_force_delete_branch() throws Exception { - prepareBranchWithCommit("unmerged_branch", myUltimate, myCommunity, myContrib); - registerNotFullyMergedDialog(DialogWrapper.OK_EXIT_CODE); - doDeleteBranch("unmerged_branch"); - for (GitRepository repository : myRepositories) { - assertTrue(!branch(repository).contains("unmerged_branch")); - } - } - - @Test - public void cancel_in_unmerged_branch_dialog_in_first_repository_should_show_notification() throws Exception { - prepareBranchWithCommit("unmerged_branch", myUltimate, myContrib); - branch(myCommunity, "unmerged_branch"); - - registerNotFullyMergedDialog(DialogWrapper.CANCEL_EXIT_CODE); - doDeleteBranch("unmerged_branch"); - assertNotify(NotificationType.ERROR, "Branch unmerged_branch wasn't deleted", "This branch is not fully merged to master."); - } - - @Test - public void cancel_in_unmerged_branch_dialog_in_not_first_repository_should_show_rollback_proposal() throws Exception { - branch(myUltimate, "unmerged_branch"); - prepareBranchWithCommit("unmerged_branch", myCommunity, myContrib); - - registerNotFullyMergedDialog(DialogWrapper.CANCEL_EXIT_CODE); - doDeleteBranch("unmerged_branch"); - assertMessage(String.format("Branch %s wasn't deleted", "unmerged_branch")); - } - - @Test - public void rollback_delete_branch_should_recreate_branches() throws Exception { - branch(myUltimate, "unmerged_branch"); - prepareBranchWithCommit("unmerged_branch", myCommunity); - branch(myContrib, "unmerged_branch"); - - registerNotFullyMergedDialog(DialogWrapper.CANCEL_EXIT_CODE); - myMessageManager.nextAnswer(Messages.OK); - doDeleteBranch("unmerged_branch"); - - for (GitRepository repository : myRepositories) { - assertTrue(branch(repository).contains("unmerged_branch")); - } - } - - @Test - public void deny_rollback_delete_branch_should_do_nothing() throws Exception { - branch(myUltimate, "unmerged_branch"); - prepareBranchWithCommit("unmerged_branch", myCommunity); - branch(myContrib, "unmerged_branch"); - - registerNotFullyMergedDialog(DialogWrapper.CANCEL_EXIT_CODE); - myMessageManager.nextAnswer(Messages.CANCEL); - doDeleteBranch("unmerged_branch"); - - assertTrue(branch(myCommunity).contains("unmerged_branch")); - assertTrue(branch(myContrib).contains("unmerged_branch")); - assertTrue(!branch(myUltimate).contains("unmerged_branch")); - } - - @Test - public void merge_simple_without_problems() throws Exception { - prepareBranchWithCommit("feature", myUltimate, myCommunity, myContrib); - doMerge("feature"); - assertNotify(NotificationType.INFORMATION, "Merged feature to master
Delete feature"); - - assertFile(myUltimate, "unmerged_branch_file", "content"); - assertFile(myCommunity, "unmerged_branch_file", "content"); - assertFile(myContrib, "unmerged_branch_file", "content"); - } - - private static void assertFile(GitRepository repository, String path, String content) throws IOException { - VirtualFile branchFile = repository.getRoot().findChild(path); - assertNotNull(branchFile); - assertTrue(branchFile.exists()); - assertEquals(new String(branchFile.contentsToByteArray()), content); - } - - @Test - public void merge_up_to_date_branch() throws Exception { - branch(myUltimate, "master2"); - branch(myCommunity, "master2"); - branch(myContrib, "master2"); - - doMerge("master2"); - - assertNotify(NotificationType.INFORMATION, "Already up-to-date
Delete master2"); - } - - @Test - public void merge_one_simple_and_other_up_to_date() throws Exception { - branch(myUltimate, "master2"); - branch(myContrib, "master2"); - prepareBranchWithCommit("master2", myCommunity); - - doMerge("master2"); - - assertNotify(NotificationType.INFORMATION, "Merged master2 to master
Delete master2"); - assertFile(myCommunity, "unmerged_branch_file", "content"); - assertNull(myUltimate.getRoot().findChild("unmerged_branch_file")); - } - - @Test - public void merge_with_unmerged_files_in_first_repo_should_show_notification() throws Exception { - prepareBranchWithCommit("feature"); - GitTestScenarioGenerator.prepareUnmergedFiles(myUltimate); - doMerge("feature"); - assertNotify(NotificationType.ERROR, unmergedFilesErrorNotificationDescription("merge")); - } - - @Test - public void merge_with_unmerged_files_in_second_repo_should_propose_to_rollback() throws Exception { - prepareBranchWithCommit("feature"); - GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity); - doMerge("feature"); - assertMessage(unmergedFilesErrorTitle("merge")); - } - - @Test - public void rollback_merge_should_reset_merge() throws Exception { - prepareBranchWithCommit("feature"); - String ultimateTip = tip(myUltimate); - GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity); - myMessageManager.nextAnswer(Messages.OK); - doMerge("feature"); - assertMessage(unmergedFilesErrorTitle("merge")); - assertBranch("master"); - assertEquals(tip(myUltimate), ultimateTip); - } - - @Test - public void deny_rollback_merge_should_leave_as_is() throws Exception { - prepareBranchWithCommit("feature"); - String ultimateTip = tip(myUltimate); - GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity); - myMessageManager.nextAnswer(Messages.CANCEL); - doMerge("feature"); - assertMessage(unmergedFilesErrorTitle("merge")); - assertBranch("master"); - assertFalse(tip(myUltimate).equals(ultimateTip)); - } - - @Test - public void merge_with_untracked_files_overwritten_by_merge_in_first_repo_should_show_notification() throws Exception { - test_untracked_files_overwritten_by_in_first_repo(false); - } - - @Test - public void merge_with_untracked_files_overwritten_by_merge_in_second_repo_should_show_rollback_proposal_with_file_list() throws Exception { - test_checkout_with_untracked_files_overwritten_by_in_second_repo(false); - } - - @Test - public void merge_with_local_changes_overwritten_by_merge_should_show_smart_checkout_dialog() throws Exception { - test_operation_with_local_changes_overwritten_by_should_show_smart_checkout_dialog(false); - } - - @Test - public void agree_to_smart_merge_should_smart_merge() throws Exception { - prepare_agree_to_smart_operation(false); - for (GitRepository repository : myRepositories) { - assertEquals(read(repository, "local.txt"), "master\ninitial content\nfeature content\n"); - } - } - - @Test - public void deny_to_smart_merge_in_first_repo_should_show_notification() throws Exception { - test_deny_to_smart_operation_in_first_repo_should_show_notification(false); - } - - @Test - public void deny_to_smart_merge_in_second_repo_should_show_rollback_proposal() throws Exception { - test_deny_to_smart_operation_in_second_repo_should_show_rollback_proposal(false); - } - - private void registerNotFullyMergedDialog(final int answer) { - myDialogManager.registerDialogHandler(GitBranchIsNotFullyMergedDialog.class, new TestDialogHandler() { - @Override - public int handleDialog(GitBranchIsNotFullyMergedDialog dialog) { - return answer; - } - }); - } - - private void prepareBranchWithCommit(String branch, GitRepository... repositories) throws IOException { - if (repositories.length == 0) { - repositories = new GitRepository[] { myUltimate, myCommunity, myContrib }; - } - for (GitRepository repository : repositories) { - checkout(repository, "-b", branch); - createAddCommit(repository, "unmerged_branch_file"); - checkout(repository, "master"); - refresh(repository); - } - } - - private void doCheckoutNewBranch() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { - callPrivateBranchOperationsProcessorMethod("doCheckoutNewBranch", NEW_BRANCH); - } - - private void doMerge(String branch) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { - GitBrancher processor = ServiceManager.getService(myProject, GitBrancher.class); - Method method = GitBrancherImpl.class.getDeclaredMethod("doMerge", String.class, Boolean.TYPE, ProgressIndicator.class); - method.setAccessible(true); - method.invoke(processor, branch, true, myRepositories, new EmptyProgressIndicator()); - - // sync refresh is needed, because the refresh inside GitMergeOperation is asynchronous. - for (GitRepository repository : myRepositories) { - repository.getRoot().refresh(false, true); - } - } - - private void doCheckoutOrMerge(boolean checkout, String branch) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { - if (checkout) { - doCheckout(branch, null); - } - else { - doMerge(branch); - } - } - - private static String unmergedFilesErrorNotificationDescription(String operation) - throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { - Method method = GitBranchOperation.class.getDeclaredMethod("unmergedFilesErrorNotificationDescription", String.class); - method.setAccessible(true); - return (String) method.invoke(null, operation); - } - - private static String unmergedFilesErrorTitle(String operation) - throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { - Method method = GitBranchOperation.class.getDeclaredMethod("unmergedFilesErrorTitle", String.class); - method.setAccessible(true); - return (String) method.invoke(null, operation); - } - - private void callPrivateBranchOperationsProcessorMethod(String methodName, String branchName) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { - // call private doCheckoutNewBranch instead of public checkoutNewBranch to avoid dealing with background process creation - // same for other branch operations - GitBrancher processor = ServiceManager.getService(myProject, GitBrancher.class); - Method method = GitBrancherImpl.class.getDeclaredMethod(methodName, String.class, ProgressIndicator.class); - method.setAccessible(true); - method.invoke(processor, branchName, myRepositories, new EmptyProgressIndicator()); - } - - private void doDeleteBranch(@NotNull String branchName) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { - callPrivateBranchOperationsProcessorMethod("doDelete", branchName); - } - - private void doCheckout(@NotNull String branchName, @Nullable String newBranch) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { - GitBrancher processor = ServiceManager.getService(myProject, GitBrancher.class); - Method doCheckout = GitBrancherImpl.class.getDeclaredMethod("doCheckout", ProgressIndicator.class, String.class, String.class); - doCheckout.setAccessible(true); - doCheckout.invoke(processor, new EmptyProgressIndicator(), branchName, newBranch, myRepositories); - } - - private void assertBranch(String branch) throws IOException { - for (GitRepository repository : myRepositories) { - assertBranch(repository, branch); - } - } - - private static void assertBranch(GitRepository repository, String branchName) throws IOException { - String currentBranch = currentBranch(repository); - assertNotNull(currentBranch); - assertEquals(currentBranch, branchName, "Expected " + branchName + " in [" + getShortRepositoryName(repository) + "]"); - } - - private void assertNotify(NotificationType type, String content) { - assertNotify(type, null, content); - } - - private void assertNotify(NotificationType type, @Nullable String title, String content) { - Notification notification = myNotificationManager.getLastNotification(); - assertNotNull(notification); - assertEquals(stripHtmlAndBreaks(notification.getContent()), stripHtmlAndBreaks(content)); - assertEquals(notification.getType(), type); - if (title != null) { - assertEquals(stripHtmlAndBreaks(notification.getTitle()), stripHtmlAndBreaks(title)); - } - } - - @NotNull - private static String stripHtmlAndBreaks(@NotNull String text) { - return StringUtil.stripHtml(text, true).replace("\n", ""); - } - - private void assertMessage(String title) { - assertMessage(title, null, null, null); - } - - private void assertMessage(@Nullable String title, @Nullable String description, @Nullable String yesButton, @Nullable String noButton) { - TestMessageManager.Message message = myMessageManager.getLastMessage(); - assertNotNull(message); - if (title != null) { - assertEquals(stripHtmlAndBreaks(message.getTitle()), stripHtmlAndBreaks(title)); - } - if (description != null) { - assertEquals(stripHtmlAndBreaks(message.getDescription()), stripHtmlAndBreaks(description)); - } - if (yesButton != null) { - assertEquals(message.getYesText(), yesButton); - } - if (noButton != null) { - assertEquals(message.getNoText(), noButton); - } - } - -} diff --git a/plugins/git4idea/tests/git4idea/branch/GitBranchWorkerTest.groovy b/plugins/git4idea/tests/git4idea/branch/GitBranchWorkerTest.groovy new file mode 100644 index 000000000000..97f0d9301027 --- /dev/null +++ b/plugins/git4idea/tests/git4idea/branch/GitBranchWorkerTest.groovy @@ -0,0 +1,801 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package git4idea.branch +import com.intellij.notification.NotificationListener +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.util.ProgressIndicatorBase +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.vcs.FilePathImpl +import com.intellij.openapi.vcs.changes.Change +import com.intellij.openapi.vcs.changes.CurrentContentRevision +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.testFramework.vcs.MockChangeListManager +import git4idea.PlatformFacade +import git4idea.commands.Git +import git4idea.history.browser.GitCommit +import git4idea.repo.GitRepository +import git4idea.repo.GitRepositoryImpl +import git4idea.test.* +import org.jetbrains.annotations.NotNull +import org.junit.After +import org.junit.Before +import org.junit.Test + +import static groovy.util.GroovyTestCase.assertEquals +import static junit.framework.Assert.* +/** + * + * @author Kirill Likhodedov + */ +@Mixin(GitExecutor) +@Mixin(GitScenarios) +class GitBranchWorkerTest { + + private String myRootDir + private GitMockProject myProject + private PlatformFacade myPlatformFacade + private Git myGit + + private GitRepository myUltimate + private GitRepository myCommunity + private GitRepository myContrib + + private List myRepositories + + @Before + public void setUp() { + myRootDir = FileUtil.createTempDirectory("", "").getPath() + myProject = new GitMockProject(myRootDir) + myPlatformFacade = new GitTestPlatformFacade() + myGit = new GitTestImpl() + + cd(myRootDir) + def community = mkdir("community") + def contrib = mkdir("contrib") + + [ myRootDir, community, contrib ].each { initRepo(it) } + + myUltimate = createRepository(myRootDir) + myCommunity = createRepository(community) + myContrib = createRepository(contrib) + + cd(myRootDir) + touch(".gitignore", "community\ncontrib") + git("add .gitignore") + git("commit -m gitignore") + + myRepositories = [ myUltimate, myCommunity, myContrib ] + myRepositories.each { ((GitTestRepositoryManager)myPlatformFacade.getRepositoryManager(myProject)).add(it) } + } + + private GitRepository createRepository(String rootDir) { + // TODO this smells hacky + // the constructor and notifyListeners() should probably be private + // getPresentableUrl should probably be final, and we should have a better VirtualFile implementation for tests. + new GitRepositoryImpl(new GitMockVirtualFile(rootDir), myPlatformFacade, myProject, myProject, true) { + @Override + protected void notifyListeners() { + } + + @Override + String getPresentableUrl() { + return rootDir; + } + } + } + + private void initRepo(String repoRoot) { + cd repoRoot + git("init") + touch("file.txt") + git("add file.txt") + git("commit -m initial") + } + + @After + public void tearDown() { + FileUtil.delete(new File(myRootDir)) + Disposer.dispose(myProject) + } + + def assertCurrentBranch(GitRepository repository, String name) { + def curBranch = git(repository, "branch").split("\n").find { it -> it.contains("*") }.replace('*', ' ').trim() + assertEquals("Current branch is incorrect in ${repository}", name, curBranch) + } + + def assertCurrentBranch(String name) { + myRepositories.each { assertCurrentBranch(it, name) } + } + + def checkoutNewBranch(String name, def uiHandler) { + GitBranchWorker brancher = new GitBranchWorker(myProject, myPlatformFacade, myGit, uiHandler as GitBranchUiHandler) + brancher.checkoutNewBranch(name, myRepositories) + } + + def checkoutBranch(String name, def uiHandler) { + GitBranchWorker brancher = new GitBranchWorker(myProject, myPlatformFacade, myGit, uiHandler as GitBranchUiHandler) + brancher.checkout(name, myRepositories) + } + + def mergeBranch(String name, def uiHandler) { + GitBranchWorker brancher = new GitBranchWorker(myProject, myPlatformFacade, myGit, uiHandler as GitBranchUiHandler) + brancher.merge(name, GitBrancher.DeleteOnMergeOption.PROPOSE, myRepositories) + } + + def deleteBranch(String name, def uiHandler) { + GitBranchWorker brancher = new GitBranchWorker(myProject, myPlatformFacade, myGit, uiHandler as GitBranchUiHandler) + brancher.deleteBranch(name, myRepositories) + } + + @Test + public void "create new branch without problems"() { + def successMessage = null; + checkoutNewBranch "feature", [ notifySuccess: { String message -> successMessage = message } ] + + assertCurrentBranch("feature") + assertEquals("Notification about successful branch creation is incorrect", "Branch ${bcode("feature")} was created", successMessage) + } + + static String bcode(def s) { + "${s}" + } + + @Test + public void "create new branch with unmerged files in first repo should show notification"() { + unmergedFiles(myUltimate) + + boolean notificationShown = false; + checkoutNewBranch("feature", [ showUnmergedFilesNotification: { String s, List l -> notificationShown = true } ]) + + assertTrue("Unmerged files notification was not shown", notificationShown) + } + + @Test + public void "create new branch with unmerged files in second repo should propose to rollback"() { + unmergedFiles(myCommunity) + + boolean rollbackProposed = false; + checkoutNewBranch "feature", [ showUnmergedFilesMessageWithRollback: { String s1, String s2 -> rollbackProposed = true ; false } ] + + assertTrue("Rollback was not proposed if unmerged files prevented checkout in the second repository", rollbackProposed) + } + + @Test + public void "rollback create new branch should delete branch"() { + unmergedFiles(myCommunity) + + checkoutNewBranch "feature", [ + showUnmergedFilesMessageWithRollback: { String s1, String s2 -> true }, + notifySuccess: { String title, String message -> } + ] + + assertCurrentBranch("master"); + assertBranchDeleted(myUltimate, "feature") + } + + @Test + public void "deny rollback create new branch should leave new branch"() { + unmergedFiles(myCommunity) + + checkoutNewBranch "feature", [ showUnmergedFilesMessageWithRollback: { String s1, String s2 -> false } ] + + assertCurrentBranch(myUltimate, "feature") + assertCurrentBranch(myCommunity, "master") + assertCurrentBranch(myContrib, "master") + } + + @Test + public void "checkout without problems"() { + branchWithCommit(myRepositories, "feature") + + def successMessage = null; + checkoutBranch "feature", [ notifySuccess: { String message -> successMessage = message } ] + + assertCurrentBranch("feature") + assertEquals("Notification about successful branch checkout is incorrect", "Checked out ${bcode("feature")}", successMessage) + } + + @Test + public void "checkout_with_unmerged_files_in_first_repo_should_show_notification"() { + branchWithCommit(myRepositories, "feature") + unmergedFiles(myUltimate) + + boolean notificationShown = false; + checkoutBranch("feature", [ showUnmergedFilesNotification: { String s, List l -> notificationShown = true } ]) + + assertTrue("Unmerged files notification was not shown", notificationShown) + } + + @Test + public void checkout_with_unmerged_file_in_second_repo_should_propose_to_rollback() { + branchWithCommit(myRepositories, "feature") + unmergedFiles(myCommunity) + + boolean rollbackProposed = false; + checkoutBranch "feature", [ showUnmergedFilesMessageWithRollback: { String s1, String s2 -> rollbackProposed = true ; false } ] + + assertTrue("Rollback was not proposed if unmerged files prevented checkout in the second repository", rollbackProposed) + } + + @Test + public void rollback_checkout_should_return_to_previous_branch() { + branchWithCommit(myRepositories, "feature") + unmergedFiles(myCommunity) + + checkoutBranch "feature", [ + showUnmergedFilesMessageWithRollback: { String s1, String s2 -> true }, + notifySuccess: { String title, String message -> } + ] + + assertCurrentBranch("master"); + } + + @Test + public void deny_rollback_checkout_should_do_nothing() { + branchWithCommit(myRepositories, "feature") + unmergedFiles(myCommunity) + + checkoutBranch "feature", [ showUnmergedFilesMessageWithRollback: { String s1, String s2 -> false } ] + + assertCurrentBranch(myUltimate, "feature") + assertCurrentBranch(myCommunity, "master") + assertCurrentBranch(myContrib, "master") + } + + def branchOperation(def operation, String name, def uiHandler) { + if (operation == "checkout") { + checkoutBranch(name, uiHandler) + } + else { + mergeBranch(name, uiHandler) + } + } + + @Test + public void "checkout with untracked files overwritten by checkout in first repo should show notification"() { + test_untracked_files_overwritten_by_in_first_repo("checkout"); + } + + @Test + public void "merge with untracked files overwritten by checkout in first repo should show notification"() { + test_untracked_files_overwritten_by_in_first_repo("merge"); + } + + def test_untracked_files_overwritten_by_in_first_repo(String operation) { + branchWithCommit(myRepositories, "feature") + untrackedFileOverwrittenBy(myUltimate, "feature") + + boolean notificationShown = false; + branchOperation operation, "feature", [ + showUntrackedFilesNotification : { String s, Collection c -> notificationShown = true } + ] + + assertTrue "Untracked files notification was not shown", notificationShown + } + + @Test + public void "checkout with untracked files overwritten by checkout in second repo should show rollback proposal with file list"() { + test_checkout_with_untracked_files_overwritten_by_in_second_repo("checkout"); + } + + @Test + public void "merge with untracked files overwritten by checkout in second repo should show rollback proposal with file list"() { + test_checkout_with_untracked_files_overwritten_by_in_second_repo("merge"); + } + + def test_checkout_with_untracked_files_overwritten_by_in_second_repo(String operation) { + branchWithCommit(myRepositories, "feature") + def untracked = untrackedFileOverwrittenBy(myCommunity, "feature") + + Collection untrackedFiles = null; + branchOperation operation, "feature", [ + showUntrackedFilesDialogWithRollback : { String s, String p, Collection files -> untrackedFiles = files; false } + ] + + assertTrue "Untracked files dialog was not shown", untrackedFiles != null + assertEquals "Incorrect set of untracked files was shown in the dialog", + untracked, + untrackedFiles.collect { FileUtil.getRelativePath(myCommunity.root.path, it.path, File.separatorChar) } + } + + @Test + public void "checkout with local changes overwritten by checkout should show smart checkout dialog"() { + test_operation_with_local_changes_overwritten_by_should_show_smart_checkout_dialog("checkout"); + } + + @Test + public void "merge with local changes overwritten by checkout should show smart checkout dialog"() { + test_operation_with_local_changes_overwritten_by_should_show_smart_checkout_dialog("merge"); + } + + def test_operation_with_local_changes_overwritten_by_should_show_smart_checkout_dialog(String operation) { + def localChanges = prepareLocalChangesOverwrittenBy(myUltimate) + + List changes = null; + branchOperation(operation, "feature", [ + showSmartOperationDialog: { Project p, List cs, String op, boolean force -> + changes = cs + DialogWrapper.CANCEL_EXIT_CODE + } + ]) + + assertNotNull "Local changes were not shown in the dialog", changes + assertEquals "Incorrect set of local changes was shown in the dialog", + localChanges, + changes.collect({ FileUtil.getRelativePath(myUltimate.root.path, it.afterRevision.file.path, File.separatorChar) }) + } + + Change[] changesFromFiles(Collection paths) { + paths.collect { + toChange(it) + } + } + + Change toChange(String relPath) { + // we don't care about the before revision + new Change(null, CurrentContentRevision.create(new FilePathImpl(new GitMockVirtualFile(myRootDir + "/" + relPath)))) + } + + @Test + public void "agree to smart checkout should smart checkout"() { + agree_to_smart_operation("checkout", "Checked out feature") + + assertCurrentBranch("feature"); + cd myUltimate + def actual = cat("local.txt") + assertEquals("Content doesn't match", +"""line with branch changes +common content +common content +common content +line with master changes +""", actual) + } + + @Test + public void "agree to smart merge should smart merge"() { + agree_to_smart_operation("merge", "Merged feature to master
Delete feature") + + cd myUltimate + def actual = cat("local.txt") + assertEquals("Content doesn't match", +"""line with branch changes +common content +common content +common content +line with master changes +""", actual) + } + + def agree_to_smart_operation(String operation, String expectedSuccessMessage) { + prepareLocalChangesOverwrittenBy(myUltimate) + + AgreeToSmartOperationTestUiHandler handler = new AgreeToSmartOperationTestUiHandler() + branchOperation(operation, "feature", handler) + + assertNotNull "No success notification was shown", handler.mySuccessMessage + assertEquals "Success message is incorrect", expectedSuccessMessage, handler.mySuccessMessage + } + + def prepareLocalChangesOverwrittenBy(GitRepository repository) { + def localChanges = localChangesOverwrittenByWithoutConflict(repository, "feature") + // TODO we'd better avoid manual adding changes to the ChangeListManager. + // Probably we should create GitTestChangeListManager that would fairly call git status and analyze the output. + // Maybe we could reuse GitChangeProvider or at least GitNewChangesCollector. + ((MockChangeListManager)myPlatformFacade.getChangeListManager(myProject)).addChanges(changesFromFiles(localChanges)) + + myRepositories.each { + if (it != repository) { + branchWithCommit(it, "feature") + } + } + localChanges + } + + @Test + public void "deny to smart checkout in first repo should show nothing"() { + test_deny_to_smart_operation_in_first_repo_should_show_notification("checkout"); + } + + @Test + public void "deny to smart merge in first repo should show nothing"() { + test_deny_to_smart_operation_in_first_repo_should_show_notification("merge"); + } + + public void test_deny_to_smart_operation_in_first_repo_should_show_notification(String operation) { + prepareLocalChangesOverwrittenBy(myUltimate) + + def errorMessage = null + branchOperation(operation, "feature", [ + showSmartOperationDialog : { Project p, List cs, String op, boolean f -> GitSmartOperationDialog.CANCEL_EXIT_CODE }, + notifyError: { String title, String message -> errorMessage = message } + ] as GitBranchUiHandler ) + + assertNull "Error message was not shown", errorMessage + assertCurrentBranch("master"); + } + + @Test + public void "deny to smart checkout in second repo should show rollback proposal"() { + test_deny_to_smart_operation_in_second_repo_should_show_rollback_proposal("checkout"); + assertCurrentBranch(myUltimate, "feature") + assertCurrentBranch(myCommunity, "master") + assertCurrentBranch(myContrib, "master") + } + + @Test + public void "deny to smart merge in second repo should show rollback proposal"() { + test_deny_to_smart_operation_in_second_repo_should_show_rollback_proposal("merge"); + } + + public void test_deny_to_smart_operation_in_second_repo_should_show_rollback_proposal(String operation) { + prepareLocalChangesOverwrittenBy(myCommunity) + + def rollbackMsg = null + branchOperation(operation, "feature", [ + showSmartOperationDialog : { Project p, List cs, String op, boolean f -> GitSmartOperationDialog.CANCEL_EXIT_CODE }, + notifyErrorWithRollbackProposal: { String t, String m, String rp -> rollbackMsg = m ; false } + ] as GitBranchUiHandler ) + + assertNotNull "Rollback proposal was not shown", rollbackMsg + } + + @Test + public void "rollback of 'checkout branch as new branch' should delete branches"() { + branchWithCommit(myRepositories, "feature") + touch("feature.txt", "feature_content") + git("add feature.txt") + git("commit -m feature_changes") + git("checkout master") + + unmergedFiles(myCommunity) + + boolean rollbackProposed = false; + GitBranchWorker brancher = new GitBranchWorker(myProject, myPlatformFacade, myGit, [ + showUnmergedFilesMessageWithRollback: { String s1, String s2 -> rollbackProposed = true ; true } + ] as GitBranchUiHandler) + brancher.checkoutNewBranchStartingFrom("newBranch", "feature", myRepositories) + + assertTrue("Rollback was not proposed if unmerged files prevented checkout in the second repository", rollbackProposed) + assertCurrentBranch("master"); + myRepositories.each { + assertTrue "Branch 'newBranch' should have been deleted on rollback", + git(it, "branch").split("\n").grep( { it.contains("newBranch") }).isEmpty() + } + } + + @Test + public void "delete branch that is fully merged should go without problems"() { + myRepositories.each { cd it ; git("branch todelete") } + + def msg = null + deleteBranch("todelete", [ + notifySuccess: { String message -> msg = message} + ]); + + assertNotNull "Successful notification was not shown", msg + assertEquals "Successful notification is incorrect", "Deleted branch ${bcode("todelete")}", msg + } + + @Test + public void "delete unmerged branch should show dialog"() { + prepareUnmergedBranch(myCommunity) + + boolean dialogShown = false + deleteBranch("todelete", [ + showBranchIsNotFullyMergedDialog : { Project p, Map h, String ub, List mb, String bb -> dialogShown = true ; false }, + notifyErrorWithRollbackProposal: { String t, String m, String rp -> false } + ]); + + assertTrue "'Branch is not fully merged' dialog was not shown", dialogShown + } + + private void prepareUnmergedBranch(GitRepository unmergedRepo) { + myRepositories.each { + git(it, "branch todelete") + } + cd unmergedRepo + git("checkout todelete") + touch("afile.txt", "content") + git("add afile.txt") + git("commit -m unmerged_commit") + git("checkout master") + } + + void assertBranchDeleted(String name) { + myRepositories.each { assertBranchDeleted(it, name) } + } + + @Test + public void "ok in unmerged branch dialog should force delete branch"() { + prepareUnmergedBranch(myUltimate) + + deleteBranch("todelete", [ + showBranchIsNotFullyMergedDialog : { Project p, Map h, String ub, List mb, String bb -> true }, + notifySuccess: { String message -> } + ]); + + assertBranchDeleted("todelete") + } + + @Test + public void "cancel in unmerged branch dialog in not first repository should show rollback proposal"() { + prepareUnmergedBranch(myCommunity) + + def rollbackMsg = null + deleteBranch("todelete", [ + showBranchIsNotFullyMergedDialog : { Project p, Map h, String ub, List mb, String bb -> false }, + notifyErrorWithRollbackProposal: { String t, String m, String rp -> rollbackMsg = m ; false } + ]); + + assertNotNull "Rollback messages was not shown", rollbackMsg + } + + @Test + public void "rollback delete branch should recreate branches"() { + prepareUnmergedBranch(myCommunity) + + def rollbackMsg = null + deleteBranch("todelete", [ + showBranchIsNotFullyMergedDialog : { Project p, Map h, String ub, List mb, String bb -> false }, + notifyErrorWithRollbackProposal: { String t, String m, String rp -> rollbackMsg = m ; true } + ]); + + assertNotNull "Rollback messages was not shown", rollbackMsg + assertBranchExists(myUltimate, "todelete") + assertBranchExists(myCommunity, "todelete") + assertBranchExists(myContrib, "todelete") + } + + @Test + public void "deny rollback delete branch should do nothing"() { + prepareUnmergedBranch(myCommunity) + + def rollbackMsg = null + deleteBranch("todelete", [ + showBranchIsNotFullyMergedDialog : { Project p, Map h, String ub, List mb, String bb -> false }, + notifyErrorWithRollbackProposal: { String t, String m, String rp -> rollbackMsg = m ; false } + ]); + + assertNotNull "Rollback messages was not shown", rollbackMsg + + assertBranchDeleted(myUltimate, "todelete") + assertBranchExists(myCommunity, "todelete") + assertBranchExists(myContrib, "todelete") + } + + @Test + public void "delete branch merged to head but unmerged to upstream should show dialog"() { + // inspired by IDEA-83604 + // for the sake of simplicity we deal with a single myCommunity repository here + + // prepare parent repository + // create it under ultimate not to bother with removing it after the test (tearDown will clean automatically) + cd myUltimate + git("clone --bare $myCommunity parent.git") + + // initialize feature branch and push to make origin/feature, set up tracking + cd myCommunity + git("checkout -b feature"); + git ("remote add origin ${myUltimate.root.path}/parent.git"); + git ("push -u origin feature") + + // create a commit and merge it to master, but not to feature's upstream + touch("feature.txt", "feature content") + git("add feature.txt") + git("commit -m feature_branch") + git("checkout master") + git("merge feature") + + // delete feature fully merged to current HEAD, but not to the upstream + boolean dialogShown = false; + GitBranchWorker brancher = new GitBranchWorker(myProject, myPlatformFacade, myGit, [ + showBranchIsNotFullyMergedDialog : { Project p, Map h, String ub, List mb, String bb -> dialogShown = true; false } + ] as GitBranchUiHandler) + brancher.deleteBranch("feature", [myCommunity]) + + assertTrue "'Branch is not fully merged' dialog was not shown", dialogShown + } + + def assertBranchDeleted(GitRepository repo, String branch) { + assertFalse("Branch $branch should have been deleted from $repo", git(repo, "branch").contains(branch)) + } + + def assertBranchExists(GitRepository repo, String branch) { + assertTrue("Branch $branch should exist in $repo", git(repo, "branch").contains(branch)) + } + + @Test + public void "simple merge without problems"() { + branchWithCommit(myRepositories, "master2", "branch_file.txt", "branch content") + + def message = null + mergeBranch("master2", [ + notifySuccess: { String t, String m, NotificationListener l -> message = m } + ]); + + assertNotNull "Success message wasn't shown", message + assertEquals "Success message is incorrect", + "Merged ${bcode("master2")} to ${bcode("master")}
Delete master2", message + assertFile(myUltimate, "branch_file.txt", "branch content"); + assertFile(myCommunity, "branch_file.txt", "branch content"); + assertFile(myContrib, "branch_file.txt", "branch content"); + } + + private void assertFile(GitRepository repository, String path, String content) throws IOException { + cd repository + assertEquals "Content doesn't match", content, cat(path) + } + + @Test + public void "merge branch that is up-to-date"() { + myRepositories.each { cd it ; git("branch master2") } + + def message = null + mergeBranch("master2", [ + notifySuccess: { String t, String m, NotificationListener l -> message = m } + ]); + + assertNotNull "Success message wasn't shown", message + assertEquals "Success message is incorrect", "Already up-to-date
Delete master2", message + } + + @Test + public void "merge one simple and other up to date"() { + branchWithCommit(myCommunity, "master2", "branch_file.txt", "branch content") + [myUltimate, myContrib].each { cd it ; git("branch master2") } + + def message = null + mergeBranch("master2", [ + notifySuccess: { String t, String m, NotificationListener l -> message = m } + ]); + + assertNotNull "Success message wasn't shown", message + assertEquals "Success message is incorrect", + "Merged ${bcode("master2")} to ${bcode("master")}
Delete master2", message + assertFile(myCommunity, "branch_file.txt", "branch content"); + } + + @Test + public void "merge with unmerged files in first repo should show notification"() { + branchWithCommit(myRepositories, "feature") + unmergedFiles(myUltimate) + + boolean notificationShown = false; + mergeBranch("feature", [ + showUnmergedFilesNotification: { String s, List l -> notificationShown = true } + ]) + + assertTrue("Unmerged files notification was not shown", notificationShown) + } + + @Test + public void "merge with unmerged files in second repo should propose to rollback"() { + branchWithCommit(myRepositories, "feature") + unmergedFiles(myCommunity) + + boolean rollbackProposed = false; + mergeBranch "feature", [ + showUnmergedFilesMessageWithRollback: { String s1, String s2 -> rollbackProposed = true ; false } + ] + + assertTrue("Rollback was not proposed if unmerged files prevented checkout in the second repository", rollbackProposed) + } + + @Test + public void "rollback merge should reset merge"() { + branchWithCommit(myRepositories, "feature") + String ultimateTip = tip(myUltimate) + unmergedFiles(myCommunity) + + mergeBranch "feature", [ + showUnmergedFilesMessageWithRollback: { String s1, String s2 -> true }, + getProgressIndicator: { new ProgressIndicatorBase() } + ] + + assertEquals "Merge in ultimate should have been reset", ultimateTip, tip(myUltimate) + } + + private String tip(GitRepository repo) { + cd repo + git("rev-list -1 HEAD") + } + + @Test + public void "deny rollback merge should leave as is"() { + branchWithCommit(myRepositories, "feature") + cd myUltimate + String ultimateTipAfterMerge = git("rev-list -1 feature") + unmergedFiles(myCommunity) + + mergeBranch "feature", [ + showUnmergedFilesMessageWithRollback: { String s1, String s2 -> false } + ] + + assertEquals "Merge in ultimate should have been reset", ultimateTipAfterMerge, tip(myUltimate) + } + + // TODO Somehow I wasn't able to make dynamic partial implementations, because both overloaded notifySuccess() methods are needed, + // therefore there are duplicate entries in the map => only one method gets implemented. + class AgreeToSmartOperationTestUiHandler implements GitBranchUiHandler { + String mySuccessMessage + + @NotNull + @Override + ProgressIndicator getProgressIndicator() { + new ProgressIndicatorBase() + } + + @Override + int showSmartOperationDialog(@NotNull Project project, @NotNull List changes, @NotNull String operation, boolean force) { + GitSmartOperationDialog.SMART_EXIT_CODE + } + + @Override + boolean showBranchIsNotFullyMergedDialog(@NotNull Project project, @NotNull Map> history, @NotNull String unmergedBranch, @NotNull List mergedToBranches, @NotNull String baseBranch) { + throw new UnsupportedOperationException() + } + + @Override + void notifySuccess(@NotNull String message) { + mySuccessMessage = message + } + + @Override + void notifySuccess(@NotNull String title, @NotNull String message, NotificationListener listener) { + mySuccessMessage = message + } + + @Override + void notifyError(@NotNull String title, @NotNull String message) { + throw new UnsupportedOperationException() + } + + @Override + boolean notifyErrorWithRollbackProposal(@NotNull String title, @NotNull String message, @NotNull String rollbackProposal) { + throw new UnsupportedOperationException() + } + + @Override + void showUnmergedFilesNotification(@NotNull String operationName, @NotNull Collection repositories) { + throw new UnsupportedOperationException() + } + + @Override + boolean showUnmergedFilesMessageWithRollback(@NotNull String operationName, @NotNull String rollbackProposal) { + throw new UnsupportedOperationException() + } + + @Override + void showUntrackedFilesNotification(@NotNull String operationName, @NotNull Collection untrackedFiles) { + throw new UnsupportedOperationException() + } + + @Override + boolean showUntrackedFilesDialogWithRollback(@NotNull String operationName, @NotNull String rollbackProposal, @NotNull Collection untrackedFiles) { + throw new UnsupportedOperationException() + } + + @Override + void notifySuccess(@NotNull String title, @NotNull String message) { + mySuccessMessage = message + } + } + +} diff --git a/plugins/git4idea/tests/git4idea/test/GitExecutor.groovy b/plugins/git4idea/tests/git4idea/test/GitExecutor.groovy index 8e77a52b57fb..d338044ad33c 100644 --- a/plugins/git4idea/tests/git4idea/test/GitExecutor.groovy +++ b/plugins/git4idea/tests/git4idea/test/GitExecutor.groovy @@ -15,8 +15,10 @@ */ package git4idea.test +import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.text.StringUtil import com.intellij.util.ArrayUtil +import git4idea.repo.GitRepository /** * @@ -26,8 +28,22 @@ class GitExecutor { private String myCurrentDir - void cd(String path) { + def shortenPath(String path) { + def split = path.split("/") + if (split.size() > 3) { + // split[0] is empty, because the path starts from / + return "/${split[1]}/.../${split[-2]}/${split[-1]}" + } + return path + } + + def cd(String path) { myCurrentDir = path + println "cd ${shortenPath(path)}" + } + + def cd(GitRepository repository) { + cd repository.root.path } String git(String command) { @@ -36,4 +52,35 @@ class GitExecutor { return new GitTestRunEnv(new File(myCurrentDir)).run(split.get(0), params); } + String git(GitRepository repository, String command) { + cd repository.root.path + git command + } + + def touch(String fileName) { + File file = new File(myCurrentDir, fileName) + assert !file.exists() + file.createNewFile() + file.path + } + + def touch(String fileName, String content) { + touch(fileName) + echo(fileName, content) + } + + def echo(String fileName, String content) { + new File(myCurrentDir, fileName).withWriterAppend("UTF-8") { it.write(content) } + } + + def mkdir(String dirName) { + File file = new File(myCurrentDir, dirName) + file.mkdir() + file.path + } + + def cat(String fileName) { + FileUtil.loadFile(new File(myCurrentDir, fileName)) + } + } diff --git a/plugins/git4idea/tests/git4idea/test/GitMockProject.groovy b/plugins/git4idea/tests/git4idea/test/GitMockProject.groovy index 1ca6ec70893b..c1f4ab5cde63 100644 --- a/plugins/git4idea/tests/git4idea/test/GitMockProject.groovy +++ b/plugins/git4idea/tests/git4idea/test/GitMockProject.groovy @@ -102,7 +102,7 @@ class GitMockProject implements Project { @Override boolean isDefault() { - throw new UnsupportedOperationException() + false } @Override diff --git a/plugins/git4idea/tests/git4idea/test/GitMockVcsManager.groovy b/plugins/git4idea/tests/git4idea/test/GitMockVcsManager.groovy index 793a9374e10f..333f6938645b 100644 --- a/plugins/git4idea/tests/git4idea/test/GitMockVcsManager.groovy +++ b/plugins/git4idea/tests/git4idea/test/GitMockVcsManager.groovy @@ -36,12 +36,15 @@ public class GitMockVcsManager extends ProjectLevelVcsManager { PlatformFacade myPlatformFacade Collection myRoots = [] boolean myProjectRootMapping = false + AbstractVcs myVcs GitMockVcsManager(Project project, PlatformFacade facade) { myProject = project myPlatformFacade = facade + myVcs = facade.getVcs(project) } + // TODO remove and use getting all roots from GitRepositoryManager. void addRoots(String... roots) { roots.each { myRoots << it } } @@ -223,7 +226,9 @@ public class GitMockVcsManager extends ProjectLevelVcsManager { @Override VcsRoot[] getAllVcsRoots() { - throw new UnsupportedOperationException() + myPlatformFacade.getRepositoryManager(myProject).repositories.collect { + new VcsRoot(myVcs, it.root) + } } @Override diff --git a/plugins/git4idea/tests/git4idea/test/GitMockVirtualFile.groovy b/plugins/git4idea/tests/git4idea/test/GitMockVirtualFile.groovy index 67e824703c6f..8d2c59c22671 100644 --- a/plugins/git4idea/tests/git4idea/test/GitMockVirtualFile.groovy +++ b/plugins/git4idea/tests/git4idea/test/GitMockVirtualFile.groovy @@ -16,13 +16,14 @@ package git4idea.test import com.intellij.mock.MockVirtualFileSystem +import com.intellij.openapi.fileTypes.FileType +import com.intellij.openapi.fileTypes.FileTypes import com.intellij.openapi.project.Project import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileSystem import org.jetbrains.annotations.NotNull import org.jetbrains.annotations.Nullable - /** * VirtualFile test implementation based on {@link java.io.File}. * @@ -30,6 +31,8 @@ import org.jetbrains.annotations.Nullable */ class GitMockVirtualFile extends VirtualFile { + private static VirtualFileSystem ourFileSystem = new MockVirtualFileSystem() + private String myPath; @NotNull @@ -60,7 +63,7 @@ class GitMockVirtualFile extends VirtualFile { @NotNull @Override VirtualFileSystem getFileSystem() { - new MockVirtualFileSystem() + ourFileSystem } @Override @@ -119,7 +122,6 @@ class GitMockVirtualFile extends VirtualFile { @Override void refresh(boolean asynchronous, boolean recursive, Runnable postRunnable) { - throw new UnsupportedOperationException(); } @Override @@ -138,6 +140,11 @@ class GitMockVirtualFile extends VirtualFile { myPath; } + @Override + FileType getFileType() { + return FileTypes.PLAIN_TEXT; + } + boolean equals(o) { if (this.is(o)) return true if (getClass() != o.class) return false diff --git a/plugins/git4idea/tests/git4idea/test/GitScenarios.groovy b/plugins/git4idea/tests/git4idea/test/GitScenarios.groovy new file mode 100644 index 000000000000..f9b0697a20cb --- /dev/null +++ b/plugins/git4idea/tests/git4idea/test/GitScenarios.groovy @@ -0,0 +1,135 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package git4idea.test + +import git4idea.repo.GitRepository + +/** + * Create popular scenarios used in multiple tests, for example: + * - create a branch and commit something there; + * - make some unmerged files in the working tree; + * - make the situation when local changes would be overwritten by merge. + * + * @author Kirill Likhodedov + */ +@Mixin(GitExecutor) +class GitScenarios { + + private static final String BRANCH_FOR_UNMERGED_CONFLICTS = "unmerged_files_branch_" + Math.random(); + + private static final def LOCAL_CHANGES_OVERWRITTEN_BY = [ + initial: +"""common content +common content +common content +""", + branchLine: "line with branch changes", + masterLine: "line with master changes" + ] + + /** + * Create a branch with a commit and return back to master. + */ + def branchWithCommit(GitRepository repository, String name, String file = "branch_file.txt", String content = "branch content") { + cd repository + git("checkout -b $name") + touch(file, content) + git("add $file") + git("commit -m branch_content") + + git("checkout master") + } + + /** + * Create a branch with a commit and return back to master. + */ + def branchWithCommit(Collection repositories, String name, + String file = "branch_file.txt", String content = "branch content") { + repositories.each { branchWithCommit(it, name, file, content) } + } + + /** + * Make an unmerged file in the repository. + */ + def unmergedFiles(GitRepository repository) { + cd repository + touch("unmerged.txt", "initial content") + git("add unmerged.txt") + git("commit -m initial_content") + + git("checkout -b $BRANCH_FOR_UNMERGED_CONFLICTS") + echo("unmerged.txt", "branch content") + git("commit -am branch_content") + + git("checkout master") + echo("unmerged.txt", "master content") + git("commit -am master_content") + + git("merge $BRANCH_FOR_UNMERGED_CONFLICTS") + git("branch -D $BRANCH_FOR_UNMERGED_CONFLICTS") + } + + /** + * Create an untracked file in master and a tracked file with the same name in the branch. + * This produces the "some untracked files would be overwritten by..." error when trying to checkout or merge. + * Branch with the given name shall exist. + */ + def untrackedFileOverwrittenBy(GitRepository repository, String branch) { + cd repository + git("checkout $branch") + touch("untracked.txt", "branch content") + git("add untracked.txt") + git("commit -m untracked_file") + git("checkout master") + touch("untracked.txt", "master content") + [ "untracked.txt" ] + } + + /** + * Creates a file in both master and branch so that the content differs, but can be merged without conflicts. + * That way, git checkout/merge will fail with "local changes would be overwritten by checkout/merge", + * but smart checkout/merge (stash-checkout/merge-unstash) would succeed without conflicts. + * + * NB: the branch should not exist before this is called! + */ + def localChangesOverwrittenByWithoutConflict(GitRepository repository, String branch) { + cd repository + + echo("local.txt", LOCAL_CHANGES_OVERWRITTEN_BY.initial) + git("add local.txt") + git("commit -m initial_changes") + + git("checkout -b $branch") + prepend("local.txt", LOCAL_CHANGES_OVERWRITTEN_BY.branchLine) + git("add local.txt") + git("commit -m branch_changes") + + git("checkout master") + append("local.txt", LOCAL_CHANGES_OVERWRITTEN_BY.masterLine) + + [ "local.txt" ] + } + + def append(String fileName, String content) { + echo(fileName, content) + } + + def prepend(String fileName, String content) { + def previousContent = cat(fileName) + new File(myCurrentDir, fileName).withWriter("UTF-8") { it.write(content + "\n" + previousContent) } + } + +} diff --git a/plugins/git4idea/tests/git4idea/test/GitTestImpl.groovy b/plugins/git4idea/tests/git4idea/test/GitTestImpl.groovy index 4547a21efb22..6148227d56d0 100644 --- a/plugins/git4idea/tests/git4idea/test/GitTestImpl.groovy +++ b/plugins/git4idea/tests/git4idea/test/GitTestImpl.groovy @@ -14,6 +14,7 @@ * limitations under the License. */ package git4idea.test +import com.intellij.execution.process.ProcessOutputTypes import com.intellij.openapi.project.Project import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.text.StringUtil @@ -21,11 +22,15 @@ import com.intellij.openapi.vcs.VcsException import com.intellij.openapi.vfs.VirtualFile import git4idea.commands.Git import git4idea.commands.GitCommandResult +import git4idea.commands.GitImpl import git4idea.commands.GitLineHandlerListener +import git4idea.history.browser.GitCommit import git4idea.push.GitPushSpec import git4idea.repo.GitRepository import org.jetbrains.annotations.NotNull import org.jetbrains.annotations.Nullable + +import java.lang.reflect.Method /** * @author Kirill Likhodedov */ @@ -72,6 +77,11 @@ public class GitTestImpl implements Git { Arrays.asList(StringUtil.splitByLines(output))) } + @Override + GitCommandResult diff(GitRepository repository, List parameters, String range) { + execute(repository, "diff ${parameters.join(" ")} $range") + } + @NotNull @Override GitCommandResult checkAttr(@NotNull GitRepository repository, @NotNull Collection attributes, @NotNull Collection files) { @@ -79,83 +89,82 @@ public class GitTestImpl implements Git { cd root String output = git("check-attr " + attributes.join(" ") + " -- " + files.collect({it -> FileUtil.getRelativePath(root, it.path, (char)'/')}).join(" ")) - return new GitCommandResult(!output.contains("fatal"), 0, Collections.emptyList(), - Arrays.asList(StringUtil.splitByLines(output))) + commandResult(output) } @NotNull @Override - public GitCommandResult merge(@NotNull GitRepository repository, - @NotNull String branchToMerge, - @Nullable List additionalParams, + GitCommandResult stashSave(@NotNull GitRepository repository, @NotNull String message) { + execute(repository, "stash save $message") + } + + @NotNull + @Override + GitCommandResult stashPop(@NotNull GitRepository repository, GitLineHandlerListener... listeners) { + execute(repository, "stash pop") + } + + @Override + List history(GitRepository repository, String range) { + [] + } + + @NotNull + @Override + public GitCommandResult merge(@NotNull GitRepository repository, @NotNull String branchToMerge, @Nullable List additionalParams, @NotNull GitLineHandlerListener... listeners) { - throw new UnsupportedOperationException(); + execute(repository, "merge ${additionalParams.join(" ")} $branchToMerge", listeners) } @NotNull @Override - public GitCommandResult checkout(@NotNull GitRepository repository, - @NotNull String reference, - @Nullable String newBranch, - boolean force, + public GitCommandResult checkout(@NotNull GitRepository repository, @NotNull String reference, @Nullable String newBranch, boolean force, @NotNull GitLineHandlerListener... listeners) { - throw new UnsupportedOperationException(); + execute(repository, "checkout ${force ? "--force" : ""} ${newBranch != null ? "-b $newBranch" : ""} $reference", listeners) } @NotNull @Override - public GitCommandResult checkoutNewBranch(@NotNull GitRepository repository, - @NotNull String branchName, + public GitCommandResult checkoutNewBranch(@NotNull GitRepository repository, @NotNull String branchName, @Nullable GitLineHandlerListener listener) { - throw new UnsupportedOperationException(); + execute(repository, "checkout -b ${branchName}", listener) } @NotNull @Override - public GitCommandResult createNewTag(@NotNull GitRepository repository, - @NotNull String tagName, - @Nullable GitLineHandlerListener listener, - @NotNull String reference) { - throw new UnsupportedOperationException(); - } - - @NotNull - @Override - public GitCommandResult branchDelete(@NotNull GitRepository repository, - @NotNull String branchName, - boolean force, + public GitCommandResult branchDelete(@NotNull GitRepository repository, @NotNull String branchName, boolean force, @NotNull GitLineHandlerListener... listeners) { - throw new UnsupportedOperationException(); + execute(repository, "branch ${force ? "-D" : "-d"} $branchName", listeners) } @NotNull @Override public GitCommandResult branchContains(@NotNull GitRepository repository, @NotNull String commit) { - throw new UnsupportedOperationException(); + execute(repository, "branch --contains $commit") } @NotNull @Override public GitCommandResult branchCreate(@NotNull GitRepository repository, @NotNull String branchName) { - throw new UnsupportedOperationException(); + execute(repository, "branch $branchName") } @NotNull @Override public GitCommandResult resetHard(@NotNull GitRepository repository, @NotNull String revision) { - throw new UnsupportedOperationException(); + execute(repository, "reset --hard $revision") } @NotNull @Override public GitCommandResult resetMerge(@NotNull GitRepository repository, @Nullable String revision) { - throw new UnsupportedOperationException(); + execute(repository, "reset --merge $revision") } @NotNull @Override public GitCommandResult tip(@NotNull GitRepository repository, @NotNull String branchName) { - throw new UnsupportedOperationException(); + execute(repository, "rev-list -1 $branchName") } @NotNull @@ -193,7 +202,41 @@ public class GitTestImpl implements Git { @NotNull @Override public GitCommandResult getUnmergedFiles(@NotNull GitRepository repository) { + execute(repository, "ls-files --unmerged") + } + + @NotNull + @Override + public GitCommandResult createNewTag(@NotNull GitRepository repository, + @NotNull String tagName, + @Nullable GitLineHandlerListener listener, + @NotNull String reference) { throw new UnsupportedOperationException(); } + private static GitCommandResult commandResult(String output) { + boolean success = !output.split("\n").collect { isError(it) }.contains(true) + return new GitCommandResult(success, 0, Collections.emptyList(), Arrays.asList(StringUtil.splitByLines(output))) + } + + private static boolean isError(String s) { + // we don't want to make that method public, since it is reused only in the test. + Method m = GitImpl.class.getDeclaredMethod("isError", String.class) + m.setAccessible(true) + return m.invoke(null, s) as boolean + } + + static def feedOutput(String output, GitLineHandlerListener... listeners) { + listeners.each { GitLineHandlerListener listener -> + output.split("\n").each { listener.onLineAvailable(it, ProcessOutputTypes.STDERR) } + } + } + + def execute(GitRepository repository, String operation, GitLineHandlerListener... listeners) { + cd repository.root.path + def out = git(operation) + feedOutput(out, listeners) + commandResult(out) + } + } diff --git a/plugins/git4idea/tests/git4idea/test/GitTestPlatformFacade.java b/plugins/git4idea/tests/git4idea/test/GitTestPlatformFacade.groovy similarity index 63% rename from plugins/git4idea/tests/git4idea/test/GitTestPlatformFacade.java rename to plugins/git4idea/tests/git4idea/test/GitTestPlatformFacade.groovy index 5d3864fe451b..a4eff499bc96 100644 --- a/plugins/git4idea/tests/git4idea/test/GitTestPlatformFacade.java +++ b/plugins/git4idea/tests/git4idea/test/GitTestPlatformFacade.groovy @@ -13,33 +13,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package git4idea.test; - -import com.intellij.ide.plugins.IdeaPluginDescriptor; -import com.intellij.mock.MockLocalFileSystem; -import com.intellij.openapi.application.ModalityState; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.roots.ProjectRootManager; -import com.intellij.openapi.ui.DialogWrapper; -import com.intellij.openapi.util.Computable; -import com.intellij.openapi.util.io.FileUtil; -import com.intellij.openapi.vcs.AbstractVcs; -import com.intellij.openapi.vcs.AbstractVcsHelper; -import com.intellij.openapi.vcs.ProjectLevelVcsManager; -import com.intellij.openapi.vcs.changes.ChangeListManager; -import com.intellij.openapi.vfs.LocalFileSystem; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.testFramework.vcs.MockChangeListManager; -import git4idea.Notificator; -import git4idea.PlatformFacade; -import git4idea.repo.GitRepositoryManager; -import git4idea.tests.TestDialogManager; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.File; -import java.io.IOException; - +package git4idea.test +import com.intellij.ide.SaveAndSyncHandler +import com.intellij.ide.plugins.IdeaPluginDescriptor +import com.intellij.mock.MockLocalFileSystem +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ex.ProjectManagerEx +import com.intellij.openapi.roots.ProjectRootManager +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.util.Computable +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.vcs.AbstractVcs +import com.intellij.openapi.vcs.AbstractVcsHelper +import com.intellij.openapi.vcs.ProjectLevelVcsManager +import com.intellij.openapi.vcs.changes.ChangeListManagerEx +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.testFramework.vcs.MockChangeListManager +import git4idea.Notificator +import git4idea.PlatformFacade +import git4idea.config.GitVcsApplicationSettings +import git4idea.config.GitVcsSettings +import git4idea.repo.GitRepositoryManager +import git4idea.tests.TestDialogManager +import org.jetbrains.annotations.NotNull +import org.jetbrains.annotations.Nullable /** * * @author Kirill Likhodedov @@ -51,7 +50,7 @@ public class GitTestPlatformFacade implements PlatformFacade { private TestNotificator myNotificator; private TestDialogManager myTestDialogManager; private GitMockProjectRootManager myProjectRootManager; - private ChangeListManager myChangeListManager; + private ChangeListManagerEx myChangeListManager; private GitTestRepositoryManager myRepositoryManager; private MockVcsHelper myVcsHelper; @@ -120,7 +119,7 @@ public class GitTestPlatformFacade implements PlatformFacade { } @Override - public ChangeListManager getChangeListManager(@NotNull Project project) { + public ChangeListManagerEx getChangeListManager(@NotNull Project project) { return myChangeListManager; } @@ -161,6 +160,42 @@ public class GitTestPlatformFacade implements PlatformFacade { } } + @NotNull + @Override + public GitVcsSettings getSettings(Project project) { + return new GitVcsSettings(new GitVcsApplicationSettings()); + } + + @Override + public void saveAllDocuments() { + } + + @Nullable + @Override + public VirtualFile getVirtualFileByPath(@NotNull String path) { + return new GitMockVirtualFile(path); + } + + @NotNull + @Override + public ProjectManagerEx getProjectManager() { + [ + blockReloadingProjectOnExternalChanges: {}, + unblockReloadingProjectOnExternalChanges: {} + ] as ProjectManagerEx + } + + @NotNull + @Override + SaveAndSyncHandler getSaveAndSyncHandler() { + [ + blockSaveOnFrameDeactivation: {}, + blockSyncOnFrameActivation: {}, + unblockSaveOnFrameDeactivation: {}, + unblockSyncOnFrameActivation: {} + ] as SaveAndSyncHandler + } + @NotNull @Override public AbstractVcs getVcs(@NotNull Project project) { diff --git a/plugins/git4idea/tests/git4idea/test/GitTestRepositoryManager.java b/plugins/git4idea/tests/git4idea/test/GitTestRepositoryManager.groovy similarity index 76% rename from plugins/git4idea/tests/git4idea/test/GitTestRepositoryManager.java rename to plugins/git4idea/tests/git4idea/test/GitTestRepositoryManager.groovy index e6c789294932..538f858420bf 100644 --- a/plugins/git4idea/tests/git4idea/test/GitTestRepositoryManager.java +++ b/plugins/git4idea/tests/git4idea/test/GitTestRepositoryManager.groovy @@ -13,26 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package git4idea.test; - -import com.intellij.openapi.vcs.FilePath; -import com.intellij.openapi.vfs.VirtualFile; -import git4idea.repo.GitRepository; -import git4idea.repo.GitRepositoryChangeListener; -import git4idea.repo.GitRepositoryManager; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - +package git4idea.test +import com.intellij.openapi.vcs.FilePath +import com.intellij.openapi.vfs.VirtualFile +import git4idea.repo.GitRepository +import git4idea.repo.GitRepositoryChangeListener +import git4idea.repo.GitRepositoryManager +import org.jetbrains.annotations.NotNull +import org.jetbrains.annotations.Nullable /** * @author Kirill Likhodedov */ public class GitTestRepositoryManager implements GitRepositoryManager { - private final Collection myRepositories = new ArrayList(); + private final List myRepositories = new ArrayList(); public void add(GitRepository repository) { myRepositories.add(repository); @@ -61,12 +55,12 @@ public class GitTestRepositoryManager implements GitRepositoryManager { @NotNull @Override public List getRepositories() { - throw new UnsupportedOperationException(); + myRepositories } @Override public boolean moreThanOneRoot() { - throw new UnsupportedOperationException(); + myRepositories.size() > 1 } @Override diff --git a/plugins/git4idea/tests/git4idea/test/GitTestScenarioGenerator.java b/plugins/git4idea/tests/git4idea/test/GitTestScenarioGenerator.java deleted file mode 100644 index 10cb1fa5e5e2..000000000000 --- a/plugins/git4idea/tests/git4idea/test/GitTestScenarioGenerator.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2000-2012 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package git4idea.test; - -import git4idea.repo.GitRepository; -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; - -import static git4idea.test.GitExec.*; - -/** - * @author Kirill Likhodedov - */ -public class GitTestScenarioGenerator { - - private static final String BRANCH_FOR_UNMERGED_CONFLICTS = "unmerged_files_branch_" + Math.random(); - - public static void prepareUnmergedFiles(@NotNull GitRepository... repositories) throws IOException { - for (GitRepository repository : repositories) { - String unmergedFile = "unmerged"; - create(repository, unmergedFile, "master content"); - addCommit(repository, unmergedFile); - - checkoutBranchForUnmergedConflicts(repository); - edit(repository, unmergedFile, "feature content"); - addCommit(repository, unmergedFile); - - checkout(repository, "master"); - edit(repository, unmergedFile, "master feature"); - addCommit(repository, unmergedFile); - - merge(repository, BRANCH_FOR_UNMERGED_CONFLICTS); - refresh(repository); - } - } - - private static void checkoutBranchForUnmergedConflicts(GitRepository repository) throws IOException { - String branches = branch(repository); - if (!branches.contains(BRANCH_FOR_UNMERGED_CONFLICTS)) { - checkout(repository, "-b", BRANCH_FOR_UNMERGED_CONFLICTS); - } else { - checkout(repository, BRANCH_FOR_UNMERGED_CONFLICTS); - } - } -} diff --git a/plugins/git4idea/tests/git4idea/test/GitTestUtil.java b/plugins/git4idea/tests/git4idea/test/GitTestUtil.java index 86b486d852d1..d6c409954803 100644 --- a/plugins/git4idea/tests/git4idea/test/GitTestUtil.java +++ b/plugins/git4idea/tests/git4idea/test/GitTestUtil.java @@ -17,21 +17,15 @@ package git4idea.test; import com.intellij.openapi.application.PluginPathManager; import com.intellij.openapi.command.WriteCommandAction; -import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.CharsetToolkit; import com.intellij.openapi.vfs.VirtualFile; -import git4idea.MessageManager; -import git4idea.Notificator; -import git4idea.PlatformFacade; import git4idea.tests.GitTestRepository; -import git4idea.tests.TestDialogManager; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.picocontainer.MutablePicoContainer; import java.io.File; import java.io.FilenameFilter; @@ -43,9 +37,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertNotNull; -import static org.testng.Assert.fail; +import static org.testng.Assert.*; /** * @author Kirill Likhodedov @@ -253,36 +245,4 @@ public class GitTestUtil { return "\nExpected:\n" + expected + "\nActual:\n" + actual; } - /** - * Registers {@link git4idea.tests.TestDialogManager} as the {@link git4idea.DialogManager} implementation. - * @param project - */ - public static TestDialogManager registerDialogManager(@NotNull Project project) { - return new TestDialogManager(); - } - - public static TestMessageManager registerMessageManager(@NotNull Project project) { - final String key = "git4idea.MessageManager"; - final MutablePicoContainer picoContainer = (MutablePicoContainer) project.getPicoContainer(); - picoContainer.unregisterComponent(key); - picoContainer.registerComponentImplementation(key, TestMessageManager.class); - return (TestMessageManager)MessageManager.getInstance(project); - } - - public static TestNotificator registerNotificationManager(Project project) { - final String key = "git4idea.Notificator"; - final MutablePicoContainer picoContainer = (MutablePicoContainer) project.getPicoContainer(); - picoContainer.unregisterComponent(key); - picoContainer.registerComponentImplementation(key, TestNotificator.class); - return (TestNotificator)Notificator.getInstance(project); - } - - public static GitTestPlatformFacade registerPlatformFacade(@NotNull Project project) { - final String key = "git4idea.PlatformFacade"; - final MutablePicoContainer picoContainer = (MutablePicoContainer) project.getPicoContainer(); - picoContainer.unregisterComponent(key); - picoContainer.registerComponentImplementation(key, GitTestPlatformFacade.class); - return (GitTestPlatformFacade)ServiceManager.getService(project, PlatformFacade.class); - } - } diff --git a/plugins/git4idea/tests/git4idea/test/MockGit.groovy b/plugins/git4idea/tests/git4idea/test/MockGit.groovy index d41e6cf77e09..884a9a0698ef 100644 --- a/plugins/git4idea/tests/git4idea/test/MockGit.groovy +++ b/plugins/git4idea/tests/git4idea/test/MockGit.groovy @@ -95,6 +95,11 @@ class MockGit implements Git { throw new UnsupportedOperationException() } + @Override + GitCommandResult diff(GitRepository repository, List parameters, String range) { + throw new UnsupportedOperationException() + } + @NotNull @Override GitCommandResult merge(@NotNull GitRepository repository, @NotNull String branchToMerge, @Nullable List additionalParams, @@ -202,6 +207,23 @@ class MockGit implements Git { } + @NotNull + @Override + GitCommandResult stashSave(@NotNull GitRepository repository, @NotNull String message) { + throw new UnsupportedOperationException() + } + + @NotNull + @Override + GitCommandResult stashPop(@NotNull GitRepository repository, GitLineHandlerListener... listeners) { + throw new UnsupportedOperationException() + } + + @Override + List history(GitRepository repository, String range) { + throw new UnsupportedOperationException() + } + private void produceOutput(String output, GitLineHandlerListener... listeners) { for (String line : output.split("\n")) { // for simplicity all output goes to OUTPUT, no ERROR listeners.each { it.onLineAvailable(line, ProcessOutputTypes.STDOUT) } diff --git a/plugins/git4idea/tests/git4idea/test/TestMessageManager.java b/plugins/git4idea/tests/git4idea/test/TestMessageManager.java deleted file mode 100644 index fed1d7160f9d..000000000000 --- a/plugins/git4idea/tests/git4idea/test/TestMessageManager.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2000-2012 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package git4idea.test; - -import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.Messages; -import git4idea.MessageManager; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import javax.swing.*; - -/** - * @author Kirill Likhodedov - */ -public class TestMessageManager extends MessageManager { - - private Message myLastMessage; - private int myNextAnswer = DEFAULT_ANSWER; - private static final int DEFAULT_ANSWER = Messages.CANCEL; - - public static class Message { - @NotNull private final String myTitle; - @NotNull private final String myDescription; - @NotNull private final String myYesText; - @NotNull private final String myNoText; - - public Message(@NotNull String title, @NotNull String description, @NotNull String yesText, @NotNull String noText) { - myTitle = title; - myDescription = description; - myYesText = yesText; - myNoText = noText; - } - - @NotNull - public String getTitle() { - return myTitle; - } - - @NotNull - public String getDescription() { - return myDescription; - } - - @NotNull - public String getYesText() { - return myYesText; - } - - @NotNull - public String getNoText() { - return myNoText; - } - } - - @Nullable - public Message getLastMessage() { - return myLastMessage; - } - - public void nextAnswer(int answer) { - myNextAnswer = answer; - } - - @Override - protected int doShowYesNoDialog(Project project, String description, String title, String yesText, String noText, @Nullable Icon icon) { - myLastMessage = new Message(title, description, yesText, noText); - return myNextAnswer; - } - - -}