mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Merge branch 'merge_from_popup'
This commit is contained in:
@@ -18,10 +18,13 @@ package git4idea;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.project.Project;
|
||||
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.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.vfs.AbstractVcsVirtualFile;
|
||||
import com.intellij.openapi.vfs.LocalFileSystem;
|
||||
@@ -673,4 +676,34 @@ public class GitUtil {
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
*/
|
||||
public class GitVcs extends AbstractVcs<CommittedChangeList> {
|
||||
public static final NotificationGroup NOTIFICATION_GROUP_ID = NotificationGroup.toolWindowGroup("Git Messages", ChangesViewContentManager.TOOLWINDOW_ID, true);
|
||||
public static final NotificationGroup IMPORTANT_ERROR_NOTIFICATION = new NotificationGroup("Git Important Errors", NotificationDisplayType.STICKY_BALLOON, true);
|
||||
public static final NotificationGroup IMPORTANT_ERROR_NOTIFICATION = new NotificationGroup("Git Important Messages", NotificationDisplayType.STICKY_BALLOON, true);
|
||||
|
||||
static {
|
||||
NotificationsConfigurationImpl.remove("Git");
|
||||
|
||||
@@ -48,6 +48,10 @@ public class NotificationManager {
|
||||
notify(notificationGroup, title, message, type, null);
|
||||
}
|
||||
|
||||
public void notifyError(String title, String message) {
|
||||
notify(GitVcs.IMPORTANT_ERROR_NOTIFICATION, title, message, NotificationType.ERROR, null);
|
||||
}
|
||||
|
||||
protected static Notification createNotification(NotificationGroup notificationGroup, String title, String message,
|
||||
NotificationType type, NotificationListener listener) {
|
||||
return notificationGroup.createNotification(title, message, type, listener);
|
||||
|
||||
@@ -18,26 +18,34 @@ package git4idea.branch;
|
||||
import com.intellij.notification.Notification;
|
||||
import com.intellij.notification.NotificationListener;
|
||||
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.ui.Messages;
|
||||
import com.intellij.openapi.ui.VerticalFlowLayout;
|
||||
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.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.GitUtil;
|
||||
import git4idea.GitVcs;
|
||||
import git4idea.MessageManager;
|
||||
import git4idea.NotificationManager;
|
||||
import git4idea.*;
|
||||
import git4idea.commands.GitMessageWithFilesDetector;
|
||||
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.ArrayList;
|
||||
import java.util.Collection;
|
||||
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,
|
||||
@@ -47,10 +55,7 @@ import static com.intellij.openapi.util.text.StringUtil.pluralize;
|
||||
*/
|
||||
abstract class GitBranchOperation {
|
||||
|
||||
static final String UNMERGED_FILES_ERROR_TITLE = "Can't checkout because of unmerged files";
|
||||
static final String UNMERGED_FILES_ERROR_NOTIFICATION_DESCRIPTION =
|
||||
"You have to <a href='resolve'>resolve</a> all merge conflicts before checkout.<br/>" +
|
||||
"After resolving conflicts you also probably would want to commit your files to the current branch.";
|
||||
private static final Logger LOG = Logger.getInstance(GitBranchOperation.class);
|
||||
|
||||
@NotNull protected final Project myProject;
|
||||
@NotNull private final Collection<GitRepository> myRepositories;
|
||||
@@ -78,6 +83,14 @@ abstract class GitBranchOperation {
|
||||
@NotNull
|
||||
protected abstract String getRollbackProposal();
|
||||
|
||||
/**
|
||||
* Returns a short downcased name of the operation.
|
||||
* It is used by some dialogs or notifications which are common to several operations.
|
||||
* Some operations (like checkout new branch) can be not mentioned in these dialogs, so their operation names would be not used.
|
||||
*/
|
||||
@NotNull
|
||||
protected abstract String getOperationName();
|
||||
|
||||
/**
|
||||
* @return next repository that wasn't handled (e.g. checked out) yet.
|
||||
*/
|
||||
@@ -135,8 +148,19 @@ abstract class GitBranchOperation {
|
||||
return myRemainingRepositories;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected List<GitRepository> getRemainingRepositoriesExceptGiven(@NotNull final GitRepository currentRepository) {
|
||||
List<GitRepository> repositories = new ArrayList<GitRepository>(myRemainingRepositories);
|
||||
repositories.remove(currentRepository);
|
||||
return repositories;
|
||||
}
|
||||
|
||||
protected void notifySuccess(@NotNull String message) {
|
||||
NotificationManager.getInstance(myProject).notify(GitVcs.NOTIFICATION_GROUP_ID, "", message, NotificationType.INFORMATION);
|
||||
}
|
||||
|
||||
protected void notifySuccess() {
|
||||
NotificationManager.getInstance(myProject).notify(GitVcs.NOTIFICATION_GROUP_ID, "", getSuccessMessage(), NotificationType.INFORMATION);
|
||||
notifySuccess(getSuccessMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,6 +190,27 @@ abstract class GitBranchOperation {
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
@@ -201,10 +246,12 @@ abstract class GitBranchOperation {
|
||||
final AtomicBoolean ok = new AtomicBoolean();
|
||||
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
|
||||
@Override public void run() {
|
||||
String description = "<html>You have to resolve all merge conflicts before checkout.<br/>" + getRollbackProposal() + "</html>";
|
||||
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, UNMERGED_FILES_ERROR_TITLE, "Rollback", "Don't rollback", Messages.getErrorIcon()));
|
||||
ok.set(Messages.OK == MessageManager.showYesNoDialog(myProject, description, unmergedFilesErrorTitle(),
|
||||
"Rollback", "Don't rollback", Messages.getErrorIcon()));
|
||||
}
|
||||
});
|
||||
if (ok.get()) {
|
||||
@@ -213,13 +260,15 @@ abstract class GitBranchOperation {
|
||||
}
|
||||
|
||||
private void showUnmergedFilesNotification() {
|
||||
String title = UNMERGED_FILES_ERROR_TITLE;
|
||||
String description = UNMERGED_FILES_ERROR_NOTIFICATION_DESCRIPTION;
|
||||
NotificationManager.getInstance(myProject).notify(GitVcs.IMPORTANT_ERROR_NOTIFICATION, title, description, NotificationType.ERROR, new NotificationListener() {
|
||||
String title = unmergedFilesErrorTitle();
|
||||
String description = unmergedFilesErrorNotificationDescription();
|
||||
NotificationManager.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 checking out.").
|
||||
setMergeDescription("The following files have unresolved conflicts. You need to resolve them before " +
|
||||
getOperationName() + ".").
|
||||
setErrorNotificationTitle("Unresolved files remain.");
|
||||
new GitConflictResolver(myProject, GitUtil.getRoots(getRepositories()), params).merge();
|
||||
}
|
||||
@@ -227,4 +276,139 @@ abstract class GitBranchOperation {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously refreshes the VFS root directory of the given repository.
|
||||
*/
|
||||
protected static void refreshRoot(@NotNull GitRepository repository) {
|
||||
repository.getRoot().refresh(true, true);
|
||||
}
|
||||
|
||||
protected void fatalLocalChangesError(@NotNull String reference) {
|
||||
String title = String.format("Couldn't %s %s", getOperationName(), reference);
|
||||
String message = String.format("Local changes would be overwritten by %s.<br/>You should stash or commit them.<br/>", getOperationName());
|
||||
if (wereSuccessful()) {
|
||||
showFatalErrorDialogWithRollback(title, message);
|
||||
}
|
||||
else {
|
||||
showFatalNotification(title, message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the error "The following untracked working tree files would be overwritten by checkout/merge".
|
||||
* If there were no repositories that succeeded the operation, shows a notification with a link to the list of these untracked files.
|
||||
* If some repositories succeeded, shows a dialog with the list of these files and a proposal to rollback the operation of those
|
||||
* repositories.
|
||||
*/
|
||||
protected void fatalUntrackedFilesError(@NotNull Collection<VirtualFile> untrackedFiles) {
|
||||
if (wereSuccessful()) {
|
||||
showUntrackedFilesDialogWithRollback(untrackedFiles);
|
||||
}
|
||||
else {
|
||||
showUntrackedFilesNotification(untrackedFiles);
|
||||
}
|
||||
}
|
||||
|
||||
private void showUntrackedFilesNotification(@NotNull Collection<VirtualFile> untrackedFiles) {
|
||||
UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject, untrackedFiles, getOperationName());
|
||||
}
|
||||
|
||||
private void showUntrackedFilesDialogWithRollback(@NotNull Collection<VirtualFile> untrackedFiles) {
|
||||
String title = "Couldn't " + getOperationName();
|
||||
String description = UntrackedFilesNotifier.createUntrackedFilesOverwrittenDescription(getOperationName(), true);
|
||||
|
||||
final SelectFilesDialog dialog = new UntrackedFilesDialog(myProject, new ArrayList<VirtualFile>(untrackedFiles),
|
||||
stripHtml(description, true));
|
||||
dialog.setTitle(title);
|
||||
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
DialogManager.getInstance(myProject).showDialog(dialog);
|
||||
}
|
||||
});
|
||||
|
||||
if (dialog.isOK()) {
|
||||
rollback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO this is non-optimal and even incorrect, since such diff shows the difference between committed changes
|
||||
* For each of the given repositories looks to the diff between current branch and the given branch and converts it to the list of
|
||||
* 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>> 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);
|
||||
if (!changesInRepo.isEmpty()) {
|
||||
changes.put(repository, changesInRepo);
|
||||
}
|
||||
}
|
||||
catch (VcsException e) {
|
||||
// ignoring the exception: this is not fatal if we won't collect such a diff from other repositories.
|
||||
// At worst, use will get double dialog proposing the smart checkout.
|
||||
LOG.warn(String.format("Couldn't collect diff between %s and %s in %s", currentBranch, otherBranch, repository.getRoot()), e);
|
||||
}
|
||||
}
|
||||
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);
|
||||
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}.
|
||||
* Then all remaining (non successful repositories) are searched if they are about to fail with the same problem.
|
||||
* All collected local changes which prevent the operation, together with these repositories, are returned.
|
||||
* @param currentRepository The first repository which failed the operation.
|
||||
* @param localChangesOverwrittenBy The detector of local changes would be overwritten by merge/checkout.
|
||||
* @param currentBranch Current branch.
|
||||
* @param nextBranch Branch to compare with (the branch to be checked out, or the branch to be merged).
|
||||
* @return Repositories that have failed or would fail with the "local changes" error, together with these local changes.
|
||||
*/
|
||||
@NotNull
|
||||
protected Pair<List<GitRepository>, List<Change>> getConflictingRepositoriesAndAffectedChanges(
|
||||
@NotNull GitRepository currentRepository, @NotNull GitMessageWithFilesDetector localChangesOverwrittenBy,
|
||||
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);
|
||||
// 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);
|
||||
|
||||
Set<GitRepository> otherProblematicRepositories = conflictingChangesInRepositories.keySet();
|
||||
List<GitRepository> allConflictingRepositories = new ArrayList<GitRepository>(otherProblematicRepositories);
|
||||
allConflictingRepositories.add(currentRepository);
|
||||
for (List<Change> changes : conflictingChangesInRepositories.values()) {
|
||||
affectedChanges.addAll(changes);
|
||||
}
|
||||
|
||||
return Pair.create(allConflictingRepositories, affectedChanges);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -39,7 +39,9 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Executor of Git branching operations.
|
||||
@@ -172,11 +174,7 @@ public final class GitBranchOperationsProcessor {
|
||||
}
|
||||
|
||||
private void doDelete(final String branchName, ProgressIndicator indicator) {
|
||||
GitDeleteBranchOperation operation = new GitDeleteBranchOperation(myProject, myRepositories, branchName, getCurrentBranch(), indicator);
|
||||
operation.execute();
|
||||
for (GitRepository repository : myRepositories) {
|
||||
repository.update(GitRepository.TrackedTopic.BRANCHES, GitRepository.TrackedTopic.CONFIG);
|
||||
}
|
||||
new GitDeleteBranchOperation(myProject, myRepositories, branchName, getCurrentBranch(), indicator).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,6 +234,22 @@ public final class GitBranchOperationsProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
public void merge(@NotNull final String branchName) {
|
||||
new CommonBackgroundTask(myProject, "Merging " + branchName, myCallInAwtAfterExecution) {
|
||||
@Override public void execute(@NotNull ProgressIndicator indicator) {
|
||||
doMerge(branchName, indicator);
|
||||
}
|
||||
}.runInBackground();
|
||||
}
|
||||
|
||||
private void doMerge(@NotNull String branchName, @NotNull ProgressIndicator indicator) {
|
||||
Map<GitRepository, String> revisions = new HashMap<GitRepository, String>();
|
||||
for (GitRepository repository : myRepositories) {
|
||||
revisions.put(repository, repository.getCurrentRevision());
|
||||
}
|
||||
new GitMergeOperation(myProject, myRepositories, branchName, getCurrentBranch(), revisions, indicator).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes common operations before/after executing the actual branch operation.
|
||||
*/
|
||||
|
||||
@@ -37,13 +37,13 @@ import static git4idea.util.GitUIUtil.code;
|
||||
*
|
||||
* @author Kirill Likhodedov
|
||||
*/
|
||||
public class GitCheckoutNewBranchOperation extends GitBranchOperation {
|
||||
class GitCheckoutNewBranchOperation extends GitBranchOperation {
|
||||
|
||||
@NotNull private final Project myProject;
|
||||
@NotNull private final String myNewBranchName;
|
||||
@NotNull private final String myPreviousBranch;
|
||||
|
||||
public GitCheckoutNewBranchOperation(@NotNull Project project, @NotNull Collection<GitRepository> repositories,
|
||||
GitCheckoutNewBranchOperation(@NotNull Project project, @NotNull Collection<GitRepository> repositories,
|
||||
@NotNull String newBranchName, @NotNull String previousBranch,
|
||||
@NotNull ProgressIndicator indicator) {
|
||||
super(project, repositories, indicator);
|
||||
@@ -58,7 +58,7 @@ public class GitCheckoutNewBranchOperation extends GitBranchOperation {
|
||||
while (hasMoreRepositories() && !fatalErrorHappened) {
|
||||
final GitRepository repository = next();
|
||||
|
||||
GitSimpleEventDetector unmergedDetector = new GitSimpleEventDetector(GitSimpleEventDetector.Event.UNMERGED);
|
||||
GitSimpleEventDetector unmergedDetector = new GitSimpleEventDetector(GitSimpleEventDetector.Event.UNMERGED_PREVENTING_CHECKOUT);
|
||||
GitCommandResult result = Git.checkoutNewBranch(repository, myNewBranchName, unmergedDetector);
|
||||
|
||||
if (result.success()) {
|
||||
@@ -98,6 +98,12 @@ public class GitCheckoutNewBranchOperation extends GitBranchOperation {
|
||||
"<br/>You may rollback (checkout back to " + myPreviousBranch + " and delete " + myNewBranchName + ") not to let branches diverge.";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected String getOperationName() {
|
||||
return "checkout";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void rollback() {
|
||||
GitCompoundResult checkoutResult = new GitCompoundResult(myProject);
|
||||
|
||||
@@ -19,42 +19,21 @@ 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.ui.VerticalFlowLayout;
|
||||
import com.intellij.openapi.util.Clock;
|
||||
import com.intellij.openapi.util.Condition;
|
||||
import com.intellij.openapi.util.io.FileUtil;
|
||||
import com.intellij.openapi.vcs.VcsException;
|
||||
import com.intellij.openapi.util.Pair;
|
||||
import com.intellij.openapi.vcs.changes.Change;
|
||||
import com.intellij.openapi.vcs.changes.ChangeListManager;
|
||||
import com.intellij.openapi.vcs.changes.ui.SelectFilesDialog;
|
||||
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
|
||||
import com.intellij.openapi.vcs.merge.MergeDialogCustomizer;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.ui.components.JBLabel;
|
||||
import com.intellij.util.ArrayUtil;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.intellij.util.continuation.ContinuationContext;
|
||||
import com.intellij.util.text.DateFormatUtil;
|
||||
import com.intellij.util.ui.UIUtil;
|
||||
import com.intellij.vcsUtil.VcsUtil;
|
||||
import git4idea.DialogManager;
|
||||
import git4idea.GitUtil;
|
||||
import git4idea.GitVcs;
|
||||
import git4idea.commands.*;
|
||||
import git4idea.merge.GitConflictResolver;
|
||||
import git4idea.repo.GitRepository;
|
||||
import git4idea.stash.GitChangesSaver;
|
||||
import git4idea.update.GitComplexProcess;
|
||||
import git4idea.util.GitPreservingProcess;
|
||||
import git4idea.util.GitUIUtil;
|
||||
import git4idea.util.UntrackedFilesNotifier;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.util.*;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static com.intellij.openapi.util.text.StringUtil.*;
|
||||
import static git4idea.commands.GitMessageWithFilesDetector.Event.LOCAL_CHANGES_OVERWRITTEN_BY_CHECKOUT;
|
||||
import static git4idea.commands.GitMessageWithFilesDetector.Event.UNTRACKED_FILES_OVERWRITTEN_BY;
|
||||
import static git4idea.util.GitUIUtil.code;
|
||||
@@ -68,15 +47,16 @@ import static git4idea.util.GitUIUtil.code;
|
||||
*
|
||||
* @author Kirill Likhodedov
|
||||
*/
|
||||
public class GitCheckoutOperation extends GitBranchOperation {
|
||||
class GitCheckoutOperation extends GitBranchOperation {
|
||||
|
||||
private static final Logger LOG = Logger.getInstance(GitCheckoutOperation.class);
|
||||
public static final String ROLLBACK_PROPOSAL_FORMAT = "You may rollback (checkout back to %s) not to let branches diverge.";
|
||||
|
||||
@NotNull private final String myStartPointReference;
|
||||
@Nullable private final String myNewBranch;
|
||||
@NotNull private final String myPreviousBranch;
|
||||
|
||||
public GitCheckoutOperation(@NotNull Project project, @NotNull Collection<GitRepository> repositories,
|
||||
GitCheckoutOperation(@NotNull Project project, @NotNull Collection<GitRepository> repositories,
|
||||
@NotNull String startPointReference, @Nullable String newBranch, @NotNull String previousBranch,
|
||||
@NotNull ProgressIndicator indicator) {
|
||||
super(project, repositories, indicator);
|
||||
@@ -93,7 +73,7 @@ public class GitCheckoutOperation extends GitBranchOperation {
|
||||
|
||||
VirtualFile root = repository.getRoot();
|
||||
GitMessageWithFilesDetector localChangesOverwrittenByCheckout = new GitMessageWithFilesDetector(LOCAL_CHANGES_OVERWRITTEN_BY_CHECKOUT, root);
|
||||
GitSimpleEventDetector unmergedFiles = new GitSimpleEventDetector(GitSimpleEventDetector.Event.UNMERGED);
|
||||
GitSimpleEventDetector unmergedFiles = new GitSimpleEventDetector(GitSimpleEventDetector.Event.UNMERGED_PREVENTING_CHECKOUT);
|
||||
GitMessageWithFilesDetector untrackedOverwrittenByCheckout = new GitMessageWithFilesDetector(UNTRACKED_FILES_OVERWRITTEN_BY, root);
|
||||
|
||||
GitCommandResult result = Git.checkout(repository, myStartPointReference, myNewBranch, false,
|
||||
@@ -129,27 +109,19 @@ public class GitCheckoutOperation extends GitBranchOperation {
|
||||
|
||||
private boolean smartCheckoutOrNotify(@NotNull GitRepository repository,
|
||||
@NotNull GitMessageWithFilesDetector localChangesOverwrittenByCheckout) {
|
||||
// get changes overwritten by checkout from the error message captured from Git
|
||||
List<Change> affectedChanges = getChangesAffectedByCheckout(repository, localChangesOverwrittenByCheckout.getRelativeFilePaths(), true);
|
||||
// get all other conflicting changes
|
||||
Map<GitRepository, List<Change>> conflictingChangesInRepositories = collectLocalChangesOnAllOtherRepositories(repository);
|
||||
Set<GitRepository> otherProblematicRepositories = conflictingChangesInRepositories.keySet();
|
||||
List<GitRepository> allConflictingRepositories = new ArrayList<GitRepository>(otherProblematicRepositories);
|
||||
allConflictingRepositories.add(repository);
|
||||
for (List<Change> changes : conflictingChangesInRepositories.values()) {
|
||||
affectedChanges.addAll(changes);
|
||||
}
|
||||
Pair<List<GitRepository>, List<Change>> conflictingRepositoriesAndAffectedChanges =
|
||||
getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByCheckout, myPreviousBranch, myStartPointReference);
|
||||
List<GitRepository> allConflictingRepositories = conflictingRepositoriesAndAffectedChanges.getFirst();
|
||||
List<Change> affectedChanges = conflictingRepositoriesAndAffectedChanges.getSecond();
|
||||
|
||||
int smartCheckoutDecision = GitWouldBeOverwrittenByCheckoutDialog.showAndGetAnswer(myProject, affectedChanges);
|
||||
if (smartCheckoutDecision == GitWouldBeOverwrittenByCheckoutDialog.SMART_CHECKOUT) {
|
||||
int smartCheckoutDecision = GitSmartOperationDialog.showAndGetAnswer(myProject, affectedChanges, "checkout", true);
|
||||
if (smartCheckoutDecision == GitSmartOperationDialog.SMART_EXIT_CODE) {
|
||||
boolean smartCheckedOutSuccessfully = smartCheckout(allConflictingRepositories, myStartPointReference, myNewBranch, getIndicator());
|
||||
if (smartCheckedOutSuccessfully) {
|
||||
GitRepository[] otherRepositories = ArrayUtil.toObjectArray(otherProblematicRepositories, GitRepository.class);
|
||||
|
||||
markSuccessful(repository);
|
||||
markSuccessful(otherRepositories);
|
||||
refresh(repository);
|
||||
refresh(otherRepositories);
|
||||
for (GitRepository conflictingRepository : allConflictingRepositories) {
|
||||
markSuccessful(conflictingRepository);
|
||||
refresh(conflictingRepository);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
@@ -157,94 +129,27 @@ public class GitCheckoutOperation extends GitBranchOperation {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (smartCheckoutDecision == GitWouldBeOverwrittenByCheckoutDialog.FORCE_CHECKOUT_EXIT_CODE) {
|
||||
else if (smartCheckoutDecision == GitSmartOperationDialog.FORCE_EXIT_CODE) {
|
||||
return checkoutOrNotify(allConflictingRepositories, myStartPointReference, myNewBranch, true);
|
||||
}
|
||||
else {
|
||||
fatalLocalChangesError();
|
||||
fatalLocalChangesError(myStartPointReference);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void fatalLocalChangesError() {
|
||||
String title = "Couldn't checkout " + myStartPointReference;
|
||||
String message = "Local changes would be overwritten by checkout.<br/>Stash or commit them before checking out a branch.<br/>";
|
||||
if (wereSuccessful()) {
|
||||
showFatalErrorDialogWithRollback(title, message);
|
||||
}
|
||||
else {
|
||||
showFatalNotification(title, message);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map<GitRepository, List<Change>> collectLocalChangesOnAllOtherRepositories(@NotNull final GitRepository currentRepository) {
|
||||
// get changes in all other repositories (except those which already have succeeded) to avoid multiple dialogs proposing smart checkout
|
||||
List<GitRepository> remainingRepositories = ContainerUtil.filter(getRepositories(), new Condition<GitRepository>() {
|
||||
@Override
|
||||
public boolean value(GitRepository repo) {
|
||||
return !repo.equals(currentRepository) && !getSuccessfulRepositories().contains(repo);
|
||||
}
|
||||
});
|
||||
return collectChangesConflictingWithCheckout(remainingRepositories);
|
||||
}
|
||||
|
||||
private void fatalUntrackedFilesError(@NotNull Collection<VirtualFile> untrackedFiles) {
|
||||
if (wereSuccessful()) {
|
||||
showUntrackedFilesDialogWithRollback(untrackedFiles);
|
||||
}
|
||||
else {
|
||||
showUntrackedFilesNotification(untrackedFiles);
|
||||
}
|
||||
}
|
||||
|
||||
private void showUntrackedFilesNotification(@NotNull Collection<VirtualFile> untrackedFiles) {
|
||||
UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject, untrackedFiles, "checkout");
|
||||
}
|
||||
|
||||
private void showUntrackedFilesDialogWithRollback(@NotNull Collection<VirtualFile> untrackedFiles) {
|
||||
String title = "Couldn't checkout";
|
||||
String description = UntrackedFilesNotifier.createUntrackedFilesOverwrittenDescription("checkout", true);
|
||||
|
||||
final SelectFilesDialog dialog = new UntrackedFilesDialog(myProject, new ArrayList<VirtualFile>(untrackedFiles),
|
||||
stripHtml(description, true));
|
||||
dialog.setTitle(title);
|
||||
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
DialogManager.getInstance(myProject).showDialog(dialog);
|
||||
}
|
||||
});
|
||||
|
||||
if (dialog.isOK()) {
|
||||
rollback();
|
||||
}
|
||||
}
|
||||
|
||||
private class UntrackedFilesDialog extends SelectFilesDialog {
|
||||
|
||||
public UntrackedFilesDialog(@NotNull Project project, @NotNull List<VirtualFile> originalFiles, @NotNull String prompt) {
|
||||
super(project, originalFiles, prompt, null, 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;
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected String getRollbackProposal() {
|
||||
return "However checkout has succeeded for the following " + repositories() + ":<br/>" +
|
||||
successfulRepositoriesJoined() +
|
||||
"<br/>You may rollback (checkout back to " + myPreviousBranch + ") not to let branches diverge.";
|
||||
"<br/>" + String.format(ROLLBACK_PROPOSAL_FORMAT, myPreviousBranch);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected String getOperationName() {
|
||||
return "checkout";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -284,26 +189,6 @@ public class GitCheckoutOperation extends GitBranchOperation {
|
||||
return "Couldn't checkout " + myStartPointReference;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map<GitRepository, List<Change>> collectChangesConflictingWithCheckout(@NotNull Collection<GitRepository> repositories) {
|
||||
Map<GitRepository, List<Change>> changes = new HashMap<GitRepository, List<Change>>();
|
||||
for (GitRepository repository : repositories) {
|
||||
try {
|
||||
Collection<String> diff = GitUtil.getPathsDiffBetweenRefs(myPreviousBranch, myStartPointReference, myProject, repository.getRoot());
|
||||
List<Change> changesInRepo = getChangesAffectedByCheckout(repository, diff, false);
|
||||
if (!changesInRepo.isEmpty()) {
|
||||
changes.put(repository, changesInRepo);
|
||||
}
|
||||
}
|
||||
catch (VcsException e) {
|
||||
// ignoring the exception: this is not fatal if we won't collect such a diff from other repositories.
|
||||
// At worst, use will get double dialog proposing the smart checkout.
|
||||
LOG.warn(String.format("Couldn't collect diff between %s and %s in %s", myPreviousBranch, myStartPointReference, repository.getRoot()), e);
|
||||
}
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getSuccessMessage() {
|
||||
@@ -315,76 +200,18 @@ public 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) {
|
||||
final GitChangesSaver saver = configureSaver(reference, indicator);
|
||||
|
||||
final AtomicBoolean result = new AtomicBoolean();
|
||||
GitComplexProcess.Operation checkoutOperation = new GitComplexProcess.Operation() {
|
||||
@Override public void run(ContinuationContext context) {
|
||||
boolean savedSuccessfully = save(repositories, saver);
|
||||
if (savedSuccessfully) {
|
||||
try {
|
||||
result.set(checkoutOrNotify(repositories, reference, newBranch, false));
|
||||
} finally {
|
||||
saver.restoreLocalChanges(context);
|
||||
}
|
||||
}
|
||||
GitPreservingProcess preservingProcess = new GitPreservingProcess(myProject, repositories, "checkout", reference, indicator, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
result.set(checkoutOrNotify(repositories, reference, newBranch, false));
|
||||
}
|
||||
};
|
||||
GitComplexProcess.execute(myProject, "checkout", checkoutOperation);
|
||||
});
|
||||
preservingProcess.execute();
|
||||
return result.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the saver, actually notifications and texts in the GitConflictResolver used inside.
|
||||
*/
|
||||
private GitChangesSaver configureSaver(final String reference, ProgressIndicator indicator) {
|
||||
GitChangesSaver saver = GitChangesSaver.getSaver(myProject, indicator, String.format("Checkout %s at %s",
|
||||
reference,
|
||||
DateFormatUtil.formatDateTime(Clock.getTime())));
|
||||
MergeDialogCustomizer mergeDialogCustomizer = new MergeDialogCustomizer() {
|
||||
@Override
|
||||
public String getMultipleFileMergeDescription(Collection<VirtualFile> files) {
|
||||
return String.format(
|
||||
"<html>Uncommitted changes that were saved before checkout have conflicts with files from <code>%s</code></html>",
|
||||
reference);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLeftPanelTitle(VirtualFile file) {
|
||||
return "Uncommitted changes";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRightPanelTitle(VirtualFile file, VcsRevisionNumber lastRevisionNumber) {
|
||||
return String.format("<html>Changes from <b><code>%s</code></b></html>", reference);
|
||||
}
|
||||
};
|
||||
|
||||
GitConflictResolver.Params params = new GitConflictResolver.Params().
|
||||
setReverse(true).
|
||||
setMergeDialogCustomizer(mergeDialogCustomizer).
|
||||
setErrorNotificationTitle("Local changes were not restored");
|
||||
|
||||
saver.setConflictResolverParams(params);
|
||||
return saver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves local changes. In case of error shows a notification and returns false.
|
||||
*/
|
||||
private boolean save(@NotNull Collection<GitRepository> repositories, @NotNull GitChangesSaver saver) {
|
||||
try {
|
||||
saver.saveLocalChanges(GitUtil.getRoots(repositories));
|
||||
return true;
|
||||
} catch (VcsException e) {
|
||||
LOG.info("Couldn't save local changes", e);
|
||||
notifyError("Couldn't save uncommitted changes.",
|
||||
String.format("Tried to save uncommitted changes in %s before checkout, but failed with an error.<br/>%s",
|
||||
saver.getSaverName(), join(e.getMessages())));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks out or shows an error message.
|
||||
*/
|
||||
@@ -401,45 +228,10 @@ public class GitCheckoutOperation extends GitBranchOperation {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forms the list of the changes, that would be overwritten by checkout.
|
||||
*
|
||||
* @param repository
|
||||
* @param affectedPaths paths returned by Git.
|
||||
* @param relativePaths Are the paths specified relative or absolute.
|
||||
* @return List of Changes is these paths.
|
||||
*/
|
||||
private List<Change> getChangesAffectedByCheckout(@NotNull GitRepository repository, @NotNull Collection<String> affectedPaths, boolean relativePaths) {
|
||||
ChangeListManager changeListManager = ChangeListManager.getInstance(myProject);
|
||||
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;
|
||||
}
|
||||
|
||||
private static void refresh(GitRepository... repositories) {
|
||||
for (GitRepository repository : repositories) {
|
||||
refreshRoot(repository);
|
||||
// repository state will be auto-updated with this VFS refresh => no need to call GitRepository#update().
|
||||
}
|
||||
}
|
||||
|
||||
private static void refreshRoot(GitRepository repository) {
|
||||
repository.getRoot().refresh(true, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class GitDeleteBranchOperation extends GitBranchOperation {
|
||||
private final String myBranchName;
|
||||
private final String myCurrentBranch;
|
||||
|
||||
public GitDeleteBranchOperation(@NotNull Project project, @NotNull Collection<GitRepository> repositories,
|
||||
GitDeleteBranchOperation(@NotNull Project project, @NotNull Collection<GitRepository> repositories,
|
||||
@NotNull String branchName, @NotNull String currentBranch, @NotNull ProgressIndicator indicator) {
|
||||
super(project, repositories, indicator);
|
||||
myBranchName = branchName;
|
||||
@@ -142,6 +142,12 @@ class GitDeleteBranchOperation extends GitBranchOperation {
|
||||
"<br/>You may rollback (recreate " + myBranchName + " in these roots) not to let branches diverge.";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected String getOperationName() {
|
||||
return "branch deletion";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private GitCompoundResult forceDelete(@NotNull String branchName, @NotNull Collection<GitRepository> possibleFailedRepositories) {
|
||||
GitCompoundResult compoundResult = new GitCompoundResult(myProject);
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
* 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.NotificationType;
|
||||
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 git4idea.GitUtil;
|
||||
import git4idea.GitVcs;
|
||||
import git4idea.NotificationManager;
|
||||
import git4idea.commands.*;
|
||||
import git4idea.merge.GitConflictResolver;
|
||||
import git4idea.merge.GitMergeCommittingConflictResolver;
|
||||
import git4idea.merge.GitMerger;
|
||||
import git4idea.repo.GitRepository;
|
||||
import git4idea.util.GitPreservingProcess;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static git4idea.commands.GitMessageWithFilesDetector.Event.LOCAL_CHANGES_OVERWRITTEN_BY_MERGE;
|
||||
import static git4idea.commands.GitMessageWithFilesDetector.Event.UNTRACKED_FILES_OVERWRITTEN_BY;
|
||||
|
||||
/**
|
||||
* @author Kirill Likhodedov
|
||||
*/
|
||||
class GitMergeOperation extends GitBranchOperation {
|
||||
|
||||
private static final Logger LOG = Logger.getInstance(GitMergeOperation.class);
|
||||
public static final String ROLLBACK_PROPOSAL = "You may rollback (reset to the commit before merging) not to let branches diverge.";
|
||||
|
||||
@NotNull private final ChangeListManager myChangeListManager;
|
||||
@NotNull private final String myBranchToMerge;
|
||||
@NotNull private final String myCurrentBranch;
|
||||
@NotNull private final Map<GitRepository, String> myCurrentRevisionsBeforeMerge;
|
||||
|
||||
// true in value if unstash will be needed after resolving conflicts.
|
||||
@NotNull private final Map<GitRepository, Boolean> myConflictedRepositories = new HashMap<GitRepository, Boolean>();
|
||||
private GitPreservingProcess myPreservingProcess;
|
||||
|
||||
protected GitMergeOperation(@NotNull Project project, @NotNull Collection<GitRepository> repositories,
|
||||
@NotNull String branchToMerge,
|
||||
@NotNull String currentBranch,
|
||||
@NotNull Map<GitRepository, String> currentRevisionsBeforeMerge,
|
||||
@NotNull ProgressIndicator indicator) {
|
||||
super(project, repositories, indicator);
|
||||
myBranchToMerge = branchToMerge;
|
||||
myCurrentBranch = currentBranch;
|
||||
myCurrentRevisionsBeforeMerge = currentRevisionsBeforeMerge;
|
||||
myChangeListManager = ChangeListManager.getInstance(myProject);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute() {
|
||||
boolean fatalErrorHappened = false;
|
||||
int alreadyUpToDateRepositories = 0;
|
||||
while (hasMoreRepositories() && !fatalErrorHappened) {
|
||||
final GitRepository repository = next();
|
||||
|
||||
VirtualFile root = repository.getRoot();
|
||||
GitMessageWithFilesDetector localChangesOverwrittenByMerge = new GitMessageWithFilesDetector(LOCAL_CHANGES_OVERWRITTEN_BY_MERGE, root);
|
||||
GitSimpleEventDetector unmergedFiles = new GitSimpleEventDetector(GitSimpleEventDetector.Event.UNMERGED_PREVENTING_MERGE);
|
||||
GitMessageWithFilesDetector untrackedOverwrittenByMerge = new GitMessageWithFilesDetector(UNTRACKED_FILES_OVERWRITTEN_BY, root);
|
||||
GitSimpleEventDetector mergeConflict = new GitSimpleEventDetector(GitSimpleEventDetector.Event.MERGE_CONFLICT);
|
||||
GitSimpleEventDetector alreadyUpToDateDetector = new GitSimpleEventDetector(GitSimpleEventDetector.Event.ALREADY_UP_TO_DATE);
|
||||
|
||||
GitCommandResult result = Git.merge(repository, myBranchToMerge,
|
||||
localChangesOverwrittenByMerge, unmergedFiles, untrackedOverwrittenByMerge, mergeConflict,
|
||||
alreadyUpToDateDetector);
|
||||
if (result.success()) {
|
||||
refresh(repository);
|
||||
markSuccessful(repository);
|
||||
if (alreadyUpToDateDetector.hasHappened()) {
|
||||
alreadyUpToDateRepositories += 1;
|
||||
}
|
||||
}
|
||||
else if (unmergedFiles.hasHappened()) {
|
||||
fatalUnmergedFilesError();
|
||||
fatalErrorHappened = true;
|
||||
}
|
||||
else if (localChangesOverwrittenByMerge.wasMessageDetected()) {
|
||||
boolean smartMergeSucceeded = proposeSmartMergePerformAndNotify(repository, localChangesOverwrittenByMerge);
|
||||
if (!smartMergeSucceeded) {
|
||||
fatalErrorHappened = true;
|
||||
}
|
||||
}
|
||||
else if (mergeConflict.hasHappened()) {
|
||||
myConflictedRepositories.put(repository, Boolean.FALSE);
|
||||
refresh(repository);
|
||||
markSuccessful(repository);
|
||||
}
|
||||
else if (untrackedOverwrittenByMerge.wasMessageDetected()) {
|
||||
fatalUntrackedFilesError(untrackedOverwrittenByMerge.getFiles());
|
||||
fatalErrorHappened = true;
|
||||
}
|
||||
else {
|
||||
fatalError(getCommonErrorTitle(), result.getErrorOutputAsJoinedString());
|
||||
fatalErrorHappened = true;
|
||||
}
|
||||
}
|
||||
|
||||
boolean allConflictsResolved = resolveConflicts();
|
||||
|
||||
if (!fatalErrorHappened && allConflictsResolved) {
|
||||
if (alreadyUpToDateRepositories < getRepositories().size()) {
|
||||
notifySuccess();
|
||||
}
|
||||
else {
|
||||
notifySuccess("Already up-to-date.");
|
||||
}
|
||||
}
|
||||
|
||||
restoreLocalChanges();
|
||||
}
|
||||
|
||||
private boolean resolveConflicts() {
|
||||
if (!myConflictedRepositories.isEmpty()) {
|
||||
GitMerger merger = new GitMerger(myProject);
|
||||
GitConflictResolver.Params params = new GitConflictResolver.Params();
|
||||
GitMergeCommittingConflictResolver conflictResolver =
|
||||
new GitMergeCommittingConflictResolver(myProject, merger, GitUtil.getRoots(myConflictedRepositories.keySet()), params, true) {
|
||||
@Override
|
||||
protected void notifyUnresolvedRemain() {
|
||||
NotificationManager.getInstance(myProject).notify(
|
||||
GitVcs.IMPORTANT_ERROR_NOTIFICATION, "Merged branch " + myBranchToMerge + " with conflicts",
|
||||
"Unresolved conflicts remain in the project. <a href='resolve'>Resolve now.</a>", NotificationType.WARNING,
|
||||
getResolveLinkListener());
|
||||
}
|
||||
};
|
||||
return conflictResolver.merge();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean proposeSmartMergePerformAndNotify(@NotNull GitRepository repository,
|
||||
@NotNull GitMessageWithFilesDetector localChangesOverwrittenByMerge) {
|
||||
Pair<List<GitRepository>, List<Change>> conflictingRepositoriesAndAffectedChanges =
|
||||
getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByMerge, myCurrentBranch, myBranchToMerge);
|
||||
List<GitRepository> allConflictingRepositories = conflictingRepositoriesAndAffectedChanges.getFirst();
|
||||
List<Change> affectedChanges = conflictingRepositoriesAndAffectedChanges.getSecond();
|
||||
|
||||
int smartCheckoutDecision = GitSmartOperationDialog.showAndGetAnswer(myProject, affectedChanges, "merge", false);
|
||||
if (smartCheckoutDecision == GitSmartOperationDialog.SMART_EXIT_CODE) {
|
||||
return doSmartMerge(allConflictingRepositories);
|
||||
}
|
||||
else {
|
||||
fatalLocalChangesError(myBranchToMerge);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void restoreLocalChanges() {
|
||||
if (myPreservingProcess != null) {
|
||||
myPreservingProcess.load();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean doSmartMerge(@NotNull final Collection<GitRepository> repositories) {
|
||||
final AtomicBoolean success = new AtomicBoolean();
|
||||
myPreservingProcess = new GitPreservingProcess(myProject, repositories, "merge", myBranchToMerge, getIndicator(),
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
success.set(doMerge(repositories));
|
||||
}
|
||||
});
|
||||
myPreservingProcess.execute(new Computable<Boolean>() {
|
||||
@Override
|
||||
public Boolean compute() {
|
||||
return myConflictedRepositories.isEmpty();
|
||||
}
|
||||
});
|
||||
return success.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs merge in the given repositories.
|
||||
* Handle only merge conflict situation: all other cases should have been handled before and are treated as errors.
|
||||
* Conflict is treated as a success: the repository with conflict is remembered and will be handled later along with all other conflicts.
|
||||
* If an error happens in one repository, the method doesn't go further in others, and shows a notification.
|
||||
*
|
||||
* @return true if merge has succeeded without errors (but possibly with conflicts) in all repositories;
|
||||
* false if it failed at least in one of them.
|
||||
*/
|
||||
private boolean doMerge(@NotNull Collection<GitRepository> repositories) {
|
||||
for (GitRepository repository : repositories) {
|
||||
GitSimpleEventDetector mergeConflict = new GitSimpleEventDetector(GitSimpleEventDetector.Event.MERGE_CONFLICT);
|
||||
GitCommandResult result = Git.merge(repository, myBranchToMerge, mergeConflict);
|
||||
if (!result.success()) {
|
||||
if (mergeConflict.hasHappened()) {
|
||||
myConflictedRepositories.put(repository, Boolean.TRUE);
|
||||
refresh(repository);
|
||||
markSuccessful(repository);
|
||||
}
|
||||
else {
|
||||
fatalError(getCommonErrorTitle(), result.getErrorOutputAsJoinedString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
refresh(repository);
|
||||
markSuccessful(repository);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String getCommonErrorTitle() {
|
||||
return "Couldn't merge " + myBranchToMerge;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void rollback() {
|
||||
Collection<GitRepository> repositoriesForSmartRollback = new ArrayList<GitRepository>();
|
||||
Collection<GitRepository> repositoriesForSimpleRollback = new ArrayList<GitRepository>();
|
||||
for (GitRepository repository : getSuccessfulRepositories()) {
|
||||
if (thereAreLocalChangesIn(repository)) {
|
||||
repositoriesForSmartRollback.add(repository);
|
||||
}
|
||||
else {
|
||||
repositoriesForSimpleRollback.add(repository);
|
||||
}
|
||||
}
|
||||
|
||||
GitCompoundResult result = smartRollback(repositoriesForSmartRollback);
|
||||
for (GitRepository repository : repositoriesForSimpleRollback) {
|
||||
result.append(repository, rollback(repository));
|
||||
}
|
||||
|
||||
if (!result.totalSuccess()) {
|
||||
NotificationManager.getInstance(myProject).notifyError("Error during rollback", result.getErrorOutputWithReposIndication());
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private GitCompoundResult smartRollback(@NotNull final Collection<GitRepository> repositories) {
|
||||
final GitCompoundResult result = new GitCompoundResult(myProject);
|
||||
myPreservingProcess = new GitPreservingProcess(myProject, repositories, "merge", myBranchToMerge, getIndicator(),
|
||||
new Runnable() {
|
||||
@Override public void run() {
|
||||
for (GitRepository repository : repositories) {
|
||||
result.append(repository, rollback(repository));
|
||||
}
|
||||
}
|
||||
});
|
||||
myPreservingProcess.execute();
|
||||
return result;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private GitCommandResult rollback(@NotNull GitRepository repository) {
|
||||
return Git.hardReset(repository, myCurrentRevisionsBeforeMerge.get(repository));
|
||||
}
|
||||
|
||||
private boolean thereAreLocalChangesIn(@NotNull GitRepository repository) {
|
||||
return !myChangeListManager.getChangesIn(repository.getRoot()).isEmpty();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getSuccessMessage() {
|
||||
return String.format("Merged <b><code>%s</code></b> to <b><code>%s</code></b>", myBranchToMerge, myCurrentBranch);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected String getRollbackProposal() {
|
||||
return "However merge has succeeded for the following " + repositories() + ":<br/>" +
|
||||
successfulRepositoriesJoined() +
|
||||
"<br/>" + ROLLBACK_PROPOSAL;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected String getOperationName() {
|
||||
return "merge";
|
||||
}
|
||||
|
||||
private static void refresh(GitRepository... repositories) {
|
||||
for (GitRepository repository : repositories) {
|
||||
refreshRoot(repository);
|
||||
repository.update(GitRepository.TrackedTopic.ALL_CURRENT);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+28
-17
@@ -17,6 +17,7 @@ package git4idea.branch;
|
||||
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.ui.DialogWrapper;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.openapi.vcs.changes.Change;
|
||||
import com.intellij.openapi.vcs.changes.ui.ChangesBrowser;
|
||||
import com.intellij.ui.IdeBorderFactory;
|
||||
@@ -31,29 +32,33 @@ import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* The dialog that is shown when the error "The following files would be overwritten by checkout" happens.
|
||||
* Displays the list of these files and proposes to make a "smart" checkout.
|
||||
* The dialog that is shown when the error
|
||||
* "Your local changes to the following files would be overwritten by merge/checkout"
|
||||
* happens.
|
||||
* Displays the list of these files and proposes to make a "smart" merge or checkout.
|
||||
*
|
||||
* @author Kirill Likhodedov
|
||||
*/
|
||||
// TODO "don't ask again" option
|
||||
class GitWouldBeOverwrittenByCheckoutDialog extends DialogWrapper {
|
||||
class GitSmartOperationDialog extends DialogWrapper {
|
||||
|
||||
public static final int SMART_CHECKOUT = OK_EXIT_CODE;
|
||||
public static final int FORCE_CHECKOUT_EXIT_CODE = NEXT_USER_EXIT_CODE;
|
||||
public static final int SMART_EXIT_CODE = OK_EXIT_CODE;
|
||||
public static final int FORCE_EXIT_CODE = NEXT_USER_EXIT_CODE;
|
||||
|
||||
private final Project myProject;
|
||||
private final List<Change> myChanges;
|
||||
@NotNull private final String myCapitalizedOperationTitle;
|
||||
private final boolean myForceButton;
|
||||
|
||||
/**
|
||||
* @return true if smart checkout has to be performed, false if user doesn't want to checkout.
|
||||
* Shows the dialog with the list of local changes preventing merge/checkout and returns the dialog exit code.
|
||||
*/
|
||||
static int showAndGetAnswer(@NotNull final Project project, @NotNull final List<Change> changes) {
|
||||
static int showAndGetAnswer(@NotNull final Project project, @NotNull final List<Change> changes, @NotNull final String operationTitle,
|
||||
final boolean forceButton) {
|
||||
final AtomicInteger exitCode = new AtomicInteger();
|
||||
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
GitWouldBeOverwrittenByCheckoutDialog dialog = new GitWouldBeOverwrittenByCheckoutDialog(project, changes);
|
||||
GitSmartOperationDialog dialog = new GitSmartOperationDialog(project, changes, operationTitle, forceButton);
|
||||
DialogManager.getInstance(project).showDialog(dialog);
|
||||
exitCode.set(dialog.getExitCode());
|
||||
}
|
||||
@@ -61,19 +66,25 @@ class GitWouldBeOverwrittenByCheckoutDialog extends DialogWrapper {
|
||||
return exitCode.get();
|
||||
}
|
||||
|
||||
private GitWouldBeOverwrittenByCheckoutDialog(@NotNull Project project, @NotNull List<Change> changes) {
|
||||
private GitSmartOperationDialog(@NotNull Project project, @NotNull List<Change> changes, @NotNull String operationTitle,
|
||||
boolean forceButton) {
|
||||
super(project);
|
||||
myProject = project;
|
||||
myChanges = changes;
|
||||
setOKButtonText("Smart checkout");
|
||||
setCancelButtonText("Don't checkout");
|
||||
myCapitalizedOperationTitle = StringUtil.capitalize(operationTitle);
|
||||
myForceButton = forceButton;
|
||||
setOKButtonText("Smart " + myCapitalizedOperationTitle);
|
||||
setCancelButtonText("Don't " + myCapitalizedOperationTitle);
|
||||
getCancelAction().putValue(FOCUSED_ACTION, Boolean.TRUE);
|
||||
init();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Action[] createLeftSideActions() {
|
||||
return new Action[] {new ForceCheckoutAction() };
|
||||
if (myForceButton) {
|
||||
return new Action[] {new ForceCheckoutAction(myCapitalizedOperationTitle) };
|
||||
}
|
||||
return new Action[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -94,19 +105,19 @@ class GitWouldBeOverwrittenByCheckoutDialog extends DialogWrapper {
|
||||
|
||||
@Override
|
||||
protected String getDimensionServiceKey() {
|
||||
return GitWouldBeOverwrittenByCheckoutDialog.class.getName();
|
||||
return GitSmartOperationDialog.class.getName();
|
||||
}
|
||||
|
||||
|
||||
private class ForceCheckoutAction extends AbstractAction {
|
||||
|
||||
ForceCheckoutAction() {
|
||||
super("Force checkout");
|
||||
ForceCheckoutAction(@NotNull String operationTitle) {
|
||||
super("Force " + operationTitle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
close(FORCE_CHECKOUT_EXIT_CODE);
|
||||
close(FORCE_EXIT_CODE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,19 @@ public class Git {
|
||||
return run(handler, true);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static GitCommandResult merge(@NotNull GitRepository repository, @NotNull String branchToMerge,
|
||||
@NotNull GitLineHandlerListener... listeners) {
|
||||
final GitLineHandler mergeHandler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.MERGE);
|
||||
mergeHandler.setSilent(false);
|
||||
mergeHandler.addParameters(branchToMerge);
|
||||
for (GitLineHandlerListener listener : listeners) {
|
||||
mergeHandler.addLineListener(listener);
|
||||
}
|
||||
return run(mergeHandler);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@code git checkout <reference>} <br/>
|
||||
* {@code git checkout -b <newBranch> <reference>}
|
||||
@@ -225,6 +238,13 @@ public class Git {
|
||||
return run(h);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static GitCommandResult hardReset(@NotNull GitRepository repository, @NotNull String revision) {
|
||||
final GitLineHandler handler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.RESET);
|
||||
handler.addParameters("--hard", revision);
|
||||
return run(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last (tip) commit on the given branch.<br/>
|
||||
* {@code git rev-list -1 <branchName>}
|
||||
|
||||
@@ -40,6 +40,9 @@ public class GitMessageWithFilesDetector implements GitLineHandlerListener {
|
||||
private final VirtualFile myRoot;
|
||||
|
||||
public enum Event {
|
||||
LOCAL_CHANGES_OVERWRITTEN_BY_MERGE(
|
||||
"Your local changes to the following files would be overwritten by merge",
|
||||
"commit your changes or stash them before"),
|
||||
LOCAL_CHANGES_OVERWRITTEN_BY_CHECKOUT(
|
||||
"Your local changes to the following files would be overwritten by checkout",
|
||||
"commit your changes or stash them before"),
|
||||
|
||||
@@ -26,8 +26,11 @@ public class GitSimpleEventDetector implements GitLineHandlerListener {
|
||||
private boolean myHappened;
|
||||
|
||||
public enum Event {
|
||||
UNMERGED("you need to resolve your current index first"),
|
||||
BRANCH_NOT_FULLY_MERGED("is not fully merged");
|
||||
UNMERGED_PREVENTING_CHECKOUT("you need to resolve your current index first"),
|
||||
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"),
|
||||
ALREADY_UP_TO_DATE("Already up-to-date");
|
||||
|
||||
private final String myDetectionString;
|
||||
|
||||
|
||||
@@ -118,7 +118,6 @@ public class GitConflictResolver {
|
||||
* the method shows a notification and returns {@code false}.
|
||||
* </p>
|
||||
*
|
||||
* @param roots Git repositories to look for unmerged files.
|
||||
* @return {@code true} if there is nothing to merge anymore, {@code false} if unmerged files remain or in the case of error.
|
||||
*/
|
||||
public final boolean merge() {
|
||||
@@ -221,6 +220,11 @@ public class GitConflictResolver {
|
||||
}
|
||||
|
||||
|
||||
@NotNull
|
||||
protected NotificationListener getResolveLinkListener() {
|
||||
return new ResolveNotificationListener();
|
||||
}
|
||||
|
||||
private class ResolveNotificationListener implements NotificationListener {
|
||||
@Override public void hyperlinkUpdate(@NotNull final Notification notification, @NotNull HyperlinkEvent event) {
|
||||
if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED && event.getDescription().equals("resolve")) {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.merge;
|
||||
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.vcs.VcsException;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Conflict resolver that makes a merge commit after all conflicts are resolved.
|
||||
*
|
||||
* @author Kirill Likhodedov
|
||||
*/
|
||||
public class GitMergeCommittingConflictResolver extends GitConflictResolver {
|
||||
private final Collection<VirtualFile> myMergingRoots;
|
||||
private final boolean myRefreshAfterCommit;
|
||||
private final GitMerger myMerger;
|
||||
|
||||
public GitMergeCommittingConflictResolver(Project project, GitMerger merger, Collection<VirtualFile> mergingRoots, Params params,
|
||||
boolean refreshAfterCommit) {
|
||||
super(project, mergingRoots, params);
|
||||
myMerger = merger;
|
||||
myMergingRoots = mergingRoots;
|
||||
myRefreshAfterCommit = refreshAfterCommit;
|
||||
}
|
||||
|
||||
@Override protected boolean proceedAfterAllMerged() throws VcsException {
|
||||
myMerger.mergeCommit(myMergingRoots);
|
||||
if (myRefreshAfterCommit) {
|
||||
for (VirtualFile root : myMergingRoots) {
|
||||
root.refresh(true, true);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -67,21 +67,24 @@ public class GitStashChangesSaver extends GitChangesSaver {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void load(ContinuationContext context) {
|
||||
protected void load(@NotNull ContinuationContext context) {
|
||||
try {
|
||||
load();
|
||||
}
|
||||
catch (VcsException e) {
|
||||
context.handleException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void load() throws VcsException {
|
||||
for (VirtualFile root : myStashedRoots) {
|
||||
try {
|
||||
loadRoot(root);
|
||||
}
|
||||
catch (VcsException e) {
|
||||
context.handleException(e);
|
||||
return;
|
||||
}
|
||||
loadRoot(root);
|
||||
}
|
||||
// we'll refresh more but this way we needn't compute what files under roots etc
|
||||
LocalFileSystem.getInstance().refreshIoFiles(myChangeManager.getAffectedPaths());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Override
|
||||
protected boolean wereChangesSaved() {
|
||||
return !myStashedRoots.isEmpty();
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@ class GitBranchPopupActions {
|
||||
ActionGroup createActions(@Nullable DefaultActionGroup toInsert) {
|
||||
DefaultActionGroup popupGroup = new DefaultActionGroup(null, false);
|
||||
|
||||
//popupGroup.addAction(new CurrentBranchAction(GitBranchUiUtil.getDisplayableBranchText(myRepository), "in root " + GitUIUtil.getShortRepositoryName(myRepository)));
|
||||
popupGroup.addAction(new NewBranchAction(myProject, Collections.singletonList(myRepository), myRepository));
|
||||
popupGroup.addAction(new CheckoutRevisionActions(myProject, myRepository));
|
||||
|
||||
@@ -80,23 +79,6 @@ class GitBranchPopupActions {
|
||||
return popupGroup;
|
||||
}
|
||||
|
||||
///**
|
||||
// * "Current branch:" item which is disabled and is just a label to display the current branch.
|
||||
// */
|
||||
//static class CurrentBranchAction extends DumbAwareAction {
|
||||
// CurrentBranchAction(@NotNull String currentBranchName, @Nullable String rootName) {
|
||||
// super("", String.format("Current branch is %s %s", currentBranchName, rootName), null);
|
||||
// getTemplatePresentation().setText("Current Branch: " + currentBranchName, false); // no mnemonics
|
||||
// }
|
||||
//
|
||||
// @Override public void actionPerformed(AnActionEvent e) {
|
||||
// }
|
||||
//
|
||||
// @Override public void update(AnActionEvent e) {
|
||||
// e.getPresentation().setEnabled(false); // this action works as a label
|
||||
// }
|
||||
//}
|
||||
|
||||
static class NewBranchAction extends DumbAwareAction {
|
||||
private final Project myProject;
|
||||
private final List<GitRepository> myRepositories;
|
||||
@@ -205,6 +187,7 @@ class GitBranchPopupActions {
|
||||
new CheckoutAction(myProject, myRepositories, myBranchName, mySelectedRepository),
|
||||
new CheckoutAsNewBranch(myProject, myRepositories, myBranchName, mySelectedRepository),
|
||||
new CompareAction(myProject, myRepositories, myBranchName, mySelectedRepository),
|
||||
new MergeAction(myProject, myRepositories, myBranchName, mySelectedRepository),
|
||||
new DeleteAction(myProject, myRepositories, myBranchName, mySelectedRepository)
|
||||
};
|
||||
}
|
||||
@@ -308,6 +291,7 @@ class GitBranchPopupActions {
|
||||
return new AnAction[] {
|
||||
new CheckoutRemoteBranchAction(myProject, myRepositories, myBranchName, mySelectedRepository),
|
||||
new CompareAction(myProject, myRepositories, myBranchName, mySelectedRepository),
|
||||
new MergeAction(myProject, myRepositories, myBranchName, mySelectedRepository),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -342,7 +326,6 @@ class GitBranchPopupActions {
|
||||
return myRemoteBranchName.substring(slashPosition+1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class CompareAction extends DumbAwareAction {
|
||||
@@ -352,10 +335,8 @@ class GitBranchPopupActions {
|
||||
private final String myBranchName;
|
||||
private final GitRepository mySelectedRepository;
|
||||
|
||||
public CompareAction(@NotNull Project project,
|
||||
@NotNull List<GitRepository> repositories,
|
||||
@NotNull String branchName,
|
||||
GitRepository selectedRepository) {
|
||||
public CompareAction(@NotNull Project project, @NotNull List<GitRepository> repositories, @NotNull String branchName,
|
||||
@NotNull GitRepository selectedRepository) {
|
||||
super("Compare");
|
||||
myProject = project;
|
||||
myRepositories = repositories;
|
||||
@@ -369,4 +350,27 @@ class GitBranchPopupActions {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class MergeAction extends DumbAwareAction {
|
||||
|
||||
private final Project myProject;
|
||||
private final List<GitRepository> myRepositories;
|
||||
private final String myBranchName;
|
||||
private final GitRepository mySelectedRepository;
|
||||
|
||||
public MergeAction(@NotNull Project project, @NotNull List<GitRepository> repositories, @NotNull String branchName,
|
||||
@NotNull GitRepository selectedRepository) {
|
||||
super("Merge");
|
||||
myProject = project;
|
||||
myRepositories = repositories;
|
||||
myBranchName = branchName;
|
||||
mySelectedRepository = selectedRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void actionPerformed(AnActionEvent e) {
|
||||
new GitBranchOperationsProcessor(myProject, myRepositories, mySelectedRepository).merge(myBranchName);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,14 +15,12 @@
|
||||
*/
|
||||
package git4idea.update;
|
||||
|
||||
import com.intellij.ide.SaveAndSyncHandler;
|
||||
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.ChangeListManager;
|
||||
import com.intellij.util.continuation.*;
|
||||
import git4idea.repo.GitRepository;
|
||||
import git4idea.repo.GitRepositoryManager;
|
||||
import git4idea.util.GitFreezingProcess;
|
||||
|
||||
/**
|
||||
* This class executes a Git task, surrounding it with a couple of preparation and completion tasks, such as:
|
||||
@@ -50,16 +48,12 @@ public class GitComplexProcess {
|
||||
private final Operation myOperation;
|
||||
|
||||
private final String myFreezeReason;
|
||||
private final ProjectManagerEx myProjectManager;
|
||||
private final GitRepositoryManager myRepositoryManager;
|
||||
private final ChangeListManager myChangeListManager;
|
||||
|
||||
private final TaskDescriptor BLOCK = new TaskDescriptor("", Where.AWT) {
|
||||
@Override public void run(ContinuationContext context) {
|
||||
myProjectManager.blockReloadingProjectOnExternalChanges();
|
||||
FileDocumentManager.getInstance().saveAllDocuments();
|
||||
SaveAndSyncHandler.getInstance().blockSaveOnFrameDeactivation();
|
||||
SaveAndSyncHandler.getInstance().blockSyncOnFrameActivation();
|
||||
GitFreezingProcess.saveAndBlock();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -77,9 +71,7 @@ public class GitComplexProcess {
|
||||
|
||||
private final TaskDescriptor UNBLOCK = new TaskDescriptor("", Where.AWT) {
|
||||
@Override public void run(ContinuationContext context) {
|
||||
myProjectManager.unblockReloadingProjectOnExternalChanges();
|
||||
SaveAndSyncHandler.getInstance().unblockSaveOnFrameDeactivation();
|
||||
SaveAndSyncHandler.getInstance().unblockSyncOnFrameActivation();
|
||||
GitFreezingProcess.unblock();
|
||||
}
|
||||
|
||||
@Override public boolean isHaveMagicCure() {
|
||||
@@ -99,7 +91,6 @@ public class GitComplexProcess {
|
||||
myOperation = operation;
|
||||
myFreezeReason = "Local changes are not available until Git " + myTitle + " is finished.";
|
||||
|
||||
myProjectManager = ProjectManagerEx.getInstanceEx();
|
||||
myRepositoryManager = GitRepositoryManager.getInstance(project);
|
||||
myChangeListManager = ChangeListManager.getInstance(myProject);
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import git4idea.GitBranch;
|
||||
import git4idea.GitVcs;
|
||||
import git4idea.branch.GitBranchPair;
|
||||
import git4idea.merge.GitConflictResolver;
|
||||
import git4idea.merge.GitMergeCommittingConflictResolver;
|
||||
import git4idea.merge.GitMerger;
|
||||
import git4idea.rebase.GitRebaser;
|
||||
import git4idea.stash.GitChangesSaver;
|
||||
@@ -297,7 +298,7 @@ public class GitUpdateProcess {
|
||||
GitConflictResolver.Params params = new GitConflictResolver.Params();
|
||||
params.setErrorNotificationTitle("Can't update");
|
||||
params.setMergeDescription("You have unfinished merge. These conflicts must be resolved before update.");
|
||||
return !new MergeCommittingConflictResolver(myProject, myMerger, mergingRoots, params).merge();
|
||||
return !new GitMergeCommittingConflictResolver(myProject, myMerger, mergingRoots, params, false).merge();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -308,7 +309,7 @@ public class GitUpdateProcess {
|
||||
GitConflictResolver.Params params = new GitConflictResolver.Params();
|
||||
params.setErrorNotificationTitle("Can't update");
|
||||
params.setMergeDescription("Unmerged files detected. These conflicts must be resolved before update.");
|
||||
return !new MergeCommittingConflictResolver(myProject, myMerger, myRoots, params).merge();
|
||||
return !new GitMergeCommittingConflictResolver(myProject, myMerger, myRoots, params, false).merge();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -338,22 +339,4 @@ public class GitUpdateProcess {
|
||||
}
|
||||
}.merge();
|
||||
}
|
||||
|
||||
// conflict resolver that makes a merge commit after all conflicts are resolved
|
||||
private static class MergeCommittingConflictResolver extends GitConflictResolver {
|
||||
private final Collection<VirtualFile> myMergingRoots;
|
||||
private final GitMerger myMerger;
|
||||
|
||||
public MergeCommittingConflictResolver(Project project, GitMerger merger, Collection<VirtualFile> mergingRoots, Params params) {
|
||||
super(project, mergingRoots, params);
|
||||
myMerger = merger;
|
||||
myMergingRoots = mergingRoots;
|
||||
}
|
||||
|
||||
@Override protected boolean proceedAfterAllMerged() throws VcsException {
|
||||
myMerger.mergeCommit(myMergingRoots);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import com.intellij.ide.SaveAndSyncHandler;
|
||||
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.ChangeListManager;
|
||||
import com.intellij.util.continuation.GatheringContinuationContext;
|
||||
import com.intellij.util.ui.UIUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* Executes an action surrounding it with freezing-unfreezing operations.
|
||||
* It is a simple linear alternative to {@link git4idea.update.GitComplexProcess} performing tasks in a single method instead of
|
||||
* using continuations logic.
|
||||
*
|
||||
* @author Kirill Likhodedov
|
||||
*/
|
||||
public class GitFreezingProcess {
|
||||
|
||||
@NotNull private final String myOperationTitle;
|
||||
@NotNull private final Runnable myRunnable;
|
||||
@NotNull private final ChangeListManager myChangeListManager;
|
||||
|
||||
public GitFreezingProcess(@NotNull Project project, @NotNull String operationTitle, @NotNull Runnable runnable) {
|
||||
myOperationTitle = operationTitle;
|
||||
myRunnable = runnable;
|
||||
myChangeListManager = ChangeListManager.getInstance(project);
|
||||
}
|
||||
|
||||
public void execute() {
|
||||
try {
|
||||
saveAndBlockInAwt();
|
||||
freeze();
|
||||
try {
|
||||
myRunnable.run();
|
||||
}
|
||||
finally {
|
||||
unfreeze();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
unblockInAwt();
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveAndBlock() {
|
||||
ProjectManagerEx.getInstanceEx().blockReloadingProjectOnExternalChanges();
|
||||
FileDocumentManager.getInstance().saveAllDocuments();
|
||||
SaveAndSyncHandler.getInstance().blockSaveOnFrameDeactivation();
|
||||
SaveAndSyncHandler.getInstance().blockSyncOnFrameActivation();
|
||||
}
|
||||
|
||||
private static void saveAndBlockInAwt() {
|
||||
final AtomicReference<RuntimeException> exception = new AtomicReference<RuntimeException>();
|
||||
// if an error happens, let it be thrown there (in awt) + throw it here to call unblock if anything has already been blocked
|
||||
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
saveAndBlock();
|
||||
}
|
||||
catch (Throwable t) {
|
||||
RuntimeException re = new RuntimeException(t);
|
||||
exception.set(re);
|
||||
throw re;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
RuntimeException re = exception.get();
|
||||
if (re != null) {
|
||||
throw re;
|
||||
}
|
||||
}
|
||||
|
||||
private static void unblockInAwt() {
|
||||
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
unblock();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void unblock() {
|
||||
ProjectManagerEx.getInstanceEx().unblockReloadingProjectOnExternalChanges();
|
||||
SaveAndSyncHandler.getInstance().unblockSaveOnFrameDeactivation();
|
||||
SaveAndSyncHandler.getInstance().unblockSyncOnFrameActivation();
|
||||
}
|
||||
|
||||
private void freeze() {
|
||||
myChangeListManager.freeze(new GatheringContinuationContext(),
|
||||
"Local changes are not available until Git " + myOperationTitle + " is finished.");
|
||||
}
|
||||
|
||||
private void unfreeze() {
|
||||
myChangeListManager.letGo();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
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.util.Computable;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.openapi.vcs.VcsException;
|
||||
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
|
||||
import com.intellij.openapi.vcs.merge.MergeDialogCustomizer;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.util.text.DateFormatUtil;
|
||||
import git4idea.GitUtil;
|
||||
import git4idea.NotificationManager;
|
||||
import git4idea.merge.GitConflictResolver;
|
||||
import git4idea.repo.GitRepository;
|
||||
import git4idea.stash.GitStashChangesSaver;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import static com.intellij.openapi.util.text.StringUtil.join;
|
||||
|
||||
/**
|
||||
* Executes a Git operation on a number of repositories surrounding it by stash-unstash procedure.
|
||||
* I.e. stashes changes, executes the operation and then unstashes it.
|
||||
*
|
||||
* @author Kirill Likhodedov
|
||||
*/
|
||||
public class GitPreservingProcess {
|
||||
|
||||
private static final Logger LOG = Logger.getInstance(GitPreservingProcess.class);
|
||||
|
||||
@NotNull private final Project myProject;
|
||||
@NotNull private final Collection<GitRepository> myRepositories;
|
||||
@NotNull private final String myOperationTitle;
|
||||
@NotNull private final String myDestinationName;
|
||||
@NotNull private final ProgressIndicator myProgressIndicator;
|
||||
@NotNull private final Runnable myOperation;
|
||||
@NotNull private final String myStashMessage;
|
||||
|
||||
private GitStashChangesSaver mySaver;
|
||||
private boolean myLoaded;
|
||||
private final Object LOAD_LOCK = new Object();
|
||||
|
||||
public GitPreservingProcess(@NotNull Project project, @NotNull Collection<GitRepository> repositories,
|
||||
@NotNull String operationTitle, @NotNull String destinationName,
|
||||
@NotNull ProgressIndicator indicator, @NotNull Runnable operation) {
|
||||
myProject = project;
|
||||
myRepositories = repositories;
|
||||
myOperationTitle = operationTitle;
|
||||
myDestinationName = destinationName;
|
||||
myProgressIndicator = indicator;
|
||||
myOperation = operation;
|
||||
myStashMessage = String.format("%s %s at %s", StringUtil.capitalize(myOperationTitle), myDestinationName,
|
||||
DateFormatUtil.formatDateTime(Clock.getTime()));
|
||||
}
|
||||
|
||||
public void execute() {
|
||||
execute(null);
|
||||
}
|
||||
|
||||
public void execute(@Nullable final Computable<Boolean> autoLoadDecision) {
|
||||
Runnable operation = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mySaver = configureSaver();
|
||||
boolean savedSuccessfully = save();
|
||||
if (savedSuccessfully) {
|
||||
try {
|
||||
myOperation.run();
|
||||
}
|
||||
finally {
|
||||
if (autoLoadDecision == null || autoLoadDecision.compute()) {
|
||||
load();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
new GitFreezingProcess(myProject, myOperationTitle, operation).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the saver, actually notifications and texts in the GitConflictResolver used inside.
|
||||
*/
|
||||
private GitStashChangesSaver configureSaver() {
|
||||
GitStashChangesSaver saver = new GitStashChangesSaver(myProject, myProgressIndicator, myStashMessage);
|
||||
MergeDialogCustomizer mergeDialogCustomizer = new MergeDialogCustomizer() {
|
||||
@Override
|
||||
public String getMultipleFileMergeDescription(Collection<VirtualFile> files) {
|
||||
return String.format(
|
||||
"<html>Uncommitted changes that were saved before %s have conflicts with files from <code>%s</code></html>",
|
||||
myOperationTitle, myDestinationName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLeftPanelTitle(VirtualFile file) {
|
||||
return "Uncommitted changes from stash";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRightPanelTitle(VirtualFile file, VcsRevisionNumber lastRevisionNumber) {
|
||||
return String.format("<html>Changes from <b><code>%s</code></b></html>", myDestinationName);
|
||||
}
|
||||
};
|
||||
|
||||
GitConflictResolver.Params params = new GitConflictResolver.Params().
|
||||
setReverse(true).
|
||||
setMergeDialogCustomizer(mergeDialogCustomizer).
|
||||
setErrorNotificationTitle("Local changes were not restored");
|
||||
|
||||
saver.setConflictResolverParams(params);
|
||||
return saver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves local changes. In case of error shows a notification and returns false.
|
||||
*/
|
||||
private boolean save() {
|
||||
try {
|
||||
mySaver.saveLocalChanges(GitUtil.getRoots(myRepositories));
|
||||
return true;
|
||||
} catch (VcsException e) {
|
||||
LOG.info("Couldn't save local changes", e);
|
||||
NotificationManager.getInstance(myProject).notifyError(
|
||||
"Couldn't save uncommitted changes.",
|
||||
String.format("Tried to save uncommitted changes in stash before %s, but failed with an error.<br/>%s",
|
||||
myOperationTitle, join(e.getMessages())));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void load() {
|
||||
synchronized (LOAD_LOCK) {
|
||||
if (myLoaded) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mySaver.load();
|
||||
myLoaded = true;
|
||||
}
|
||||
catch (VcsException e) {
|
||||
LOG.info("Couldn't load local changes", e);
|
||||
NotificationManager.getInstance(myProject).notifyError("Couldn't restore uncommitted changes",
|
||||
String.format("Tried to unstash uncommitted changes, but failed with error.<br/>%s",join(e.getMessages())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -141,14 +141,14 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase {
|
||||
public void create_new_branch_with_unmerged_files_in_first_repo_should_show_notification() throws Exception {
|
||||
GitTestScenarioGenerator.prepareUnmergedFiles(myUltimate);
|
||||
doCheckoutNewBranch();
|
||||
assertNotify(NotificationType.ERROR, GitBranchOperation.UNMERGED_FILES_ERROR_NOTIFICATION_DESCRIPTION);
|
||||
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(GitBranchOperation.UNMERGED_FILES_ERROR_TITLE);
|
||||
assertMessage(unmergedFilesErrorTitle("checkout"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -156,7 +156,7 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase {
|
||||
GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity, myContrib);
|
||||
myMessageManager.nextAnswer(Messages.OK);
|
||||
doCheckoutNewBranch();
|
||||
assertMessage(GitBranchOperation.UNMERGED_FILES_ERROR_TITLE);
|
||||
assertMessage(unmergedFilesErrorTitle("checkout"));
|
||||
assertBranch("master");
|
||||
assertTrue(!branch(myUltimate).contains(NEW_BRANCH));
|
||||
}
|
||||
@@ -166,53 +166,53 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase {
|
||||
GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity, myContrib);
|
||||
myMessageManager.nextAnswer(Messages.CANCEL);
|
||||
doCheckoutNewBranch();
|
||||
assertMessage(GitBranchOperation.UNMERGED_FILES_ERROR_TITLE);
|
||||
assertMessage(unmergedFilesErrorTitle("checkout"));
|
||||
|
||||
assertBranch(myUltimate, NEW_BRANCH);
|
||||
assertBranch(myCommunity, MASTER);
|
||||
assertBranch(myContrib, MASTER);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void checkout_without_problems() throws Exception {
|
||||
prepareBranchForSimpleCheckout();
|
||||
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 {
|
||||
prepareBranchForSimpleCheckout();
|
||||
prepareBranchWithCommit("feature");
|
||||
GitTestScenarioGenerator.prepareUnmergedFiles(myUltimate);
|
||||
doCheckout("feature", null);
|
||||
assertNotify(NotificationType.ERROR, GitBranchOperation.UNMERGED_FILES_ERROR_NOTIFICATION_DESCRIPTION);
|
||||
assertNotify(NotificationType.ERROR, unmergedFilesErrorNotificationDescription("checkout"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void checkout_with_unmerged_file_in_second_repo_should_propose_to_rollback() throws Exception {
|
||||
prepareBranchForSimpleCheckout();
|
||||
prepareBranchWithCommit("feature");
|
||||
GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity);
|
||||
doCheckout("feature", null);
|
||||
assertMessage(GitBranchOperation.UNMERGED_FILES_ERROR_TITLE);
|
||||
assertMessage(unmergedFilesErrorTitle("checkout"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rollback_checkout_should_return_to_previous_branch() throws Exception {
|
||||
prepareBranchForSimpleCheckout();
|
||||
prepareBranchWithCommit("feature");
|
||||
GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity);
|
||||
myMessageManager.nextAnswer(Messages.OK);
|
||||
doCheckout("feature", null);
|
||||
assertMessage(GitBranchOperation.UNMERGED_FILES_ERROR_TITLE);
|
||||
assertMessage(unmergedFilesErrorTitle("checkout"));
|
||||
assertBranch("master");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deny_rollback_checkout_should_do_nothing() throws Exception {
|
||||
prepareBranchForSimpleCheckout();
|
||||
prepareBranchWithCommit("feature");
|
||||
GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity);
|
||||
myMessageManager.nextAnswer(Messages.CANCEL);
|
||||
doCheckout("feature", null);
|
||||
assertMessage(GitBranchOperation.UNMERGED_FILES_ERROR_TITLE);
|
||||
assertMessage(unmergedFilesErrorTitle("checkout"));
|
||||
assertBranch(myUltimate, "feature");
|
||||
assertBranch(myCommunity, "master");
|
||||
assertBranch(myContrib, "master");
|
||||
@@ -220,21 +220,30 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase {
|
||||
|
||||
@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");
|
||||
|
||||
doCheckout("feature", null);
|
||||
assertNotify(NotificationType.ERROR, UntrackedFilesNotifier.createUntrackedFilesOverwrittenDescription("checkout", false));
|
||||
doCheckoutOrMerge(checkout, "feature");
|
||||
String operation = checkout ? "checkout" : "merge";
|
||||
assertNotify(NotificationType.ERROR, UntrackedFilesNotifier.createUntrackedFilesOverwrittenDescription(operation, false));
|
||||
}
|
||||
|
||||
@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.GitCheckoutOperation");
|
||||
Class gitCheckoutOperationClass = Class.forName("git4idea.branch.GitBranchOperation");
|
||||
Class[] classes = gitCheckoutOperationClass.getDeclaredClasses();
|
||||
Class untrackedFilesDialogClass = null;
|
||||
for (Class aClass : classes) {
|
||||
@@ -243,7 +252,7 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase {
|
||||
}
|
||||
}
|
||||
assertNotNull(untrackedFilesDialogClass);
|
||||
|
||||
|
||||
final AtomicBoolean dialogShown = new AtomicBoolean();
|
||||
final Class finalUntrackedFilesDialogClass = untrackedFilesDialogClass;
|
||||
myDialogManager.registerDialogHandler(untrackedFilesDialogClass, new TestDialogHandler() {
|
||||
@@ -256,43 +265,36 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase {
|
||||
}
|
||||
});
|
||||
|
||||
doCheckout("feature", null);
|
||||
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(GitWouldBeOverwrittenByCheckoutDialog.class, new TestDialogHandler<GitWouldBeOverwrittenByCheckoutDialog>() {
|
||||
myDialogManager.registerDialogHandler(GitSmartOperationDialog.class, new TestDialogHandler<GitSmartOperationDialog>() {
|
||||
@Override
|
||||
public int handleDialog(GitWouldBeOverwrittenByCheckoutDialog dialog) {
|
||||
public int handleDialog(GitSmartOperationDialog dialog) {
|
||||
dialogShown.set(true);
|
||||
return DialogWrapper.CANCEL_EXIT_CODE;
|
||||
}
|
||||
});
|
||||
|
||||
doCheckout("feature", null);
|
||||
doCheckoutOrMerge(checkout, "feature");
|
||||
assertTrue(dialogShown.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void agree_to_smart_checkout_should_smart_checkout() throws Exception {
|
||||
prepareLocalChangesAndBranchWithSameModifiedFilesWithoutConflicts(myUltimate);
|
||||
prepareLocalChangesAndBranchWithSameModifiedFilesWithoutConflicts(myCommunity);
|
||||
prepareLocalChangesAndBranchWithSameModifiedFilesWithoutConflicts(myContrib);
|
||||
myDialogManager.registerDialogHandler(GitWouldBeOverwrittenByCheckoutDialog.class,
|
||||
new TestDialogHandler<GitWouldBeOverwrittenByCheckoutDialog>() {
|
||||
@Override
|
||||
public int handleDialog(GitWouldBeOverwrittenByCheckoutDialog dialog) {
|
||||
return DialogWrapper.OK_EXIT_CODE;
|
||||
}
|
||||
});
|
||||
|
||||
doCheckout("feature", null);
|
||||
prepare_agree_to_smart_operation(true);
|
||||
assertBranch("feature");
|
||||
for (GitRepository repository : myRepositories) {
|
||||
refresh(repository);
|
||||
@@ -301,53 +303,81 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@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(GitWouldBeOverwrittenByCheckoutDialog.class, new TestDialogHandler<GitWouldBeOverwrittenByCheckoutDialog>() {
|
||||
myDialogManager.registerDialogHandler(GitSmartOperationDialog.class, new TestDialogHandler<GitSmartOperationDialog>() {
|
||||
@Override
|
||||
public int handleDialog(GitWouldBeOverwrittenByCheckoutDialog dialog) {
|
||||
public int handleDialog(GitSmartOperationDialog dialog) {
|
||||
return DialogWrapper.CANCEL_EXIT_CODE;
|
||||
}
|
||||
});
|
||||
|
||||
doCheckout("feature", null);
|
||||
assertNotify(NotificationType.ERROR, "Couldn't checkout feature", stripHtmlAndBreaks("Local changes would be overwritten by checkout." +
|
||||
"Stash or commit them before checking out a branch."));
|
||||
doCheckoutOrMerge(checkout, "feature");
|
||||
String operation = checkout ? "checkout" : "merge";
|
||||
assertNotify(NotificationType.ERROR, "Couldn't " + operation + " feature",
|
||||
stripHtmlAndBreaks("Local changes would be overwritten by " + operation + "." + "You should stash or commit them."));
|
||||
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(GitWouldBeOverwrittenByCheckoutDialog.class, new TestDialogHandler<GitWouldBeOverwrittenByCheckoutDialog>() {
|
||||
myDialogManager.registerDialogHandler(GitSmartOperationDialog.class, new TestDialogHandler<GitSmartOperationDialog>() {
|
||||
@Override
|
||||
public int handleDialog(GitWouldBeOverwrittenByCheckoutDialog dialog) {
|
||||
public int handleDialog(GitSmartOperationDialog dialog) {
|
||||
return DialogWrapper.CANCEL_EXIT_CODE;
|
||||
}
|
||||
});
|
||||
|
||||
doCheckout("feature", null);
|
||||
assertMessage("Couldn't checkout feature",
|
||||
"Local changes would be overwritten by checkout.<br/>Stash or commit them before checking out a branch.<br/>" +
|
||||
"However checkout has succeeded for the following repositories:<br/>" +
|
||||
myUltimate.getPresentableUrl() +
|
||||
"<br/>You may rollback (checkout back to master) not to let branches diverge.",
|
||||
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",
|
||||
"Local changes would be overwritten by " + operationName + ".<br/>You should stash or commit them.<br/>" +
|
||||
"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 {
|
||||
prepareBranchForSimpleCheckout();
|
||||
prepareBranchWithCommit("feature");
|
||||
GitTestScenarioGenerator.prepareUnmergedFiles(myCommunity);
|
||||
myMessageManager.nextAnswer(Messages.OK);
|
||||
doCheckout("feature", "newBranch");
|
||||
assertMessage(GitBranchOperation.UNMERGED_FILES_ERROR_TITLE);
|
||||
assertMessage(unmergedFilesErrorTitle("checkout"));
|
||||
assertBranch("master");
|
||||
for (GitRepository repository : myRepositories) {
|
||||
assertFalse(branch(repository).contains("newBranch"), "Branch newBranch wasn't deleted from repository " + getShortRepositoryName(
|
||||
@@ -372,14 +402,6 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase {
|
||||
create(repository, "untracked.txt");
|
||||
}
|
||||
|
||||
private void prepareBranchForSimpleCheckout() throws IOException {
|
||||
for (GitRepository repository : myRepositories) {
|
||||
checkout(repository, "-b", "feature");
|
||||
createAddCommit(repository, "feature_file.txt");
|
||||
checkout(repository, "master");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void delete_branch_without_problems() throws Exception {
|
||||
for (GitRepository repository : myRepositories) {
|
||||
@@ -389,10 +411,10 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase {
|
||||
doDeleteBranch("master1");
|
||||
assertNotify(NotificationType.INFORMATION, "Deleted branch master1");
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void delete_unmerged_branch_should_show_dialog() throws Exception {
|
||||
prepareUnmergedBranch(myUltimate, myCommunity, myContrib);
|
||||
prepareBranchWithCommit("unmerged_branch", myUltimate, myCommunity, myContrib);
|
||||
|
||||
final AtomicBoolean dialogShown = new AtomicBoolean();
|
||||
myDialogManager.registerDialogHandler(GitBranchIsNotFullyMergedDialog.class, new TestDialogHandler<GitBranchIsNotFullyMergedDialog>() {
|
||||
@@ -405,41 +427,41 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase {
|
||||
doDeleteBranch("unmerged_branch");
|
||||
assertTrue(dialogShown.get());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void ok_in_unmerged_branch_dialog_should_force_delete_branch() throws Exception {
|
||||
prepareUnmergedBranch(myUltimate, myCommunity, myContrib);
|
||||
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 {
|
||||
prepareUnmergedBranch(myUltimate, myContrib);
|
||||
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");
|
||||
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");
|
||||
prepareUnmergedBranch(myCommunity, myContrib);
|
||||
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");
|
||||
prepareUnmergedBranch(myCommunity);
|
||||
prepareBranchWithCommit("unmerged_branch", myCommunity);
|
||||
branch(myContrib, "unmerged_branch");
|
||||
|
||||
registerNotFullyMergedDialog(DialogWrapper.CANCEL_EXIT_CODE);
|
||||
@@ -450,13 +472,13 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase {
|
||||
assertTrue(branch(repository).contains("unmerged_branch"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void deny_rollback_delete_branch_should_do_nothing() throws Exception {
|
||||
branch(myUltimate, "unmerged_branch");
|
||||
prepareUnmergedBranch(myCommunity);
|
||||
prepareBranchWithCommit("unmerged_branch", myCommunity);
|
||||
branch(myContrib, "unmerged_branch");
|
||||
|
||||
|
||||
registerNotFullyMergedDialog(DialogWrapper.CANCEL_EXIT_CODE);
|
||||
myMessageManager.nextAnswer(Messages.CANCEL);
|
||||
doDeleteBranch("unmerged_branch");
|
||||
@@ -466,6 +488,121 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase {
|
||||
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");
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
@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");
|
||||
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
|
||||
@@ -475,9 +612,12 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase {
|
||||
});
|
||||
}
|
||||
|
||||
private static void prepareUnmergedBranch(GitRepository... repositories) throws IOException {
|
||||
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", "unmerged_branch");
|
||||
checkout(repository, "-b", branch);
|
||||
createAddCommit(repository, "unmerged_branch_file");
|
||||
checkout(repository, "master");
|
||||
refresh(repository);
|
||||
@@ -488,6 +628,37 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase {
|
||||
callPrivateBranchOperationsProcessorMethod("doCheckoutNewBranch", NEW_BRANCH);
|
||||
}
|
||||
|
||||
private void doMerge(String branch) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
|
||||
callPrivateBranchOperationsProcessorMethod("doMerge", branch);
|
||||
// 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
|
||||
@@ -547,10 +718,10 @@ public class GitBranchOperationsTest extends AbstractVcsTestCase {
|
||||
TestMessageManager.Message message = myMessageManager.getLastMessage();
|
||||
assertNotNull(message);
|
||||
if (title != null) {
|
||||
assertEquals(message.getTitle(), title);
|
||||
assertEquals(stripHtmlAndBreaks(message.getTitle()), stripHtmlAndBreaks(title));
|
||||
}
|
||||
if (description != null) {
|
||||
assertEquals(message.getDescription(), description);
|
||||
assertEquals(stripHtmlAndBreaks(message.getDescription()), stripHtmlAndBreaks(description));
|
||||
}
|
||||
if (yesButton != null) {
|
||||
assertEquals(message.getYesText(), yesButton);
|
||||
|
||||
@@ -121,6 +121,10 @@ public class GitExec {
|
||||
run(repository, "merge", branch);
|
||||
}
|
||||
|
||||
public static String tip(@NotNull GitRepository repository) throws IOException {
|
||||
return run(repository, "rev-list", "-1", "HEAD");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String run(@NotNull GitRepository repository, @NotNull String command, String... params) throws IOException {
|
||||
return new GitTestRunEnv(new File(repository.getRoot().getPath())).run(command, params);
|
||||
|
||||
@@ -27,13 +27,15 @@ import static git4idea.test.GitExec.*;
|
||||
*/
|
||||
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);
|
||||
|
||||
checkoutFeatureBranch(repository);
|
||||
checkoutBranchForUnmergedConflicts(repository);
|
||||
edit(repository, unmergedFile, "feature content");
|
||||
addCommit(repository, unmergedFile);
|
||||
|
||||
@@ -41,17 +43,17 @@ public class GitTestScenarioGenerator {
|
||||
edit(repository, unmergedFile, "master feature");
|
||||
addCommit(repository, unmergedFile);
|
||||
|
||||
merge(repository, "feature");
|
||||
merge(repository, BRANCH_FOR_UNMERGED_CONFLICTS);
|
||||
refresh(repository);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkoutFeatureBranch(GitRepository repository) throws IOException {
|
||||
private static void checkoutBranchForUnmergedConflicts(GitRepository repository) throws IOException {
|
||||
String branches = branch(repository);
|
||||
if (!branches.contains("feature")) {
|
||||
checkout(repository, "-b", "feature");
|
||||
if (!branches.contains(BRANCH_FOR_UNMERGED_CONFLICTS)) {
|
||||
checkout(repository, "-b", BRANCH_FOR_UNMERGED_CONFLICTS);
|
||||
} else {
|
||||
checkout(repository, "feature");
|
||||
checkout(repository, BRANCH_FOR_UNMERGED_CONFLICTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user