Git CherryPicker rewrite. WIP

* Move calling native methods from LowLevelAccess directly to the CherryPicker, use the Git interface to perform native calls.
* Remove code which supported cherry-picking several changesets without committing and sorted them into changelists - this functionality is considered dangerous and to be removed.
* Implement just the "auto-commit" option for now. Support multiple cherry-pick.
  On error stop cherry-picking.
  On conflict show the merge dialog, if everything is resolved, show the commit dialog;
  if committed, continue with cherry-picking, otherwise stop it.
This commit is contained in:
Kirill Likhodedov
2012-04-08 14:39:42 +04:00
parent d3a62b6ed4
commit cb7c70b10f
7 changed files with 261 additions and 342 deletions
@@ -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"),
@@ -15,85 +15,184 @@
*/
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.project.Project;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.vcs.AbstractVcsHelper;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.*;
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 git4idea.PlatformFacade;
import git4idea.commands.Git;
import git4idea.commands.GitCommandResult;
import git4idea.commands.GitSimpleEventDetector;
import git4idea.merge.GitConflictResolver;
import git4idea.repo.GitRepository;
import git4idea.repo.GitRepositoryManager;
import org.jetbrains.annotations.NotNull;
import javax.swing.event.HyperlinkEvent;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import static com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier.showOverChangesView;
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);
@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 GitVcs myVcs;
private List<GitCommit> myCommits;
@NotNull private CheckinEnvironment myCheckinEnvironment;
private LowLevelAccess myAccess;
private List<VcsException> myExceptions;
private List<VcsException> myWarnings;
private boolean myConflictsExist;
private ChangeListManager myChangeListManager;
private 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 CherryPicker(Project project, Git git, PlatformFacade platformFacade, GitRepositoryManager repositoryManager, boolean autoCommit) {
}
public void cherryPick(Map<GitRepository, List<GitCommit>> commitsInRoots) {
}
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());
private boolean cherryPick(@NotNull GitRepository repository, @NotNull List<GitCommit> commits,
@NotNull List<GitCommit> successfulCommits) {
if (myAutoCommit) {
GitSimpleEventDetector conflictDetector = new GitSimpleEventDetector(CHERRY_PICK_CONFLICT);
GitSimpleEventDetector localChangesOverwrittenDetector = new GitSimpleEventDetector(LOCAL_CHANGES_OVERWRITTEN_BY_CHERRY_PICK);
for (GitCommit commit : commits) {
GitCommandResult result = myGit.cherryPick(repository, commit.getHash().getValue(), true,
conflictDetector, localChangesOverwrittenDetector);
if (result.success()) {
successfulCommits.add(commit);
}
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 = updateChangeListManagerAndShowCommitDialogIfNeeded(commit, true);
if (!committed) {
notifyConflictWarning(commit, successfulCommits);
return false;
}
else {
successfulCommits.add(commit);
}
}
else {
updateChangeListManagerAndShowCommitDialogIfNeeded(commit, false);
notifyConflictWarning(commit, successfulCommits);
return false;
}
}
else {
// including localChangesOverwrittenDetector.hasHappened() - no special handler for now
notifyError(result.getErrorOutputAsHtmlString(), commit, successfulCommits);
return false;
}
}
}
return true;
}
private void notifyConflictWarning(GitCommit commit, List<GitCommit> successfulCommits) {
String description = commitDetails(commit);
description += getSuccessfulCommitDetailsIfAny(successfulCommits, description);
myPlatformFacade.getNotificator(myProject).notifyWeakWarning("Cherry-picked with conflicts", description);
}
private boolean updateChangeListManagerAndShowCommitDialogIfNeeded(@NotNull final GitCommit commit, boolean showCommitDialog) {
final Collection<FilePath> paths = ChangesUtil.getPaths(commit.getChanges());
refreshChangedFiles(paths);
final String commitMessage = createCommitMessage(commit, paths);
LocalChangeList changeList = createChangeListAfterUpdate(commit.getChanges(), paths, commitMessage);
if (showCommitDialog) {
return showCommitDialog(commit, changeList, commitMessage);
}
return false;
}
@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>();
myChangeListManager.invokeAfterUpdate(new Runnable() {
public void run() {
changeList.set(createChangeList(changes, commitMessage));
}
}, InvokeAfterUpdateMode.SILENT, "", new Consumer<VcsDirtyScopeManager>() {
public void consume(VcsDirtyScopeManager vcsDirtyScopeManager) {
vcsDirtyScopeManager.filePathsDirty(paths, null);
}
}, 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 GitCommit commit, @NotNull LocalChangeList changeList, @NotNull String commitMessage) {
return myPlatformFacade.getVcsHelper(myProject).commitChanges(commit.getChanges(), changeList, commitMessage);
}
private void notifyError(@NotNull String content, @NotNull GitCommit failedCommit, @NotNull List<GitCommit> successfulCommits) {
String description = "Cherry-pick failed for " + commitDetails(failedCommit) + "<br/>" + content;
description += getSuccessfulCommitDetailsIfAny(successfulCommits, description);
myPlatformFacade.getNotificator(myProject).notifyError("Cherry-pick failed", description);
}
@NotNull
private static String getSuccessfulCommitDetailsIfAny(@NotNull List<GitCommit> successfulCommits, @NotNull String description) {
if (!successfulCommits.isEmpty()) {
description += "<br/>However it succeeded for the following " + StringUtil.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);
}
return description;
}
@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);
}
@@ -101,190 +200,92 @@ 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, null);
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 CherryPickConflictResolver extends GitConflictResolver {
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);
}
@NotNull private final VirtualFile myRoot;
@NotNull private final String myCommitHash;
@NotNull private final String myCommitAuthor;
@NotNull private final String myCommitMessage;
@NotNull private final Git myGit;
@NotNull private final PlatformFacade myPlatformFacade;
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));
myGit = git;
myPlatformFacade = facade;
myRoot = root;
myCommitHash = commitHash;
myCommitAuthor = commitAuthor;
myCommitMessage = commitMessage;
}
if ((! myExceptions.isEmpty()) || (! myWarnings.isEmpty())) {
myExceptions.addAll(myWarnings);
AbstractVcsHelper.getInstance(project).showErrors(myExceptions, "Cherry-pick problems");
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() {
myPlatformFacade.getNotificator(myProject).notifyStrongWarning("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.",
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, myGit,
myPlatformFacade, myRoot,
myCommitHash, myCommitAuthor,
myCommitMessage)
.mergeNoProceed();
}
}
}
});
}
}
private void moveToCorrectLists() {
for (CherryPickedData pickedData : myCherryPickedData) {
final Collection<FilePath> filePaths = pickedData.getFiles();
final String message = pickedData.getCommitMessage();
private static class CherryPickMergeDialogCustomizer extends MergeDialogCustomizer {
if (filePaths.isEmpty()) continue;
private String myCommitHash;
private String myCommitAuthor;
private String myCommitMessage;
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()]));
}
}
}
@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));
}
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;
}
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 GroupOfListsProcessor() {
myHaveLostSomething = new HashSet<String>();
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);
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 getMultipleFileMergeDescription(Collection<VirtualFile> files) {
return "<html>Conflicts during cherry-picking commit <code>" + myCommitHash + "</code> made by " + myCommitAuthor + "<br/>" +
"<code>\"" + myCommitMessage + "\"</code></html>";
}
public Set<String> getHaveLostSomething() {
return myHaveLostSomething;
}
}
private static class CherryPickedData {
private final String myCommitMessage;
private final Collection<FilePath> myFiles;
private LocalChangeList myChangeList;
private Collection<Change> myChanges;
private CherryPickedData(@NotNull String message, @NotNull Collection<FilePath> files) {
myCommitMessage = message;
myFiles = files;
@Override
public String getLeftPanelTitle(VirtualFile file) {
return "Local changes";
}
public Collection<Change> getChanges() {
return myChanges;
}
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;
@Override
public String getRightPanelTitle(VirtualFile file, VcsRevisionNumber lastRevisionNumber) {
return "<html>Changes from cherry-pick <code>" + myCommitHash + "</code>";
}
}
@@ -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;
@@ -212,103 +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, ServiceManager.getService(Git.class), ServiceManager.getService(PlatformFacade.class), 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>";
}
}
}
@@ -19,6 +19,7 @@ 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;
@@ -58,10 +59,13 @@ 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.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;
@@ -1611,12 +1615,10 @@ public class GitLogUI implements Disposable {
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();
}
boolean autoCommit = true;
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() {
public void run() {
@@ -1629,6 +1631,22 @@ public class GitLogUI implements Disposable {
});
}
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
@NotNull
private MultiMap<VirtualFile, GitCommit> getSelectedCommitsAndCheck() {
@@ -92,7 +92,11 @@ Otherwise, please use 'git reset'
GitCommit commit = commit()
invokeCherryPick(commit)
assertMergeDialogShown()
assertChangeLists([DEFAULT, commit.getSubject()], commit.getSubject())
assertChangeLists([DEFAULT, newCommitMessage(commit)], newCommitMessage(commit))
}
String newCommitMessage(GitCommit commit) {
"${commit.description}\n(cherry-picked from ${commit.hash.value})"
}
@Test
@@ -151,7 +155,7 @@ Otherwise, please use 'git reset'
invokeCherryPick(commit)
assertMergeDialogShown()
assertTrue "Commit dialog was not shown", commitDialogShown
assertChangeLists([DEFAULT, commit.getSubject()], commit.getSubject())
assertChangeLists([DEFAULT, newCommitMessage(commit)], newCommitMessage(commit))
}
@Test
@@ -80,7 +80,8 @@ hint: and commit the result with 'git commit'
AbstractHash hash = AbstractHash.create(new SHA().toString())
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, null, null, null, null, null, null, null, null, changes, 0)
new GitCommit(hash, SHAHash.emulate(hash), "John Smith", null, null, commitMessage, commitMessage, null, null, null, null, null, null,
null, changes, 0)
}
void assertOnlyDefaultChangelist() {
@@ -113,8 +114,8 @@ hint: and commit the result with 'git commit'
ChangeListManager changeListManager = myPlatformFacade.getChangeListManager(myProject)
List<LocalChangeList> lists = changeListManager.changeLists
Collection<String> listNames = lists.collect { it.name }
assertEquals changeLists.toSet(), listNames.toSet()
assertEquals activeChangelist, changeListManager.defaultChangeList.name
assertEquals "Change lists are different", changeLists.toSet(), listNames.toSet()
assertEquals "Wrong active changelist", activeChangelist, changeListManager.defaultChangeList.name
}
String notificationContent(GitCommit commit) {