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:
Kirill Likhodedov
2012-10-10 16:22:30 +04:00
parent 6cc70316fc
commit 05515fbbcf
50 changed files with 2397 additions and 1832 deletions
@@ -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) {
+12 -47
View File
@@ -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)
}
}
@@ -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) {
@@ -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;
}
}