Merge branch 'cherrypick'

Conflicts:
	platform/vcs-api/src/com/intellij/openapi/vcs/AbstractVcsHelper.java
	platform/vcs-impl/src/com/intellij/openapi/vcs/impl/AbstractVcsHelperImpl.java
This commit is contained in:
Kirill Likhodedov
2012-04-16 17:54:13 +04:00
59 changed files with 3103 additions and 840 deletions
@@ -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<Change> myChanges = new ArrayList<Change>();
private final String myName;
public MockChangeList(String name) {
myName = name;
}
public void add(Change change) {
myChanges.add(change);
}
@Override
public Collection<Change> 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();
}
}
+1
View File
@@ -20,6 +20,7 @@
<orderEntry type="module" module-name="community-resources" scope="RUNTIME" />
<orderEntry type="library" exported="" scope="TEST" name="Mocks" level="project" />
<orderEntry type="module" module-name="java-runtime" exported="" />
<orderEntry type="module" module-name="vcs-impl" />
</component>
</module>
@@ -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<String, MockChangeList> myChangeLists = new HashMap<String, MockChangeList>();
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<VcsDirtyScopeManager> dirtyScopeManager,
ModalityState state) {
afterUpdate.run();
}
@Override
public boolean ensureUpToDate(boolean canBeCanceled) {
throw new UnsupportedOperationException();
}
@Override
public List<LocalChangeList> getChangeListsCopy() {
return new ArrayList<LocalChangeList>(myChangeLists.values());
}
@NotNull
@Override
public List<LocalChangeList> getChangeLists() {
return getChangeListsCopy();
}
@Override
public List<File> getAffectedPaths() {
throw new UnsupportedOperationException();
}
@NotNull
@Override
public List<VirtualFile> getAffectedFiles() {
throw new UnsupportedOperationException();
}
@Override
public boolean isFileAffected(VirtualFile file) {
throw new UnsupportedOperationException();
}
@NotNull
@Override
public Collection<Change> getAllChanges() {
Collection<Change> changes = new ArrayList<Change>();
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<Change> 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<Change> getChangesIn(VirtualFile dir) {
throw new UnsupportedOperationException();
}
@NotNull
@Override
public Collection<Change> 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<Change> changes) {
throw new UnsupportedOperationException();
}
@Override
public void commitChangesSynchronously(LocalChangeList changeList, List<Change> changes) {
throw new UnsupportedOperationException();
}
@Override
public boolean commitChangesSynchronouslyWithResult(LocalChangeList changeList, List<Change> changes) {
throw new UnsupportedOperationException();
}
@Override
public void reopenFiles(List<FilePath> paths) {
throw new UnsupportedOperationException();
}
@Override
public List<CommitExecutor> 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<VirtualFile> 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();
}
}
@@ -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;
}
}
@@ -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<Change> changes, @NotNull LocalChangeList initialChangeList,
@NotNull String commitMessage, @Nullable CommitExecutor executor);
}
@@ -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() {
@@ -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<Document> markCommittingDocuments(@NotNull Project project, @NotNull List<Change> changes) {
Collection<Document> committingDocs = new ArrayList<Document>();
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<Document> committingDocs) {
for (Document doc : committingDocs) {
doc.putUserData(DOCUMENT_BEING_COMMITTED_KEY, null);
}
myCommittingDocuments.clear();
}
private void commitCompleted(final List<VcsException> allExceptions, final GeneralCommitProcessor processor) {
@@ -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<Change> changes, @NotNull LocalChangeList initialChangeList,
@NotNull String commitMessage, @Nullable CommitExecutor executor) {
return CommitChangeListDialog.commitChanges(myProject, changes, initialChangeList, executor, commitMessage);
}
private void addDirectMessages(VcsErrorViewPanel vcsErrorViewPanel, List<VcsException> abstractVcsExceptions) {
for (final VcsException exception : abstractVcsExceptions) {
+13 -1
View File
@@ -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);
}
}
@@ -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);
}
@@ -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) {
@@ -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<VirtualFile> untrackedFiles) {
UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject, untrackedFiles, getOperationName());
UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(myProject, ServiceManager.getService(myProject, PlatformFacade.class),
untrackedFiles, getOperationName(), null);
}
private void showUntrackedFilesDialogWithRollback(@NotNull Collection<VirtualFile> untrackedFiles) {
@@ -201,7 +201,8 @@ class GitCheckoutOperation extends GitBranchOperation {
private boolean smartCheckout(@NotNull final List<GitRepository> repositories, @NotNull final String reference, @Nullable final String newBranch, @NotNull ProgressIndicator indicator) {
final AtomicBoolean result = new AtomicBoolean();
GitPreservingProcess preservingProcess = new GitPreservingProcess(myProject, 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));
@@ -209,7 +209,7 @@ class GitMergeOperation extends GitBranchOperation {
private boolean doSmartMerge(@NotNull final Collection<GitRepository> 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<GitRepository> 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);
}
@@ -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<VirtualFile> unmergedFiles(Project project, VirtualFile root) throws VcsException {
HashSet<VirtualFile> unmerged = new HashSet<VirtualFile>();
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<VirtualFile> rc = new ArrayList<VirtualFile>(unmerged.size());
rc.addAll(unmerged);
Collections.sort(rc, GitUtil.VIRTUAL_FILE_COMPARATOR);
return rc;
}
}
/**
* Parse changes from lines
*
+21 -16
View File
@@ -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<VirtualFile> untrackedFiles(@NotNull Project project,
@NotNull VirtualFile root,
Set<VirtualFile> untrackedFiles(@NotNull Project project, @NotNull VirtualFile root,
@Nullable Collection<VirtualFile> files) throws VcsException;
// relativePaths are guaranteed to fit into command line length limitations.
@NotNull
Collection<VirtualFile> untrackedFilesNoChunk(@NotNull Project project,
@NotNull VirtualFile root,
@Nullable List<String> relativePaths)
throws VcsException;
Collection<VirtualFile> untrackedFilesNoChunk(@NotNull Project project, @NotNull VirtualFile root,
@Nullable List<String> 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);
}
@@ -73,4 +73,9 @@ public class GitCommandResult {
public String getErrorOutputAsJoinedString() {
return StringUtil.join(myErrorOutput, "\n");
}
@NotNull
public String getOutputAsJoinedString() {
return StringUtil.join(myOutput, "\n");
}
}
@@ -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 &lt;reference&gt;} <br/>
* {@code git checkout -b &lt;newBranch&gt; &lt;reference&gt;}
*/
@NotNull
@Override
public GitCommandResult checkout(@NotNull GitRepository repository,
@NotNull String reference,
@@ -179,6 +181,7 @@ public class GitImpl implements Git {
/**
* {@code git checkout -b &lt;branchName&gt;}
*/
@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 <reference>} or {@code git branch -D <reference>}
*/
@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.<br/>
* {@code git rev-list -1 <branchName>}
*/
@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);
}
@@ -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"),
@@ -123,7 +123,7 @@
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
<grid id="4d15a" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<grid id="4d15a" layout-manager="GridLayoutManager" row-count="3" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
@@ -133,7 +133,7 @@
<children>
<component id="9d661" class="javax.swing.JCheckBox" binding="myAutoUpdateIfPushRejected">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<iconTextGap value="4"/>
@@ -142,13 +142,22 @@
</component>
<component id="2e80d" class="com.intellij.ui.components.JBCheckBox" binding="mySyncBranchControl">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Control &amp;branches from different roots synchronously"/>
<toolTipText value="If selected, you would be able to checkout, compare, delete and create new branches in all Git roots with a single action"/>
</properties>
</component>
<component id="5a98" class="javax.swing.JCheckBox" binding="myAutoCommitOnCherryPick">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<iconTextGap value="4"/>
<text value="Commit automatically on cherry-pick"/>
</properties>
</component>
</children>
</grid>
</children>
@@ -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());
}
}
@@ -74,6 +74,7 @@ public class GitVcsSettings implements PersistentStateComponent<GitVcsSettings.S
public String RECENT_GIT_ROOT_PATH = null;
public Map<String, String> RECENT_BRANCH_BY_REPOSITORY = new HashMap<String, String>();
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<GitVcsSettings.S
myState.RECENT_COMMON_BRANCH = branch;
}
public void setAutoCommitOnCherryPick(boolean autoCommit) {
myState.AUTO_COMMIT_ON_CHERRY_PICK = autoCommit;
}
public boolean isAutoCommitOnCherryPick() {
return myState.AUTO_COMMIT_ON_CHERRY_PICK;
}
}
@@ -15,74 +15,306 @@
*/
package git4idea.history.browser;
import com.intellij.lifecycle.PeriodicalTasksCloser;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationListener;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.vcs.AbstractVcsHelper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.*;
import com.intellij.openapi.vcs.changes.ui.CommitHelper;
import com.intellij.openapi.vcs.checkin.CheckinEnvironment;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
import com.intellij.openapi.vcs.merge.MergeDialogCustomizer;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Consumer;
import git4idea.GitVcs;
import com.intellij.util.WaitForProgressToShow;
import git4idea.PlatformFacade;
import git4idea.commands.Git;
import git4idea.commands.GitCommandResult;
import git4idea.commands.GitMessageWithFilesDetector;
import git4idea.commands.GitSimpleEventDetector;
import git4idea.merge.GitConflictResolver;
import git4idea.repo.GitRepository;
import git4idea.util.UntrackedFilesNotifier;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import javax.swing.event.HyperlinkEvent;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier.showOverChangesView;
import static com.intellij.openapi.util.text.StringUtil.pluralize;
import static git4idea.commands.GitMessageWithFilesDetector.Event.UNTRACKED_FILES_OVERWRITTEN_BY;
import static git4idea.commands.GitSimpleEventDetector.Event.CHERRY_PICK_CONFLICT;
import static git4idea.commands.GitSimpleEventDetector.Event.LOCAL_CHANGES_OVERWRITTEN_BY_CHERRY_PICK;
public class CherryPicker {
private static final Logger LOG = Logger.getInstance(CherryPicker.class);
private final GitVcs myVcs;
private final List<GitCommit> 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<VcsException> myExceptions;
private final List<VcsException> myWarnings;
private boolean myConflictsExist;
private final ChangeListManager myChangeListManager;
private final List<CherryPickedData> myCherryPickedData;
public CherryPicker(GitVcs vcs, final List<GitCommit> 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<VcsException>();
myWarnings = new ArrayList<VcsException>();
myCherryPickedData = new ArrayList<CherryPickedData>();
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<GitRepository, List<GitCommit>> commitsInRoots) {
List<GitCommit> successfulCommits = new ArrayList<GitCommit>();
for (Map.Entry<GitRepository, List<GitCommit>> 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<GitCommit> commits,
@NotNull List<GitCommit> 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)
+ "<br/>Some untracked working tree files would be overwritten by cherry-pick.<br/>" +
"Please move, remove or add them before you can cherry-pick. <a href='view'>View them</a>";
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.<br/>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<GitCommit> 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<GitCommit> successfulCommits) {
NotificationListener resolveLinkListener = new ResolveLinkListener(myProject, myGit, myPlatformFacade, repository.getRoot(),
commit.getShortHash().getString(), commit.getAuthor(),
commit.getSubject());
String description = commitDetails(commit)
+ "<br/>Unresolved conflicts remain in the working tree. <a href='resolve'>Resolve them.<a/>";
description += getSuccessfulCommitDetailsIfAny(successfulCommits);
myPlatformFacade.getNotificator(myProject).notifyStrongWarning("Cherry-picked with conflicts", description, resolveLinkListener);
}
private void notifyCommitCancelled(@NotNull GitCommit commit, @NotNull List<GitCommit> 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<FilePath> 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<Change> changes, @NotNull final Collection<FilePath> paths,
@NotNull final String commitMessage) {
final AtomicReference<LocalChangeList> changeList = new AtomicReference<LocalChangeList>();
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<VcsDirtyScopeManager>() {
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<FilePath> 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<Change> 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<GitCommit> successfulCommits) {
String description = commitDetails(failedCommit) + "<br/>" + content;
description += getSuccessfulCommitDetailsIfAny(successfulCommits);
myPlatformFacade.getNotificator(myProject).notifyError("Cherry-pick failed", description);
}
@NotNull
private static String getSuccessfulCommitDetailsIfAny(@NotNull List<GitCommit> successfulCommits) {
String description = "";
if (!successfulCommits.isEmpty()) {
description += "<hr/>However cherry-pick succeeded for the following " + pluralize("commit", successfulCommits.size()) + ":<br/>";
description += getCommitsDetails(successfulCommits);
}
return description;
}
private void notifySuccess(@NotNull List<GitCommit> successfulCommits) {
String description = getCommitsDetails(successfulCommits);
myPlatformFacade.getNotificator(myProject).notifySuccess("Cherry-pick successful", description);
}
@NotNull
private static String getCommitsDetails(@NotNull List<GitCommit> successfulCommits) {
String description = "";
for (GitCommit commit : successfulCommits) {
description += commitDetails(commit) + "<br/>";
}
return description.substring(0, description.length() - "<br/>".length());
}
@NotNull
private static String commitDetails(@NotNull GitCommit commit) {
return commit.getShortHash().toString() + " \"" + commit.getSubject() + "\"";
}
private void refreshChangedFiles(@NotNull Collection<FilePath> 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<FilePath> getAllChangedFiles() {
Collection<FilePath> files = new ArrayList<FilePath>();
for (CherryPickedData data : myCherryPickedData) {
files.addAll(data.getFiles());
private LocalChangeList createChangeList(@NotNull List<Change> 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<VcsDirtyScopeManager>() {
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<FilePath> filePaths = pickedData.getFiles();
final String message = pickedData.getCommitMessage();
private static class CherryPickConflictResolver extends GitConflictResolver {
if (filePaths.isEmpty()) continue;
final List<Change> 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<Change> pathsToChanges(@NotNull Collection<FilePath> filePaths) {
final List<Change> changes = new ArrayList<Change>(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<String> myMessagesInOrder = new ArrayList<String>(myCherryPickedData.size());
Map<String, Collection<FilePath>> myFilesToMove = new HashMap<String, Collection<FilePath>>(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<String> 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<Change> changes = commit.getChanges();
final Collection<FilePath> 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<VcsException> exceptions, Set<String> lostSet,
Map<String, Collection<FilePath>> 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<String> myHaveLostSomething;
private static class CherryPickMergeDialogCustomizer extends MergeDialogCustomizer {
private GroupOfListsProcessor() {
myHaveLostSomething = new HashSet<String>();
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<String> messagesInOrder, final Map<String, Collection<FilePath>> filesToMove) {
// remove those that are in newer lists
for (int i = 1; i < messagesInOrder.size(); i++) {
final String message = messagesInOrder.get(i);
final Collection<FilePath> currentFiles = filesToMove.get(message);
@Override
public String getMultipleFileMergeDescription(Collection<VirtualFile> files) {
return "<html>Conflicts during cherry-picking commit <code>" + myCommitHash + "</code> made by " + myCommitAuthor + "<br/>" +
"<code>\"" + myCommitMessage + "\"</code></html>";
}
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 "<html>Changes from cherry-pick <code>" + myCommitHash + "</code>";
}
}
/*
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<Change> myChanges;
@NotNull private final String myCommitMessage;
private boolean myCommitFailed;
CherryPickCommitExecutor(@NotNull Project project, @NotNull PlatformFacade platformFacade,
@NotNull List<Change> 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<Change> changes, String commitMessage) {
return null;
}
@Override
public boolean canExecute(Collection<Change> changes, String commitMessage) {
return true;
}
@Override
public void execute(Collection<Change> changes, String commitMessage) {
final Collection<Document> committingDocs = markCommittingDocs();
try {
CheckinEnvironment ce = myPlatformFacade.getVcs(myProject).getCheckinEnvironment();
if (ce != null) {
try {
List<VcsException> 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<String> 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<FilePath> myFiles;
private LocalChangeList myChangeList;
private Collection<Change> myChanges;
@Override
public String getHelpId() {
return null;
}
private CherryPickedData(@NotNull String message, @NotNull Collection<FilePath> files) {
myCommitMessage = message;
myFiles = files;
}
private void unmarkCommittingDocs(final Collection<Document> committingDocs) {
myPlatformFacade.runReadAction(new Runnable() {
@Override
public void run() {
CommitHelper.unmarkCommittingDocuments(committingDocs);
}
});
}
public Collection<Change> getChanges() {
return myChanges;
}
@NotNull
private Collection<Document> markCommittingDocs() {
final Collection<Document> committingDocs = new ArrayList<Document>();
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<FilePath> getFiles() {
return myFiles;
}
public void setChanges(List<Change> changes) {
myChanges = changes;
}
public void setChangeList(LocalChangeList changeList) {
myChangeList = changeList;
}
}
@@ -56,14 +56,6 @@ public interface LowLevelAccess {
void loadAllTags(final Collection<String> 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<String> startingPoints, @NotNull final Collection<ChangesFilter.Filter> filters,
final AsynchConsumer<CommitHashPlusParents> consumer, Getter<Boolean> isCanceled, int useMaxCnt,
final boolean topoOrder) throws VcsException;
@@ -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<VcsException> 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<br/>" +
"<a href='resolve'>Resolve</a> 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<VirtualFile> files) {
return "<html>Conflicts during cherry-picking commit <code>" + myCommitHash + "</code> made by " + myCommitAuthor + "<br/>" +
"<code>\"" + myCommitMessage + "\"</code></html>";
}
@Override
public String getLeftPanelTitle(VirtualFile file) {
return "Local changes";
}
@Override
public String getRightPanelTitle(VirtualFile file, VcsRevisionNumber lastRevisionNumber) {
return "<html>Changes from cherry-pick <code>" + myCommitHash + "</code>";
}
}
}
@@ -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<GitCommit> part = (List<GitCommit>)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<GitRepository, List<GitCommit>> 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<GitRepository, List<GitCommit>> prepareCommitsForCherryPick(MultiMap<VirtualFile, GitCommit> commits) {
Map<GitRepository, List<GitCommit>> commitsInRoots = new HashMap<GitRepository, List<GitCommit>>();
GitRepositoryManager repositoryManager = ServiceManager.getService(myProject, GitRepositoryManager.class);
for (Map.Entry<VirtualFile, Collection<GitCommit>> entry : commits.entrySet()) {
List<GitCommit> sortedCommits = new ArrayList<GitCommit>(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
@@ -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<VirtualFile> 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<VirtualFile> roots, @NotNull Params params) {
public GitConflictResolver(@NotNull Project project, @NotNull Git git, @NotNull PlatformFacade platformFacade,
@NotNull Collection<VirtualFile> 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<VirtualFile> initiallyUnmergedFiles = GitMergeUtil.getUnmergedFiles(myProject, myRoots);
final Collection<VirtualFile> 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<VirtualFile> unmergedFilesAfterResolve = GitMergeUtil.getUnmergedFiles(myProject, myRoots);
final Collection<VirtualFile> 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<VirtualFile> 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<VirtualFile>(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<VirtualFile> getUnmergedFiles(@NotNull Collection<VirtualFile> roots) throws VcsException {
final Collection<VirtualFile> unmergedFiles = new HashSet<VirtualFile>();
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<VirtualFile> 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<VirtualFile> unmergedFiles(VirtualFile root) throws VcsException {
HashSet<VirtualFile> unmerged = new HashSet<VirtualFile>();
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<VirtualFile> rc = new ArrayList<VirtualFile>(unmerged.size());
rc.addAll(unmerged);
Collections.sort(rc, GitUtil.VIRTUAL_FILE_COMPARATOR);
return rc;
}
}
}
@@ -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<VirtualFile> mergingRoots, Params params,
boolean refreshAfterCommit) {
super(project, mergingRoots, params);
public GitMergeCommittingConflictResolver(Project project, @NotNull Git git, GitMerger merger, Collection<VirtualFile> mergingRoots,
Params params, boolean refreshAfterCommit) {
super(project, git, ServiceManager.getService(PlatformFacade.class), mergingRoots, params);
myMerger = merger;
myMergingRoots = mergingRoots;
myRefreshAfterCommit = refreshAfterCommit;
@@ -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<VirtualFile> getUnmergedFiles(@NotNull Project project, @NotNull Collection<VirtualFile> roots) throws VcsException {
final Collection<VirtualFile> unmergedFiles = new HashSet<VirtualFile>();
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<VirtualFile> getUnmergedFiles(@NotNull Project project, @NotNull VirtualFile root) throws VcsException {
return GitChangeUtils.unmergedFiles(project, root);
}
}
@@ -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<GitRebaseUtils.CommitInfo> 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<GitRebaseUtils.CommitInfo>();
@@ -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");
}
@@ -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<GitRepositoryChangeListener> 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<Object> myNotifier;
private volatile State myState;
private volatile String myCurrentRevision;
private volatile GitBranch myCurrentBranch;
private volatile GitBranchesCollection myBranches = GitBranchesCollection.EMPTY;
private volatile GitConfig myConfig;
Topic<GitRepositoryChangeListener> 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<Object>(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<GitRemote> getRemotes() {
return myConfig.getRemotes();
}
Collection<GitRemote> 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<Object> {
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();
}
@@ -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<Object> 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<Object>(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<GitRemote> 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<Object> {
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;
}
}
@@ -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);
}
@@ -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 {
@@ -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);
@@ -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);
}
@@ -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<VirtualFile> myStashedRoots = new HashSet<VirtualFile>(); // 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<VirtualFile> myStashedRoots;
public UnstashConflictResolver(@NotNull Project project, @NotNull Set<VirtualFile> stashedRoots, @Nullable Params params) {
super(project, stashedRoots, makeParamsOrUse(params));
public UnstashConflictResolver(@NotNull Project project, @NotNull Git git, @NotNull Set<VirtualFile> stashedRoots, @Nullable Params params) {
super(project, git, ServiceManager.getService(PlatformFacade.class), stashedRoots, makeParamsOrUse(params));
myStashedRoots = stashedRoots;
}
@@ -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;
}
@@ -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<VirtualFile, GitBranchPair> 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;
}
@@ -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<VirtualFile, GitBranchPair> trackedBranches,
public GitRebaseUpdater(@NotNull Project project, @NotNull Git git, @NotNull VirtualFile root,
@NotNull final Map<VirtualFile, GitBranchPair> 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;
}
@@ -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<VirtualFile> myRoots;
private final UpdatedFiles myUpdatedFiles;
private final ProgressIndicator myProgressIndicator;
@@ -62,7 +65,7 @@ public class GitUpdateProcess {
private boolean myResult;
private final Map<VirtualFile, GitUpdater> myUpdaters;
private final Collection<VirtualFile> 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<VirtualFile, GitUpdater>();
myRootsToSave = new HashSet<VirtualFile>(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<VirtualFile> 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 <b>continue rebase</b>. <br/> You also may <b>abort rebase</b> 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);
}
@@ -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<VirtualFile, GitBranchPair> 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<VirtualFile, GitBranchPair> trackedBranches,
@NotNull ProgressIndicator progressIndicator, @NotNull UpdatedFiles updatedFiles) {
protected GitUpdater(@NotNull Project project, @NotNull Git git, @NotNull VirtualFile root,
@NotNull Map<VirtualFile, GitBranchPair> 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<VirtualFile, GitBranchPair> trackedBranches,
public static GitUpdater getUpdater(@NotNull Project project, @NotNull Git git, @NotNull Map<VirtualFile, GitBranchPair> 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<VirtualFile, GitBranchPair> 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
@@ -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<GitRepository> 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<GitRepository> repositories,
public GitPreservingProcess(@NotNull Project project, @NotNull Git git, @NotNull Collection<GitRepository> 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<VirtualFile> files) {
@@ -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<VirtualFile> untrackedFiles, @NotNull final String operation) {
public static void notifyUntrackedFilesOverwrittenBy(@NotNull final Project project, @NotNull PlatformFacade platformFacade,
@NotNull final Collection<VirtualFile> 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<VirtualFile>(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<VirtualFile>(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 + "<br/>" + description2;
}
else {
notificationDesc = "Some" + description1 + "<br/>" + description2 + " <a href=''>View them</a>";
notificationDesc = "Some" + description1 + "<br/>" + description2 + " <a href='view'>View them</a>";
}
return notificationDesc;
}
@@ -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 <file>..." 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)}<br/>
Your local changes would be overwritten by cherry-pick.<br/>
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)}<br/>
Some untracked working tree files would be overwritten by cherry-pick.<br/>
Please move, remove or add them before you can cherry-pick. <a href='view'>View them</a>
""",
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)}<br/>
Unresolved conflicts remain in the working tree. <a href='resolve'>Resolve them.<a/>
""",
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)}<br/>
Your local changes would be overwritten by cherry-pick.<br/>
Commit your changes or stash them to proceed.
<hr/>
However cherry-pick succeeded for the following commit:<br/>
${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)}<br/>
Not cherry-picked ${commitDetails(commit2)} - all changes have already been applied""", NotificationType.WARNING)
}
}
@@ -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 <paths>' or 'git rm <paths>'
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<Change> changes = new ArrayList<Change>();
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<GitCommit> commits) {
myCherryPicker.cherryPick(Collections.<GitRepository, List<GitCommit>> 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<String> changeLists, String activeChangelist) {
ChangeListManager changeListManager = myPlatformFacade.getChangeListManager(myProject)
List<LocalChangeList> lists = changeListManager.changeLists
Collection<String> 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("<br/>")
}
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
}
}
}
@@ -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)}<br/>
Your local changes would be overwritten by cherry-pick.<br/>
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;
}
}
}
@@ -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"));
}
@@ -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 {
@@ -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))
}
}
@@ -37,4 +37,8 @@ class GitGTestUtil {
StringUtil.stripHtml(s, true).replace('\n', '');
}
static String stripLineBreaksAndMultiSpaces(String s) {
s.replace('\n', '').replaceAll(" {3,}", " ")
}
}
@@ -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<GitRemote> 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();
}
}
@@ -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) {
@@ -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<GitRepository> myRepositories = new ArrayList<GitRepository>();
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<GitRepository> 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();
}
}
@@ -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<OperationName, Queue<OperationExecutor>> myExecutors = new HashMap<OperationName, Queue<OperationExecutor>>()
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<OperationExecutor> exs = myExecutors.get(name)
if (exs == null) {
exs = new ArrayDeque<OperationExecutor>()
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<VirtualFile> untrackedFiles(Project project, VirtualFile root, Collection<VirtualFile> files) {
Set<VirtualFile> untrackedFiles(@NotNull Project project, @NotNull VirtualFile root, Collection<VirtualFile> files) {
throw new UnsupportedOperationException()
}
@NotNull
@Override
Collection<VirtualFile> untrackedFilesNoChunk(Project project, VirtualFile root, List<String> relativePaths) {
Collection<VirtualFile> untrackedFilesNoChunk(@NotNull Project project, @NotNull VirtualFile root, List<String> 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<OperationExecutor> 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()})"
}
}
@@ -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<VcsException> abstractVcsExceptions, @NotNull String tabDisplayName) {
throw new UnsupportedOperationException();
}
@Override
public void showErrors(Map<HotfixData, List<VcsException>> exceptionGroups, @NotNull String tabDisplayName) {
throw new UnsupportedOperationException();
}
@Override
public List<VcsException> 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<CommittedChangeList> changelists) {
throw new UnsupportedOperationException();
}
@Override
public void showChangesBrowser(List<CommittedChangeList> 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<Change> changes, @Nls String title) {
throw new UnsupportedOperationException();
}
@Override
public <T extends CommittedChangeList, U extends ChangeBrowserSettings> T chooseCommittedChangeList(CommittedChangesProvider<T, U> 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<VirtualFile> showMergeDialog(List<VirtualFile> files,
MergeProvider provider,
@NotNull MergeDialogCustomizer mergeDialogCustomizer) {
myMergeDialogShown = true;
return Collections.emptyList();
}
public boolean mergeDialogWasShown() {
return myMergeDialogShown;
}
@NotNull
@Override
public List<VirtualFile> showMergeDialog(List<VirtualFile> files, MergeProvider provider) {
return showMergeDialog(files, provider, new MergeDialogCustomizer());
}
@NotNull
@Override
public List<VirtualFile> showMergeDialog(List<VirtualFile> 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<Change> changes) {
throw new UnsupportedOperationException();
}
@Override
public Collection<VirtualFile> selectFilesToProcess(List<VirtualFile> files,
String title,
@Nullable String prompt,
String singleFileTitle,
String singleFilePromptTemplate,
VcsShowConfirmationOption confirmationOption) {
throw new UnsupportedOperationException();
}
@Override
public Collection<FilePath> selectFilePathsToProcess(List<FilePath> files,
String title,
@Nullable String prompt,
String singleFileTitle,
String singleFilePromptTemplate,
VcsShowConfirmationOption confirmationOption) {
throw new UnsupportedOperationException();
}
@Override
public boolean commitChanges(@NotNull List<Change> 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);
}
}
@@ -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);
@@ -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();
}
@@ -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<Change> changes, @NotNull LocalChangeList initialChangeList,
@NotNull String commitMessage, @Nullable CommitExecutor executor) {
throw new UnsupportedOperationException();
}
public void addListener(VcsHelperListener listener) {
myListeners.add(listener);
}