diff --git a/platform/testFramework/src/com/intellij/testFramework/vcs/MockChangeList.java b/platform/testFramework/src/com/intellij/testFramework/vcs/MockChangeList.java new file mode 100644 index 000000000000..f1242a36fa58 --- /dev/null +++ b/platform/testFramework/src/com/intellij/testFramework/vcs/MockChangeList.java @@ -0,0 +1,86 @@ +/* + * 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 com.intellij.testFramework.vcs; + +import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vcs.changes.LocalChangeList; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * @author Kirill Likhodedov + */ +public class MockChangeList extends LocalChangeList { + + Collection myChanges = new ArrayList(); + private final String myName; + + public MockChangeList(String name) { + myName = name; + } + + public void add(Change change) { + myChanges.add(change); + } + + @Override + public Collection getChanges() { + return myChanges; + } + + @NotNull + @Override + public String getName() { + return myName; + } + + @Override + public void setName(@NotNull String name) { + throw new UnsupportedOperationException(); + } + + @Override + public String getComment() { + throw new UnsupportedOperationException(); + } + + @Override + public void setComment(String comment) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isDefault() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isReadOnly() { + throw new UnsupportedOperationException(); + } + + @Override + public void setReadOnly(boolean isReadOnly) { + throw new UnsupportedOperationException(); + } + + @Override + public LocalChangeList copy() { + throw new UnsupportedOperationException(); + } +} diff --git a/platform/testFramework/testFramework.iml b/platform/testFramework/testFramework.iml index a234c1da1384..2a50c47971ee 100644 --- a/platform/testFramework/testFramework.iml +++ b/platform/testFramework/testFramework.iml @@ -20,6 +20,7 @@ + diff --git a/platform/testFramework/testSrc/com/intellij/testFramework/vcs/MockChangeListManager.java b/platform/testFramework/testSrc/com/intellij/testFramework/vcs/MockChangeListManager.java new file mode 100644 index 000000000000..fd299b982115 --- /dev/null +++ b/platform/testFramework/testSrc/com/intellij/testFramework/vcs/MockChangeListManager.java @@ -0,0 +1,321 @@ +package com.intellij.testFramework.vcs; + +import com.intellij.openapi.application.ModalityState; +import com.intellij.openapi.vcs.FilePath; +import com.intellij.openapi.vcs.FileStatus; +import com.intellij.openapi.vcs.changes.*; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.Consumer; +import com.intellij.util.continuation.ContinuationPause; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.util.*; + +/** + * @author Kirill Likhodedov + */ +public class MockChangeListManager extends ChangeListManager { + + public static final String DEFAULT_CHANGE_LIST_NAME = "Default"; + + private final Map myChangeLists = new HashMap(); + private LocalChangeList myActiveChangeList; + private final MockChangeList myDefaultChangeList; + + public MockChangeListManager() { + myDefaultChangeList = new MockChangeList(DEFAULT_CHANGE_LIST_NAME); + myChangeLists.put(DEFAULT_CHANGE_LIST_NAME, myDefaultChangeList); + myActiveChangeList = myDefaultChangeList; + } + + public void addChanges(Change... changes) { + MockChangeList changeList = myChangeLists.get(DEFAULT_CHANGE_LIST_NAME); + for (Change change : changes) { + changeList.add(change); + } + } + + @Override + public void scheduleUpdate() { + throw new UnsupportedOperationException(); + } + + @Override + public void scheduleUpdate(boolean updateUnversionedFiles) { + throw new UnsupportedOperationException(); + } + + @Override + public void invokeAfterUpdate(Runnable afterUpdate, + InvokeAfterUpdateMode mode, + String title, + ModalityState state) { + throw new UnsupportedOperationException(); + } + + @Override + public void invokeAfterUpdate(Runnable afterUpdate, + InvokeAfterUpdateMode mode, + String title, + Consumer dirtyScopeManager, + ModalityState state) { + afterUpdate.run(); + } + + @Override + public boolean ensureUpToDate(boolean canBeCanceled) { + throw new UnsupportedOperationException(); + } + + @Override + public List getChangeListsCopy() { + return new ArrayList(myChangeLists.values()); + } + + @NotNull + @Override + public List getChangeLists() { + return getChangeListsCopy(); + } + + @Override + public List getAffectedPaths() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public List getAffectedFiles() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isFileAffected(VirtualFile file) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Collection getAllChanges() { + Collection changes = new ArrayList(); + for (MockChangeList list : myChangeLists.values()) { + changes.addAll(list.getChanges()); + } + return changes; + } + + @Override + public LocalChangeList findChangeList(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public LocalChangeList getChangeList(String id) { + throw new UnsupportedOperationException(); + } + + @Override + public LocalChangeList getDefaultChangeList() { + return myActiveChangeList; + } + + @Override + public boolean isDefaultChangeList(ChangeList list) { + throw new UnsupportedOperationException(); + } + + @Override + public LocalChangeList getChangeList(Change change) { + throw new UnsupportedOperationException(); + } + + @Override + public String getChangeListNameIfOnlyOne(Change[] changes) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Runnable prepareForChangeDeletion(Collection changes) { + throw new UnsupportedOperationException(); + } + + @Override + public Change getChange(@NotNull VirtualFile file) { + throw new UnsupportedOperationException(); + } + + @Override + public LocalChangeList getChangeList(@NotNull VirtualFile file) { + throw new UnsupportedOperationException(); + } + + @Override + public Change getChange(FilePath file) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isUnversioned(VirtualFile file) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public FileStatus getStatus(VirtualFile file) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Collection getChangesIn(VirtualFile dir) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Collection getChangesIn(FilePath path) { + throw new UnsupportedOperationException(); + } + + @Override + public void addChangeListListener(ChangeListListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeChangeListListener(ChangeListListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public void registerCommitExecutor(CommitExecutor executor) { + throw new UnsupportedOperationException(); + } + + @Override + public void commitChanges(LocalChangeList changeList, List changes) { + throw new UnsupportedOperationException(); + } + + @Override + public void commitChangesSynchronously(LocalChangeList changeList, List changes) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean commitChangesSynchronouslyWithResult(LocalChangeList changeList, List changes) { + throw new UnsupportedOperationException(); + } + + @Override + public void reopenFiles(List paths) { + throw new UnsupportedOperationException(); + } + + @Override + public List getRegisteredExecutors() { + throw new UnsupportedOperationException(); + } + + @Override + public void addFilesToIgnore(IgnoredFileBean... ignoredFiles) { + throw new UnsupportedOperationException(); + } + + @Override + public void setFilesToIgnore(IgnoredFileBean... ignoredFiles) { + throw new UnsupportedOperationException(); + } + + @Override + public IgnoredFileBean[] getFilesToIgnore() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isIgnoredFile(@NotNull VirtualFile file) { + throw new UnsupportedOperationException(); + } + + @Override + public String getSwitchedBranch(VirtualFile file) { + throw new UnsupportedOperationException(); + } + + @Override + public String getDefaultListName() { + throw new UnsupportedOperationException(); + } + + @Override + public void freeze(ContinuationPause context, String reason) { + throw new UnsupportedOperationException(); + } + + @Override + public void letGo() { + throw new UnsupportedOperationException(); + } + + @Override + public String isFreezed() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isFreezedWithNotification(@Nullable String modalTitle) { + throw new UnsupportedOperationException(); + } + + @Override + public List getModifiedWithoutEditing() { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public LocalChangeList addChangeList(@NotNull String name, @Nullable String comment) { + MockChangeList changeList = new MockChangeList(name); + myChangeLists.put(name, changeList); + return changeList; + } + + @Override + public void setDefaultChangeList(@NotNull LocalChangeList list) { + myActiveChangeList = list; + } + + @Override + public void removeChangeList(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeChangeList(LocalChangeList list) { + myChangeLists.remove(list.getName()); + if (myActiveChangeList.equals(list)) { + myActiveChangeList = myDefaultChangeList; + } + } + + @Override + public void moveChangesTo(LocalChangeList list, Change[] changes) { + } + + @Override + public boolean setReadOnly(String name, boolean value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean editName(@NotNull String fromName, @NotNull String toName) { + throw new UnsupportedOperationException(); + } + + @Override + public String editComment(@NotNull String fromName, String newComment) { + throw new UnsupportedOperationException(); + } +} diff --git a/platform/testFramework/testSrc/com/intellij/testFramework/vcs/MockContentRevision.java b/platform/testFramework/testSrc/com/intellij/testFramework/vcs/MockContentRevision.java new file mode 100644 index 000000000000..ca558aa04891 --- /dev/null +++ b/platform/testFramework/testSrc/com/intellij/testFramework/vcs/MockContentRevision.java @@ -0,0 +1,45 @@ + +package com.intellij.testFramework.vcs; + +import com.intellij.openapi.vcs.changes.ContentRevision; +import com.intellij.openapi.vcs.VcsException; +import com.intellij.openapi.vcs.FilePath; +import com.intellij.openapi.vcs.history.VcsRevisionNumber; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; + +/** + * @author yole + */ +public class MockContentRevision implements ContentRevision { + private final FilePath myPath; + private final VcsRevisionNumber myRevisionNumber; + + public MockContentRevision(final FilePath path, final VcsRevisionNumber revisionNumber) { + myPath = path; + myRevisionNumber = revisionNumber; + } + + @Override + @Nullable + public String getContent() throws VcsException { + return null; + } + + @Override + @NotNull + public FilePath getFile() { + return myPath; + } + + @Override + @NotNull + public VcsRevisionNumber getRevisionNumber() { + return myRevisionNumber; + } + + @Override + public String toString() { + return myPath.getName() + ":" + myRevisionNumber; + } +} diff --git a/platform/vcs-api/src/com/intellij/openapi/vcs/AbstractVcsHelper.java b/platform/vcs-api/src/com/intellij/openapi/vcs/AbstractVcsHelper.java index 8f36673d2417..12834d4124ed 100644 --- a/platform/vcs-api/src/com/intellij/openapi/vcs/AbstractVcsHelper.java +++ b/platform/vcs-api/src/com/intellij/openapi/vcs/AbstractVcsHelper.java @@ -21,6 +21,8 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.vcs.annotate.AnnotationProvider; import com.intellij.openapi.vcs.annotate.FileAnnotation; import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vcs.changes.CommitExecutor; +import com.intellij.openapi.vcs.changes.LocalChangeList; import com.intellij.openapi.vcs.history.VcsFileRevision; import com.intellij.openapi.vcs.history.VcsHistoryProvider; import com.intellij.openapi.vcs.merge.MergeDialogCustomizer; @@ -162,4 +164,14 @@ public abstract class AbstractVcsHelper { @Nullable String cancelActionName) { return selectFilePathsToProcess(files, title, prompt, singleFileTitle, singleFilePromptTemplate, confirmationOption); }; + + + /** + * Shows commit dialog, fills it with the given changes and given commit message, initially selects the given changelist. + * @return true if user commits the changes, and commit succeeds; false if user presses Cancel or commit fails with errors. + */ + public abstract boolean commitChanges(@NotNull List changes, @NotNull LocalChangeList initialChangeList, + @NotNull String commitMessage, @Nullable CommitExecutor executor); + + } diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/LocalChangeListImpl.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/LocalChangeListImpl.java index e61493548731..437146751815 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/LocalChangeListImpl.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/LocalChangeListImpl.java @@ -237,14 +237,8 @@ public class LocalChangeListImpl extends LocalChangeList { public synchronized boolean equals(final Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - final LocalChangeListImpl list = (LocalChangeListImpl)o; - - if (myIsDefault != list.myIsDefault) return false; - if (!myName.equals(list.myName)) return false; - if (myIsReadOnly != list.myIsReadOnly) return false; - - return true; + return myName.equals(list.myName); } public int hashCode() { diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ui/CommitHelper.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ui/CommitHelper.java index 436b293b1f29..f37a591b5fc7 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ui/CommitHelper.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/ui/CommitHelper.java @@ -462,20 +462,42 @@ public class CommitHelper { } private void markCommittingDocuments() { - for (Change change : myIncludedChanges) { - Document doc = ChangesUtil.getFilePath(change).getDocument(); - if (doc != null) { - doc.putUserData(DOCUMENT_BEING_COMMITTED_KEY, myProject); - myCommittingDocuments.add(doc); - } - } + myCommittingDocuments.addAll(markCommittingDocuments(myProject, myIncludedChanges)); } private void unmarkCommittingDocuments() { - for (Document doc : myCommittingDocuments) { + unmarkCommittingDocuments(myCommittingDocuments); + myCommittingDocuments.clear(); + } + + /** + * Marks {@link Document documents} related to the given changes as "being committed". + * @return documents which were marked that way. + * @see #unmarkCommittingDocuments(java.util.Collection) + * @see VetoSavingCommittingDocumentsAdapter + */ + @NotNull + public static Collection markCommittingDocuments(@NotNull Project project, @NotNull List changes) { + Collection committingDocs = new ArrayList(); + for (Change change : changes) { + Document doc = ChangesUtil.getFilePath(change).getDocument(); + if (doc != null) { + doc.putUserData(DOCUMENT_BEING_COMMITTED_KEY, project); + committingDocs.add(doc); + } + } + return committingDocs; + } + + /** + * Removes the "being committed marker" from the given {@link Document documents}. + * @see #markCommittingDocuments(com.intellij.openapi.project.Project, java.util.List) + * @see VetoSavingCommittingDocumentsAdapter + */ + public static void unmarkCommittingDocuments(@NotNull Collection committingDocs) { + for (Document doc : committingDocs) { doc.putUserData(DOCUMENT_BEING_COMMITTED_KEY, null); } - myCommittingDocuments.clear(); } private void commitCompleted(final List allExceptions, final GeneralCommitProcessor processor) { diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/impl/AbstractVcsHelperImpl.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/impl/AbstractVcsHelperImpl.java index 1ae8907d3b65..52c75a6a300e 100644 --- a/platform/vcs-impl/src/com/intellij/openapi/vcs/impl/AbstractVcsHelperImpl.java +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/impl/AbstractVcsHelperImpl.java @@ -49,6 +49,8 @@ import com.intellij.openapi.vcs.annotate.AnnotationProvider; import com.intellij.openapi.vcs.annotate.FileAnnotation; import com.intellij.openapi.vcs.changes.BackgroundFromStartOption; import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vcs.changes.CommitExecutor; +import com.intellij.openapi.vcs.changes.LocalChangeList; import com.intellij.openapi.vcs.changes.committed.*; import com.intellij.openapi.vcs.changes.ui.*; import com.intellij.openapi.vcs.history.*; @@ -218,6 +220,12 @@ public class AbstractVcsHelperImpl extends AbstractVcsHelper { } }); } + + @Override + public boolean commitChanges(@NotNull List changes, @NotNull LocalChangeList initialChangeList, + @NotNull String commitMessage, @Nullable CommitExecutor executor) { + return CommitChangeListDialog.commitChanges(myProject, changes, initialChangeList, executor, commitMessage); + } private void addDirectMessages(VcsErrorViewPanel vcsErrorViewPanel, List abstractVcsExceptions) { for (final VcsException exception : abstractVcsExceptions) { diff --git a/plugins/git4idea/src/git4idea/Notificator.java b/plugins/git4idea/src/git4idea/Notificator.java index b1b11f4ab7f7..9943af7cf3e4 100644 --- a/plugins/git4idea/src/git4idea/Notificator.java +++ b/plugins/git4idea/src/git4idea/Notificator.java @@ -68,11 +68,23 @@ public class Notificator { } public void notifyError(@NotNull String title, @NotNull String message) { - notify(GitVcs.IMPORTANT_ERROR_NOTIFICATION, title, message, NotificationType.ERROR, null); + notifyError(title, message, null); + } + + public void notifyError(@NotNull String title, @NotNull String message, @Nullable NotificationListener listener) { + notify(GitVcs.IMPORTANT_ERROR_NOTIFICATION, title, message, NotificationType.ERROR, listener); } public void notifySuccess(@NotNull String title, @NotNull String message) { notify(GitVcs.NOTIFICATION_GROUP_ID, title, message, NotificationType.INFORMATION, null); } + public void notifyWeakWarning(@NotNull String title, @NotNull String message, @Nullable NotificationListener listener) { + notify(GitVcs.MINOR_NOTIFICATION, title, message, NotificationType.WARNING, listener); + } + + public void notifyStrongWarning(@NotNull String title, @NotNull String content, @Nullable NotificationListener listener) { + notify(GitVcs.IMPORTANT_ERROR_NOTIFICATION, title, content, NotificationType.WARNING, listener); + } + } diff --git a/plugins/git4idea/src/git4idea/PlatformFacade.java b/plugins/git4idea/src/git4idea/PlatformFacade.java index bad5f7f3aa00..79aa28977fd8 100644 --- a/plugins/git4idea/src/git4idea/PlatformFacade.java +++ b/plugins/git4idea/src/git4idea/PlatformFacade.java @@ -15,15 +15,17 @@ */ package git4idea; +import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.util.Computable; import com.intellij.openapi.vcs.AbstractVcs; +import com.intellij.openapi.vcs.AbstractVcsHelper; import com.intellij.openapi.vcs.ProjectLevelVcsManager; import com.intellij.openapi.vcs.changes.ChangeListManager; import com.intellij.openapi.vfs.LocalFileSystem; -import git4idea.config.GitVcsSettings; +import git4idea.repo.GitRepositoryManager; import org.jetbrains.annotations.NotNull; /** @@ -65,12 +67,17 @@ public interface PlatformFacade { void runReadAction(@NotNull Runnable runnable); - /** - * @return the instance of {@link git4idea.config.GitVcsSettings} - */ - GitVcsSettings getGitWorkspaceSettings(@NotNull Project project); + void runWriteAction(@NotNull Runnable runnable); + + void invokeAndWait(@NotNull Runnable runnable, @NotNull ModalityState modalityState); ChangeListManager getChangeListManager(@NotNull Project project); LocalFileSystem getLocalFileSystem(); + + @NotNull + AbstractVcsHelper getVcsHelper(@NotNull Project project); + + @NotNull + GitRepositoryManager getRepositoryManager(@NotNull Project project); } diff --git a/plugins/git4idea/src/git4idea/PlatformFacadeImpl.java b/plugins/git4idea/src/git4idea/PlatformFacadeImpl.java index 9664d0dfff06..01b29d3d4797 100644 --- a/plugins/git4idea/src/git4idea/PlatformFacadeImpl.java +++ b/plugins/git4idea/src/git4idea/PlatformFacadeImpl.java @@ -16,15 +16,18 @@ package git4idea; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ModalityState; +import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.util.Computable; import com.intellij.openapi.vcs.AbstractVcs; +import com.intellij.openapi.vcs.AbstractVcsHelper; import com.intellij.openapi.vcs.ProjectLevelVcsManager; import com.intellij.openapi.vcs.changes.ChangeListManager; import com.intellij.openapi.vfs.LocalFileSystem; -import git4idea.config.GitVcsSettings; +import git4idea.repo.GitRepositoryManager; import org.jetbrains.annotations.NotNull; /** @@ -66,8 +69,13 @@ public class PlatformFacadeImpl implements PlatformFacade { } @Override - public GitVcsSettings getGitWorkspaceSettings(@NotNull Project project) { - return GitVcsSettings.getInstance(project); + public void runWriteAction(@NotNull Runnable runnable) { + ApplicationManager.getApplication().runWriteAction(runnable); + } + + @Override + public void invokeAndWait(@NotNull Runnable runnable, @NotNull ModalityState modalityState) { + ApplicationManager.getApplication().invokeAndWait(runnable, modalityState); } @Override @@ -80,6 +88,18 @@ public class PlatformFacadeImpl implements PlatformFacade { return LocalFileSystem.getInstance(); } + @NotNull + @Override + public AbstractVcsHelper getVcsHelper(@NotNull Project project) { + return AbstractVcsHelper.getInstance(project); + } + + @NotNull + @Override + public GitRepositoryManager getRepositoryManager(@NotNull Project project) { + return ServiceManager.getService(project, GitRepositoryManager.class); + } + @NotNull @Override public AbstractVcs getVcs(@NotNull Project project) { diff --git a/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java b/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java index f06edf61b637..144ae4320ce5 100644 --- a/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitBranchOperation.java @@ -297,7 +297,7 @@ abstract class GitBranchOperation { 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(); + new GitConflictResolver(myProject, myGit, ServiceManager.getService(PlatformFacade.class), GitUtil.getRoots(getRepositories()), params).merge(); } } }); @@ -333,7 +333,8 @@ abstract class GitBranchOperation { } private void showUntrackedFilesNotification(@NotNull Collection untrackedFiles) { - UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject, untrackedFiles, getOperationName()); + UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject, ServiceManager.getService(myProject, PlatformFacade.class), + untrackedFiles, getOperationName(), null); } private void showUntrackedFilesDialogWithRollback(@NotNull Collection untrackedFiles) { diff --git a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java index 770fdfe6a94f..139ac72b22aa 100644 --- a/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitCheckoutOperation.java @@ -201,7 +201,8 @@ class GitCheckoutOperation extends GitBranchOperation { private boolean smartCheckout(@NotNull final List repositories, @NotNull final String reference, @Nullable final String newBranch, @NotNull ProgressIndicator indicator) { final AtomicBoolean result = new AtomicBoolean(); - GitPreservingProcess preservingProcess = new GitPreservingProcess(myProject, repositories, "checkout", reference, indicator, new Runnable() { + GitPreservingProcess preservingProcess = new GitPreservingProcess(myProject, myGit, repositories, "checkout", reference, indicator, + new Runnable() { @Override public void run() { result.set(checkoutOrNotify(repositories, reference, newBranch, false)); diff --git a/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java b/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java index 468a7f85d17f..afc13e1eb852 100644 --- a/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java +++ b/plugins/git4idea/src/git4idea/branch/GitMergeOperation.java @@ -209,7 +209,7 @@ class GitMergeOperation extends GitBranchOperation { private boolean doSmartMerge(@NotNull final Collection repositories) { final AtomicBoolean success = new AtomicBoolean(); - myPreservingProcess = new GitPreservingProcess(myProject, repositories, "merge", myBranchToMerge, getIndicator(), + myPreservingProcess = new GitPreservingProcess(myProject, myGit, repositories, "merge", myBranchToMerge, getIndicator(), new Runnable() { @Override public void run() { @@ -303,7 +303,8 @@ class GitMergeOperation extends GitBranchOperation { private GitCompoundResult smartRollback(@NotNull final Collection repositories) { LOG.info("Starting smart rollback..."); final GitCompoundResult result = new GitCompoundResult(myProject); - GitPreservingProcess preservingProcess = new GitPreservingProcess(myProject, repositories, "merge", myBranchToMerge, getIndicator(), + GitPreservingProcess preservingProcess = new GitPreservingProcess(myProject, myGit, repositories, "merge", myBranchToMerge, + getIndicator(), new Runnable() { @Override public void run() { for (GitRepository repository : repositories) { @@ -361,7 +362,7 @@ class GitMergeOperation extends GitBranchOperation { private class MyMergeConflictResolver extends GitMergeCommittingConflictResolver { public MyMergeConflictResolver() { - super(GitMergeOperation.this.myProject, new GitMerger(GitMergeOperation.this.myProject), + super(GitMergeOperation.this.myProject, myGit, new GitMerger(GitMergeOperation.this.myProject), GitUtil.getRoots(GitMergeOperation.this.myConflictedRepositories.keySet()), new Params(), true); } diff --git a/plugins/git4idea/src/git4idea/changes/GitChangeUtils.java b/plugins/git4idea/src/git4idea/changes/GitChangeUtils.java index 789ff3a680d7..731e9be9e63b 100644 --- a/plugins/git4idea/src/git4idea/changes/GitChangeUtils.java +++ b/plugins/git4idea/src/git4idea/changes/GitChangeUtils.java @@ -23,7 +23,6 @@ import com.intellij.openapi.vcs.FileStatus; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.changes.ContentRevision; -import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ArrayUtil; import git4idea.GitContentRevision; @@ -32,9 +31,9 @@ import git4idea.GitUtil; import git4idea.commands.GitCommand; import git4idea.commands.GitHandler; import git4idea.commands.GitSimpleHandler; -import git4idea.util.StringScanner; import git4idea.history.browser.SHAHash; import git4idea.history.wholeTree.AbstractHash; +import git4idea.util.StringScanner; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -57,49 +56,6 @@ public class GitChangeUtils { private GitChangeUtils() { } - /** - * Parse changes from lines - * - * @param project the context project - * @param root the git root - * @return a set of unmerged files - * @throws VcsException if the input format does not matches expected format - */ - public static List unmergedFiles(Project project, VirtualFile root) throws VcsException { - HashSet unmerged = new HashSet(); - String rootPath = root.getPath(); - GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.LS_FILES); - h.setNoSSH(true); - h.setSilent(true); - h.addParameters("--unmerged"); - LocalFileSystem lfs = LocalFileSystem.getInstance(); - for (StringScanner s = new StringScanner(h.run()); s.hasMoreData();) { - if (s.isEol()) { - s.nextLine(); - continue; - } - s.boundedToken('\t'); - final String relative = s.line(); - String path = rootPath + "/" + GitUtil.unescapePath(relative); - VirtualFile file = lfs.refreshAndFindFileByPath(path); - if (file != null) { - // the file name is in the delete- or rename- conflict, so it is shown in the list of unmerged files, - // but the file itself doesn't exist. In that case we just ignore the file. - file.refresh(false, false); - unmerged.add(file); - } - } - if (unmerged.size() == 0) { - return Collections.emptyList(); - } - else { - ArrayList rc = new ArrayList(unmerged.size()); - rc.addAll(unmerged); - Collections.sort(rc, GitUtil.VIRTUAL_FILE_COMPARATOR); - return rc; - } - } - /** * Parse changes from lines * diff --git a/plugins/git4idea/src/git4idea/commands/Git.java b/plugins/git4idea/src/git4idea/commands/Git.java index a4dfbb727e37..beb4ffaf0b54 100644 --- a/plugins/git4idea/src/git4idea/commands/Git.java +++ b/plugins/git4idea/src/git4idea/commands/Git.java @@ -33,42 +33,38 @@ import java.util.Set; */ public interface Git { + @NotNull GitCommandResult init(@NotNull Project project, @NotNull VirtualFile root, @NotNull GitLineHandlerListener... listeners); @NotNull - Set untrackedFiles(@NotNull Project project, - @NotNull VirtualFile root, + Set untrackedFiles(@NotNull Project project, @NotNull VirtualFile root, @Nullable Collection files) throws VcsException; // relativePaths are guaranteed to fit into command line length limitations. @NotNull - Collection untrackedFilesNoChunk(@NotNull Project project, - @NotNull VirtualFile root, - @Nullable List relativePaths) - throws VcsException; + Collection untrackedFilesNoChunk(@NotNull Project project, @NotNull VirtualFile root, + @Nullable List relativePaths) throws VcsException; @NotNull GitCommandResult clone(@NotNull Project project, @NotNull File parentDirectory, @NotNull String url, @NotNull String clonedDirectoryName); @NotNull - GitCommandResult merge(@NotNull GitRepository repository, @NotNull String branchToMerge, - @NotNull GitLineHandlerListener... listeners); + GitCommandResult merge(@NotNull GitRepository repository, @NotNull String branchToMerge, @NotNull GitLineHandlerListener... listeners); - GitCommandResult checkout(@NotNull GitRepository repository, - @NotNull String reference, - @Nullable String newBranch, - boolean force, + @NotNull + GitCommandResult checkout(@NotNull GitRepository repository, @NotNull String reference, @Nullable String newBranch, boolean force, @NotNull GitLineHandlerListener... listeners); + @NotNull GitCommandResult checkoutNewBranch(@NotNull GitRepository repository, @NotNull String branchName, @Nullable GitLineHandlerListener listener); + @NotNull GitCommandResult createNewTag(@NotNull GitRepository repository, @NotNull String tagName, - @Nullable GitLineHandlerListener listener, String reference); + @Nullable GitLineHandlerListener listener, @NotNull String reference); - GitCommandResult branchDelete(@NotNull GitRepository repository, - @NotNull String branchName, - boolean force, + @NotNull + GitCommandResult branchDelete(@NotNull GitRepository repository, @NotNull String branchName, boolean force, @NotNull GitLineHandlerListener... listeners); @NotNull @@ -83,6 +79,7 @@ public interface Git { @NotNull GitCommandResult resetMerge(@NotNull GitRepository repository, @Nullable String revision); + @NotNull GitCommandResult tip(@NotNull GitRepository repository, @NotNull String branchName); @NotNull @@ -92,4 +89,12 @@ public interface Git { @NotNull GitCommandResult push(@NotNull GitRepository repository, @NotNull GitPushSpec pushSpec, @NotNull GitLineHandlerListener... listeners); + + @NotNull + GitCommandResult cherryPick(@NotNull GitRepository repository, @NotNull String hash, boolean autoCommit, + @NotNull GitLineHandlerListener... listeners); + + @NotNull + GitCommandResult getUnmergedFiles(@NotNull GitRepository repository); + } diff --git a/plugins/git4idea/src/git4idea/commands/GitCommandResult.java b/plugins/git4idea/src/git4idea/commands/GitCommandResult.java index 147b4ce6cfe9..0c53924bd630 100644 --- a/plugins/git4idea/src/git4idea/commands/GitCommandResult.java +++ b/plugins/git4idea/src/git4idea/commands/GitCommandResult.java @@ -73,4 +73,9 @@ public class GitCommandResult { public String getErrorOutputAsJoinedString() { return StringUtil.join(myErrorOutput, "\n"); } + + @NotNull + public String getOutputAsJoinedString() { + return StringUtil.join(myOutput, "\n"); + } } diff --git a/plugins/git4idea/src/git4idea/commands/GitImpl.java b/plugins/git4idea/src/git4idea/commands/GitImpl.java index c70436c9f58f..9f9eeae22688 100644 --- a/plugins/git4idea/src/git4idea/commands/GitImpl.java +++ b/plugins/git4idea/src/git4idea/commands/GitImpl.java @@ -51,6 +51,7 @@ public class GitImpl implements Git { /** * Calls 'git init' on the specified directory. */ + @NotNull @Override public GitCommandResult init(@NotNull Project project, @NotNull VirtualFile root, @NotNull GitLineHandlerListener... listeners) { GitLineHandler h = new GitLineHandler(project, root, GitCommand.INIT); @@ -153,6 +154,7 @@ public class GitImpl implements Git { * {@code git checkout <reference>}
* {@code git checkout -b <newBranch> <reference>} */ + @NotNull @Override public GitCommandResult checkout(@NotNull GitRepository repository, @NotNull String reference, @@ -179,6 +181,7 @@ public class GitImpl implements Git { /** * {@code git checkout -b <branchName>} */ + @NotNull @Override public GitCommandResult checkoutNewBranch(@NotNull GitRepository repository, @NotNull String branchName, @Nullable GitLineHandlerListener listener) { @@ -192,9 +195,10 @@ public class GitImpl implements Git { return run(h); } + @NotNull @Override public GitCommandResult createNewTag(@NotNull GitRepository repository, @NotNull String tagName, - @Nullable GitLineHandlerListener listener, String reference) { + @Nullable GitLineHandlerListener listener, @NotNull String reference) { final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.TAG); h.setSilent(false); h.addParameters(tagName); @@ -210,6 +214,7 @@ public class GitImpl implements Git { /** * {@code git branch -d } or {@code git branch -D } */ + @NotNull @Override public GitCommandResult branchDelete(@NotNull GitRepository repository, @NotNull String branchName, @@ -272,6 +277,7 @@ public class GitImpl implements Git { * Returns the last (tip) commit on the given branch.
* {@code git rev-list -1 } */ + @NotNull @Override public GitCommandResult tip(@NotNull GitRepository repository, @NotNull String branchName) { final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.REV_LIST); @@ -287,9 +293,7 @@ public class GitImpl implements Git { final GitLineHandlerPasswordRequestAware h = new GitLineHandlerPasswordRequestAware(repository.getProject(), repository.getRoot(), GitCommand.PUSH); h.setSilent(false); - for (GitLineHandlerListener listener : listeners) { - h.addLineListener(listener); - } + addListeners(h, listeners); h.addParameters(remote); h.addParameters(spec); return run(h, true); @@ -305,6 +309,36 @@ public class GitImpl implements Git { return push(repository, remote.getName(), pushSpec.getSource().getName() + ":" + destination, listeners); } + @Override + @NotNull + public GitCommandResult cherryPick(@NotNull GitRepository repository, @NotNull String hash, boolean autoCommit, + @NotNull GitLineHandlerListener... listeners) { + final GitLineHandler handler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.CHERRY_PICK); + handler.addParameters("-x"); + if (!autoCommit) { + handler.addParameters("-n"); + } + handler.addParameters(hash); + addListeners(handler, listeners); + handler.setSilent(false); + return run(handler); + } + + @NotNull + @Override + public GitCommandResult getUnmergedFiles(@NotNull GitRepository repository) { + GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.LS_FILES); + h.addParameters("--unmerged"); + h.setSilent(true); + return run(h); + } + + private static void addListeners(@NotNull GitLineHandler handler, @NotNull GitLineHandlerListener... listeners) { + for (GitLineHandlerListener listener : listeners) { + handler.addLineListener(listener); + } + } + private GitCommandResult run(@NotNull GitLineHandler handler) { return run(handler, false); } diff --git a/plugins/git4idea/src/git4idea/commands/GitSimpleEventDetector.java b/plugins/git4idea/src/git4idea/commands/GitSimpleEventDetector.java index 216db2ad3aa8..b057ad1aa2ca 100644 --- a/plugins/git4idea/src/git4idea/commands/GitSimpleEventDetector.java +++ b/plugins/git4idea/src/git4idea/commands/GitSimpleEventDetector.java @@ -26,6 +26,8 @@ public class GitSimpleEventDetector implements GitLineHandlerListener { private boolean myHappened; public enum Event { + CHERRY_PICK_CONFLICT("fter resolving the conflicts"), // a is uppercase in 1.7.0.5 and lowercase in 1.7.9.2 + LOCAL_CHANGES_OVERWRITTEN_BY_CHERRY_PICK("would be overwritten by merge"), 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"), diff --git a/plugins/git4idea/src/git4idea/config/GitVcsPanel.form b/plugins/git4idea/src/git4idea/config/GitVcsPanel.form index 701079981a94..3feefe8e52be 100644 --- a/plugins/git4idea/src/git4idea/config/GitVcsPanel.form +++ b/plugins/git4idea/src/git4idea/config/GitVcsPanel.form @@ -123,7 +123,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -142,13 +142,22 @@ - + + + + + + + + + + diff --git a/plugins/git4idea/src/git4idea/config/GitVcsPanel.java b/plugins/git4idea/src/git4idea/config/GitVcsPanel.java index 6d869ee727af..6eedc1507ba3 100644 --- a/plugins/git4idea/src/git4idea/config/GitVcsPanel.java +++ b/plugins/git4idea/src/git4idea/config/GitVcsPanel.java @@ -54,6 +54,7 @@ public class GitVcsPanel { private JComboBox myConvertTextFilesComboBox; // The conversion policy private JCheckBox myAutoUpdateIfPushRejected; private JBCheckBox mySyncBranchControl; + private JCheckBox myAutoCommitOnCherryPick; public GitVcsPanel(@NotNull Project project) { myVcs = GitVcs.getInstance(project); @@ -124,6 +125,7 @@ public class GitVcsPanel { myConvertTextFilesComboBox.setSelectedItem(crlfPolicyItem(settings)); myAutoUpdateIfPushRejected.setSelected(settings.autoUpdateIfPushRejected()); mySyncBranchControl.setSelected(settings.getSyncSetting() == GitBranchSyncSetting.SYNC); + myAutoCommitOnCherryPick.setSelected(settings.isAutoCommitOnCherryPick()); } /** @@ -161,7 +163,8 @@ public class GitVcsPanel { (settings.isIdeaSsh() != IDEA_SSH.equals(mySSHExecutableComboBox.getSelectedItem())) || !crlfPolicyItem(settings).equals(myConvertTextFilesComboBox.getSelectedItem()) || !settings.autoUpdateIfPushRejected() == myAutoUpdateIfPushRejected.isSelected() || - ((settings.getSyncSetting() == GitBranchSyncSetting.SYNC) != mySyncBranchControl.isSelected()); + ((settings.getSyncSetting() == GitBranchSyncSetting.SYNC) != mySyncBranchControl.isSelected() || + settings.isAutoCommitOnCherryPick() == myAutoCommitOnCherryPick.isSelected()); } /** @@ -189,5 +192,6 @@ public class GitVcsPanel { } settings.setLineSeparatorsConversion(conversionPolicy); settings.setSyncSetting(mySyncBranchControl.isSelected() ? GitBranchSyncSetting.SYNC : GitBranchSyncSetting.DONT); + settings.setAutoCommitOnCherryPick(myAutoCommitOnCherryPick.isSelected()); } } diff --git a/plugins/git4idea/src/git4idea/config/GitVcsSettings.java b/plugins/git4idea/src/git4idea/config/GitVcsSettings.java index 61ccb83bc04a..80099983ee20 100644 --- a/plugins/git4idea/src/git4idea/config/GitVcsSettings.java +++ b/plugins/git4idea/src/git4idea/config/GitVcsSettings.java @@ -74,6 +74,7 @@ public class GitVcsSettings implements PersistentStateComponent RECENT_BRANCH_BY_REPOSITORY = new HashMap(); public String RECENT_COMMON_BRANCH = null; + public boolean AUTO_COMMIT_ON_CHERRY_PICK = false; } public GitVcsSettings(GitVcsApplicationSettings appSettings) { @@ -190,4 +191,12 @@ public class GitVcsSettings implements PersistentStateComponent myCommits; - @NotNull private final CheckinEnvironment myCheckinEnvironment; - private final LowLevelAccess myAccess; + @NotNull private final Project myProject; + @NotNull private final Git myGit; + @NotNull private final PlatformFacade myPlatformFacade; + @NotNull private final ChangeListManager myChangeListManager; + private final boolean myAutoCommit; - private final List myExceptions; - private final List myWarnings; - private boolean myConflictsExist; - private final ChangeListManager myChangeListManager; - - private final List myCherryPickedData; - - public CherryPicker(GitVcs vcs, final List commits, LowLevelAccess access) { - myVcs = vcs; - myCommits = commits; - myAccess = access; - - myChangeListManager = PeriodicalTasksCloser.getInstance().safeGetComponent(myVcs.getProject(), ChangeListManager.class); - CheckinEnvironment ce = myVcs.getCheckinEnvironment(); - LOG.assertTrue(ce != null); - myCheckinEnvironment = ce; - - myExceptions = new ArrayList(); - myWarnings = new ArrayList(); - myCherryPickedData = new ArrayList(); + public CherryPicker(@NotNull Project project, @NotNull Git git, @NotNull PlatformFacade platformFacade, boolean autoCommit) { + myProject = project; + myGit = git; + myPlatformFacade = platformFacade; + myAutoCommit = autoCommit; + myChangeListManager = myPlatformFacade.getChangeListManager(myProject); } - public void execute() { - for (GitCommit commit : myCommits) { - cherryPickStep(commit); + public void cherryPick(@NotNull Map> commitsInRoots) { + List successfulCommits = new ArrayList(); + for (Map.Entry> entry : commitsInRoots.entrySet()) { + if (!cherryPick(entry.getKey(), entry.getValue(), successfulCommits)) { + return; + } } - - // remove those that are in newer lists - checkListsForSamePaths(); - - refreshChangedFiles(); - findAndProcessChangedForVcs(); - - showResults(); + notifySuccess(successfulCommits); } - private void refreshChangedFiles() { - for (FilePath file : getAllChangedFiles()) { - VirtualFile vf = LocalFileSystem.getInstance().refreshAndFindFileByPath(file.getPath()); + // return true to continue with other roots, false to break execution + private boolean cherryPick(@NotNull GitRepository repository, @NotNull List commits, + @NotNull List successfulCommits) { + for (GitCommit commit : commits) { + GitSimpleEventDetector conflictDetector = new GitSimpleEventDetector(CHERRY_PICK_CONFLICT); + GitSimpleEventDetector localChangesOverwrittenDetector = new GitSimpleEventDetector(LOCAL_CHANGES_OVERWRITTEN_BY_CHERRY_PICK); + GitMessageWithFilesDetector untrackedFilesDetector = new GitMessageWithFilesDetector(UNTRACKED_FILES_OVERWRITTEN_BY, + repository.getRoot()); + GitCommandResult result = myGit.cherryPick(repository, commit.getHash().getValue(), myAutoCommit, + conflictDetector, localChangesOverwrittenDetector, untrackedFilesDetector); + if (result.success()) { + if (myAutoCommit) { + successfulCommits.add(commit); + } + else { + boolean committed = updateChangeListManagerShowCommitDialogAndRemoveChangeListOnSuccess(repository, commit, successfulCommits); + if (!committed) { + notifyCommitCancelled(commit, successfulCommits); + return false; + } + } + } + else if (conflictDetector.hasHappened()) { + boolean mergeCompleted = new CherryPickConflictResolver(myProject, myGit, myPlatformFacade, repository.getRoot(), + commit.getShortHash().getString(), commit.getAuthor(), + commit.getSubject()).merge(); + + if (mergeCompleted) { + boolean committed = updateChangeListManagerShowCommitDialogAndRemoveChangeListOnSuccess(repository, commit, successfulCommits); + if (!committed) { + notifyCommitCancelled(commit, successfulCommits); + return false; + } + } + else { + updateChangeListManager(commit); + notifyConflictWarning(repository, commit, successfulCommits); + return false; + } + } + else if (untrackedFilesDetector.wasMessageDetected()) { + String description = commitDetails(commit) + + "
Some untracked working tree files would be overwritten by cherry-pick.
" + + "Please move, remove or add them before you can cherry-pick. View them"; + description += getSuccessfulCommitDetailsIfAny(successfulCommits); + + UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject, myPlatformFacade, untrackedFilesDetector.getFiles(), + "cherry-pick", description); + return false; + } + else if (localChangesOverwrittenDetector.hasHappened()) { + notifyError("Your local changes would be overwritten by cherry-pick.
Commit your changes or stash them to proceed.", + commit, successfulCommits); + return false; + } + else { + notifyError(result.getErrorOutputAsHtmlString(), commit, successfulCommits); + return false; + } + } + return true; + } + + private boolean updateChangeListManagerShowCommitDialogAndRemoveChangeListOnSuccess(@NotNull GitRepository repository, + @NotNull GitCommit commit, + @NotNull List successfulCommits) { + CherryPickData data = updateChangeListManager(commit); + boolean committed = showCommitDialog(repository, commit, data.myChangeList, data.myCommitMessage); + if (committed) { + removeChangeList(data); + successfulCommits.add(commit); + return true; + } + return false; + } + + private void removeChangeList(CherryPickData list) { + myChangeListManager.setDefaultChangeList(list.myPreviouslyDefaultChangeList); + if (!myChangeListManager.getDefaultChangeList().equals(list.myChangeList)) { + myChangeListManager.removeChangeList(list.myChangeList); + } + } + + private void notifyConflictWarning(@NotNull GitRepository repository, @NotNull GitCommit commit, + @NotNull List successfulCommits) { + NotificationListener resolveLinkListener = new ResolveLinkListener(myProject, myGit, myPlatformFacade, repository.getRoot(), + commit.getShortHash().getString(), commit.getAuthor(), + commit.getSubject()); + String description = commitDetails(commit) + + "
Unresolved conflicts remain in the working tree. Resolve them."; + description += getSuccessfulCommitDetailsIfAny(successfulCommits); + myPlatformFacade.getNotificator(myProject).notifyStrongWarning("Cherry-picked with conflicts", description, resolveLinkListener); + } + + private void notifyCommitCancelled(@NotNull GitCommit commit, @NotNull List successfulCommits) { + String description = commitDetails(commit); + description += getSuccessfulCommitDetailsIfAny(successfulCommits); + myPlatformFacade.getNotificator(myProject).notifyWeakWarning("Cherry-pick cancelled", description, null); + } + + private CherryPickData updateChangeListManager(@NotNull final GitCommit commit) { + final Collection paths = ChangesUtil.getPaths(commit.getChanges()); + refreshChangedFiles(paths); + final String commitMessage = createCommitMessage(commit, paths); + LocalChangeList previouslyDefaultChangeList = myChangeListManager.getDefaultChangeList(); + LocalChangeList changeList = createChangeListAfterUpdate(commit.getChanges(), paths, commitMessage); + return new CherryPickData(changeList, commitMessage, previouslyDefaultChangeList); + } + + @NotNull + private LocalChangeList createChangeListAfterUpdate(@NotNull final List changes, @NotNull final Collection paths, + @NotNull final String commitMessage) { + final AtomicReference changeList = new AtomicReference(); + myPlatformFacade.invokeAndWait(new Runnable() { + @Override + public void run() { + myChangeListManager.invokeAfterUpdate(new Runnable() { + public void run() { + changeList.set(createChangeList(changes, commitMessage)); + } + }, InvokeAfterUpdateMode.SYNCHRONOUS_NOT_CANCELLABLE, "", + new Consumer() { + public void consume(VcsDirtyScopeManager vcsDirtyScopeManager) { + vcsDirtyScopeManager.filePathsDirty(paths, null); + } + }, ModalityState.NON_MODAL + ); + } + }, ModalityState.NON_MODAL); + + + return changeList.get(); + } + + @NotNull + private String createCommitMessage(@NotNull GitCommit commit, @NotNull Collection paths) { + CheckinEnvironment ce = myPlatformFacade.getVcs(myProject).getCheckinEnvironment(); + String message = ce == null ? null : ce.getDefaultMessageFor(ArrayUtil.toObjectArray(paths, FilePath.class)); + message = message == null ? commit.getDescription() + "\n(cherry-picked from " + commit.getShortHash().getString() + ")" : message; + return message; + } + + private boolean showCommitDialog(@NotNull final GitRepository repository, @NotNull final GitCommit commit, + @NotNull final LocalChangeList changeList, @NotNull final String commitMessage) { + final AtomicBoolean commitSucceeded = new AtomicBoolean(); + myPlatformFacade.invokeAndWait(new Runnable() { + @Override + public void run() { + cancelCherryPick(repository); + List changes = commit.getChanges(); + CherryPickCommitExecutor executor = new CherryPickCommitExecutor(myProject, myPlatformFacade, changes, commitMessage); + boolean commitNotCancelled = myPlatformFacade.getVcsHelper(myProject).commitChanges(changes, changeList, commitMessage, executor); + commitSucceeded.set(commitNotCancelled && !executor.hasCommitFailed()); + } + }, ModalityState.NON_MODAL); + return commitSucceeded.get(); + } + + /** + * We control the cherry-pick workflow ourselves + we want to use partial commits ('git commit --only'), which is prohibited during + * cherry-pick, i.e. until the CHERRY_PICK_HEAD exists. + */ + private void cancelCherryPick(@NotNull GitRepository repository) { + if (myAutoCommit) { + removeCherryPickHead(repository); + } + } + + private void removeCherryPickHead(@NotNull GitRepository repository) { + File cherryPickHeadFile = new File(repository.getGitDir().getPath(), "CHERRY_PICK_HEAD"); + final VirtualFile cherryPickHead = myPlatformFacade.getLocalFileSystem().refreshAndFindFileByIoFile(cherryPickHeadFile); + + if (cherryPickHead != null && cherryPickHead.exists()) { + myPlatformFacade.runWriteAction(new Runnable() { + @Override + public void run() { + try { + cherryPickHead.delete(this); + } + catch (IOException e) { + // if CHERRY_PICK_HEAD is not deleted, the partial commit will fail, and the user will be notified anyway. + // So here we just log the fact. It is happens relatively often, maybe some additional solution will follow. + LOG.error(e); + } + } + }); + } + else { + LOG.info("Cancel cherry-pick in " + repository.getPresentableUrl() + ": no CHERRY_PICK_HEAD found"); + } + } + + private void notifyError(@NotNull String content, @NotNull GitCommit failedCommit, @NotNull List successfulCommits) { + String description = commitDetails(failedCommit) + "
" + content; + description += getSuccessfulCommitDetailsIfAny(successfulCommits); + myPlatformFacade.getNotificator(myProject).notifyError("Cherry-pick failed", description); + } + + @NotNull + private static String getSuccessfulCommitDetailsIfAny(@NotNull List successfulCommits) { + String description = ""; + if (!successfulCommits.isEmpty()) { + description += "
However cherry-pick succeeded for the following " + pluralize("commit", successfulCommits.size()) + ":
"; + description += getCommitsDetails(successfulCommits); + } + return description; + } + + private void notifySuccess(@NotNull List successfulCommits) { + String description = getCommitsDetails(successfulCommits); + myPlatformFacade.getNotificator(myProject).notifySuccess("Cherry-pick successful", description); + } + + @NotNull + private static String getCommitsDetails(@NotNull List successfulCommits) { + String description = ""; + for (GitCommit commit : successfulCommits) { + description += commitDetails(commit) + "
"; + } + return description.substring(0, description.length() - "
".length()); + } + + @NotNull + private static String commitDetails(@NotNull GitCommit commit) { + return commit.getShortHash().toString() + " \"" + commit.getSubject() + "\""; + } + + private void refreshChangedFiles(@NotNull Collection filePaths) { + for (FilePath file : filePaths) { + VirtualFile vf = myPlatformFacade.getLocalFileSystem().refreshAndFindFileByPath(file.getPath()); if (vf != null) { vf.refresh(false, false); } @@ -90,190 +322,230 @@ public class CherryPicker { } @NotNull - private Collection getAllChangedFiles() { - Collection files = new ArrayList(); - for (CherryPickedData data : myCherryPickedData) { - files.addAll(data.getFiles()); + private LocalChangeList createChangeList(@NotNull List changes, @NotNull String commitMessage) { + if (!changes.isEmpty()) { + final LocalChangeList changeList = myChangeListManager.addChangeList(commitMessage, commitMessage); + myChangeListManager.moveChangesTo(changeList, changes.toArray(new Change[changes.size()])); + myChangeListManager.setDefaultChangeList(changeList); + return changeList; } - return files; + return myChangeListManager.getDefaultChangeList(); } - private void findAndProcessChangedForVcs() { - myChangeListManager.invokeAfterUpdate(new Runnable() { - public void run() { - moveToCorrectLists(); - } - }, InvokeAfterUpdateMode.SILENT, "", new Consumer() { - public void consume(VcsDirtyScopeManager vcsDirtyScopeManager) { - vcsDirtyScopeManager.filePathsDirty(getAllChangedFiles(), null); - } - }, ModalityState.NON_MODAL); - } + private static class CherryPickData { + private final LocalChangeList myChangeList; + private final String myCommitMessage; + private final LocalChangeList myPreviouslyDefaultChangeList; - private void showResults() { - final Project project = myVcs.getProject(); - if (myExceptions.isEmpty() && !myConflictsExist) { - showOverChangesView(project, "Successful cherry-pick into working tree, please commit changes", MessageType.INFO); - } else { - if (myExceptions.isEmpty()) { - showOverChangesView(project, "Unresolved conflicts while cherry-picking. Resolve conflicts, then commit changes", - MessageType.WARNING); - } else { - showOverChangesView(project, "Errors in cherry-pick", MessageType.ERROR); - } - } - if ((! myExceptions.isEmpty()) || (! myWarnings.isEmpty())) { - myExceptions.addAll(myWarnings); - AbstractVcsHelper.getInstance(project).showErrors(myExceptions, "Cherry-pick problems"); + private CherryPickData(LocalChangeList list, String message, LocalChangeList previouslyDefaultChangeList) { + myChangeList = list; + myCommitMessage = message; + myPreviouslyDefaultChangeList = previouslyDefaultChangeList; } } - private void moveToCorrectLists() { - for (CherryPickedData pickedData : myCherryPickedData) { - final Collection filePaths = pickedData.getFiles(); - final String message = pickedData.getCommitMessage(); + private static class CherryPickConflictResolver extends GitConflictResolver { - if (filePaths.isEmpty()) continue; - - final List changes = pathsToChanges(filePaths); - pickedData.setChanges(changes); - if (!changes.isEmpty()) { - final LocalChangeList cl = myChangeListManager.addChangeList(message, null); - pickedData.setChangeList(cl); - myChangeListManager.moveChangesTo(cl, changes.toArray(new Change[changes.size()])); - } + public CherryPickConflictResolver(@NotNull Project project, @NotNull Git git, @NotNull PlatformFacade facade, @NotNull VirtualFile root, + @NotNull String commitHash, @NotNull String commitAuthor, @NotNull String commitMessage) { + super(project, git, facade, Collections.singleton(root), makeParams(commitHash, commitAuthor, commitMessage)); } + + private static Params makeParams(String commitHash, String commitAuthor, String commitMessage) { + Params params = new Params(); + params.setErrorNotificationTitle("Cherry-picked with conflicts"); + params.setMergeDialogCustomizer(new CherryPickMergeDialogCustomizer(commitHash, commitAuthor, commitMessage)); + return params; + } + + @Override + protected void notifyUnresolvedRemain() { + // we show a [possibly] compound notification after cherry-picking all commits. + } + } - @NotNull - private List pathsToChanges(@NotNull Collection filePaths) { - final List changes = new ArrayList(filePaths.size()); - for (FilePath filePath : filePaths) { - changes.add(myChangeListManager.getChange(filePath)); + private static class ResolveLinkListener implements NotificationListener { + @NotNull private final Project myProject; + @NotNull private final Git myGit; + @NotNull private final PlatformFacade myFacade; + @NotNull private final VirtualFile myRoot; + @NotNull private final String myHash; + @NotNull private final String myAuthor; + @NotNull private final String myMessage; + + public ResolveLinkListener(@NotNull Project project, @NotNull Git git, @NotNull PlatformFacade facade, @NotNull VirtualFile root, + @NotNull String commitHash, @NotNull String commitAuthor, @NotNull String commitMessage) { + + myProject = project; + myGit = git; + myFacade = facade; + myRoot = root; + myHash = commitHash; + myAuthor = commitAuthor; + myMessage = commitMessage; } - return changes; - } - private void checkListsForSamePaths() { - List myMessagesInOrder = new ArrayList(myCherryPickedData.size()); - Map> myFilesToMove = new HashMap>(myCherryPickedData.size()); - for (CherryPickedData data : myCherryPickedData) { - myMessagesInOrder.add(data.getCommitMessage()); - myFilesToMove.put(data.getCommitMessage(), data.getFiles()); - } - final GroupOfListsProcessor listsProcessor = new GroupOfListsProcessor(); - listsProcessor.process(myMessagesInOrder, myFilesToMove); - final Set lostSet = listsProcessor.getHaveLostSomething(); - markFilesMovesToNewerLists(myWarnings, lostSet, myFilesToMove); - } - - private void cherryPickStep(@NotNull GitCommit commit) { - try { - if (!myAccess.cherryPick(commit)) { - myConflictsExist = true; - } - } - catch (VcsException e) { - myExceptions.add(e); - } - final List changes = commit.getChanges(); - - final Collection paths = ChangesUtil.getPaths(changes); - String message = myCheckinEnvironment.getDefaultMessageFor(paths.toArray(new FilePath[paths.size()])); - message = (message == null) ? commit.getDescription() + " (cherry picked from commit " + commit.getShortHash() + ")" : message; - - myCherryPickedData.add(new CherryPickedData(message, paths)); - } - - private static void markFilesMovesToNewerLists(List exceptions, Set lostSet, - Map> filesToMove) { - if (! lostSet.isEmpty()) { - final StringBuilder sb = new StringBuilder("Some changes are moved from following list(s) to other:"); - boolean first = true; - for (String s : lostSet) { - if (filesToMove.get(s).isEmpty()) { - final VcsException exc = - new VcsException("Changelist not created since all files moved to other cherry-pick(s): '" + s + "'"); - exc.setIsWarning(true); - exceptions.add(exc); - continue; + @Override + public void hyperlinkUpdate(@NotNull Notification notification, + @NotNull HyperlinkEvent event) { + if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { + if (event.getDescription().equals("resolve")) { + new CherryPickConflictResolver(myProject, myGit, myFacade, myRoot, myHash, myAuthor, myMessage).mergeNoProceed(); } - sb.append(s); - if (! first) { - sb.append(", "); - } - first = false; - } - if (! first) { - final VcsException exc = new VcsException(sb.toString()); - exc.setIsWarning(true); - exceptions.add(exc); } } } - private static class GroupOfListsProcessor { - private final Set myHaveLostSomething; + private static class CherryPickMergeDialogCustomizer extends MergeDialogCustomizer { - private GroupOfListsProcessor() { - myHaveLostSomething = new HashSet(); + private String myCommitHash; + private String myCommitAuthor; + private String myCommitMessage; + + public CherryPickMergeDialogCustomizer(String commitHash, String commitAuthor, String commitMessage) { + myCommitHash = commitHash; + myCommitAuthor = commitAuthor; + myCommitMessage = commitMessage; } - public void process(final List messagesInOrder, final Map> filesToMove) { - // remove those that are in newer lists - for (int i = 1; i < messagesInOrder.size(); i++) { - final String message = messagesInOrder.get(i); - final Collection currentFiles = filesToMove.get(message); + @Override + public String getMultipleFileMergeDescription(Collection files) { + return "Conflicts during cherry-picking commit " + myCommitHash + " made by " + myCommitAuthor + "
" + + "\"" + myCommitMessage + "\""; + } - for (int j = 0; j < i; j++) { - final String previous = messagesInOrder.get(j); - final boolean somethingChanged = filesToMove.get(previous).removeAll(currentFiles); - if (somethingChanged) { - myHaveLostSomething.add(previous); + @Override + public String getLeftPanelTitle(VirtualFile file) { + return "Local changes"; + } + + @Override + public String getRightPanelTitle(VirtualFile file, VcsRevisionNumber lastRevisionNumber) { + return "Changes from cherry-pick " + myCommitHash + ""; + } + } + + /* + Commit procedure is overridden by the executor with its own CommitSession. + The reason of that is the asynchronous nature of the CommitHelper: it returns, we continue cherry-picking and occasionally pick + the next commit in the queue, and only then Git is called for commit. Thus it commits two cherry-picks at once, which is wrong. + + Here we call GitCheckinEnvironment manually + */ + private static class CherryPickCommitExecutor implements CommitExecutor { + + @NotNull private final Project myProject; + @NotNull private final PlatformFacade myPlatformFacade; + @NotNull private final List myChanges; + @NotNull private final String myCommitMessage; + private boolean myCommitFailed; + + CherryPickCommitExecutor(@NotNull Project project, @NotNull PlatformFacade platformFacade, + @NotNull List changes, @NotNull String commitMessage) { + myProject = project; + myPlatformFacade = platformFacade; + myChanges = changes; + myCommitMessage = commitMessage; + } + + @Nls + @Override + public String getActionText() { + return "Commit"; + } + + @NotNull + @Override + public CommitSession createCommitSession() { + return new CherryPickCommitSession(); + } + + public boolean hasCommitFailed() { + return myCommitFailed; + } + + private class CherryPickCommitSession implements CommitSession { + @Override + public JComponent getAdditionalConfigurationUI() { + return null; + } + + @Override + public JComponent getAdditionalConfigurationUI(Collection changes, String commitMessage) { + return null; + } + + @Override + public boolean canExecute(Collection changes, String commitMessage) { + return true; + } + + @Override + public void execute(Collection changes, String commitMessage) { + final Collection committingDocs = markCommittingDocs(); + try { + CheckinEnvironment ce = myPlatformFacade.getVcs(myProject).getCheckinEnvironment(); + if (ce != null) { + try { + List exceptions = ce.commit(myChanges, myCommitMessage); + VcsDirtyScopeManager.getInstance(myProject).filePathsDirty(ChangesUtil.getPaths(myChanges), null); + if (exceptions != null && !exceptions.isEmpty()) { + VcsException exception = exceptions.get(0); + handleError(exception); + } + } + catch (Throwable e) { + LOG.error(e); + handleError(e); + } } } + finally { + unmarkCommittingDocs(committingDocs); + } } - } - public Set getHaveLostSomething() { - return myHaveLostSomething; - } - } + private void handleError(Throwable exception) { + myCommitFailed = true; + final String errorMessage = exception.getMessage(); + WaitForProgressToShow.runOrInvokeLaterAboveProgress(new Runnable() { + public void run() { + Messages.showErrorDialog(myProject, errorMessage, "Commit Failed"); + } + }, null, myProject); + } - private static class CherryPickedData { + @Override + public void executionCanceled() { + } - private final String myCommitMessage; - private final Collection myFiles; - private LocalChangeList myChangeList; - private Collection myChanges; + @Override + public String getHelpId() { + return null; + } - private CherryPickedData(@NotNull String message, @NotNull Collection files) { - myCommitMessage = message; - myFiles = files; - } + private void unmarkCommittingDocs(final Collection committingDocs) { + myPlatformFacade.runReadAction(new Runnable() { + @Override + public void run() { + CommitHelper.unmarkCommittingDocuments(committingDocs); + } + }); + } - public Collection getChanges() { - return myChanges; - } + @NotNull + private Collection markCommittingDocs() { + final Collection committingDocs = new ArrayList(); + myPlatformFacade.runReadAction(new Runnable() { + @Override + public void run() { + committingDocs.addAll(CommitHelper.markCommittingDocuments(myProject, myChanges)); + } + }); + return committingDocs; + } - public LocalChangeList getChangeList() { - return myChangeList; - } - - public String getCommitMessage() { - return myCommitMessage; - } - - public Collection getFiles() { - return myFiles; - } - - public void setChanges(List changes) { - myChanges = changes; - } - - public void setChangeList(LocalChangeList changeList) { - myChangeList = changeList; } } diff --git a/plugins/git4idea/src/git4idea/history/browser/LowLevelAccess.java b/plugins/git4idea/src/git4idea/history/browser/LowLevelAccess.java index 2771f3046361..2aca5e7395b7 100644 --- a/plugins/git4idea/src/git4idea/history/browser/LowLevelAccess.java +++ b/plugins/git4idea/src/git4idea/history/browser/LowLevelAccess.java @@ -56,14 +56,6 @@ public interface LowLevelAccess { void loadAllTags(final Collection sink) throws VcsException; - /** - * Cherry-picks the specified commit. - * Doesn't autocommit - instead puts the changes into a separate changelist. - * In the case of merge conflict provides the Conflict Resolver dialog. - * @return true if all conflicts were resolved or there were no merge conflicts; false if unresolved files remain. - * @throws VcsException - */ - boolean cherryPick(GitCommit hash) throws VcsException; void loadHashesWithParents(final @NotNull Collection startingPoints, @NotNull final Collection filters, final AsynchConsumer consumer, Getter isCanceled, int useMaxCnt, final boolean topoOrder) throws VcsException; diff --git a/plugins/git4idea/src/git4idea/history/browser/LowLevelAccessImpl.java b/plugins/git4idea/src/git4idea/history/browser/LowLevelAccessImpl.java index 64f8707fb354..8f8ea572719f 100644 --- a/plugins/git4idea/src/git4idea/history/browser/LowLevelAccessImpl.java +++ b/plugins/git4idea/src/git4idea/history/browser/LowLevelAccessImpl.java @@ -18,6 +18,7 @@ package git4idea.history.browser; import com.intellij.notification.Notification; import com.intellij.notification.NotificationListener; import com.intellij.notification.NotificationType; +import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Getter; @@ -29,11 +30,9 @@ import com.intellij.openapi.vcs.merge.MergeDialogCustomizer; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ArrayUtil; import com.intellij.util.AsynchConsumer; -import git4idea.GitBranch; -import git4idea.GitTag; -import git4idea.GitUtil; -import git4idea.GitVcs; +import git4idea.*; import git4idea.branch.GitBranchesCollection; +import git4idea.commands.Git; import git4idea.commands.GitCommand; import git4idea.commands.GitLineHandler; import git4idea.commands.GitLineHandlerAdapter; @@ -43,6 +42,7 @@ import git4idea.history.wholeTree.AbstractHash; import git4idea.history.wholeTree.CommitHashPlusParents; import git4idea.merge.GitConflictResolver; import git4idea.repo.GitRepository; +import git4idea.repo.GitRepositoryImpl; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -121,7 +121,7 @@ public class LowLevelAccessImpl implements LowLevelAccess { if (! child.exists()) { throw new VcsException("No git repository in " + myRoot.getPath()); } - repository = GitRepository.getLightInstance(myRoot, myProject, myProject); + repository = GitRepositoryImpl.getLightInstance(myRoot, myProject, myProject); repository.getBranches(); } GitBranchesCollection branches = repository.getBranches(); @@ -212,102 +212,4 @@ public class LowLevelAccessImpl implements LowLevelAccess { GitTag.listAsStrings(myProject, myRoot, sink, null); } - public boolean cherryPick(GitCommit commit) throws VcsException { - final GitLineHandler handler = new GitLineHandler(myProject, myRoot, GitCommand.CHERRY_PICK); - handler.addParameters("-x", "-n", commit.getHash().getValue()); - handler.endOptions(); - handler.setNoSSH(true); - - final AtomicBoolean conflict = new AtomicBoolean(); - - handler.addLineListener(new GitLineHandlerAdapter() { - public void onLineAvailable(String line, Key outputType) { - if (line.toLowerCase().contains("after resolving the conflicts")) { - conflict.set(true); - } - } - }); - handler.runInCurrentThread(null); - - if (conflict.get()) { - return new CherryPickConflictResolver(myProject, myRoot, commit.getShortHash().getString(), commit.getAuthor(), commit.getSubject()).merge(); - } else { - final List errors = handler.errors(); - if (!errors.isEmpty()) { - throw errors.get(0); - } else { // no conflicts, no errors - return true; - } - } - } - - private static class CherryPickConflictResolver extends GitConflictResolver { - - private VirtualFile myRoot; - private String myCommitHash; - private String myCommitAuthor; - private String myCommitMessage; - - public CherryPickConflictResolver(Project project, VirtualFile root, String commitHash, String commitAuthor, String commitMessage) { - super(project, Collections.singleton(root), makeParams(commitHash, commitAuthor, commitMessage)); - myRoot = root; - myCommitHash = commitHash; - myCommitAuthor = commitAuthor; - myCommitMessage = commitMessage; - } - - private static Params makeParams(String commitHash, String commitAuthor, String commitMessage) { - Params params = new Params(); - params.setErrorNotificationTitle("Cherry-picked with conflicts"); - params.setMergeDialogCustomizer(new CherryPickMergeDialogCustomizer(commitHash, commitAuthor, commitMessage)); - return params; - } - - @Override - protected void notifyUnresolvedRemain() { - GitVcs.IMPORTANT_ERROR_NOTIFICATION.createNotification("Conflicts were not resolved during cherry-pick", - "Cherry-pick is not complete, you have unresolved merges in your working tree
" + - "
Resolve conflicts.", - NotificationType.WARNING, new NotificationListener() { - @Override - public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) { - if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { - if (event.getDescription().equals("resolve")) { - new CherryPickConflictResolver(myProject, myRoot, myCommitHash, myCommitAuthor, myCommitMessage).mergeNoProceed(); - } - } - } - }).notify(myProject); - } - } - - private static class CherryPickMergeDialogCustomizer extends MergeDialogCustomizer { - - private String myCommitHash; - private String myCommitAuthor; - private String myCommitMessage; - - public CherryPickMergeDialogCustomizer(String commitHash, String commitAuthor, String commitMessage) { - myCommitHash = commitHash; - myCommitAuthor = commitAuthor; - myCommitMessage = commitMessage; - } - - @Override - public String getMultipleFileMergeDescription(Collection files) { - return "Conflicts during cherry-picking commit " + myCommitHash + " made by " + myCommitAuthor + "
" + - "\"" + myCommitMessage + "\""; - } - - @Override - public String getLeftPanelTitle(VirtualFile file) { - return "Local changes"; - } - - @Override - public String getRightPanelTitle(VirtualFile file, VcsRevisionNumber lastRevisionNumber) { - return "Changes from cherry-pick " + myCommitHash + ""; - } - } - } diff --git a/plugins/git4idea/src/git4idea/history/wholeTree/GitLogUI.java b/plugins/git4idea/src/git4idea/history/wholeTree/GitLogUI.java index 33387f731093..2d978e3afba7 100644 --- a/plugins/git4idea/src/git4idea/history/wholeTree/GitLogUI.java +++ b/plugins/git4idea/src/git4idea/history/wholeTree/GitLogUI.java @@ -17,12 +17,14 @@ import com.intellij.ide.actions.ContextHelpAction; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.actionSystem.impl.SimpleDataContext; -import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.diff.impl.CaptionIcon; import com.intellij.openapi.diff.impl.patch.formove.FilePathComparator; import com.intellij.openapi.ide.CopyPasteManager; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; @@ -58,10 +60,14 @@ import com.intellij.util.ui.UIUtil; import com.intellij.vcsUtil.VcsUtil; import git4idea.GitUtil; import git4idea.GitVcs; +import git4idea.PlatformFacade; import git4idea.branch.GitBranchOperationsProcessor; import git4idea.changes.GitChangeUtils; +import git4idea.commands.Git; +import git4idea.config.GitVcsSettings; import git4idea.history.browser.*; import git4idea.repo.GitRepository; +import git4idea.repo.GitRepositoryManager; import git4idea.ui.branch.GitBranchUiUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -1608,17 +1614,14 @@ public class GitLogUI implements Disposable { myIdsInProgress.add(commit.getShortHash()); } - final Application application = ApplicationManager.getApplication(); - application.executeOnPooledThread(new Runnable() { - public void run() { - for (VirtualFile file : commits.keySet()) { - final List part = (List)commits.get(file); - // earliest first!!! - Collections.reverse(part); - new CherryPicker(GitVcs.getInstance(myProject), part, new LowLevelAccessImpl(myProject, file)).execute(); - } + new Task.Backgroundable(myProject, "Cherry-picking", false) { + public void run(@NotNull ProgressIndicator indicator) { + boolean autoCommit = GitVcsSettings.getInstance(myProject).isAutoCommitOnCherryPick(); + Map> commitsInRoots = prepareCommitsForCherryPick(commits); + new CherryPicker(myProject, ServiceManager.getService(Git.class), ServiceManager.getService(PlatformFacade.class), autoCommit) + .cherryPick(commitsInRoots); - application.invokeLater(new Runnable() { + ApplicationManager.getApplication().invokeLater(new Runnable() { public void run() { for (GitCommit commit : commits.values()) { myIdsInProgress.remove(commit.getShortHash()); @@ -1626,7 +1629,23 @@ public class GitLogUI implements Disposable { } }); } - }); + }.queue(); + } + + private Map> prepareCommitsForCherryPick(MultiMap commits) { + Map> commitsInRoots = new HashMap>(); + GitRepositoryManager repositoryManager = ServiceManager.getService(myProject, GitRepositoryManager.class); + for (Map.Entry> entry : commits.entrySet()) { + List sortedCommits = new ArrayList(entry.getValue()); + // earliest first!!! + Collections.reverse(sortedCommits); + GitRepository repository = repositoryManager.getRepositoryForRoot(entry.getKey()); + if (repository == null) { + continue; + } + commitsInRoots.put(repository, sortedCommits); + } + return commitsInRoots; } // newest first diff --git a/plugins/git4idea/src/git4idea/merge/GitConflictResolver.java b/plugins/git4idea/src/git4idea/merge/GitConflictResolver.java index 351bb674b5cf..e546dfe61b38 100644 --- a/plugins/git4idea/src/git4idea/merge/GitConflictResolver.java +++ b/plugins/git4idea/src/git4idea/merge/GitConflictResolver.java @@ -21,18 +21,26 @@ import com.intellij.notification.NotificationType; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.AbstractVcsHelper; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.merge.MergeDialogCustomizer; import com.intellij.openapi.vcs.merge.MergeProvider; +import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ui.UIUtil; +import git4idea.GitUtil; import git4idea.GitVcs; +import git4idea.PlatformFacade; +import git4idea.commands.Git; +import git4idea.commands.GitCommandResult; +import git4idea.repo.GitRepository; +import git4idea.repo.GitRepositoryManager; +import git4idea.util.StringScanner; import org.jetbrains.annotations.NotNull; import javax.swing.event.HyperlinkEvent; -import java.util.ArrayList; -import java.util.Collection; +import java.util.*; /** * @@ -43,12 +51,14 @@ public class GitConflictResolver { private static final Logger LOG = Logger.getInstance(GitConflictResolver.class); - protected final Project myProject; + @NotNull protected final Project myProject; + @NotNull private final Git myGit; + @NotNull private final PlatformFacade myPlatformFacade; private final Collection myRoots; private final Params myParams; + @NotNull private final GitRepositoryManager myRepositoryManager; private final AbstractVcsHelper myVcsHelper; - private final GitVcs myVcs; /** * Customizing parameters - mostly String notification texts, etc. @@ -94,13 +104,15 @@ public class GitConflictResolver { } - public GitConflictResolver(@NotNull Project project, @NotNull Collection roots, @NotNull Params params) { + public GitConflictResolver(@NotNull Project project, @NotNull Git git, @NotNull PlatformFacade platformFacade, + @NotNull Collection roots, @NotNull Params params) { myProject = project; + myGit = git; + myPlatformFacade = platformFacade; myRoots = roots; myParams = params; - - myVcsHelper = AbstractVcsHelper.getInstance(project); - myVcs = GitVcs.getInstance(project); + myRepositoryManager = myPlatformFacade.getRepositoryManager(myProject); + myVcsHelper = myPlatformFacade.getVcsHelper(project); } /** @@ -141,8 +153,12 @@ public class GitConflictResolver { protected boolean proceedAfterAllMerged() throws VcsException { return true; } - - protected final boolean mergeNoProceed() { + + /** + * Invoke the merge dialog, but execute nothing after merge is completed. + * @return true if all changes were merged, false if unresolved merges remain. + */ + public final boolean mergeNoProceed() { return merge(true); } @@ -165,12 +181,12 @@ public class GitConflictResolver { } private void notifyWarning(String title, String content) { - GitVcs.IMPORTANT_ERROR_NOTIFICATION.createNotification(title, content, NotificationType.WARNING, new ResolveNotificationListener()).notify(myProject); + myPlatformFacade.getNotificator(myProject).notifyStrongWarning(title, content, new ResolveNotificationListener()); } private boolean merge(boolean mergeDialogInvokedFromNotification) { try { - final Collection initiallyUnmergedFiles = GitMergeUtil.getUnmergedFiles(myProject, myRoots); + final Collection initiallyUnmergedFiles = getUnmergedFiles(myRoots); if (initiallyUnmergedFiles.isEmpty()) { LOG.info("merge: no unmerged files"); return mergeDialogInvokedFromNotification ? true : proceedIfNothingToMerge(); @@ -178,7 +194,7 @@ public class GitConflictResolver { else { showMergeDialog(initiallyUnmergedFiles); - final Collection unmergedFilesAfterResolve = GitMergeUtil.getUnmergedFiles(myProject, myRoots); + final Collection unmergedFilesAfterResolve = getUnmergedFiles(myRoots); if (unmergedFilesAfterResolve.isEmpty()) { LOG.info("merge no more unmerged files"); return mergeDialogInvokedFromNotification ? true : proceedAfterAllMerged(); @@ -201,7 +217,8 @@ public class GitConflictResolver { private void showMergeDialog(final Collection initiallyUnmergedFiles) { UIUtil.invokeAndWaitIfNeeded(new Runnable() { @Override public void run() { - final MergeProvider mergeProvider = myParams.reverse ? myVcs.getReverseMergeProvider() : myVcs.getMergeProvider(); + final MergeProvider mergeProvider = myParams.reverse ? + new GitMergeProvider(myProject, true) : new GitMergeProvider(myProject, false); myVcsHelper.showMergeDialog(new ArrayList(initiallyUnmergedFiles), mergeProvider, myParams.myMergeDialogCustomizer); } }); @@ -238,4 +255,77 @@ public class GitConflictResolver { } } + /** + * @return unmerged files in the given Git roots, all in a single collection. + * @see #getUnmergedFiles(com.intellij.openapi.vfs.VirtualFile) + */ + private Collection getUnmergedFiles(@NotNull Collection roots) throws VcsException { + final Collection unmergedFiles = new HashSet(); + for (VirtualFile root : roots) { + unmergedFiles.addAll(getUnmergedFiles(root)); + } + return unmergedFiles; + } + + /** + * @return unmerged files in the given Git root. + * @see #getUnmergedFiles(java.util.Collection + */ + private Collection getUnmergedFiles(@NotNull VirtualFile root) throws VcsException { + return unmergedFiles(root); + } + + /** + * Parse changes from lines + * + * + * @param root the git root + * @return a set of unmerged files + * @throws com.intellij.openapi.vcs.VcsException if the input format does not matches expected format + */ + private List unmergedFiles(VirtualFile root) throws VcsException { + HashSet unmerged = new HashSet(); + String rootPath = root.getPath(); + + GitRepository repository = myRepositoryManager.getRepositoryForRoot(root); + if (repository == null) { + LOG.error("Repository not found for root " + root); + return Collections.emptyList(); + } + + GitCommandResult result = myGit.getUnmergedFiles(repository); + if (!result.success()) { + throw new VcsException(result.getErrorOutputAsJoinedString()); + } + + String output = StringUtil.join(result.getOutput(), "\n"); + + LocalFileSystem lfs = myPlatformFacade.getLocalFileSystem(); + for (StringScanner s = new StringScanner(output); s.hasMoreData();) { + if (s.isEol()) { + s.nextLine(); + continue; + } + s.boundedToken('\t'); + final String relative = s.line(); + String path = rootPath + "/" + GitUtil.unescapePath(relative); + VirtualFile file = lfs.refreshAndFindFileByPath(path); + if (file != null) { + // the file name is in the delete- or rename- conflict, so it is shown in the list of unmerged files, + // but the file itself doesn't exist. In that case we just ignore the file. + file.refresh(false, false); + unmerged.add(file); + } + } + if (unmerged.size() == 0) { + return Collections.emptyList(); + } + else { + ArrayList rc = new ArrayList(unmerged.size()); + rc.addAll(unmerged); + Collections.sort(rc, GitUtil.VIRTUAL_FILE_COMPARATOR); + return rc; + } + } + } diff --git a/plugins/git4idea/src/git4idea/merge/GitMergeCommittingConflictResolver.java b/plugins/git4idea/src/git4idea/merge/GitMergeCommittingConflictResolver.java index 4c48ab6dd0dd..d72c7b796618 100644 --- a/plugins/git4idea/src/git4idea/merge/GitMergeCommittingConflictResolver.java +++ b/plugins/git4idea/src/git4idea/merge/GitMergeCommittingConflictResolver.java @@ -15,9 +15,13 @@ */ package git4idea.merge; +import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vfs.VirtualFile; +import git4idea.PlatformFacade; +import git4idea.commands.Git; +import org.jetbrains.annotations.NotNull; import java.util.Collection; @@ -31,9 +35,9 @@ public class GitMergeCommittingConflictResolver extends GitConflictResolver { private final boolean myRefreshAfterCommit; private final GitMerger myMerger; - public GitMergeCommittingConflictResolver(Project project, GitMerger merger, Collection mergingRoots, Params params, - boolean refreshAfterCommit) { - super(project, mergingRoots, params); + public GitMergeCommittingConflictResolver(Project project, @NotNull Git git, GitMerger merger, Collection mergingRoots, + Params params, boolean refreshAfterCommit) { + super(project, git, ServiceManager.getService(PlatformFacade.class), mergingRoots, params); myMerger = merger; myMergingRoots = mergingRoots; myRefreshAfterCommit = refreshAfterCommit; diff --git a/plugins/git4idea/src/git4idea/merge/GitMergeUtil.java b/plugins/git4idea/src/git4idea/merge/GitMergeUtil.java index fd922b294bb4..bcdbe705db5f 100644 --- a/plugins/git4idea/src/git4idea/merge/GitMergeUtil.java +++ b/plugins/git4idea/src/git4idea/merge/GitMergeUtil.java @@ -34,7 +34,6 @@ import com.intellij.openapi.vfs.VirtualFile; import git4idea.GitRevisionNumber; import git4idea.GitVcs; import git4idea.actions.GitRepositoryAction; -import git4idea.changes.GitChangeUtils; import git4idea.i18n.GitBundle; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; @@ -44,7 +43,6 @@ import javax.swing.*; import java.io.File; import java.util.ArrayList; import java.util.Collection; -import java.util.HashSet; import java.util.List; /** @@ -189,25 +187,4 @@ public class GitMergeUtil { public static boolean isMergeInProgress(@NotNull VirtualFile root) { return getMergeHead(root) != null; } - - /** - * @return unmerged files in the given Git roots, all in a single collection. - * @see #getUnmergedFiles(com.intellij.openapi.project.Project, com.intellij.openapi.vfs.VirtualFile) - */ - public static Collection getUnmergedFiles(@NotNull Project project, @NotNull Collection roots) throws VcsException { - final Collection unmergedFiles = new HashSet(); - for (VirtualFile root : roots) { - unmergedFiles.addAll(getUnmergedFiles(project, root)); - } - return unmergedFiles; - } - - /** - * @return unmerged files in the given Git root. - * @see #getUnmergedFiles(com.intellij.openapi.project.Project, java.util.Collection) - */ - public static Collection getUnmergedFiles(@NotNull Project project, @NotNull VirtualFile root) throws VcsException { - return GitChangeUtils.unmergedFiles(project, root); - } - } diff --git a/plugins/git4idea/src/git4idea/rebase/GitRebaser.java b/plugins/git4idea/src/git4idea/rebase/GitRebaser.java index 1ee4146473ca..24e2d9ed7c5b 100644 --- a/plugins/git4idea/src/git4idea/rebase/GitRebaser.java +++ b/plugins/git4idea/src/git4idea/rebase/GitRebaser.java @@ -15,6 +15,7 @@ */ package git4idea.rebase; +import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; @@ -24,6 +25,7 @@ import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vfs.VirtualFile; import git4idea.GitUtil; import git4idea.GitVcs; +import git4idea.PlatformFacade; import git4idea.commands.*; import git4idea.merge.GitConflictResolver; import git4idea.util.GitUIUtil; @@ -47,10 +49,12 @@ public class GitRebaser { private GitVcs myVcs; private List mySkippedCommits; private static final Logger LOG = Logger.getInstance(GitRebaser.class); + @NotNull private final Git myGit; private final @Nullable ProgressIndicator myProgressIndicator; - public GitRebaser(Project project, ProgressIndicator progressIndicator) { + public GitRebaser(Project project, @NotNull Git git, ProgressIndicator progressIndicator) { myProject = project; + myGit = git; myProgressIndicator = progressIndicator; myVcs = GitVcs.getInstance(project); mySkippedCommits = new ArrayList(); @@ -172,7 +176,7 @@ public class GitRebaser { private boolean handleRebaseFailure(final VirtualFile root, final GitLineHandler h, GitRebaseProblemDetector rebaseConflictDetector) { if (rebaseConflictDetector.isMergeConflict()) { LOG.info("handleRebaseFailure merge conflict"); - return new GitConflictResolver(myProject, Collections.singleton(root), makeParamsForRebaseConflict()) { + return new GitConflictResolver(myProject, myGit, ServiceManager.getService(PlatformFacade.class), Collections.singleton(root), makeParamsForRebaseConflict()) { @Override protected boolean proceedIfNothingToMerge() { return continueRebase(root, "--continue"); } diff --git a/plugins/git4idea/src/git4idea/repo/GitRepository.java b/plugins/git4idea/src/git4idea/repo/GitRepository.java index 12f571199f28..5fb09189c8f2 100644 --- a/plugins/git4idea/src/git4idea/repo/GitRepository.java +++ b/plugins/git4idea/src/git4idea/repo/GitRepository.java @@ -15,23 +15,14 @@ */ package git4idea.repo; -import com.intellij.openapi.Disposable; import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.Disposer; -import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.util.Consumer; -import com.intellij.util.concurrency.QueueProcessor; -import com.intellij.util.messages.MessageBus; -import com.intellij.util.messages.MessageBusConnection; import com.intellij.util.messages.Topic; import git4idea.GitBranch; -import git4idea.GitUtil; import git4idea.branch.GitBranchesCollection; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.io.File; import java.util.Collection; /** @@ -67,30 +58,14 @@ import java.util.Collection; * * @author Kirill Likhodedov */ -public final class GitRepository implements Disposable { +public interface GitRepository { - public static final Topic GIT_REPO_CHANGE = Topic.create("GitRepository change", GitRepositoryChangeListener.class); - - private static final Object STUB_OBJECT = new Object(); - - private final Project myProject; - private final VirtualFile myRootDir; - private final GitRepositoryReader myReader; - private final VirtualFile myGitDir; - private final MessageBus myMessageBus; - private final GitUntrackedFilesHolder myUntrackedFilesHolder; - private final QueueProcessor myNotifier; - - private volatile State myState; - private volatile String myCurrentRevision; - private volatile GitBranch myCurrentBranch; - private volatile GitBranchesCollection myBranches = GitBranchesCollection.EMPTY; - private volatile GitConfig myConfig; + Topic GIT_REPO_CHANGE = Topic.create("GitRepository change", GitRepositoryChangeListener.class); /** * Current state of the repository. */ - public enum State { + enum State { /** * HEAD is on branch, no merge process is in progress (and no rebase as well). */ @@ -121,141 +96,40 @@ public final class GitRepository implements Disposable { * GitRepository tracks the updates of some information about Git repository, caches this information and provides methods to access to * it. The pieces of this information are called Topics. They can be used to update the repository. */ - public enum TrackedTopic { - STATE { - @Override void update(GitRepository repository) { - repository.updateState(); - } - }, - CURRENT_REVISION { - @Override void update(GitRepository repository) { - repository.updateCurrentRevision(); - } - }, - CURRENT_BRANCH { - @Override void update(GitRepository repository) { - repository.updateCurrentBranch(); - } - }, - BRANCHES { - @Override void update(GitRepository repository) { - repository.updateBranchList(); - } - }, - CONFIG { - @Override void update(GitRepository repository) { - repository.updateConfig(); - } - }, - ALL_CURRENT { - @Override void update(GitRepository repository) { - STATE.update(repository); - CURRENT_REVISION.update(repository); - CURRENT_BRANCH.update(repository); - } - }, - ALL { - @Override void update(GitRepository repository) { - ALL_CURRENT.update(repository); - BRANCHES.update(repository); - CONFIG.update(repository); - } - }; - - abstract void update(GitRepository repository); - } - - /** - * Get the GitRepository instance from the {@link GitRepositoryManager}. - * If you need to have an instance of GitRepository for a repository outside the project, use - * {@link #getLightInstance(com.intellij.openapi.vfs.VirtualFile, com.intellij.openapi.project.Project, com.intellij.openapi.Disposable)}. - */ - private GitRepository(@NotNull VirtualFile rootDir, @NotNull Project project, @NotNull Disposable parentDisposable) { - myRootDir = rootDir; - myProject = project; - Disposer.register(parentDisposable, this); - - myGitDir = GitUtil.findGitDir(myRootDir); - assert myGitDir != null : ".git directory wasn't found under " + rootDir.getPresentableUrl(); - - myReader = new GitRepositoryReader(VfsUtil.virtualToIoFile(myGitDir)); - - myUntrackedFilesHolder = new GitUntrackedFilesHolder(this); - Disposer.register(this, myUntrackedFilesHolder); - - myMessageBus = project.getMessageBus(); - myNotifier = new QueueProcessor(new NotificationConsumer(myProject, myMessageBus), myProject.getDisposed()); - update(TrackedTopic.ALL); - } - - /** - * Returns the temporary light instance of GitRepository. - * It lacks functionality of auto-updating GitRepository on Git internal files change, and also stored a stub instance of - * {@link GitUntrackedFilesHolder}. - */ - @NotNull - public static GitRepository getLightInstance(@NotNull VirtualFile root, @NotNull Project project, @NotNull Disposable parentDisposable) { - return new GitRepository(root, project, parentDisposable); - } - - /** - * Returns the full-functional instance of GitRepository - with UntrackedFilesHolder and GitRepositoryUpdater. - * This is used for repositories registered in project, and should be optained via {@link GitRepositoryManager}. - */ - public static GitRepository getFullInstance(@NotNull VirtualFile root, @NotNull Project project, @NotNull Disposable parentDisposable) { - GitRepository repository = new GitRepository(root, project, parentDisposable); - repository.myUntrackedFilesHolder.setupVfsListener(project); - repository.setupUpdater(); - return repository; - } - - private void setupUpdater() { - GitRepositoryUpdater updater = new GitRepositoryUpdater(this); - Disposer.register(this, updater); - } - - @Override - public void dispose() { + enum TrackedTopic { + STATE, + CURRENT_REVISION, + CURRENT_BRANCH, + BRANCHES, + CONFIG, + ALL_CURRENT, + ALL } @NotNull - public VirtualFile getRoot() { - return myRootDir; - } + VirtualFile getRoot(); @NotNull - public VirtualFile getGitDir() { - return myGitDir; - } + VirtualFile getGitDir(); @NotNull - public String getPresentableUrl() { - return getRoot().getPresentableUrl(); - } + String getPresentableUrl(); @NotNull - public Project getProject() { - return myProject; - } + Project getProject(); @NotNull - public GitUntrackedFilesHolder getUntrackedFilesHolder() { - return myUntrackedFilesHolder; - } + GitUntrackedFilesHolder getUntrackedFilesHolder(); @NotNull - public State getState() { - return myState; - } + State getState(); /** * Returns the hash of the revision, which HEAD currently points to. * Returns null only in the case of a fresh repository, when no commit have been made. */ @Nullable - public String getCurrentRevision() { - return myCurrentRevision; - } + String getCurrentRevision(); /** * Returns the current branch of this Git repository. @@ -264,22 +138,13 @@ public final class GitRepository implements Disposable { * Returns null, if the repository is not on a branch and not in the REBASING state. */ @Nullable - public GitBranch getCurrentBranch() { - return myCurrentBranch; - } - - /** - * @return local and remote branches in this repository. - */ - @NotNull - public GitBranchesCollection getBranches() { - return new GitBranchesCollection(myBranches); - } + GitBranch getCurrentBranch(); @NotNull - public GitConfig getConfig() { - return myConfig; - } + GitBranchesCollection getBranches(); + + @NotNull + GitConfig getConfig(); /** * Returns remotes defined in this Git repository. @@ -291,125 +156,26 @@ public final class GitRepository implements Disposable { * @return GitRemotes defined for this repository. */ @NotNull - public Collection getRemotes() { - return myConfig.getRemotes(); - } + Collection getRemotes(); - public boolean isMergeInProgress() { - return getState() == State.MERGING; - } + boolean isMergeInProgress(); - public boolean isRebaseInProgress() { - return getState() == State.REBASING; - } + boolean isRebaseInProgress(); - public boolean isOnBranch() { - return getState() != State.DETACHED && getState() != State.REBASING; - } + boolean isOnBranch(); /** * @return true if current repository is "fresh", i.e. if no commits have been made yet. */ - public boolean isFresh() { - return getCurrentRevision() == null; - } + boolean isFresh(); - public void addListener(GitRepositoryChangeListener listener) { - MessageBusConnection connection = myMessageBus.connect(); - Disposer.register(this, connection); - connection.subscribe(GIT_REPO_CHANGE, listener); - } + void addListener(@NotNull GitRepositoryChangeListener listener); /** * Synchronously updates the GitRepository by reading information from the specified topics. */ - public void update(TrackedTopic... topics) { - for (TrackedTopic topic : topics) { - topic.update(this); - } - notifyListeners(); - } - - private void updateConfig() { - File configFile = new File(VfsUtil.virtualToIoFile(myGitDir), "config"); - myConfig = GitConfig.read(configFile); - } + void update(TrackedTopic... topics); - /** - * Reads current state and notifies listeners about the change. - */ - private void updateState() { - myState = myReader.readState(); - } - - /** - * Reads current revision and notifies listeners about the change. - */ - private void updateCurrentRevision() { - myCurrentRevision = myReader.readCurrentRevision(); - } - - /** - * Reads current branch and notifies listeners about the change. - */ - private void updateCurrentBranch() { - myCurrentBranch = myReader.readCurrentBranch(); - } - - private void updateBranchList() { - myBranches = myReader.readBranches(); - } - - private void notifyListeners() { - myNotifier.add(STUB_OBJECT); // we don't have parameters for listeners - } - - private static class NotificationConsumer implements Consumer { - - private final Project myProject; - private final MessageBus myMessageBus; - - NotificationConsumer(Project project, MessageBus messageBus) { - myProject = project; - myMessageBus = messageBus; - } - - @Override - public void consume(Object o) { - if (!Disposer.isDisposed(myProject)) { - myMessageBus.syncPublisher(GIT_REPO_CHANGE).repositoryChanged(); - } - } - } - - public String toLogString() { - return String.format("GitRepository{myCurrentBranch=%s, myCurrentRevision='%s', myState=%s, myRootDir=%s}", - myCurrentBranch, myCurrentRevision, myState, myRootDir); - } - - @Override - public String toString() { - return getPresentableUrl(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - GitRepository that = (GitRepository)o; - - if (myProject != null ? !myProject.equals(that.myProject) : that.myProject != null) return false; - if (myRootDir != null ? !myRootDir.equals(that.myRootDir) : that.myRootDir != null) return false; - - return true; - } - - @Override - public int hashCode() { - int result = myProject != null ? myProject.hashCode() : 0; - result = 31 * result + (myRootDir != null ? myRootDir.hashCode() : 0); - return result; - } + String toLogString(); } diff --git a/plugins/git4idea/src/git4idea/repo/GitRepositoryImpl.java b/plugins/git4idea/src/git4idea/repo/GitRepositoryImpl.java new file mode 100644 index 000000000000..8982ac8ea3fd --- /dev/null +++ b/plugins/git4idea/src/git4idea/repo/GitRepositoryImpl.java @@ -0,0 +1,314 @@ +/* + * 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.repo; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.Consumer; +import com.intellij.util.concurrency.QueueProcessor; +import com.intellij.util.messages.MessageBus; +import com.intellij.util.messages.MessageBusConnection; +import git4idea.GitBranch; +import git4idea.GitUtil; +import git4idea.branch.GitBranchesCollection; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.util.Collection; + +/** + * @author Kirill Likhodedov + */ +public class GitRepositoryImpl implements GitRepository, Disposable { + + private static final Object STUB_OBJECT = new Object(); + + private final Project myProject; + private final VirtualFile myRootDir; + private final GitRepositoryReader myReader; + private final VirtualFile myGitDir; + private final MessageBus myMessageBus; + private final GitUntrackedFilesHolder myUntrackedFilesHolder; + private final QueueProcessor myNotifier; + + private volatile State myState; + private volatile String myCurrentRevision; + private volatile GitBranch myCurrentBranch; + private volatile GitBranchesCollection myBranches = GitBranchesCollection.EMPTY; + private volatile GitConfig myConfig; + + /** + * Get the GitRepository instance from the {@link GitRepositoryManager}. + * If you need to have an instance of GitRepository for a repository outside the project, use + * {@link #getLightInstance(com.intellij.openapi.vfs.VirtualFile, com.intellij.openapi.project.Project, com.intellij.openapi.Disposable)}. + */ + private GitRepositoryImpl(@NotNull VirtualFile rootDir, @NotNull Project project, @NotNull Disposable parentDisposable) { + myRootDir = rootDir; + myProject = project; + Disposer.register(parentDisposable, this); + + myGitDir = GitUtil.findGitDir(myRootDir); + assert myGitDir != null : ".git directory wasn't found under " + rootDir.getPresentableUrl(); + + myReader = new GitRepositoryReader(VfsUtil.virtualToIoFile(myGitDir)); + + myUntrackedFilesHolder = new GitUntrackedFilesHolder(this); + Disposer.register(this, myUntrackedFilesHolder); + + myMessageBus = project.getMessageBus(); + myNotifier = new QueueProcessor(new NotificationConsumer(myProject, myMessageBus), myProject.getDisposed()); + update(TrackedTopic.ALL); + } + + /** + * Returns the temporary light instance of GitRepository. + * It lacks functionality of auto-updating GitRepository on Git internal files change, and also stored a stub instance of + * {@link GitUntrackedFilesHolder}. + */ + @NotNull + public static GitRepository getLightInstance(@NotNull VirtualFile root, @NotNull Project project, @NotNull Disposable parentDisposable) { + return new GitRepositoryImpl(root, project, parentDisposable); + } + + /** + * Returns the full-functional instance of GitRepository - with UntrackedFilesHolder and GitRepositoryUpdater. + * This is used for repositories registered in project, and should be optained via {@link GitRepositoryManager}. + */ + public static GitRepository getFullInstance(@NotNull VirtualFile root, @NotNull Project project, @NotNull Disposable parentDisposable) { + GitRepositoryImpl repository = new GitRepositoryImpl(root, project, parentDisposable); + repository.myUntrackedFilesHolder.setupVfsListener(project); + repository.setupUpdater(); + return repository; + } + + private void setupUpdater() { + GitRepositoryUpdater updater = new GitRepositoryUpdater(this); + Disposer.register(this, updater); + } + + @Override + public void dispose() { + } + + @Override + @NotNull + public VirtualFile getRoot() { + return myRootDir; + } + + @Override + @NotNull + public VirtualFile getGitDir() { + return myGitDir; + } + + @Override + @NotNull + public String getPresentableUrl() { + return getRoot().getPresentableUrl(); + } + + @Override + @NotNull + public Project getProject() { + return myProject; + } + + @Override + @NotNull + public GitUntrackedFilesHolder getUntrackedFilesHolder() { + return myUntrackedFilesHolder; + } + + @Override + @NotNull + public State getState() { + return myState; + } + + @Override + @Nullable + public String getCurrentRevision() { + return myCurrentRevision; + } + + @Override + @Nullable + public GitBranch getCurrentBranch() { + return myCurrentBranch; + } + + /** + * @return local and remote branches in this repository. + */ + @Override + @NotNull + public GitBranchesCollection getBranches() { + return new GitBranchesCollection(myBranches); + } + + @Override + @NotNull + public GitConfig getConfig() { + return myConfig; + } + + @Override + @NotNull + public Collection getRemotes() { + return myConfig.getRemotes(); + } + + @Override + public boolean isMergeInProgress() { + return getState() == State.MERGING; + } + + @Override + public boolean isRebaseInProgress() { + return getState() == State.REBASING; + } + + @Override + public boolean isOnBranch() { + return getState() != State.DETACHED && getState() != State.REBASING; + } + + @Override + public boolean isFresh() { + return getCurrentRevision() == null; + } + + @Override + public void addListener(@NotNull GitRepositoryChangeListener listener) { + MessageBusConnection connection = myMessageBus.connect(); + Disposer.register(this, connection); + connection.subscribe(GIT_REPO_CHANGE, listener); + } + + @Override + public void update(TrackedTopic... topics) { + for (TrackedTopic topic : topics) { + switch (topic) { + case STATE: updateState(); break; + case CURRENT_REVISION: updateCurrentRevision(); break; + case CURRENT_BRANCH: updateCurrentBranch(); break; + case BRANCHES: updateBranchList(); break; + case CONFIG: updateConfig(); break; + case ALL_CURRENT: + updateState(); + updateCurrentRevision(); + updateCurrentBranch(); + break; + case ALL: + updateState(); + updateCurrentRevision(); + updateCurrentBranch(); + updateBranchList(); + updateConfig(); + break; + } + } + notifyListeners(); + } + + private void updateConfig() { + File configFile = new File(VfsUtil.virtualToIoFile(myGitDir), "config"); + myConfig = GitConfig.read(configFile); + } + + /** + * Reads current state and notifies listeners about the change. + */ + private void updateState() { + myState = myReader.readState(); + } + + /** + * Reads current revision and notifies listeners about the change. + */ + private void updateCurrentRevision() { + myCurrentRevision = myReader.readCurrentRevision(); + } + + /** + * Reads current branch and notifies listeners about the change. + */ + private void updateCurrentBranch() { + myCurrentBranch = myReader.readCurrentBranch(); + } + + private void updateBranchList() { + myBranches = myReader.readBranches(); + } + + private void notifyListeners() { + myNotifier.add(STUB_OBJECT); // we don't have parameters for listeners + } + + private static class NotificationConsumer implements Consumer { + + private final Project myProject; + private final MessageBus myMessageBus; + + NotificationConsumer(Project project, MessageBus messageBus) { + myProject = project; + myMessageBus = messageBus; + } + + @Override + public void consume(Object o) { + if (!Disposer.isDisposed(myProject)) { + myMessageBus.syncPublisher(GIT_REPO_CHANGE).repositoryChanged(); + } + } + } + + @Override + public String toLogString() { + return String.format("GitRepository{myCurrentBranch=%s, myCurrentRevision='%s', myState=%s, myRootDir=%s}", + myCurrentBranch, myCurrentRevision, myState, myRootDir); + } + + @Override + public String toString() { + return getPresentableUrl(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + GitRepositoryImpl that = (GitRepositoryImpl)o; + + if (myProject != null ? !myProject.equals(that.myProject) : that.myProject != null) return false; + if (myRootDir != null ? !myRootDir.equals(that.myRootDir) : that.myRootDir != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = myProject != null ? myProject.hashCode() : 0; + result = 31 * result + (myRootDir != null ? myRootDir.hashCode() : 0); + return result; + } +} diff --git a/plugins/git4idea/src/git4idea/repo/GitRepositoryManagerImpl.java b/plugins/git4idea/src/git4idea/repo/GitRepositoryManagerImpl.java index 8b80d326ca11..362b4c7a74d2 100644 --- a/plugins/git4idea/src/git4idea/repo/GitRepositoryManagerImpl.java +++ b/plugins/git4idea/src/git4idea/repo/GitRepositoryManagerImpl.java @@ -203,7 +203,7 @@ public class GitRepositoryManagerImpl extends AbstractProjectComponent implement } private GitRepository createGitRepository(VirtualFile root) { - GitRepository repository = GitRepository.getFullInstance(root, myProject, this); + GitRepository repository = GitRepositoryImpl.getFullInstance(root, myProject, this); for (GitRepositoryChangeListener listener : myListeners) { repository.addListener(listener); } diff --git a/plugins/git4idea/src/git4idea/roots/GitIntegrationEnabler.java b/plugins/git4idea/src/git4idea/roots/GitIntegrationEnabler.java index da5d6886e619..f602ca2175de 100644 --- a/plugins/git4idea/src/git4idea/roots/GitIntegrationEnabler.java +++ b/plugins/git4idea/src/git4idea/roots/GitIntegrationEnabler.java @@ -79,7 +79,7 @@ public class GitIntegrationEnabler { GitCommandResult result = myGit.init(myProject, projectDir); if (result.success()) { refreshGitDir(projectDir); - notificator.notifySuccess("", "Created Git repository in \n" + projectDir.getPresentableUrl()); + notificator.notifySuccess("", "Created Git repository in " + projectDir.getPresentableUrl()); return true; } else { diff --git a/plugins/git4idea/src/git4idea/stash/GitChangesSaver.java b/plugins/git4idea/src/git4idea/stash/GitChangesSaver.java index 7b1bb953cf38..fddc877d85fe 100644 --- a/plugins/git4idea/src/git4idea/stash/GitChangesSaver.java +++ b/plugins/git4idea/src/git4idea/stash/GitChangesSaver.java @@ -26,6 +26,7 @@ import com.intellij.openapi.vcs.changes.ChangeListManagerEx; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.continuation.ContinuationContext; import git4idea.GitVcs; +import git4idea.commands.Git; import git4idea.config.GitVcsSettings; import git4idea.merge.GitConflictResolver; import org.jetbrains.annotations.NotNull; @@ -46,6 +47,7 @@ public abstract class GitChangesSaver { protected final Project myProject; protected final ChangeListManagerEx myChangeManager; + @NotNull protected final Git myGit; protected final ProgressIndicator myProgressIndicator; protected final String myStashMessage; @@ -60,25 +62,27 @@ public abstract class GitChangesSaver { * Returns an instance of the proper GitChangesSaver depending on the chosen save changes policy. * @return {@link GitStashChangesSaver}, {@link GitShelveChangesSaver} or {@link GitDumbChangesSaver} */ - public static GitChangesSaver getSaver(Project project, ProgressIndicator progressIndicator, String stashMessage) { + public static GitChangesSaver getSaver(Project project, @NotNull Git git, ProgressIndicator progressIndicator, String stashMessage) { final GitVcsSettings settings = GitVcsSettings.getInstance(project); if (settings == null) { - return getDefaultSaver(project, progressIndicator, stashMessage); + return getDefaultSaver(project, git, progressIndicator, stashMessage); } switch (settings.updateChangesPolicy()) { - case STASH: return new GitStashChangesSaver(project, progressIndicator, stashMessage); - case SHELVE: return new GitShelveChangesSaver(project, progressIndicator, stashMessage); + case STASH: return new GitStashChangesSaver(project, git, progressIndicator, stashMessage); + case SHELVE: return new GitShelveChangesSaver(project, git, progressIndicator, stashMessage); } - return getDefaultSaver(project, progressIndicator, stashMessage); + return getDefaultSaver(project, git, progressIndicator, stashMessage); } // In the case of illegal value in the settings or impossibility to get the settings. - private static GitChangesSaver getDefaultSaver(Project project, ProgressIndicator progressIndicator, String stashMessage) { - return new GitStashChangesSaver(project, progressIndicator, stashMessage); + private static GitChangesSaver getDefaultSaver(Project project, @NotNull Git git, ProgressIndicator progressIndicator, + String stashMessage) { + return new GitStashChangesSaver(project, git, progressIndicator, stashMessage); } - protected GitChangesSaver(Project project, ProgressIndicator indicator, String stashMessage) { + protected GitChangesSaver(Project project, @NotNull Git git, ProgressIndicator indicator, String stashMessage) { myProject = project; + myGit = git; myProgressIndicator = indicator; myStashMessage = stashMessage; myChangeManager = (ChangeListManagerEx)ChangeListManagerEx.getInstance(myProject); diff --git a/plugins/git4idea/src/git4idea/stash/GitShelveChangesSaver.java b/plugins/git4idea/src/git4idea/stash/GitShelveChangesSaver.java index 55d2829e6d87..14cd4d97817d 100644 --- a/plugins/git4idea/src/git4idea/stash/GitShelveChangesSaver.java +++ b/plugins/git4idea/src/git4idea/stash/GitShelveChangesSaver.java @@ -26,6 +26,7 @@ import com.intellij.openapi.vcs.changes.shelf.ShelvedChangesViewManager; import com.intellij.openapi.vcs.impl.LocalChangesUnderRoots; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.continuation.ContinuationContext; +import git4idea.commands.Git; import git4idea.i18n.GitBundle; import git4idea.rollback.GitRollbackEnvironment; import org.jetbrains.annotations.NotNull; @@ -41,8 +42,8 @@ public class GitShelveChangesSaver extends GitChangesSaver { private final ShelvedChangesViewManager myShelveViewManager; private ShelvedChangeList myShelvedChangeList; - public GitShelveChangesSaver(Project project, ProgressIndicator indicator, String stashMessage) { - super(project, indicator, stashMessage); + public GitShelveChangesSaver(Project project, Git git, ProgressIndicator indicator, String stashMessage) { + super(project, git, indicator, stashMessage); myShelveManager = ShelveChangesManager.getInstance(myProject); myShelveViewManager = ShelvedChangesViewManager.getInstance(myProject); } diff --git a/plugins/git4idea/src/git4idea/stash/GitStashChangesSaver.java b/plugins/git4idea/src/git4idea/stash/GitStashChangesSaver.java index c81bf8dbc64b..3f42006cd2b7 100644 --- a/plugins/git4idea/src/git4idea/stash/GitStashChangesSaver.java +++ b/plugins/git4idea/src/git4idea/stash/GitStashChangesSaver.java @@ -18,6 +18,7 @@ package git4idea.stash; import com.intellij.notification.Notification; import com.intellij.notification.NotificationListener; import com.intellij.notification.NotificationType; +import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; @@ -31,6 +32,7 @@ import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.continuation.ContinuationContext; import git4idea.GitVcs; +import git4idea.PlatformFacade; import git4idea.commands.*; import git4idea.config.GitVcsSettings; import git4idea.convert.GitFileSeparatorConverter; @@ -54,8 +56,8 @@ public class GitStashChangesSaver extends GitChangesSaver { private static final Logger LOG = Logger.getInstance(GitStashChangesSaver.class); private final Set myStashedRoots = new HashSet(); // save stashed roots to unstash only them - public GitStashChangesSaver(Project project, ProgressIndicator progressIndicator, String stashMessage) { - super(project, progressIndicator, stashMessage); + public GitStashChangesSaver(Project project, @NotNull Git git, ProgressIndicator progressIndicator, String stashMessage) { + super(project, git, progressIndicator, stashMessage); } @Override @@ -85,7 +87,7 @@ public class GitStashChangesSaver extends GitChangesSaver { } } - boolean conflictsResolved = new UnstashConflictResolver(myProject, myStashedRoots, myParams).merge(); + boolean conflictsResolved = new UnstashConflictResolver(myProject, myGit, myStashedRoots, myParams).merge(); if (conflictsResolved) { LOG.info("load: all conflicts resolved, dropping stash in " + myStashedRoots); for (VirtualFile root : conflictedRoots) { @@ -194,8 +196,8 @@ public class GitStashChangesSaver extends GitChangesSaver { private final Set myStashedRoots; - public UnstashConflictResolver(@NotNull Project project, @NotNull Set stashedRoots, @Nullable Params params) { - super(project, stashedRoots, makeParamsOrUse(params)); + public UnstashConflictResolver(@NotNull Project project, @NotNull Git git, @NotNull Set stashedRoots, @Nullable Params params) { + super(project, git, ServiceManager.getService(PlatformFacade.class), stashedRoots, makeParamsOrUse(params)); myStashedRoots = stashedRoots; } diff --git a/plugins/git4idea/src/git4idea/ui/GitUnstashDialog.java b/plugins/git4idea/src/git4idea/ui/GitUnstashDialog.java index f2eefbd1f9f2..0eeb561ce5a8 100644 --- a/plugins/git4idea/src/git4idea/ui/GitUnstashDialog.java +++ b/plugins/git4idea/src/git4idea/ui/GitUnstashDialog.java @@ -18,6 +18,7 @@ package git4idea.ui; import com.intellij.notification.Notification; import com.intellij.notification.NotificationListener; import com.intellij.notification.NotificationType; +import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; @@ -35,6 +36,7 @@ import com.intellij.util.Consumer; import git4idea.GitBranch; import git4idea.GitRevisionNumber; import git4idea.GitVcs; +import git4idea.PlatformFacade; import git4idea.actions.GitShowAllSubmittedFilesAction; import git4idea.commands.*; import git4idea.config.GitVersionSpecialty; @@ -449,7 +451,8 @@ public class GitUnstashDialog extends DialogWrapper { private final StashInfo myStashInfo; public UnstashConflictResolver(Project project, VirtualFile root, StashInfo stashInfo) { - super(project, Collections.singleton(root), makeParams(stashInfo)); + super(project, ServiceManager.getService(Git.class), ServiceManager.getService(PlatformFacade.class), + Collections.singleton(root), makeParams(stashInfo)); myRoot = root; myStashInfo = stashInfo; } diff --git a/plugins/git4idea/src/git4idea/update/GitMergeUpdater.java b/plugins/git4idea/src/git4idea/update/GitMergeUpdater.java index a95da045a147..1c6a940ec92b 100644 --- a/plugins/git4idea/src/git4idea/update/GitMergeUpdater.java +++ b/plugins/git4idea/src/git4idea/update/GitMergeUpdater.java @@ -15,6 +15,7 @@ */ package git4idea.update; +import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; @@ -29,11 +30,11 @@ import com.intellij.openapi.vcs.update.UpdatedFiles; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ui.UIUtil; import git4idea.GitUtil; +import git4idea.PlatformFacade; import git4idea.branch.GitBranchPair; import git4idea.commands.*; import git4idea.merge.GitConflictResolver; import git4idea.merge.GitMerger; -import git4idea.commands.GitMessageWithFilesDetector; import git4idea.util.GitUIUtil; import git4idea.util.UntrackedFilesNotifier; import org.jetbrains.annotations.NotNull; @@ -51,12 +52,12 @@ public class GitMergeUpdater extends GitUpdater { private final ChangeListManager myChangeListManager; - public GitMergeUpdater(Project project, + public GitMergeUpdater(Project project, @NotNull Git git, VirtualFile root, final Map trackedBranches, ProgressIndicator progressIndicator, UpdatedFiles updatedFiles) { - super(project, root, trackedBranches, progressIndicator, updatedFiles); + super(project, git, root, trackedBranches, progressIndicator, updatedFiles); myChangeListManager = ChangeListManager.getInstance(myProject); } @@ -111,7 +112,7 @@ public class GitMergeUpdater extends GitUpdater { if (error == MergeError.CONFLICT) { LOG.info("Conflict detected"); final boolean allMerged = - new MyConflictResolver(myProject, merger, myRoot).merge(); + new MyConflictResolver(myProject, myGit, merger, myRoot).merge(); return allMerged ? GitUpdateResult.SUCCESS : GitUpdateResult.INCOMPLETE; } else if (error == MergeError.LOCAL_CHANGES) { @@ -134,7 +135,8 @@ public class GitMergeUpdater extends GitUpdater { } else if (untrackedFilesWouldBeOverwrittenByMergeDetector.wasMessageDetected()) { LOG.info("handleMergeFailure: untracked files would be overwritten by merge"); - UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject, untrackedFilesWouldBeOverwrittenByMergeDetector.getFiles(), "merge"); + UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject, ServiceManager.getService(myProject, PlatformFacade.class), + untrackedFilesWouldBeOverwrittenByMergeDetector.getFiles(), "merge", null); return GitUpdateResult.ERROR; } else { @@ -278,8 +280,8 @@ public class GitMergeUpdater extends GitUpdater { private final GitMerger myMerger; private final VirtualFile myRoot; - public MyConflictResolver(Project project, GitMerger merger, VirtualFile root) { - super(project, Collections.singleton(root), makeParams()); + public MyConflictResolver(Project project, @NotNull Git git, GitMerger merger, VirtualFile root) { + super(project, git, ServiceManager.getService(git4idea.PlatformFacade.class), Collections.singleton(root), makeParams()); myMerger = merger; myRoot = root; } diff --git a/plugins/git4idea/src/git4idea/update/GitRebaseUpdater.java b/plugins/git4idea/src/git4idea/update/GitRebaseUpdater.java index b8602acdc4b0..32f0418fa0f2 100644 --- a/plugins/git4idea/src/git4idea/update/GitRebaseUpdater.java +++ b/plugins/git4idea/src/git4idea/update/GitRebaseUpdater.java @@ -15,6 +15,7 @@ */ package git4idea.update; +import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; @@ -23,14 +24,15 @@ import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.update.UpdatedFiles; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ui.UIUtil; +import git4idea.PlatformFacade; import git4idea.branch.GitBranchPair; import git4idea.commands.*; import git4idea.merge.GitConflictResolver; -import git4idea.commands.GitMessageWithFilesDetector; import git4idea.rebase.GitRebaseProblemDetector; import git4idea.rebase.GitRebaser; import git4idea.util.GitUIUtil; import git4idea.util.UntrackedFilesNotifier; +import org.jetbrains.annotations.NotNull; import java.util.Collections; import java.util.Map; @@ -44,13 +46,12 @@ public class GitRebaseUpdater extends GitUpdater { private static final Logger LOG = Logger.getInstance(GitRebaseUpdater.class.getName()); private final GitRebaser myRebaser; - public GitRebaseUpdater(Project project, - VirtualFile root, - final Map trackedBranches, + public GitRebaseUpdater(@NotNull Project project, @NotNull Git git, @NotNull VirtualFile root, + @NotNull final Map trackedBranches, ProgressIndicator progressIndicator, UpdatedFiles updatedFiles) { - super(project, root, trackedBranches, progressIndicator, updatedFiles); - myRebaser = new GitRebaser(myProject, myProgressIndicator); + super(project, git, root, trackedBranches, progressIndicator, updatedFiles); + myRebaser = new GitRebaser(myProject, git, myProgressIndicator); } @Override public boolean isSaveNeeded() { @@ -103,11 +104,12 @@ public class GitRebaseUpdater extends GitUpdater { final GitMessageWithFilesDetector untrackedWouldBeOverwrittenDetector) { if (rebaseConflictDetector.isMergeConflict()) { LOG.info("handleRebaseFailure merge conflict"); - final boolean allMerged = new MyConflictResolver(myProject, myRoot, myRebaser).merge(); + final boolean allMerged = new MyConflictResolver(myProject, myGit, myRoot, myRebaser).merge(); return allMerged ? GitUpdateResult.SUCCESS : GitUpdateResult.INCOMPLETE; } else if (untrackedWouldBeOverwrittenDetector.wasMessageDetected()) { LOG.info("handleRebaseFailure: untracked files would be overwritten by checkout"); - UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject, untrackedWouldBeOverwrittenDetector.getFiles(), "rebase"); + UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject, ServiceManager.getService(myProject, PlatformFacade.class), + untrackedWouldBeOverwrittenDetector.getFiles(), "rebase", null); return GitUpdateResult.ERROR; } else { LOG.info("handleRebaseFailure error " + pullHandler.errors()); @@ -156,8 +158,8 @@ public class GitRebaseUpdater extends GitUpdater { private final GitRebaser myRebaser; private final VirtualFile myRoot; - public MyConflictResolver(Project project, VirtualFile root, GitRebaser rebaser) { - super(project, Collections.singleton(root), makeParams()); + public MyConflictResolver(Project project, @NotNull Git git, VirtualFile root, GitRebaser rebaser) { + super(project, git, ServiceManager.getService(PlatformFacade.class), Collections.singleton(root), makeParams()); myRebaser = rebaser; myRoot = root; } diff --git a/plugins/git4idea/src/git4idea/update/GitUpdateProcess.java b/plugins/git4idea/src/git4idea/update/GitUpdateProcess.java index a270dbfe7e3c..e1390c294642 100644 --- a/plugins/git4idea/src/git4idea/update/GitUpdateProcess.java +++ b/plugins/git4idea/src/git4idea/update/GitUpdateProcess.java @@ -16,6 +16,7 @@ package git4idea.update; import com.intellij.notification.NotificationType; +import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; @@ -30,6 +31,7 @@ import com.intellij.util.text.DateFormatUtil; import git4idea.GitBranch; import git4idea.GitVcs; import git4idea.branch.GitBranchPair; +import git4idea.commands.Git; import git4idea.merge.GitConflictResolver; import git4idea.merge.GitMergeCommittingConflictResolver; import git4idea.merge.GitMerger; @@ -52,6 +54,7 @@ public class GitUpdateProcess { private final Project myProject; private final GitVcs myVcs; + @NotNull private final Git myGit; private final Set myRoots; private final UpdatedFiles myUpdatedFiles; private final ProgressIndicator myProgressIndicator; @@ -62,7 +65,7 @@ public class GitUpdateProcess { private boolean myResult; private final Map myUpdaters; private final Collection myRootsToSave; - + public enum UpdateMethod { MERGE, REBASE, @@ -75,10 +78,11 @@ public class GitUpdateProcess { myProject = project; myRoots = roots; myVcs = GitVcs.getInstance(project); + myGit = ServiceManager.getService(Git.class); myUpdatedFiles = updatedFiles; myProgressIndicator = progressIndicator; myMerger = new GitMerger(myProject); - mySaver = GitChangesSaver.getSaver(myProject, myProgressIndicator, + mySaver = GitChangesSaver.getSaver(myProject, myGit, myProgressIndicator, "Uncommitted changes before update operation at " + DateFormatUtil.formatDateTime(Clock.getTime())); myUpdaters = new HashMap(); myRootsToSave = new HashSet(1); @@ -134,11 +138,11 @@ public class GitUpdateProcess { for (VirtualFile root : myRoots) { final GitUpdater updater; if (updateMethod == UpdateMethod.MERGE) { - updater = new GitMergeUpdater(myProject, root, myTrackedBranches, myProgressIndicator, myUpdatedFiles); + updater = new GitMergeUpdater(myProject, myGit, root, myTrackedBranches, myProgressIndicator, myUpdatedFiles); } else if (updateMethod == UpdateMethod.REBASE) { - updater = new GitRebaseUpdater(myProject, root, myTrackedBranches, myProgressIndicator, myUpdatedFiles); + updater = new GitRebaseUpdater(myProject, myGit, root, myTrackedBranches, myProgressIndicator, myUpdatedFiles); } else { - updater = GitUpdater.getUpdater(myProject, myTrackedBranches, root, myProgressIndicator, myUpdatedFiles); + updater = GitUpdater.getUpdater(myProject, myGit, myTrackedBranches, root, myProgressIndicator, myUpdatedFiles); } if (updater.isUpdateNeeded()) { @@ -301,7 +305,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 GitMergeCommittingConflictResolver(myProject, myMerger, mergingRoots, params, false).merge(); + return !new GitMergeCommittingConflictResolver(myProject, myGit, myMerger, mergingRoots, params, false).merge(); } /** @@ -312,7 +316,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 GitMergeCommittingConflictResolver(myProject, myMerger, myRoots, params, false).merge(); + return !new GitMergeCommittingConflictResolver(myProject, myGit, myMerger, myRoots, params, false).merge(); } /** @@ -320,7 +324,7 @@ public class GitUpdateProcess { * @return true if rebase is in progress, which means that update can't continue. */ private boolean checkRebaseInProgress() { - final GitRebaser rebaser = new GitRebaser(myProject, myProgressIndicator); + final GitRebaser rebaser = new GitRebaser(myProject, myGit, myProgressIndicator); final Collection rebasingRoots = rebaser.getRebasingRoots(); if (rebasingRoots.isEmpty()) { return false; @@ -332,7 +336,7 @@ public class GitUpdateProcess { params.setMergeDescription("You have unfinished rebase process. These conflicts must be resolved before update."); params.setErrorNotificationAdditionalDescription("Then you may continue rebase.
You also may abort rebase to restore the original branch and stop rebasing."); params.setReverse(true); - return !new GitConflictResolver(myProject, rebasingRoots, params) { + return !new GitConflictResolver(myProject, myGit, ServiceManager.getService(git4idea.PlatformFacade.class), rebasingRoots, params) { @Override protected boolean proceedIfNothingToMerge() { return rebaser.continueRebase(rebasingRoots); } diff --git a/plugins/git4idea/src/git4idea/update/GitUpdater.java b/plugins/git4idea/src/git4idea/update/GitUpdater.java index d5ef3ce7923e..1735a3c58376 100644 --- a/plugins/git4idea/src/git4idea/update/GitUpdater.java +++ b/plugins/git4idea/src/git4idea/update/GitUpdater.java @@ -26,6 +26,7 @@ import git4idea.GitBranch; import git4idea.GitRevisionNumber; import git4idea.GitVcs; import git4idea.branch.GitBranchPair; +import git4idea.commands.Git; import git4idea.commands.GitCommand; import git4idea.commands.GitSimpleHandler; import git4idea.config.GitConfigUtil; @@ -44,7 +45,8 @@ import java.util.Map; public abstract class GitUpdater { private static final Logger LOG = Logger.getInstance(GitUpdater.class); - protected final @NotNull Project myProject; + @NotNull protected final Project myProject; + @NotNull protected final Git myGit; protected final @NotNull VirtualFile myRoot; protected final @NotNull Map myTrackedBranches; protected final @NotNull ProgressIndicator myProgressIndicator; @@ -54,9 +56,11 @@ public abstract class GitUpdater { protected GitRevisionNumber myBefore; // The revision that was before update - protected GitUpdater(@NotNull Project project, @NotNull VirtualFile root, @NotNull Map trackedBranches, - @NotNull ProgressIndicator progressIndicator, @NotNull UpdatedFiles updatedFiles) { + protected GitUpdater(@NotNull Project project, @NotNull Git git, @NotNull VirtualFile root, + @NotNull Map trackedBranches, @NotNull ProgressIndicator progressIndicator, + @NotNull UpdatedFiles updatedFiles) { myProject = project; + myGit = git; myRoot = root; myTrackedBranches = trackedBranches; myProgressIndicator = progressIndicator; @@ -70,39 +74,39 @@ public abstract class GitUpdater { * @return {@link GitMergeUpdater} or {@link GitRebaseUpdater}. */ @NotNull - public static GitUpdater getUpdater(@NotNull Project project, @NotNull Map trackedBranches, + public static GitUpdater getUpdater(@NotNull Project project, @NotNull Git git, @NotNull Map trackedBranches, @NotNull VirtualFile root, @NotNull ProgressIndicator progressIndicator, @NotNull UpdatedFiles updatedFiles) { final GitVcsSettings settings = GitVcsSettings.getInstance(project); if (settings == null) { - return getDefaultUpdaterForBranch(project, root, trackedBranches, progressIndicator, updatedFiles); + return getDefaultUpdaterForBranch(project, git, root, trackedBranches, progressIndicator, updatedFiles); } switch (settings.getUpdateType()) { case REBASE: - return new GitRebaseUpdater(project, root, trackedBranches, progressIndicator, updatedFiles); + return new GitRebaseUpdater(project, git, root, trackedBranches, progressIndicator, updatedFiles); case MERGE: - return new GitMergeUpdater(project, root, trackedBranches, progressIndicator, updatedFiles); + return new GitMergeUpdater(project, git, root, trackedBranches, progressIndicator, updatedFiles); case BRANCH_DEFAULT: // use default for the branch - return getDefaultUpdaterForBranch(project, root, trackedBranches, progressIndicator, updatedFiles); + return getDefaultUpdaterForBranch(project, git, root, trackedBranches, progressIndicator, updatedFiles); } - return getDefaultUpdaterForBranch(project, root, trackedBranches, progressIndicator, updatedFiles); + return getDefaultUpdaterForBranch(project, git, root, trackedBranches, progressIndicator, updatedFiles); } @NotNull - private static GitUpdater getDefaultUpdaterForBranch(@NotNull Project project, @NotNull VirtualFile root, + private static GitUpdater getDefaultUpdaterForBranch(@NotNull Project project, @NotNull Git git, @NotNull VirtualFile root, @NotNull Map trackedBranches, @NotNull ProgressIndicator progressIndicator, @NotNull UpdatedFiles updatedFiles) { try { final GitBranch branchName = GitBranch.current(project, root); final String rebase = GitConfigUtil.getValue(project, root, "branch." + branchName + ".rebase"); if (rebase != null && rebase.equalsIgnoreCase("true")) { - return new GitRebaseUpdater(project, root, trackedBranches, progressIndicator, updatedFiles); + return new GitRebaseUpdater(project, git, root, trackedBranches, progressIndicator, updatedFiles); } } catch (VcsException e) { LOG.info("getDefaultUpdaterForBranch branch", e); } - return new GitMergeUpdater(project, root, trackedBranches, progressIndicator, updatedFiles); + return new GitMergeUpdater(project, git, root, trackedBranches, progressIndicator, updatedFiles); } @NotNull diff --git a/plugins/git4idea/src/git4idea/util/GitPreservingProcess.java b/plugins/git4idea/src/git4idea/util/GitPreservingProcess.java index 8482f4ad01cd..73981378561b 100644 --- a/plugins/git4idea/src/git4idea/util/GitPreservingProcess.java +++ b/plugins/git4idea/src/git4idea/util/GitPreservingProcess.java @@ -28,6 +28,7 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.text.DateFormatUtil; import git4idea.GitUtil; import git4idea.Notificator; +import git4idea.commands.Git; import git4idea.merge.GitConflictResolver; import git4idea.repo.GitRepository; import git4idea.stash.GitStashChangesSaver; @@ -49,6 +50,7 @@ public class GitPreservingProcess { private static final Logger LOG = Logger.getInstance(GitPreservingProcess.class); @NotNull private final Project myProject; + @NotNull private final Git myGit; @NotNull private final Collection myRepositories; @NotNull private final String myOperationTitle; @NotNull private final String myDestinationName; @@ -61,10 +63,11 @@ public class GitPreservingProcess { private boolean myLoaded; private final Object LOAD_LOCK = new Object(); - public GitPreservingProcess(@NotNull Project project, @NotNull Collection repositories, + public GitPreservingProcess(@NotNull Project project, @NotNull Git git, @NotNull Collection repositories, @NotNull String operationTitle, @NotNull String destinationName, @NotNull ProgressIndicator indicator, @NotNull Runnable operation) { myProject = project; + myGit = git; myRepositories = repositories; myOperationTitle = operationTitle; myDestinationName = destinationName; @@ -110,7 +113,7 @@ public class GitPreservingProcess { * Configures the saver, actually notifications and texts in the GitConflictResolver used inside. */ private GitStashChangesSaver configureSaver() { - GitStashChangesSaver saver = new GitStashChangesSaver(myProject, myProgressIndicator, myStashMessage); + GitStashChangesSaver saver = new GitStashChangesSaver(myProject, myGit, myProgressIndicator, myStashMessage); MergeDialogCustomizer mergeDialogCustomizer = new MergeDialogCustomizer() { @Override public String getMultipleFileMergeDescription(Collection files) { diff --git a/plugins/git4idea/src/git4idea/util/UntrackedFilesNotifier.java b/plugins/git4idea/src/git4idea/util/UntrackedFilesNotifier.java index 800d02c2b69d..1d27df76e5a7 100644 --- a/plugins/git4idea/src/git4idea/util/UntrackedFilesNotifier.java +++ b/plugins/git4idea/src/git4idea/util/UntrackedFilesNotifier.java @@ -17,14 +17,13 @@ package git4idea.util; import com.intellij.notification.Notification; import com.intellij.notification.NotificationListener; -import com.intellij.notification.NotificationType; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.changes.ui.SelectFilesDialog; import com.intellij.openapi.vfs.VirtualFile; -import git4idea.GitVcs; -import git4idea.Notificator; +import git4idea.PlatformFacade; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.event.HyperlinkEvent; @@ -42,23 +41,31 @@ public class UntrackedFilesNotifier { /** * Displays notification about {@code untracked files would be overwritten by checkout} error. * Clicking on the link in the notification opens a simple dialog with the list of these files. - * @param operation the name of the Git operation that caused the error: {@code rebase, merge, checkout}. + * @param operation the name of the Git operation that caused the error: {@code rebase, merge, checkout}. + * @param description the content of the notification or null if the deafult content is to be used. */ - public static void notifyUntrackedFilesOverwrittenBy(final @NotNull Project project, final @NotNull Collection untrackedFiles, @NotNull final String operation) { + public static void notifyUntrackedFilesOverwrittenBy(@NotNull final Project project, @NotNull PlatformFacade platformFacade, + @NotNull final Collection untrackedFiles, + @NotNull final String operation, @Nullable String description) { final String notificationTitle = StringUtil.capitalize(operation) + " error"; - final String notificationDesc = createUntrackedFilesOverwrittenDescription(operation, false); + final String notificationDesc = description == null ? createUntrackedFilesOverwrittenDescription(operation, false) : description; final String dialogDesc = createUntrackedFilesOverwrittenDescription(operation, true); - Notificator.getInstance(project).notify(GitVcs.IMPORTANT_ERROR_NOTIFICATION, notificationTitle, notificationDesc, NotificationType.ERROR, new NotificationListener() { - @Override public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) { - SelectFilesDialog dlg = new SelectFilesDialog(project, new ArrayList(untrackedFiles), - StringUtil.stripHtml(dialogDesc, true), null, false, false) { - @Override protected Action[] createActions() { - return new Action[]{getOKAction()}; - } - }; - dlg.setTitle("Untracked Files Preventing " + StringUtil.capitalize(operation)); - dlg.show(); + platformFacade.getNotificator(project).notifyError(notificationTitle, notificationDesc, + new NotificationListener() { + @Override + public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) { + if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { + SelectFilesDialog dlg = new SelectFilesDialog(project, new ArrayList(untrackedFiles), + StringUtil.stripHtml(dialogDesc, true), null, false, false) { + @Override + protected Action[] createActions() { + return new Action[]{getOKAction()}; + } + }; + dlg.setTitle("Untracked Files Preventing " + StringUtil.capitalize(operation)); + dlg.show(); + } } }); } @@ -71,7 +78,7 @@ public class UntrackedFilesNotifier { notificationDesc = "These" + description1 + "
" + description2; } else { - notificationDesc = "Some" + description1 + "
" + description2 + " View them"; + notificationDesc = "Some" + description1 + "
" + description2 + " View them"; } return notificationDesc; } diff --git a/plugins/git4idea/tests/git4idea/cherrypick/GitAutoCommittingCherryPickTest.groovy b/plugins/git4idea/tests/git4idea/cherrypick/GitAutoCommittingCherryPickTest.groovy new file mode 100644 index 000000000000..aed9a9b0f8e7 --- /dev/null +++ b/plugins/git4idea/tests/git4idea/cherrypick/GitAutoCommittingCherryPickTest.groovy @@ -0,0 +1,251 @@ +/* + * 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.cherrypick + +import com.intellij.notification.NotificationType +import git4idea.history.browser.CherryPicker +import git4idea.history.browser.GitCommit +import org.junit.Before +import org.junit.Test + +import static git4idea.test.MockGit.* +import static git4idea.test.MockGit.OperationName.CHERRY_PICK +import static git4idea.test.MockGit.OperationName.GET_UNMERGED_FILES +import static junit.framework.Assert.assertTrue + +/** + * Cherry-pick of one or multiple commits with "commit at once" option enabled. + * + * @author Kirill Likhodedov + */ +class GitAutoCommittingCherryPickTest extends GitCherryPickTest { + + public static final String EMPTY_CHERRY_PICK = """ +# On branch master +# Your branch is ahead of 'origin/master' by 11 commits. +# +# Untracked files: +# (use "git add ..." to include in what will be committed) +# +#\t.idea/ +#\tlocal_staged.patch +#\tout/ +nothing added to commit but untracked files present (use "git add" to track) +The previous cherry-pick is now empty, possibly due to conflict resolution. +If you wish to commit it anyway, use: + + git commit --allow-empty + +Otherwise, please use 'git reset' +""" + + @Before + void setUp() { + super.setUp() + myCherryPicker = new CherryPicker(myProject, myGit, myPlatformFacade, true) + } + + @Test + void "clean tree, no conflicts, then commit & notify, no new changelists"() { + GitCommit commit = commit() + + myGit.registerOperationExecutors(new SuccessfulCherryPickExecutor(myRepository, commit)) + invokeCherryPick(commit) + + assertHeadCommit(commit) + assertOnlyDefaultChangelist() + assertNotificationShown("Cherry-pick successful", commitDetails(commit), NotificationType.INFORMATION) + } + + @Test + void "dirty tree, conflicting with commit, then show error"() { + myGit.registerOperationExecutors(new SimpleErrorOperationExecutor(CHERRY_PICK, LOCAL_CHANGES_OVERWRITTEN_BY_CHERRY_PICK)) + + def commit = commit() + invokeCherryPick(commit) + + assertNotCherryPicked() + assertOnlyDefaultChangelist() + assertNotificationShown("Cherry-pick failed", + """ + ${commitDetails(commit)}
+ Your local changes would be overwritten by cherry-pick.
+ Commit your changes or stash them to proceed. + """, + NotificationType.ERROR) + } + + @Test + void "untracked files, conflicting with commit, then show error"() { + myGit.registerOperationExecutors(new SimpleErrorOperationExecutor(CHERRY_PICK, UNTRACKED_FILES_OVERWRITTEN_BY_CHERRY_PICK)) + + def commit = commit() + invokeCherryPick(commit) + + assertNotCherryPicked() + assertOnlyDefaultChangelist() + assertNotificationShown("Cherry-pick error", + """ + ${commitDetails(commit)}
+ Some untracked working tree files would be overwritten by cherry-pick.
+ Please move, remove or add them before you can cherry-pick. View them + """, + NotificationType.ERROR) + } + + @Test + void "conflict, merge dialog, not all merged, then new & active changelist, notification"() { + prepareConflict() + myGit.registerOperationExecutors(new SimpleSuccessOperationExecutor(GET_UNMERGED_FILES, UNMERGED_FILE)) + GitCommit commit = commit() + invokeCherryPick(commit) + assertMergeDialogShown() + assertChangeLists([DEFAULT, newCommitMessage(commit)], newCommitMessage(commit)) + assertNotificationShown "Cherry-picked with conflicts", + """ + ${commitDetails(commit)}
+ Unresolved conflicts remain in the working tree. Resolve them. + """, + NotificationType.WARNING + } + + @Test + void "conflict, merge completed, then commit dialog"() { + prepareConflict() + GitCommit commit = commit() + + OKCommitDialogHandler handler = new OKCommitDialogHandler(myRepository) + myVcsHelper.registerHandler(handler) + + invokeCherryPick(commit) + assertMergeDialogShown() + assertTrue "Commit dialog was not shown", handler.wasCommitDialogShown() + } + + @Test + void "conflict, merge finished, commit succeeded, no new changelists"() { + prepareConflict() + + OKCommitDialogHandler handler = new OKCommitDialogHandler(myRepository) + myVcsHelper.registerHandler(handler) + + GitCommit commit = commit() + invokeCherryPick(commit) + assertMergeDialogShown() + assertHeadCommit(commit) + assertTrue "Commit dialog was not shown", handler.wasCommitDialogShown() + assertOnlyDefaultChangelist() + } + + @Test + void "conflict, merge ok, commit cancelled, then new & active changelist"() { + prepareConflict() + + CancelCommitDialogHandler handler = new CancelCommitDialogHandler() + myVcsHelper.registerHandler(handler) + + GitCommit commit = commit() + invokeCherryPick(commit) + assertMergeDialogShown() + assertTrue "Commit dialog was not shown", handler.wasCommitDialogShown() + assertChangeLists([DEFAULT, newCommitMessage(commit)], newCommitMessage(commit)) + } + + @Test + void "2 commits, no problems, then commit all & notify"() { + GitCommit commit1 = commit("First commit to cherry-pick") + GitCommit commit2 = commit("Second commit to cherry-pick") + myGit.registerOperationExecutors(new SuccessfulCherryPickExecutor(myRepository, commit1), + new SuccessfulCherryPickExecutor(myRepository, commit2)) + + invokeCherryPick([commit1, commit2]) + assertLastCommits commit2, commit1 + assertNotificationShown("Cherry-pick successful", notificationContent(commit1, commit2), NotificationType.INFORMATION) + } + + @Test + void "3 commits, 2nd cherry-pick conflicts with local, then 1st success, 2nd stop & error"() { + GitCommit commit1 = commit("First") + GitCommit commit2 = commit("Second") + GitCommit commit3 = commit("Third") + + myGit.registerOperationExecutors(new SuccessfulCherryPickExecutor(myRepository, commit1), + new SimpleErrorOperationExecutor(CHERRY_PICK, LOCAL_CHANGES_OVERWRITTEN_BY_CHERRY_PICK), + new SuccessfulCherryPickExecutor(myRepository, commit3)) + + invokeCherryPick([commit1, commit2, commit3]) + + assertHeadCommit(commit1) + assertNotificationShown("Cherry-pick failed", + """ + ${commitDetails(commit2)}
+ Your local changes would be overwritten by cherry-pick.
+ Commit your changes or stash them to proceed. +
+ However cherry-pick succeeded for the following commit:
+ ${commitDetails(commit1)} + """, NotificationType.ERROR) + } + + @Test + void "3 commits, 2nd conflicts with committed, then 1st success, on 2nd show merge dialog"() { + GitCommit commit1 = commit("First") + GitCommit commit2 = commit("Second") + GitCommit commit3 = commit("Third") + + myGit.registerOperationExecutors(new SuccessfulCherryPickExecutor(myRepository, commit1)) + prepareConflict() + myGit.registerOperationExecutors(new SuccessfulCherryPickExecutor(myRepository, commit3)) + + OKCommitDialogHandler handler = new OKCommitDialogHandler(myRepository) + myVcsHelper.registerHandler(handler) + invokeCherryPick([commit1, commit2, commit3]) + + assertMergeDialogShown() + assertTrue "Commit dialog was not shown", handler.wasCommitDialogShown() + assertLastCommits commit3, commit2, commit1 + } + +// @Test + void "Notify if changes have already been applied"() { + // Inspired by IDEA-73548 + myGit.registerOperationExecutors(new SimpleErrorOperationExecutor(CHERRY_PICK, EMPTY_CHERRY_PICK)) + + GitCommit commit = commit() + invokeCherryPick(commit) + + assertNotCherryPicked() + assertNotificationShown("Nothing to cherry-pick", "All changes from ${commitDetails(commit)} have already been applied", + NotificationType.WARNING) + } + +// @Test + void "1st successful, 2nd empty (all applied), then compound notification"() { + // Inspired by IDEA-73548 + GitCommit commit1 = commit() + GitCommit commit2 = commit() + myGit.registerOperationExecutors(new SuccessfulCherryPickExecutor(myRepository, commit1), + new SimpleErrorOperationExecutor(CHERRY_PICK, EMPTY_CHERRY_PICK)) + + invokeCherryPick([ commit1, commit2 ]) + + assertHeadCommit(commit1) + assertNotificationShown("Cherry-picked with problems", +"""Successfully cherry-picked ${commitDetails(commit1)}
+Not cherry-picked ${commitDetails(commit2)} - all changes have already been applied""", NotificationType.WARNING) + } + +} diff --git a/plugins/git4idea/tests/git4idea/cherrypick/GitCherryPickTest.groovy b/plugins/git4idea/tests/git4idea/cherrypick/GitCherryPickTest.groovy new file mode 100644 index 000000000000..832b5adabd83 --- /dev/null +++ b/plugins/git4idea/tests/git4idea/cherrypick/GitCherryPickTest.groovy @@ -0,0 +1,199 @@ +/* + * 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.cherrypick + +import com.intellij.mock.MockVirtualFile +import com.intellij.openapi.vcs.FilePathImpl +import com.intellij.openapi.vcs.changes.Change +import com.intellij.openapi.vcs.changes.ChangeListManager +import com.intellij.openapi.vcs.changes.LocalChangeList +import com.intellij.openapi.vcs.history.VcsRevisionNumber +import com.intellij.testFramework.vcs.MockChangeListManager +import com.intellij.testFramework.vcs.MockContentRevision +import git4idea.history.browser.CherryPicker +import git4idea.history.browser.GitCommit +import git4idea.history.browser.SHAHash +import git4idea.history.wholeTree.AbstractHash +import git4idea.repo.GitRepository +import git4idea.test.GitFastTest +import git4idea.test.GitLightRepository +import git4idea.test.MockGit +import sun.security.provider.SHA + +import static git4idea.test.MockGit.OperationName.CHERRY_PICK +import static junit.framework.Assert.assertEquals +import static junit.framework.Assert.assertTrue +import static git4idea.test.MockGit.commitMessageForCherryPick +import git4idea.test.MockVcsHelper + +/** + * Common parent for all tests on cherry-pick + * + * @author Kirill Likhodedov + */ +class GitCherryPickTest extends GitFastTest { + + public static final String DEFAULT = MockChangeListManager.DEFAULT_CHANGE_LIST_NAME; + public static final String UNMERGED_FILE = """ +100644 d87b28d6fd6e97620603e64ce70fc2f24535ec28 1\ttest.txt +100644 7b50450f5deb7cce3b5ce92ba866f1af6e58c3c6 2\ttest.txt +100644 a784477cdd0437a84751c52f72b971503deb48cb 3\ttest.txt +""" + public static final String CHERRY_PICK_CONFLICT = """ +error: could not apply ec15d8e... message +hint: after resolving the conflicts, mark the corrected paths +hint: with 'git add ' or 'git rm ' +hint: and commit the result with 'git commit' +""" + CherryPicker myCherryPicker + GitLightRepository myRepository + GitLightRepository.Commit myInitialCommit + + static final LOCAL_CHANGES_OVERWRITTEN_BY_CHERRY_PICK = + """ + error: Your local changes to the following files would be overwritten by merge: + \ttest.txt + Please, commit your changes or stash them before you can merge. + Aborting + """; + + static final UNTRACKED_FILES_OVERWRITTEN_BY_CHERRY_PICK = + """ + error: The following untracked working tree files would be overwritten by merge: + \tcp.txt + Please move or remove them before you can merge. + Aborting + """ + + void setUp() { + super.setUp() + + myRepository = new GitLightRepository() + myRepositoryManager.add(myRepository) + myInitialCommit = myRepository.commit("initial") + } + + GitCommit commit(String commitMessage = "plain commit") { + AbstractHash hash = AbstractHash.create(Integer.toHexString(new SHA().hashCode())) + List changes = new ArrayList(); + changes.add(new Change(null, new MockContentRevision(new FilePathImpl(new MockVirtualFile("name")), VcsRevisionNumber.NULL))); + new GitCommit(hash, SHAHash.emulate(hash), "John Smith", null, null, commitMessage, commitMessage, null, null, null, null, null, null, + null, changes, 0) + } + + void assertOnlyDefaultChangelist() { + assertChangeLists( [DEFAULT], DEFAULT) + } + + void invokeCherryPick(GitCommit commit) { + invokeCherryPick([commit]) + } + + void invokeCherryPick(List commits) { + myCherryPicker.cherryPick(Collections.> singletonMap(myRepository, commits)) + } + + void assertHeadCommit(GitCommit commit) { + assertEquals "Wrong commit at the HEAD", commitMessageForCherryPick(commit), myRepository.head.commitMessage + } + + void assertLastCommits(GitCommit... commits) { + GitLightRepository.Commit current = myRepository.head + int level = 0; + for (GitCommit commit : commits) { + assertEquals "Wrong commit at level $level", commitMessageForCherryPick(commit), current.commitMessage + current = current.parent + level++; + } + } + + void assertChangeLists(Collection changeLists, String activeChangelist) { + ChangeListManager changeListManager = myPlatformFacade.getChangeListManager(myProject) + List lists = changeListManager.changeLists + Collection listNames = lists.collect { it.name } + assertEquals "Change lists are different", changeLists.toSet(), listNames.toSet() + assertEquals "Wrong active changelist", activeChangelist, changeListManager.defaultChangeList.name + } + + String commitDetails(GitCommit commit) { + "${commit.shortHash.toString()} \"${commit.subject}\"" + } + + String notificationContent(GitCommit... commits) { + commits.collect { commitDetails(it) }.join("
") + } + + void assertNotCherryPicked() { + // 1. assert not committed (i.e. git cherry-pick was not performed) + assertNothingCommitted() + // 2. assert working tree not changed (i.e. git cherry-pick -n was not performed either) + assertTrue myPlatformFacade.getChangeListManager(myProject).getAllChanges().isEmpty() + } + + void assertNothingCommitted() { + assertEquals(myInitialCommit, myRepository.head) + } + + void prepareConflict() { + myGit.registerOperationExecutors(new MockGit.SimpleErrorOperationExecutor(CHERRY_PICK, CHERRY_PICK_CONFLICT), + new MockGit.SimpleSuccessOperationExecutor(MockGit.OperationName.GET_UNMERGED_FILES, UNMERGED_FILE)) + } + + void assertMergeDialogShown() { + assertTrue "Merge dialog was not shown", myVcsHelper.mergeDialogWasShown() + } + + String newCommitMessage(GitCommit commit) { + "${commit.description}\n(cherry-picked from ${commit.hash.value})" + } + + protected static class OKCommitDialogHandler implements MockVcsHelper.CommitHandler { + + private final GitLightRepository myRepository; + boolean myCommitDialogShown + + OKCommitDialogHandler(GitLightRepository repository) { + myRepository = repository + } + + @Override + boolean commit(String commitMessage) { + myCommitDialogShown = true; + myRepository.commit(commitMessage) // answering OK in the dialog => committing + return true; + } + + boolean wasCommitDialogShown() { + myCommitDialogShown + } + } + + protected static class CancelCommitDialogHandler implements MockVcsHelper.CommitHandler { + + boolean myCommitDialogShown + + @Override + boolean commit(String commitMessage) { + myCommitDialogShown = true; + return false; + } + + boolean wasCommitDialogShown() { + myCommitDialogShown + } + } + +} diff --git a/plugins/git4idea/tests/git4idea/cherrypick/GitNotCommittingCherryPickTest.groovy b/plugins/git4idea/tests/git4idea/cherrypick/GitNotCommittingCherryPickTest.groovy new file mode 100644 index 000000000000..556bcaa29a68 --- /dev/null +++ b/plugins/git4idea/tests/git4idea/cherrypick/GitNotCommittingCherryPickTest.groovy @@ -0,0 +1,152 @@ +/* + * 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.cherrypick + +import com.intellij.notification.NotificationType +import git4idea.history.browser.CherryPicker +import git4idea.history.browser.GitCommit +import git4idea.test.MockGit +import git4idea.test.MockVcsHelper +import org.junit.Before +import org.junit.Test + +import static git4idea.test.MockGit.OperationName.CHERRY_PICK +import static junit.framework.Assert.* +import git4idea.test.GitLightRepository + +/** + * Tests for {@link CherryPicker}, when the "auto-commit on cherry-pick" option is deselected. + * Most situations are equal or similar, so the majority of cherry pick tests are located in {@link GitAutoCommittingCherryPickTest}. + * + * @author Kirill Likhodedov + */ +class GitNotCommittingCherryPickTest extends GitCherryPickTest { + + @Before + void setUp() { + super.setUp() + myCherryPicker = new CherryPicker(myProject, myGit, myPlatformFacade, false) + } + + @Test + void "clean tree, no conflicts, then show commit dialog, commit on ok"() { + GitCommit commit = commit() + + myGit.registerOperationExecutors(new MockGit.SuccessfulCherryPickExecutor(myRepository, commit)) + OKCommitDialogHandler handler = new OKCommitDialogHandler(myRepository) + myVcsHelper.registerHandler(handler) + + invokeCherryPick(commit) + + assertHeadCommit(commit) + assertOnlyDefaultChangelist() + assertTrue "Commit dialog was not shown", handler.wasCommitDialogShown() + // notification is shown from the successful commit, can't check from here + } + + @Test + void "dirty tree, conflicting with commit, then show error"() { + myGit.registerOperationExecutors(new MockGit.SimpleErrorOperationExecutor(CHERRY_PICK, LOCAL_CHANGES_OVERWRITTEN_BY_CHERRY_PICK)) + + def commit = commit() + OKCommitDialogHandler handler = new OKCommitDialogHandler(myRepository) + myVcsHelper.registerHandler(handler) + + invokeCherryPick(commit) + + assertNotCherryPicked() + assertFalse "Commit dialog was shown, but it shouldn't", handler.wasCommitDialogShown() + assertOnlyDefaultChangelist() + assertNotificationShown("Cherry-pick failed", + """ + ${commitDetails(commit)}
+ Your local changes would be overwritten by cherry-pick.
+ Commit your changes or stash them to proceed. + """, + NotificationType.ERROR) + } + + @Test + void "conflict, merge ok, commit cancelled, then new & active changelist"() { + prepareConflict() + + CancelCommitDialogHandler handler = new CancelCommitDialogHandler() + myVcsHelper.registerHandler(handler) + + GitCommit commit = commit() + invokeCherryPick(commit) + assertMergeDialogShown() + assertTrue "Commit dialog was not shown", handler.wasCommitDialogShown() + assertChangeLists([DEFAULT, newCommitMessage(commit)], newCommitMessage(commit)) + } + + @Test + void "2 simple commits in a row, then 2 commit dialogs in a row"() { + GitCommit commit1 = commit() + GitCommit commit2 = commit() + + myGit.registerOperationExecutors(new MockGit.SimpleSuccessOperationExecutor(CHERRY_PICK, ""), + new MockGit.SimpleSuccessOperationExecutor(CHERRY_PICK, "")) + + CountingOKCommitHandler handler = new CountingOKCommitHandler(myRepository) + myVcsHelper.registerHandler(handler) + + invokeCherryPick([commit1, commit2]) + + assertOnlyDefaultChangelist() + assertEquals "Commit dialog shown wrong number of times", 2, handler.myCommitDialogs + assertLastCommits(commit2, commit1) + } + + @Test + void "3 commits, 2nd conflicts with committed, then 1st success, on 2nd show merge dialog"() { + GitCommit commit1 = commit("First") + GitCommit commit2 = commit("Second") + GitCommit commit3 = commit("Third") + + myGit.registerOperationExecutors(new MockGit.SimpleSuccessOperationExecutor(CHERRY_PICK, "")) + prepareConflict() + myGit.registerOperationExecutors(new MockGit.SimpleSuccessOperationExecutor(CHERRY_PICK, "")) + + CountingOKCommitHandler handler = new CountingOKCommitHandler(myRepository) + myVcsHelper.registerHandler(handler) + + invokeCherryPick([commit1, commit2, commit3]) + + assertMergeDialogShown() + assertEquals "Commit dialog shown wrong number of times", 3, handler.myCommitDialogs + assertLastCommits commit3, commit2, commit1 + } + + private static class CountingOKCommitHandler implements MockVcsHelper.CommitHandler { + + GitLightRepository myRepository + int myCommitDialogs; + + CountingOKCommitHandler(GitLightRepository repository) { + myRepository = repository + } + + @Override + boolean commit(String commitMessage) { + myCommitDialogs++ + myRepository.commit(commitMessage) + return true; + } + + } + +} diff --git a/plugins/git4idea/tests/git4idea/repo/GitRepositoryTest.java b/plugins/git4idea/tests/git4idea/repo/GitRepositoryTest.java index 95da8f264fcd..0f308f8e7e66 100644 --- a/plugins/git4idea/tests/git4idea/repo/GitRepositoryTest.java +++ b/plugins/git4idea/tests/git4idea/repo/GitRepositoryTest.java @@ -48,7 +48,7 @@ public class GitRepositoryTest extends GitTest { @BeforeMethod public void setUp(Method testMethod) throws Exception { super.setUp(testMethod); - myRepository = GitRepository.getFullInstance(myRepo.getVFRootDir(), myProject, myProject); + myRepository = GitRepositoryImpl.getFullInstance(myRepo.getVFRootDir(), myProject, myProject); myReader = new GitRepositoryReader(new File(VfsUtil.virtualToIoFile(myRepository.getRoot()), ".git")); } diff --git a/plugins/git4idea/tests/git4idea/test/GitExec.java b/plugins/git4idea/tests/git4idea/test/GitExec.java index de61dc575ee3..08d063685281 100644 --- a/plugins/git4idea/tests/git4idea/test/GitExec.java +++ b/plugins/git4idea/tests/git4idea/test/GitExec.java @@ -23,6 +23,7 @@ import com.intellij.testFramework.VfsTestUtil; import com.intellij.util.ArrayUtil; import com.intellij.util.ui.UIUtil; import git4idea.repo.GitRepository; +import git4idea.repo.GitRepositoryImpl; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -41,7 +42,7 @@ public class GitExec { public static GitRepository init(@NotNull Project project, @NotNull VirtualFile root) throws IOException { new GitTestRunEnv(new File(root.getPath())).run("init"); root.refresh(false, true); - return GitRepository.getLightInstance(root, project, project); + return GitRepositoryImpl.getLightInstance(root, project, project); } /** @@ -57,7 +58,7 @@ public class GitExec { VirtualFile root = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(new File(destinationPath)); assert root != null; root.refresh(false, true); - return bare ? null : GitRepository.getLightInstance(root, project, project); + return bare ? null : GitRepositoryImpl.getLightInstance(root, project, project); } public static String push(@NotNull GitRepository repository, String... args) throws IOException { diff --git a/plugins/git4idea/tests/git4idea/test/GitFastTest.groovy b/plugins/git4idea/tests/git4idea/test/GitFastTest.groovy index d86ce71349d1..42b05a9764cd 100644 --- a/plugins/git4idea/tests/git4idea/test/GitFastTest.groovy +++ b/plugins/git4idea/tests/git4idea/test/GitFastTest.groovy @@ -16,14 +16,14 @@ package git4idea.test import com.intellij.notification.Notification - -import static git4idea.test.GitGTestUtil.stripLineBreaksAndHtml -import static junit.framework.Assert.assertEquals -import static junit.framework.Assert.assertNotNull +import com.intellij.notification.NotificationType import com.intellij.openapi.project.Project +import com.intellij.openapi.util.io.FileUtil import git4idea.tests.TestDialogManager import org.junit.Before -import com.intellij.openapi.util.io.FileUtil + +import static junit.framework.Assert.assertEquals +import static junit.framework.Assert.assertNotNull /** * @@ -31,11 +31,15 @@ import com.intellij.openapi.util.io.FileUtil */ class GitFastTest { + public static final String TEST_NOTIFICATION_GROUP = "Test" + Project myProject GitTestPlatformFacade myPlatformFacade + GitTestRepositoryManager myRepositoryManager MockGit myGit String myProjectDir TestDialogManager myDialogManager + MockVcsHelper myVcsHelper @Before void setUp() { @@ -48,14 +52,30 @@ class GitFastTest { myPlatformFacade = new GitTestPlatformFacade() myGit = new MockGit() myDialogManager = myPlatformFacade.getDialogManager() + myRepositoryManager = (GitTestRepositoryManager) myPlatformFacade.getRepositoryManager(myProject) + myVcsHelper = (MockVcsHelper) myPlatformFacade.getVcsHelper(myProject) } void assertNotificationShown(Notification expected) { if (expected) { Notification actualNotification = (myPlatformFacade.getNotificator(myProject) as TestNotificator).lastNotification assertNotNull "No notification was shown", actualNotification - assertEquals(stripLineBreaksAndHtml(expected.content), stripLineBreaksAndHtml(actualNotification.content)) + assertEquals "Notification has wrong title", expected.title, actualNotification.title + assertEquals "Notification has wrong type", expected.type, actualNotification.type + assertEquals "Notification has wrong content", adjustTestContent(expected.content), actualNotification.content } } + // we allow more spaces and line breaks in tests to make them more readable. + // After all, notifications display html, so all line breaks and extra spaces are ignored. + String adjustTestContent(String s) { + StringBuilder res = new StringBuilder() + s.split("\n").each { res.append it.trim() } + res.toString() + } + + void assertNotificationShown(String title, String message, NotificationType type) { + assertNotificationShown(new Notification(TEST_NOTIFICATION_GROUP, title, message, type)) + } + } diff --git a/plugins/git4idea/tests/git4idea/test/GitGTestUtil.groovy b/plugins/git4idea/tests/git4idea/test/GitGTestUtil.groovy index 24f285a34146..70dbd0ac9144 100644 --- a/plugins/git4idea/tests/git4idea/test/GitGTestUtil.groovy +++ b/plugins/git4idea/tests/git4idea/test/GitGTestUtil.groovy @@ -37,4 +37,8 @@ class GitGTestUtil { StringUtil.stripHtml(s, true).replace('\n', ''); } + static String stripLineBreaksAndMultiSpaces(String s) { + s.replace('\n', '').replaceAll(" {3,}", " ") + } + } diff --git a/plugins/git4idea/tests/git4idea/test/GitLightRepository.java b/plugins/git4idea/tests/git4idea/test/GitLightRepository.java new file mode 100644 index 000000000000..cd7013becb59 --- /dev/null +++ b/plugins/git4idea/tests/git4idea/test/GitLightRepository.java @@ -0,0 +1,214 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package git4idea.test; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.VirtualFile; +import git4idea.GitBranch; +import git4idea.branch.GitBranchesCollection; +import git4idea.repo.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import sun.security.provider.SHA; + +import java.util.Collection; + +/** + * Simulates Git repository. + * Made for unit tests, not to spawn a Git process. + * Stores information about commits and branches in memory. + * + * @author Kirill Likhodedov + */ +public class GitLightRepository implements GitRepository { + + public static class Commit { + @Nullable private Commit myParent; + @NotNull private String myHash; + @NotNull private String myCommitMessage; + + public Commit(String hash, String message, Commit parent) { + myHash = hash; + myCommitMessage = message; + myParent = parent; + } + + @NotNull + public String getCommitMessage() { + return myCommitMessage; + } + + @Nullable + public Commit getParent() { + return myParent; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Commit commit = (Commit)o; + + if (!myCommitMessage.equals(commit.myCommitMessage)) return false; + if (!myHash.equals(commit.myHash)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = myHash.hashCode(); + result = 31 * result + myCommitMessage.hashCode(); + return result; + } + + } + + private static class Branch { + @NotNull private String myName; + @Nullable private Commit myCommit; + + public Branch(String name, Commit commit) { + myName = name; + myCommit = commit; + } + } + + private Branch myHead; + + public GitLightRepository() { + myHead = new Branch("master", null); + } + + public Commit commit(String commitMessage) { + Commit parent = myHead.myCommit; + String hash = new SHA().toString(); + Commit commit = new Commit(hash, commitMessage, parent); + myHead.myCommit = commit; + return commit; + } + + public Commit cherryPick(String commitMessage) { + return commit(commitMessage); + } + + @Nullable + public Commit getHead() { + return myHead.myCommit; + } + + @NotNull + @Override + public VirtualFile getRoot() { + return new GitMockVirtualFile(FileUtil.getTempDirectory()); + } + + @NotNull + @Override + public VirtualFile getGitDir() { + return new GitMockVirtualFile(getRoot().getPath() + "/.git"); + } + + @NotNull + @Override + public String getPresentableUrl() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Project getProject() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public GitUntrackedFilesHolder getUntrackedFilesHolder() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public State getState() { + throw new UnsupportedOperationException(); + } + + @Override + public String getCurrentRevision() { + throw new UnsupportedOperationException(); + } + + @Override + public GitBranch getCurrentBranch() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public GitBranchesCollection getBranches() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public GitConfig getConfig() { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Collection getRemotes() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isMergeInProgress() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isRebaseInProgress() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isOnBranch() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isFresh() { + throw new UnsupportedOperationException(); + } + + @Override + public void addListener(@NotNull GitRepositoryChangeListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public void update(TrackedTopic... topics) { + throw new UnsupportedOperationException(); + } + + @Override + public String toLogString() { + throw new UnsupportedOperationException(); + } + +} diff --git a/plugins/git4idea/tests/git4idea/test/GitTestPlatformFacade.java b/plugins/git4idea/tests/git4idea/test/GitTestPlatformFacade.java index b7cfab32f417..c3364b3e8bb7 100644 --- a/plugins/git4idea/tests/git4idea/test/GitTestPlatformFacade.java +++ b/plugins/git4idea/tests/git4idea/test/GitTestPlatformFacade.java @@ -15,20 +15,23 @@ */ package git4idea.test; +import com.intellij.mock.MockLocalFileSystem; +import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.DialogWrapper; -import com.intellij.openapi.vcs.AbstractVcs; -import com.intellij.openapi.vcs.ProjectLevelVcsManager; -import git4idea.Notificator; -import git4idea.PlatformFacade; -import git4idea.tests.TestDialogManager; -import org.jetbrains.annotations.NotNull; import com.intellij.openapi.roots.ProjectRootManager; +import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.util.Computable; -import git4idea.config.GitVcsSettings; +import com.intellij.openapi.vcs.AbstractVcs; +import com.intellij.openapi.vcs.AbstractVcsHelper; +import com.intellij.openapi.vcs.ProjectLevelVcsManager; import com.intellij.openapi.vcs.changes.ChangeListManager; import com.intellij.openapi.vfs.LocalFileSystem; -import com.intellij.mock.MockLocalFileSystem; +import com.intellij.testFramework.vcs.MockChangeListManager; +import git4idea.Notificator; +import git4idea.PlatformFacade; +import git4idea.repo.GitRepositoryManager; +import git4idea.tests.TestDialogManager; +import org.jetbrains.annotations.NotNull; /** * @@ -41,10 +44,15 @@ public class GitTestPlatformFacade implements PlatformFacade { private TestNotificator myNotificator; private TestDialogManager myTestDialogManager; private GitMockProjectRootManager myProjectRootManager; + private ChangeListManager myChangeListManager; + private GitTestRepositoryManager myRepositoryManager; + private MockVcsHelper myVcsHelper; public GitTestPlatformFacade() { myTestDialogManager = new TestDialogManager(); myProjectRootManager = new GitMockProjectRootManager(); + myChangeListManager = new MockChangeListManager(); + myRepositoryManager = new GitTestRepositoryManager(); } @NotNull @@ -95,13 +103,18 @@ public class GitTestPlatformFacade implements PlatformFacade { } @Override - public GitVcsSettings getGitWorkspaceSettings(@NotNull Project project) { - throw new UnsupportedOperationException(); + public void runWriteAction(@NotNull Runnable runnable) { + runnable.run(); + } + + @Override + public void invokeAndWait(@NotNull Runnable runnable, @NotNull ModalityState modalityState) { + runnable.run(); } @Override public ChangeListManager getChangeListManager(@NotNull Project project) { - throw new UnsupportedOperationException(); + return myChangeListManager; } @Override @@ -109,6 +122,21 @@ public class GitTestPlatformFacade implements PlatformFacade { return new MockLocalFileSystem(); } + @NotNull + @Override + public AbstractVcsHelper getVcsHelper(@NotNull Project project) { + if (myVcsHelper == null) { + myVcsHelper = new MockVcsHelper(); + } + return myVcsHelper; + } + + @NotNull + @Override + public GitRepositoryManager getRepositoryManager(@NotNull Project project) { + return myRepositoryManager; + } + @NotNull @Override public AbstractVcs getVcs(@NotNull Project project) { diff --git a/plugins/git4idea/tests/git4idea/test/GitTestRepositoryManager.java b/plugins/git4idea/tests/git4idea/test/GitTestRepositoryManager.java new file mode 100644 index 000000000000..4d4190a7bcd2 --- /dev/null +++ b/plugins/git4idea/tests/git4idea/test/GitTestRepositoryManager.java @@ -0,0 +1,75 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package git4idea.test; + +import com.intellij.openapi.vfs.VirtualFile; +import git4idea.repo.GitRepository; +import git4idea.repo.GitRepositoryChangeListener; +import git4idea.repo.GitRepositoryManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * @author Kirill Likhodedov + */ +class GitTestRepositoryManager implements GitRepositoryManager { + + private final Collection myRepositories = new ArrayList(); + + public void add(GitRepository repository) { + myRepositories.add(repository); + } + + @Override + public GitRepository getRepositoryForRoot(@Nullable VirtualFile root) { + return myRepositories.iterator().next(); + } + + @Override + public GitRepository getRepositoryForFile(@NotNull VirtualFile file) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public List getRepositories() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean moreThanOneRoot() { + throw new UnsupportedOperationException(); + } + + @Override + public void addListenerToAllRepositories(@NotNull GitRepositoryChangeListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public void updateRepository(VirtualFile root, GitRepository.TrackedTopic... topics) { + throw new UnsupportedOperationException(); + } + + @Override + public void updateAllRepositories(GitRepository.TrackedTopic... topics) { + throw new UnsupportedOperationException(); + } +} diff --git a/plugins/git4idea/tests/git4idea/test/MockGit.groovy b/plugins/git4idea/tests/git4idea/test/MockGit.groovy index fb6497932119..1bfae7b0802b 100644 --- a/plugins/git4idea/tests/git4idea/test/MockGit.groovy +++ b/plugins/git4idea/tests/git4idea/test/MockGit.groovy @@ -15,14 +15,21 @@ */ package git4idea.test +import com.intellij.execution.process.ProcessOutputTypes import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.containers.hash.HashMap import git4idea.commands.Git import git4idea.commands.GitCommandResult import git4idea.commands.GitLineHandlerListener +import git4idea.history.browser.GitCommit import git4idea.push.GitPushSpec import git4idea.repo.GitRepository import org.jetbrains.annotations.NotNull +import org.jetbrains.annotations.Nullable + +import static git4idea.test.MockGit.OperationName.CHERRY_PICK +import static git4idea.test.MockGit.OperationName.GET_UNMERGED_FILES /** * @@ -30,84 +37,240 @@ import org.jetbrains.annotations.NotNull */ class MockGit implements Git { + public static final GitCommandResult FAKE_SUCCESS_RESULT = new GitCommandResult(true, 0, Collections.emptyList(), Collections.emptyList()) + private final Map> myExecutors = new HashMap>() + + public static interface OperationExecutor { + GitCommandResult execute(); + OperationName getName(); + } + + public enum OperationName { + CHERRY_PICK, + GET_UNMERGED_FILES; + } + + /** + * Register executors for specific operations. These are put into queues, i.e. once operation is called, the executor is popped out of the + * queue. If the queue is empty or certain operation, then it is executed as by default. + */ + void registerOperationExecutors(OperationExecutor... executors) { + for (OperationExecutor executor : executors) { + OperationName name = executor.getName() + Queue exs = myExecutors.get(name) + if (exs == null) { + exs = new ArrayDeque() + myExecutors.put(name, exs) + } + exs.add(executor) + } + } + + @NotNull @Override GitCommandResult init(@NotNull Project project, @NotNull VirtualFile root, @NotNull GitLineHandlerListener... listeners) { new File(root.path, ".git").mkdir() - new GitCommandResult(true, 0, Collections.emptyList(), Collections.emptyList()) + FAKE_SUCCESS_RESULT } + @NotNull @Override - Set untrackedFiles(Project project, VirtualFile root, Collection files) { + Set untrackedFiles(@NotNull Project project, @NotNull VirtualFile root, Collection files) { throw new UnsupportedOperationException() } + @NotNull @Override - Collection untrackedFilesNoChunk(Project project, VirtualFile root, List relativePaths) { + Collection untrackedFilesNoChunk(@NotNull Project project, @NotNull VirtualFile root, List relativePaths) { throw new UnsupportedOperationException() } + @NotNull @Override - GitCommandResult clone(Project project, File parentDirectory, String url, String clonedDirectoryName) { + GitCommandResult clone(@NotNull Project project, @NotNull File parentDirectory, @NotNull String url, @NotNull String clonedDirectoryName) { throw new UnsupportedOperationException() } + @NotNull @Override - GitCommandResult merge(GitRepository repository, String branchToMerge, GitLineHandlerListener... listeners) { + GitCommandResult merge(@NotNull GitRepository repository, @NotNull String branchToMerge, @NotNull GitLineHandlerListener... listeners) { throw new UnsupportedOperationException() } + @NotNull @Override - GitCommandResult checkout(GitRepository repository, String reference, String newBranch, boolean force, GitLineHandlerListener... listeners) { + GitCommandResult checkout(@NotNull GitRepository repository, @NotNull String reference, String newBranch, boolean force, @NotNull GitLineHandlerListener... listeners) { throw new UnsupportedOperationException() } + @NotNull @Override - GitCommandResult checkoutNewBranch(GitRepository repository, String branchName, GitLineHandlerListener listener) { + GitCommandResult checkoutNewBranch(@NotNull GitRepository repository, @NotNull String branchName, GitLineHandlerListener listener) { throw new UnsupportedOperationException() } + @NotNull @Override - GitCommandResult createNewTag(GitRepository repository, String tagName, GitLineHandlerListener listener, String reference) { + GitCommandResult createNewTag(@NotNull GitRepository repository, @NotNull String tagName, GitLineHandlerListener listener, @NotNull String reference) { throw new UnsupportedOperationException() } + @NotNull @Override - GitCommandResult branchDelete(GitRepository repository, String branchName, boolean force, GitLineHandlerListener... listeners) { + GitCommandResult branchDelete(@NotNull GitRepository repository, @NotNull String branchName, boolean force, @NotNull GitLineHandlerListener... listeners) { throw new UnsupportedOperationException() } + @NotNull @Override - GitCommandResult branchContains(GitRepository repository, String commit) { + GitCommandResult branchContains(@NotNull GitRepository repository, @NotNull String commit) { throw new UnsupportedOperationException() } + @NotNull @Override - GitCommandResult branchCreate(GitRepository repository, String branchName) { + GitCommandResult branchCreate(@NotNull GitRepository repository, @NotNull String branchName) { throw new UnsupportedOperationException() } + @NotNull @Override - GitCommandResult resetHard(GitRepository repository, String revision) { + GitCommandResult resetHard(@NotNull GitRepository repository, @NotNull String revision) { throw new UnsupportedOperationException() } + @NotNull @Override - GitCommandResult resetMerge(GitRepository repository, String revision) { + GitCommandResult resetMerge(@NotNull GitRepository repository, String revision) { throw new UnsupportedOperationException() } + @NotNull @Override - GitCommandResult tip(GitRepository repository, String branchName) { + GitCommandResult tip(@NotNull GitRepository repository, @NotNull String branchName) { throw new UnsupportedOperationException() } + @NotNull @Override - GitCommandResult push(GitRepository repository, String remote, String spec, GitLineHandlerListener... listeners) { + GitCommandResult push(@NotNull GitRepository repository, @NotNull String remote, @NotNull String spec, @NotNull GitLineHandlerListener... listeners) { throw new UnsupportedOperationException() } + @NotNull @Override - GitCommandResult push(GitRepository repository, GitPushSpec pushSpec, GitLineHandlerListener... listeners) { + GitCommandResult push(@NotNull GitRepository repository, @NotNull GitPushSpec pushSpec, @NotNull GitLineHandlerListener... listeners) { throw new UnsupportedOperationException() } + + @NotNull + @Override + GitCommandResult cherryPick(@NotNull GitRepository repository, @NotNull String hash, boolean autoCommit, @NotNull GitLineHandlerListener... listeners) { + GitCommandResult result = callExecutor(CHERRY_PICK) + if (result != null) { + produceOutput(result.getOutputAsJoinedString(), listeners) + return result; + } + ((GitLightRepository)repository).cherryPick("cherry-pick from $hash") + return FAKE_SUCCESS_RESULT + } + + @NotNull + @Override + GitCommandResult getUnmergedFiles(@NotNull GitRepository repository) { + GitCommandResult result = callExecutor(GET_UNMERGED_FILES) + if (result != null) { + return result; + } + return FAKE_SUCCESS_RESULT + } + + private void produceOutput(String output, GitLineHandlerListener... listeners) { + for (String line : output.split("\n")) { // for simplicity all output goes to OUTPUT, no ERROR + listeners.each { it.onLineAvailable(line, ProcessOutputTypes.STDOUT) } + } + } + + @Nullable + private GitCommandResult callExecutor(OperationName operationName) { + Queue cherryPickExecutors = myExecutors.get(operationName) + if (cherryPickExecutors != null && !cherryPickExecutors.isEmpty()) { + OperationExecutor executor = cherryPickExecutors.poll() + return executor.execute() + } + return null; + } + + public static class SimpleErrorOperationExecutor implements MockGit.OperationExecutor { + + String myOutput + MockGit.OperationName myOperationName + + SimpleErrorOperationExecutor(MockGit.OperationName operationName, String output) { + myOutput = output; + myOperationName = operationName + } + + @Override + GitCommandResult execute() { + return result(myOutput, false); + } + + @Override + MockGit.OperationName getName() { + return myOperationName + } + } + + public static class SimpleSuccessOperationExecutor implements MockGit.OperationExecutor { + + String myOutput + MockGit.OperationName myOperationName + + SimpleSuccessOperationExecutor(MockGit.OperationName operationName, String output) { + myOutput = output; + myOperationName = operationName + } + + @Override + GitCommandResult execute() { + return result(myOutput, true); + } + + @Override + MockGit.OperationName getName() { + return myOperationName + } + } + + private static GitCommandResult result(String output, boolean success) { + new GitCommandResult(success, success ? 0 : 127, Collections.emptyList(), Collections.singletonList(output)) + } + + public static class SuccessfulCherryPickExecutor implements OperationExecutor { + + GitRepository myRepository + GitCommit myOriginalCommit + + SuccessfulCherryPickExecutor(GitRepository repository, GitCommit originalCommit) { + myRepository = repository; + myOriginalCommit = originalCommit + } + + @Override + GitCommandResult execute() { + ((GitLightRepository)myRepository).cherryPick(commitMessageForCherryPick(myOriginalCommit)) + return FAKE_SUCCESS_RESULT + } + + @Override + OperationName getName() { + return CHERRY_PICK + } + } + + static String commitMessageForCherryPick(GitCommit commit) { + "$commit.subject\n(cherry-picked from ${commit.shortHash.getString()})" + } + } diff --git a/plugins/git4idea/tests/git4idea/test/MockVcsHelper.java b/plugins/git4idea/tests/git4idea/test/MockVcsHelper.java new file mode 100644 index 000000000000..3fa663253131 --- /dev/null +++ b/plugins/git4idea/tests/git4idea/test/MockVcsHelper.java @@ -0,0 +1,204 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package git4idea.test; + +import com.intellij.ide.errorTreeView.HotfixData; +import com.intellij.openapi.vcs.*; +import com.intellij.openapi.vcs.annotate.AnnotationProvider; +import com.intellij.openapi.vcs.annotate.FileAnnotation; +import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vcs.changes.CommitExecutor; +import com.intellij.openapi.vcs.changes.LocalChangeList; +import com.intellij.openapi.vcs.history.VcsFileRevision; +import com.intellij.openapi.vcs.history.VcsHistoryProvider; +import com.intellij.openapi.vcs.merge.MergeDialogCustomizer; +import com.intellij.openapi.vcs.merge.MergeProvider; +import com.intellij.openapi.vcs.versionBrowser.ChangeBrowserSettings; +import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.io.File; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * @author Kirill Likhodedov + */ +public class MockVcsHelper extends AbstractVcsHelper { + private boolean myMergeDialogShown; + private CommitHandler myCommitHandler; + + @Override + public void showErrors(List abstractVcsExceptions, @NotNull String tabDisplayName) { + throw new UnsupportedOperationException(); + } + + @Override + public void showErrors(Map> exceptionGroups, @NotNull String tabDisplayName) { + throw new UnsupportedOperationException(); + } + + @Override + public List runTransactionRunnable(AbstractVcs vcs, TransactionRunnable runnable, Object vcsParameters) { + throw new UnsupportedOperationException(); + } + + @Override + public void showAnnotation(FileAnnotation annotation, VirtualFile file, AbstractVcs vcs) { + throw new UnsupportedOperationException(); + } + + @Override + public void showDifferences(VcsFileRevision cvsVersionOn, VcsFileRevision cvsVersionOn1, File file) { + throw new UnsupportedOperationException(); + } + + @Override + public void showChangesListBrowser(CommittedChangeList changelist, @Nls String title) { + throw new UnsupportedOperationException(); + } + + @Override + public void showChangesBrowser(List changelists) { + throw new UnsupportedOperationException(); + } + + @Override + public void showChangesBrowser(List changelists, @Nls String title) { + throw new UnsupportedOperationException(); + } + + @Override + public void showChangesBrowser(CommittedChangesProvider provider, + RepositoryLocation location, + @Nls String title, + @Nullable Component parent) { + throw new UnsupportedOperationException(); + } + + @Override + public void showWhatDiffersBrowser(@Nullable Component parent, Collection changes, @Nls String title) { + throw new UnsupportedOperationException(); + } + + @Override + public T chooseCommittedChangeList(CommittedChangesProvider provider, + RepositoryLocation location) { + throw new UnsupportedOperationException(); + } + + @Override + public void openCommittedChangesTab(AbstractVcs vcs, VirtualFile root, ChangeBrowserSettings settings, int maxCount, String title) { + throw new UnsupportedOperationException(); + } + + @Override + public void openCommittedChangesTab(CommittedChangesProvider provider, + RepositoryLocation location, + ChangeBrowserSettings settings, + int maxCount, + String title) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public List showMergeDialog(List files, + MergeProvider provider, + @NotNull MergeDialogCustomizer mergeDialogCustomizer) { + myMergeDialogShown = true; + return Collections.emptyList(); + } + + public boolean mergeDialogWasShown() { + return myMergeDialogShown; + } + + @NotNull + @Override + public List showMergeDialog(List files, MergeProvider provider) { + return showMergeDialog(files, provider, new MergeDialogCustomizer()); + } + + @NotNull + @Override + public List showMergeDialog(List files) { + throw new UnsupportedOperationException(); + } + + @Override + public void showFileHistory(VcsHistoryProvider vcsHistoryProvider, FilePath path, AbstractVcs vcs, String repositoryPath) { + throw new UnsupportedOperationException(); + } + + @Override + public void showFileHistory(VcsHistoryProvider vcsHistoryProvider, + AnnotationProvider annotationProvider, + FilePath path, + String repositoryPath, + AbstractVcs vcs) { + throw new UnsupportedOperationException(); + } + + @Override + public void showRollbackChangesDialog(List changes) { + throw new UnsupportedOperationException(); + } + + @Override + public Collection selectFilesToProcess(List files, + String title, + @Nullable String prompt, + String singleFileTitle, + String singleFilePromptTemplate, + VcsShowConfirmationOption confirmationOption) { + throw new UnsupportedOperationException(); + } + + @Override + public Collection selectFilePathsToProcess(List files, + String title, + @Nullable String prompt, + String singleFileTitle, + String singleFilePromptTemplate, + VcsShowConfirmationOption confirmationOption) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean commitChanges(@NotNull List changes, @NotNull LocalChangeList initialChangeList, + @NotNull String commitMessage, @Nullable CommitExecutor executor) { + if (myCommitHandler != null) { + return myCommitHandler.commit(commitMessage); + } + return false; + } + + public void registerHandler(CommitHandler handler) { + myCommitHandler = handler; + } + + public interface CommitHandler { + boolean commit(String commitMessage); + } + +} diff --git a/plugins/git4idea/tests/git4idea/test/TestNotificator.java b/plugins/git4idea/tests/git4idea/test/TestNotificator.java index e4f6c6a6fdae..143d6f0d93c7 100644 --- a/plugins/git4idea/tests/git4idea/test/TestNotificator.java +++ b/plugins/git4idea/tests/git4idea/test/TestNotificator.java @@ -44,11 +44,26 @@ public class TestNotificator extends Notificator { notify(notificationGroup, title, message, type, null); } + @Override + public void notifyError(@NotNull String title, @NotNull String message, @Nullable NotificationListener listener) { + myLastNotification = createNotification(title, message, NotificationType.ERROR); + } + @Override public void notifySuccess(@NotNull String title, @NotNull String message) { myLastNotification = createNotification(title, message, NotificationType.INFORMATION); } + @Override + public void notifyWeakWarning(@NotNull String title, @NotNull String message, @Nullable NotificationListener listener) { + myLastNotification = createNotification(title, message, NotificationType.WARNING); + } + + @Override + public void notifyStrongWarning(@NotNull String title, @NotNull String content, @Nullable NotificationListener listener) { + myLastNotification = createNotification(title, content, NotificationType.WARNING); + } + @NotNull private static Notification createNotification(@NotNull String title, @NotNull String message, NotificationType type) { return new Notification(TEST_NOTIFICATION_GROUP, title, message, type); diff --git a/plugins/git4idea/tests/git4idea/tests/rebase/GitRebaserReorderCommitsTest.java b/plugins/git4idea/tests/git4idea/tests/rebase/GitRebaserReorderCommitsTest.java index 00277378863d..fe53f30e06c2 100644 --- a/plugins/git4idea/tests/git4idea/tests/rebase/GitRebaserReorderCommitsTest.java +++ b/plugins/git4idea/tests/git4idea/tests/rebase/GitRebaserReorderCommitsTest.java @@ -15,7 +15,9 @@ */ package git4idea.tests.rebase; +import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.vfs.VirtualFile; +import git4idea.commands.Git; import git4idea.rebase.GitRebaser; import git4idea.tests.GitTest; import org.testng.annotations.BeforeMethod; @@ -41,7 +43,7 @@ public class GitRebaserReorderCommitsTest extends GitTest { @BeforeMethod @Override protected void setUp(Method testMethod) throws Exception { super.setUp(testMethod); - myRebaser = new GitRebaser(myProject, null); + myRebaser = new GitRebaser(myProject, ServiceManager.getService(Git.class), null); myRoot = myRepo.getVFRootDir(); myFirstCommit = makeCommit(); } diff --git a/plugins/hg4idea/testSrc/org/zmlx/hg4idea/test/HgMockVcsHelper.java b/plugins/hg4idea/testSrc/org/zmlx/hg4idea/test/HgMockVcsHelper.java index ccfa7538b3aa..ee4f0b92b2e3 100644 --- a/plugins/hg4idea/testSrc/org/zmlx/hg4idea/test/HgMockVcsHelper.java +++ b/plugins/hg4idea/testSrc/org/zmlx/hg4idea/test/HgMockVcsHelper.java @@ -20,6 +20,8 @@ import com.intellij.openapi.vcs.*; import com.intellij.openapi.vcs.annotate.AnnotationProvider; import com.intellij.openapi.vcs.annotate.FileAnnotation; import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vcs.changes.CommitExecutor; +import com.intellij.openapi.vcs.changes.LocalChangeList; import com.intellij.openapi.vcs.history.VcsFileRevision; import com.intellij.openapi.vcs.history.VcsHistoryProvider; import com.intellij.openapi.vcs.merge.MergeDialogCustomizer; @@ -168,6 +170,12 @@ public class HgMockVcsHelper extends AbstractVcsHelper { return null; } + @Override + public boolean commitChanges(@NotNull List changes, @NotNull LocalChangeList initialChangeList, + @NotNull String commitMessage, @Nullable CommitExecutor executor) { + throw new UnsupportedOperationException(); + } + public void addListener(VcsHelperListener listener) { myListeners.add(listener); }