mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Git branches refactoring, tests and some minor fixes.
REFACTORING: * Split the GitBrancher into 3 parts: - Extract all UI interactivity to the GitBranchUiHandler interface. Implement in GitBranchUiHandlerImpl. - Extract firing a background task into GitBrancher, which would be the public interface for branch operations. - Put the actual work into GitBranchWorker. - Remove the result handler from those GitBrancher methods which had it, until it is clear in what situation we need this result handler. * Extract deletion of the remote branch into GitDeleteRemoteBranchOperation. * Several improvements to lessen the dependency from platform: - move convertPathsToChanges from GitUtil to GitBranchOperation: it is used only there. - PlatformFacade.getVirtualFileForPath(). This is not the best idea, but other would be too much effort for now. - Use Git interface instead of direct GitHandler usage in several cases (stash save and pop, diff, history). * Pass the PlatformFacade in many many Git components and classes instead of calling <platform component>.getInstance(). FIX a few bugs that were discovered during refactoring & testing: * in multi-repository setup, if deletion succeeded for the first repository, and was denied for the second, don't collect information from already deleted branches. * if a branch is not fully merged, and user denies force deletion, don't show notification about deletion failure. * Make update of repository info (current revison, branch, etc.) before any active branch operation to make sure we perform operations based on actual information. TESTING: * Rewrite GitBranchOperationsTest to GitBranchWorkerTest instead that is platform independent and fast. * Enhance GitExecutor with more commands useful to work with the file system. * Introduce GitScenarios that would create some popular scenarios used in multiple tests, e. g.: - 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 * Transform GitTestPlatformFacade, GitTestRepositoryManager to Groovy. * GitMockVirtualFile: use a single instance of the file system. Otherwise files comparison fails in some cases.
This commit is contained in:
@@ -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<String, Map<VirtualFile, Collection<Change>>> getChangesByLists(@NotNull Collection<VirtualFile> rootsToSave) {
|
||||
|
||||
@@ -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 {
|
||||
* <code>git diff --name-only master..origin/master</code>
|
||||
*/
|
||||
@NotNull
|
||||
public static Collection<String> 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<String> getPathsDiffBetweenRefs(@NotNull Git git, @NotNull GitRepository repository,
|
||||
@NotNull String beforeRef, @NotNull String afterRef) throws VcsException {
|
||||
List<String> 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<String> remoteChanges = new HashSet<String>();
|
||||
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<Change> convertPathsToChanges(@NotNull GitRepository repository,
|
||||
@NotNull Collection<String> affectedPaths, boolean relativePaths) {
|
||||
ChangeListManager changeListManager = ChangeListManager.getInstance(repository.getProject());
|
||||
List<Change> affectedChanges = new ArrayList<Change>();
|
||||
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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<VirtualFile> affectedRoots,
|
||||
final List<VcsException> exceptions) throws VcsException {
|
||||
GitPusher.showPushDialogAndPerformPush(project);
|
||||
GitPusher.showPushDialogAndPerformPush(project, ServiceManager.getService(project, PlatformFacade.class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GitRepository> myRepositories;
|
||||
@NotNull private final String myCurrentBranchOrRev;
|
||||
@NotNull private final ProgressIndicator myIndicator;
|
||||
@NotNull protected final String myCurrentBranchOrRev;
|
||||
private final GitVcsSettings mySettings;
|
||||
|
||||
@NotNull private final Collection<GitRepository> mySuccessfulRepositories;
|
||||
@NotNull private final Collection<GitRepository> myRemainingRepositories;
|
||||
|
||||
protected GitBranchOperation(@NotNull Project project, @NotNull Git git, @NotNull Collection<GitRepository> repositories,
|
||||
@NotNull String currentBranchOrRev, @NotNull ProgressIndicator indicator) {
|
||||
protected GitBranchOperation(@NotNull Project project, @NotNull PlatformFacade facade, @NotNull Git git,
|
||||
@NotNull GitBranchUiHandler uiHandler, @NotNull Collection<GitRepository> repositories) {
|
||||
myProject = project;
|
||||
myFacade = facade;
|
||||
myGit = git;
|
||||
myUiHandler = uiHandler;
|
||||
myRepositories = repositories;
|
||||
myCurrentBranchOrRev = currentBranchOrRev;
|
||||
myIndicator = indicator;
|
||||
myCurrentBranchOrRev = GitBranchUtil.getCurrentBranchOrRev(repositories);
|
||||
mySuccessfulRepositories = new ArrayList<GitRepository>();
|
||||
myRemainingRepositories = new ArrayList<GitRepository>(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("<html>");
|
||||
if (!StringUtil.isEmptyOrSpaces(message)) {
|
||||
description.append(message).append("<br/>");
|
||||
}
|
||||
description.append(getRollbackProposal()).append("</html>");
|
||||
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 <a href='resolve'>resolve</a> all merge conflicts before " + operationName + ".<br/>" +
|
||||
"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 = "<html>You have to resolve all merge conflicts before " + getOperationName() + ".<br/>" +
|
||||
getRollbackProposal() + "</html>";
|
||||
// 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<VirtualFile> untrackedFiles) {
|
||||
UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject, ServiceManager.getService(myProject, PlatformFacade.class),
|
||||
untrackedFiles, getOperationName(), null);
|
||||
myUiHandler.showUntrackedFilesNotification(getOperationName(), untrackedFiles);
|
||||
}
|
||||
|
||||
private void showUntrackedFilesDialogWithRollback(@NotNull Collection<VirtualFile> untrackedFiles) {
|
||||
String title = "Couldn't " + getOperationName();
|
||||
String description = UntrackedFilesNotifier.createUntrackedFilesOverwrittenDescription(getOperationName(), false);
|
||||
|
||||
final SelectFilesDialog dialog = new UntrackedFilesDialog(myProject, new ArrayList<VirtualFile>(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<GitRepository, List<Change>> collectLocalChangesConflictingWithBranch(@NotNull Project project,
|
||||
@NotNull Collection<GitRepository> repositories,
|
||||
@NotNull String currentBranch,
|
||||
@NotNull String otherBranch) {
|
||||
Map<GitRepository, List<Change>> collectLocalChangesConflictingWithBranch(@NotNull Collection<GitRepository> repositories,
|
||||
@NotNull String currentBranch, @NotNull String otherBranch) {
|
||||
Map<GitRepository, List<Change>> changes = new HashMap<GitRepository, List<Change>>();
|
||||
for (GitRepository repository : repositories) {
|
||||
try {
|
||||
Collection<String> diff = GitUtil.getPathsDiffBetweenRefs(currentBranch, otherBranch, project, repository.getRoot());
|
||||
List<Change> changesInRepo = GitUtil.convertPathsToChanges(repository, diff, false);
|
||||
Collection<String> diff = GitUtil.getPathsDiffBetweenRefs(myGit, repository, currentBranch, otherBranch);
|
||||
List<Change> 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<VirtualFile> 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("<html>" + getRollbackProposal() + "</html>"));
|
||||
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<Change> affectedChanges = GitUtil.convertPathsToChanges(currentRepository, localChangesOverwrittenBy.getRelativeFilePaths(), true);
|
||||
List<Change> 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<GitRepository, List<Change>> conflictingChangesInRepositories =
|
||||
collectLocalChangesConflictingWithBranch(myProject, getRemainingRepositoriesExceptGiven(currentRepository), currentBranch, nextBranch);
|
||||
collectLocalChangesConflictingWithBranch(getRemainingRepositoriesExceptGiven(currentRepository), currentBranch, nextBranch);
|
||||
|
||||
Set<GitRepository> otherProblematicRepositories = conflictingChangesInRepositories.keySet();
|
||||
List<GitRepository> allConflictingRepositories = new ArrayList<GitRepository>(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<Change> convertPathsToChanges(@NotNull GitRepository repository,
|
||||
@NotNull Collection<String> affectedPaths, boolean relativePaths) {
|
||||
List<Change> affectedChanges = new ArrayList<Change>();
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>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.</p>
|
||||
* <p>The purpose of this class is to separate UI interaction from the main code, which would in particular simplify testing.</p>
|
||||
*
|
||||
* @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<GitRepository> 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<VirtualFile> untrackedFiles);
|
||||
|
||||
boolean showUntrackedFilesDialogWithRollback(@NotNull String operationName, @NotNull String rollbackProposal,
|
||||
@NotNull Collection<VirtualFile> 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<Change> changes, @NotNull String operation, boolean force);
|
||||
|
||||
boolean showBranchIsNotFullyMergedDialog(@NotNull Project project, @NotNull Map<GitRepository, List<GitCommit>> history,
|
||||
@NotNull String unmergedBranch, @NotNull List<String> mergedToBranches,
|
||||
@NotNull String baseBranch);
|
||||
|
||||
}
|
||||
@@ -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("<html>");
|
||||
if (!StringUtil.isEmptyOrSpaces(message)) {
|
||||
description.append(message).append("<br/>");
|
||||
}
|
||||
description.append(rollbackProposal).append("</html>");
|
||||
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<GitRepository> 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("<html>You have to resolve all merge conflicts before %s.<br/>%s</html>",
|
||||
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<VirtualFile> untrackedFiles) {
|
||||
UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject, ServiceManager.getService(myProject, PlatformFacade.class),
|
||||
untrackedFiles, operationName, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean showUntrackedFilesDialogWithRollback(@NotNull String operationName, @NotNull String rollbackProposal,
|
||||
@NotNull Collection<VirtualFile> 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<Change> changes, @NotNull String operation, boolean force) {
|
||||
return GitSmartOperationDialog.showAndGetAnswer(myProject, changes, "checkout", true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean showBranchIsNotFullyMergedDialog(@NotNull Project project, @NotNull final Map<GitRepository, List<GitCommit>> history,
|
||||
@NotNull final String unmergedBranch, @NotNull final List<String> 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 <a href='resolve'>resolve</a> all merge conflicts before " + operationName + ".<br/>" +
|
||||
"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<VirtualFile> originalFiles, @NotNull String prompt,
|
||||
@NotNull String rollbackProposal) {
|
||||
super(project, new ArrayList<VirtualFile>(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("<html>" + myRollbackProposal + "</html>"));
|
||||
panel.add(buttons);
|
||||
return panel;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<GitRepository> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GitRepository> 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<GitRepository> repositories) {
|
||||
updateInfo(repositories);
|
||||
for (GitRepository repository : repositories) {
|
||||
myGit.createNewTag(repository, name, null, reference);
|
||||
}
|
||||
}
|
||||
|
||||
public void checkoutNewBranchStartingFrom(@NotNull String newBranchName, @NotNull String startPoint,
|
||||
@NotNull List<GitRepository> repositories) {
|
||||
updateInfo(repositories);
|
||||
new GitCheckoutOperation(myProject, myFacade, myGit, myUiHandler, repositories, startPoint, newBranchName).execute();
|
||||
}
|
||||
|
||||
public void checkout(@NotNull final String reference, @NotNull List<GitRepository> repositories) {
|
||||
updateInfo(repositories);
|
||||
new GitCheckoutOperation(myProject, myFacade, myGit, myUiHandler, repositories, reference, null).execute();
|
||||
}
|
||||
|
||||
|
||||
public void deleteBranch(@NotNull final String branchName, @NotNull final List<GitRepository> repositories) {
|
||||
updateInfo(repositories);
|
||||
new GitDeleteBranchOperation(myProject, myFacade, myGit, myUiHandler, repositories, branchName).execute();
|
||||
}
|
||||
|
||||
public void deleteRemoteBranch(@NotNull final String branchName, @NotNull final List<GitRepository> 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<GitRepository> repositories) {
|
||||
updateInfo(repositories);
|
||||
Map<GitRepository, String> revisions = new HashMap<GitRepository, String>();
|
||||
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<GitRepository> 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<GitRepository> 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<Change> 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<GitCommit>, List<GitCommit>> loadCommitsToCompare(@NotNull GitRepository repository, @NotNull final String branchName) {
|
||||
final List<GitCommit> headToBranch;
|
||||
final List<GitCommit> 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("<html>There are no changes between <code>%s</code> and <code>%s</code></html>",
|
||||
currentBranch, branchName), "No Changes Detected");
|
||||
}
|
||||
else {
|
||||
new GitCompareBranchesDialog(myProject, branchName, currentBranch, compareInfo, selectedRepository).show();
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateInfo(@NotNull Collection<GitRepository> repositories) {
|
||||
for (GitRepository repository : repositories) {
|
||||
repository.update(GitRepository.TrackedTopic.ALL);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<GitRepository> repositories, @Nullable Consumer<Boolean> resultHandler);
|
||||
void checkoutNewBranch(@NotNull String name, @NotNull List<GitRepository> repositories);
|
||||
|
||||
/**
|
||||
* <p>Creates new tag on the selected reference.</p>
|
||||
@@ -75,7 +71,7 @@ public interface GitBrancher {
|
||||
/**
|
||||
* Creates and checks out a new local branch starting from the given reference:
|
||||
* {@code git checkout -b <branchname> <start-point>}. <br/>
|
||||
* 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<GitRepository> repositories,
|
||||
@Nullable Consumer<Boolean> resultHandler);
|
||||
void merge(@NotNull String branchName, @NotNull DeleteOnMergeOption deleteOnMerge, @NotNull List<GitRepository> repositories);
|
||||
|
||||
/**
|
||||
* What should be done after successful merging a branch: delete the merged branch, propose to delete or do nothing.
|
||||
|
||||
@@ -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<GitRepository> 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<GitRepository> repositories,
|
||||
@Nullable final Consumer<Boolean> resultHandler) {
|
||||
public void checkoutNewBranch(@NotNull final String name, @NotNull final List<GitRepository> 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<GitRepository> 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<GitRepository> repositories,
|
||||
@Nullable Consumer<Boolean> 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<GitRepository> repositories, @Nullable Runnable callInAwtLater) {
|
||||
commonCheckout(startPoint, newBranchName, repositories, callInAwtLater);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkout(@NotNull final String reference, @NotNull List<GitRepository> repositories, @Nullable Runnable callInAwtLater) {
|
||||
commonCheckout(reference, null, repositories, callInAwtLater);
|
||||
}
|
||||
|
||||
private void commonCheckout(@NotNull final String reference, @Nullable final String newBranch, @NotNull final List<GitRepository> repositories,
|
||||
@Nullable Runnable callInAwtLater) {
|
||||
public void checkout(@NotNull final String reference, @NotNull final List<GitRepository> 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<GitRepository> 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<GitRepository> 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<GitRepository> 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<GitRepository> 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<GitRepository> repositories) {
|
||||
final Collection<String> 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<String> successfullyDeletedLocalBranches = new ArrayList<String>(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<String> findTrackingBranches(@NotNull String remoteBranch, @NotNull List<GitRepository> repositories) {
|
||||
return new GitMultiRootBranchConfig(repositories).getTrackingBranches(remoteBranch);
|
||||
}
|
||||
|
||||
private boolean doDeleteRemote(@NotNull String branchName, @NotNull List<GitRepository> repositories) {
|
||||
GitCompoundResult result = new GitCompoundResult(myProject);
|
||||
for (GitRepository repository : repositories) {
|
||||
Pair<String, String> 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.<String>emptyList() : Collections.singletonList(result.getOutput()),
|
||||
success ? Collections.singletonList(result.getOutput()) : Collections.<String>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<String> 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<String> trackingBranches,
|
||||
boolean currentBranchTracksBranchToDelete,
|
||||
@NotNull List<GitRepository> 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<GitRepository> 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<GitRepository> 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<Change> 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<GitCommit>, List<GitCommit>> loadCommitsToCompare(@NotNull GitRepository repository, @NotNull final String branchName) {
|
||||
final List<GitCommit> headToBranch;
|
||||
final List<GitCommit> 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("<html>There are no changes between <code>%s</code> and <code>%s</code></html>",
|
||||
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<GitRepository> repositories, @Nullable final Consumer<Boolean> resultHandler) {
|
||||
@NotNull final List<GitRepository> 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<GitRepository> repositories,
|
||||
@Nullable Consumer<Boolean> resultHandler, @NotNull ProgressIndicator indicator) {
|
||||
Map<GitRepository, String> revisions = new HashMap<GitRepository, String>();
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Boolean> myResultHandler;
|
||||
|
||||
GitCheckoutNewBranchOperation(@NotNull Project project, @NotNull Git git, @NotNull Collection<GitRepository> repositories,
|
||||
@NotNull String newBranchName, @NotNull String previousBranch,
|
||||
@Nullable Consumer<Boolean> resultHandler, @NotNull ProgressIndicator indicator) {
|
||||
super(project, git, repositories, previousBranch, indicator);
|
||||
GitCheckoutNewBranchOperation(@NotNull Project project, PlatformFacade facade, @NotNull Git git, @NotNull GitBranchUiHandler uiHandler,
|
||||
@NotNull Collection<GitRepository> 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() + ":<br/>" +
|
||||
successfulRepositoriesJoined() +
|
||||
"<br/>You may rollback (checkout back to " + myPreviousBranch + " and delete " + myNewBranchName + ") not to let branches diverge.";
|
||||
"<br/>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<GitRepository> 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<GitRepository> 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<GitRepository> 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<GitRepository>, List<Change>> conflictingRepositoriesAndAffectedChanges =
|
||||
getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByCheckout, myPreviousBranch, myStartPointReference);
|
||||
getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByCheckout, myCurrentBranchOrRev, myStartPointReference);
|
||||
List<GitRepository> allConflictingRepositories = conflictingRepositoriesAndAffectedChanges.getFirst();
|
||||
List<Change> 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() + ":<br/>" +
|
||||
successfulRepositoriesJoined() +
|
||||
"<br/>" + String.format(ROLLBACK_PROPOSAL_FORMAT, myPreviousBranch);
|
||||
"<br/>" + 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<GitRepository> repositories, @NotNull final String reference, @Nullable final String newBranch, @NotNull ProgressIndicator indicator) {
|
||||
|
||||
private boolean smartCheckout(@NotNull final List<GitRepository> 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() {
|
||||
|
||||
@@ -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<GitRepository> 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<GitRepository> 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<GitRepository> 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<String> mergedToBranches = getMergedToBranches(unmergedBranch);
|
||||
|
||||
final Map<GitRepository, List<GitCommit>> history = new HashMap<GitRepository, List<GitCommit>>();
|
||||
|
||||
// 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<GitCommit> getUnmergedCommits(@NotNull GitRepository repository, @NotNull String branchName, @NotNull String baseBranch) {
|
||||
List<GitCommit> 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<String> getMergedToBranches(String branchName) {
|
||||
List<String> mergedToBranches = null;
|
||||
for (GitRepository repository : getRepositories()) {
|
||||
for (GitRepository repository : getRemainingRepositories()) {
|
||||
List<String> branches = getMergedToBranches(repository, branchName);
|
||||
if (mergedToBranches == null) {
|
||||
mergedToBranches = branches;
|
||||
|
||||
@@ -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<GitRepository> repositories,
|
||||
@NotNull String name) {
|
||||
super(project, facade, git, handler, repositories);
|
||||
myBranchName = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute() {
|
||||
final Collection<GitRepository> repositories = getRepositories();
|
||||
final Collection<String> trackingBranches = findTrackingBranches(myBranchName, repositories);
|
||||
String currentBranch = GitBranchUtil.getCurrentBranchOrRev(repositories);
|
||||
boolean currentBranchTracksBranchToDelete = false;
|
||||
if (trackingBranches.contains(currentBranch)) {
|
||||
currentBranchTracksBranchToDelete = true;
|
||||
trackingBranches.remove(currentBranch);
|
||||
}
|
||||
|
||||
final AtomicReference<DeleteRemoteBranchDecision> decision = new AtomicReference<DeleteRemoteBranchDecision>();
|
||||
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<String> successfullyDeletedLocalBranches = new ArrayList<String>(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<String> findTrackingBranches(@NotNull String remoteBranch, @NotNull Collection<GitRepository> repositories) {
|
||||
return new GitMultiRootBranchConfig(repositories).getTrackingBranches(remoteBranch);
|
||||
}
|
||||
|
||||
private boolean doDeleteRemote(@NotNull String branchName, @NotNull Collection<GitRepository> repositories) {
|
||||
GitCompoundResult result = new GitCompoundResult(myProject);
|
||||
for (GitRepository repository : repositories) {
|
||||
Pair<String, String> 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.<String>emptyList() : Collections.singletonList(result.getOutput()),
|
||||
success ? Collections.singletonList(result.getOutput()) : Collections.<String>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<String> 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<String> trackingBranches,
|
||||
boolean currentBranchTracksBranchToDelete,
|
||||
@NotNull Collection<GitRepository> 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<GitRepository, String> myCurrentRevisionsBeforeMerge;
|
||||
@Nullable private final Consumer<Boolean> myResultHandler;
|
||||
|
||||
// true in value, if we've stashed local changes before merge and will need to unstash after resolving conflicts.
|
||||
@NotNull private final Map<GitRepository, Boolean> myConflictedRepositories = new HashMap<GitRepository, Boolean>();
|
||||
private GitPreservingProcess myPreservingProcess;
|
||||
|
||||
GitMergeOperation(@NotNull Project project, @NotNull Git git, @NotNull Collection<GitRepository> repositories,
|
||||
@NotNull String branchToMerge, GitBrancher.DeleteOnMergeOption deleteOnMerge, @NotNull String currentBranch,
|
||||
@NotNull Map<GitRepository, String> currentRevisionsBeforeMerge,
|
||||
@Nullable Consumer<Boolean> resultHandler, @NotNull ProgressIndicator indicator) {
|
||||
super(project, git, repositories, currentBranch, indicator);
|
||||
GitMergeOperation(@NotNull Project project, PlatformFacade facade, @NotNull Git git, @NotNull GitBranchUiHandler uiHandler,
|
||||
@NotNull Collection<GitRepository> repositories,
|
||||
@NotNull String branchToMerge, GitBrancher.DeleteOnMergeOption deleteOnMerge,
|
||||
@NotNull Map<GitRepository, String> 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 + "<br/><a href='delete'>Delete " + myBranchToMerge + "</a>";
|
||||
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<GitRepository>, List<Change>> conflictingRepositoriesAndAffectedChanges =
|
||||
getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByMerge, myCurrentBranch, myBranchToMerge);
|
||||
getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByMerge, myCurrentBranchOrRev, myBranchToMerge);
|
||||
List<GitRepository> allConflictingRepositories = conflictingRepositoriesAndAffectedChanges.getFirst();
|
||||
List<Change> 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<GitRepository> 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<GitRepository> 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 <b><code>%s</code></b> to <b><code>%s</code></b>", myBranchToMerge, myCurrentBranch);
|
||||
return String.format("Merged <b><code>%s</code></b> to <b><code>%s</code></b>", myBranchToMerge, myCurrentBranchOrRev);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<String> parameters, @NotNull String range);
|
||||
|
||||
@NotNull
|
||||
GitCommandResult merge(@NotNull GitRepository repository, @NotNull String branchToMerge, @Nullable List<String> additionalParams,
|
||||
@NotNull GitLineHandlerListener... listeners);
|
||||
@@ -107,4 +111,13 @@ public interface Git {
|
||||
@NotNull
|
||||
GitCommandResult checkAttr(@NotNull GitRepository repository, @NotNull Collection<String> attributes,
|
||||
@NotNull Collection<VirtualFile> files);
|
||||
|
||||
@NotNull
|
||||
GitCommandResult stashSave(@NotNull GitRepository repository, @NotNull String message);
|
||||
|
||||
@NotNull
|
||||
GitCommandResult stashPop(@NotNull GitRepository repository, @NotNull GitLineHandlerListener... listeners);
|
||||
|
||||
@NotNull
|
||||
List<GitCommit> history(@NotNull GitRepository repository, @NotNull String range);
|
||||
}
|
||||
|
||||
@@ -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<String> 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<String> 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<GitCommit> 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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<GitRepository> rootsToUpdate, @NotNull UpdateMethod updateMethod) {
|
||||
GitUpdateProcess.UpdateMethod um = updateMethod == UpdateMethod.MERGE ? GitUpdateProcess.UpdateMethod.MERGE : GitUpdateProcess.UpdateMethod.REBASE;
|
||||
GitUpdateResult updateResult = new GitUpdateProcess(myProject, myProgressIndicator, new HashSet<GitRepository>(rootsToUpdate),
|
||||
UpdatedFiles.create()).update(um);
|
||||
GitUpdateResult updateResult = new GitUpdateProcess(myProject, myPlatformFacade, myProgressIndicator,
|
||||
new HashSet<GitRepository>(rootsToUpdate), UpdatedFiles.create()).update(um);
|
||||
for (GitRepository repository : rootsToUpdate) {
|
||||
repository.getRoot().refresh(true, true);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<String, ShelvedChangeList> 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<VirtualFile> rootsToSave) throws VcsException {
|
||||
LOG.info("save " + rootsToSave);
|
||||
final Map<String, Map<VirtualFile, Collection<Change>>> lists = new LocalChangesUnderRoots(myProject).getChangesByLists(rootsToSave);
|
||||
final Map<String, Map<VirtualFile, Collection<Change>>> lists =
|
||||
new LocalChangesUnderRoots(myChangeManager, myPlatformFacade.getVcsManager(myProject)).getChangesByLists(rootsToSave);
|
||||
|
||||
String oldProgressTitle = myProgressIndicator.getText();
|
||||
myProgressIndicator.setText(GitBundle.getString("update.shelving.changes"));
|
||||
|
||||
@@ -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<VirtualFile> myStashedRoots = new HashSet<VirtualFile>(); // 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<VirtualFile> rootsToSave) throws VcsException {
|
||||
LOG.info("save " + rootsToSave);
|
||||
final Map<VirtualFile, Collection<Change>> changes = new LocalChangesUnderRoots(myProject).getChangesUnderRoots(rootsToSave);
|
||||
final Map<VirtualFile, Collection<Change>> 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 <a href='saver'>here</a>", 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", "<br/>" + 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", "<br/>" + result.getErrorOutputAsHtmlString());
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static class UnstashConflictResolver extends GitConflictResolver {
|
||||
|
||||
private final Set<VirtualFile> myStashedRoots;
|
||||
|
||||
public UnstashConflictResolver(@NotNull Project project, @NotNull Git git, @NotNull Set<VirtualFile> 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<VirtualFile> stashedRoots, @Nullable Params params) {
|
||||
super(project, git, platformFacade, stashedRoots, makeParamsOrUse(params));
|
||||
myStashedRoots = stashedRoots;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<StashInfo> consumer) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<GitRepository> myRepositories;
|
||||
private final Collection<GitRepository> myRepositories;
|
||||
|
||||
public GitMultiRootBranchConfig(@NotNull List<GitRepository> repositories) {
|
||||
public GitMultiRootBranchConfig(@NotNull Collection<GitRepository> repositories) {
|
||||
myRepositories = repositories;
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<String> 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<String> remotelyChanged = GitUtil.getPathsDiffBetweenRefs(ServiceManager.getService(Git.class), repository,
|
||||
currentBranch, remoteBranch);
|
||||
final List<File> locallyChanged = myChangeListManager.getAffectedPaths();
|
||||
for (File localPath : locallyChanged) {
|
||||
if (remotelyChanged.contains(FilePathsHelper.convertPath(localPath.getPath()))) {
|
||||
|
||||
@@ -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<SequentialUpdatesContext> sequentialUpdatesContextRef) throws ProcessCanceledException {
|
||||
Set<VirtualFile> 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);
|
||||
}
|
||||
|
||||
@@ -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<GitRepository> 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<VirtualFile>(1);
|
||||
}
|
||||
|
||||
@@ -218,7 +221,9 @@ public class GitUpdateProcess {
|
||||
@NotNull
|
||||
private Map<VirtualFile, GitUpdater> tryFastForwardMergeForRebaseUpdaters(@NotNull Map<VirtualFile, GitUpdater> updaters) {
|
||||
Map<VirtualFile, GitUpdater> modifiedUpdaters = new HashMap<VirtualFile, GitUpdater>();
|
||||
Map<VirtualFile, Collection<Change>> changesUnderRoots = new LocalChangesUnderRoots(myProject).getChangesUnderRoots(updaters.keySet());
|
||||
Map<VirtualFile, Collection<Change>> changesUnderRoots =
|
||||
new LocalChangesUnderRoots(ChangeListManager.getInstance(myProject), ProjectLevelVcsManager.getInstance(myProject)).
|
||||
getChangesUnderRoots(updaters.keySet());
|
||||
for (Map.Entry<VirtualFile, GitUpdater> updaterEntry : updaters.entrySet()) {
|
||||
VirtualFile root = updaterEntry.getKey();
|
||||
GitUpdater updater = updaterEntry.getValue();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<GitRepository> 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<GitRepository> repositories,
|
||||
public GitPreservingProcess(@NotNull Project project, @NotNull PlatformFacade facade, @NotNull Git git,
|
||||
@NotNull Collection<GitRepository> 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<VirtualFile> files) {
|
||||
|
||||
@@ -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<GitRepository> 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<GitSmartOperationDialog>() {
|
||||
@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<GitSmartOperationDialog>() {
|
||||
@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<GitSmartOperationDialog>() {
|
||||
@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<GitSmartOperationDialog>() {
|
||||
@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:<br/>" +
|
||||
myUltimate.getPresentableUrl() +
|
||||
"<br/>" + 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<GitBranchIsNotFullyMergedDialog>() {
|
||||
@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<GitBranchIsNotFullyMergedDialog>() {
|
||||
@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<br/>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<br/>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<br/>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<GitBranchIsNotFullyMergedDialog>() {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<GitRepository> 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) {
|
||||
"<b><code>${s}</code></b>"
|
||||
}
|
||||
|
||||
@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<VirtualFile> 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<Change> changes = null;
|
||||
branchOperation(operation, "feature", [
|
||||
showSmartOperationDialog: { Project p, List<Change> 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<String> 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 <b><code>feature</code></b>")
|
||||
|
||||
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 <b><code>feature</code></b> to <b><code>master</code></b><br/><a href='delete'>Delete feature</a>")
|
||||
|
||||
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<Change> 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<Change> 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")}<br/><a href='delete'>Delete master2</a>", 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<br/><a href='delete'>Delete master2</a>", 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")}<br/><a href='delete'>Delete master2</a>", 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<Change> changes, @NotNull String operation, boolean force) {
|
||||
GitSmartOperationDialog.SMART_EXIT_CODE
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean showBranchIsNotFullyMergedDialog(@NotNull Project project, @NotNull Map<GitRepository, List<GitCommit>> history, @NotNull String unmergedBranch, @NotNull List<String> 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<GitRepository> repositories) {
|
||||
throw new UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean showUnmergedFilesMessageWithRollback(@NotNull String operationName, @NotNull String rollbackProposal) {
|
||||
throw new UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@Override
|
||||
void showUntrackedFilesNotification(@NotNull String operationName, @NotNull Collection<VirtualFile> untrackedFiles) {
|
||||
throw new UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean showUntrackedFilesDialogWithRollback(@NotNull String operationName, @NotNull String rollbackProposal, @NotNull Collection<VirtualFile> untrackedFiles) {
|
||||
throw new UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@Override
|
||||
void notifySuccess(@NotNull String title, @NotNull String message) {
|
||||
mySuccessMessage = message
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ class GitMockProject implements Project {
|
||||
|
||||
@Override
|
||||
boolean isDefault() {
|
||||
throw new UnsupportedOperationException()
|
||||
false
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -36,12 +36,15 @@ public class GitMockVcsManager extends ProjectLevelVcsManager {
|
||||
PlatformFacade myPlatformFacade
|
||||
Collection<String> 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<GitRepository> 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) }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String> parameters, String range) {
|
||||
execute(repository, "diff ${parameters.join(" ")} $range")
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
GitCommandResult checkAttr(@NotNull GitRepository repository, @NotNull Collection<String> attributes, @NotNull Collection<VirtualFile> 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<String> 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<GitCommit> history(GitRepository repository, String range) {
|
||||
[]
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public GitCommandResult merge(@NotNull GitRepository repository, @NotNull String branchToMerge, @Nullable List<String> 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+64
-29
@@ -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) {
|
||||
+11
-17
@@ -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<GitRepository> myRepositories = new ArrayList<GitRepository>();
|
||||
private final List<GitRepository> myRepositories = new ArrayList<GitRepository>();
|
||||
|
||||
public void add(GitRepository repository) {
|
||||
myRepositories.add(repository);
|
||||
@@ -61,12 +55,12 @@ public class GitTestRepositoryManager implements GitRepositoryManager {
|
||||
@NotNull
|
||||
@Override
|
||||
public List<GitRepository> getRepositories() {
|
||||
throw new UnsupportedOperationException();
|
||||
myRepositories
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean moreThanOneRoot() {
|
||||
throw new UnsupportedOperationException();
|
||||
myRepositories.size() > 1
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -95,6 +95,11 @@ class MockGit implements Git {
|
||||
throw new UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@Override
|
||||
GitCommandResult diff(GitRepository repository, List<String> parameters, String range) {
|
||||
throw new UnsupportedOperationException()
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
GitCommandResult merge(@NotNull GitRepository repository, @NotNull String branchToMerge, @Nullable List<String> 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<GitCommit> 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) }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user