mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user