Merge branch 'master' of git.labs.intellij.net:idea/community
[idea/community.git] / plugins / git4idea / src / git4idea / history / browser / CherryPicker.java
1 /*
2  * Copyright 2000-2010 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package git4idea.history.browser;
17
18 import com.intellij.lifecycle.PeriodicalTasksCloser;
19 import com.intellij.openapi.application.ModalityState;
20 import com.intellij.openapi.ui.MessageType;
21 import com.intellij.openapi.vcs.AbstractVcsHelper;
22 import com.intellij.openapi.vcs.FilePath;
23 import com.intellij.openapi.vcs.ObjectsConvertor;
24 import com.intellij.openapi.vcs.VcsException;
25 import com.intellij.openapi.vcs.changes.*;
26 import com.intellij.openapi.vcs.checkin.CheckinEnvironment;
27 import com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier;
28 import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
29 import com.intellij.openapi.vfs.newvfs.RefreshSessionImpl;
30 import com.intellij.util.Consumer;
31 import com.intellij.util.containers.SLRUCache;
32 import git4idea.GitVcs;
33
34 import java.util.*;
35
36 public class CherryPicker {
37   private final GitVcs myVcs;
38   private final List<GitCommit> myCommits;
39   private final LowLevelAccess myAccess;
40
41   private final List<VcsException> myExceptions;
42   private final List<VcsException> myWarnings;
43   private final List<FilePath> myDirtyFiles;
44   private final List<String> myMessagesInOrder;
45   private final Map<String, Collection<FilePath>> myFilesToMove;
46
47   public CherryPicker(GitVcs vcs, final List<GitCommit> commits, LowLevelAccess access) {
48     myVcs = vcs;
49     myCommits = commits;
50     myAccess = access;
51
52     myExceptions = new ArrayList<VcsException>();
53     myWarnings = new ArrayList<VcsException>();
54
55     myDirtyFiles = new ArrayList<FilePath>();
56     myMessagesInOrder = new ArrayList<String>(commits.size());
57     myFilesToMove = new HashMap<String, Collection<FilePath>>();
58   }
59
60   public void execute() {
61     final CheckinEnvironment ce = myVcs.getCheckinEnvironment();
62
63     for (int i = 0; i < myCommits.size(); i++) {
64       cherryPickStep(ce, i);
65     }
66
67     // remove those that are in newer lists
68     checkListsForSamePaths();
69
70     final RefreshSessionImpl refreshSession = new RefreshSessionImpl(true, false, new Runnable() {
71       public void run() {
72         findAndProcessChangedForVcs();
73       }
74     });
75     refreshSession.addAllFiles(ObjectsConvertor.convert(myDirtyFiles, ObjectsConvertor.FILEPATH_TO_VIRTUAL, ObjectsConvertor.NOT_NULL));
76     refreshSession.launch();
77
78     showResults();
79   }
80
81   private void findAndProcessChangedForVcs() {
82     final ChangeListManager clm = PeriodicalTasksCloser.getInstance().safeGetComponent(myVcs.getProject(), ChangeListManager.class);
83     clm.invokeAfterUpdate(new Runnable() {
84       public void run() {
85         moveToCorrectLists(clm);
86       }
87     }, InvokeAfterUpdateMode.SILENT, "", new Consumer<VcsDirtyScopeManager>() {
88       public void consume(VcsDirtyScopeManager vcsDirtyScopeManager) {
89         vcsDirtyScopeManager.filePathsDirty(myDirtyFiles, null);
90       }
91     }, ModalityState.NON_MODAL);
92   }
93
94   private void showResults() {
95     if (myExceptions.isEmpty()) {
96       VcsBalloonProblemNotifier
97         .showOverChangesView(myVcs.getProject(), "Successful cherry-pick into working tree, please commit changes", MessageType.INFO);
98     } else {
99       VcsBalloonProblemNotifier.showOverChangesView(myVcs.getProject(), "Errors in cherry-pick", MessageType.ERROR);
100     }
101     if ((! myExceptions.isEmpty()) || (! myWarnings.isEmpty())) {
102       myExceptions.addAll(myWarnings);
103       AbstractVcsHelper.getInstance(myVcs.getProject()).showErrors(myExceptions, "Cherry-pick problems");
104     }
105   }
106
107   private void moveToCorrectLists(ChangeListManager clm) {
108     for (Map.Entry<String, Collection<FilePath>> entry : myFilesToMove.entrySet()) {
109       final Collection<FilePath> filePaths = entry.getValue();
110       final String message = entry.getKey();
111
112       if (filePaths.isEmpty()) continue;
113
114       final List<Change> changes = new ArrayList<Change>(filePaths.size());
115       for (FilePath filePath : filePaths) {
116         changes.add(clm.getChange(filePath));
117       }
118       if (! changes.isEmpty()) {
119         final LocalChangeList cl = clm.addChangeList(message, null);
120         clm.moveChangesTo(cl, changes.toArray(new Change[changes.size()]));
121       }
122     }
123   }
124
125   private void checkListsForSamePaths() {
126     final GroupOfListsProcessor listsProcessor = new GroupOfListsProcessor();
127     listsProcessor.process(myMessagesInOrder, myFilesToMove);
128     final Set<String> lostSet = listsProcessor.getHaveLostSomething();
129     markFilesMovesToNewerLists(myWarnings, lostSet, myFilesToMove);
130   }
131
132   private void cherryPickStep(CheckinEnvironment ce, int i) {
133     final GitCommit commit = myCommits.get(i);
134     final SHAHash hash = commit.getHash();
135     try {
136       myAccess.cherryPick(hash);
137     }
138     catch (VcsException e) {
139       myExceptions.add(e);
140     }
141     final List<Change> changes = commit.getChanges();
142
143     final Collection<FilePath> paths = ChangesUtil.getPaths(changes);
144     String message = ce.getDefaultMessageFor(paths.toArray(new FilePath[paths.size()]));
145     message = (message == null) ? new StringBuilder().append(commit.getDescription()).append("(cherry picked from commit ")
146       .append(hash.getValue()).append(")").toString() : message;
147
148     myMessagesInOrder.add(message);
149     myFilesToMove.put(message, paths);
150     myDirtyFiles.addAll(paths);
151   }
152
153   private void markFilesMovesToNewerLists(List<VcsException> exceptions, Set<String> lostSet, Map<String, Collection<FilePath>> filesToMove) {
154     if (! lostSet.isEmpty()) {
155       final StringBuilder sb = new StringBuilder("Some changes are moved from following list(s) to other:");
156       boolean first = true;
157       for (String s : lostSet) {
158         if (filesToMove.get(s).isEmpty()) {
159           final VcsException exc =
160             new VcsException("Changelist not created since all files moved to other cherry-pick(s): '" + s + "'");
161           exc.setIsWarning(true);
162           exceptions.add(exc);
163           continue;
164         }
165         sb.append(s);
166         if (! first) {
167           sb.append(", ");
168         }
169         first = false;
170       }
171       if (! first) {
172         final VcsException exc = new VcsException(sb.toString());
173         exc.setIsWarning(true);
174         exceptions.add(exc);
175       }
176     }
177   }
178
179   private static class GroupOfListsProcessor {
180     private final Set<String> myHaveLostSomething;
181
182     private GroupOfListsProcessor() {
183       myHaveLostSomething = new HashSet<String>();
184     }
185
186     public void process(final List<String> messagesInOrder, final Map<String, Collection<FilePath>> filesToMove) {
187       // remove those that are in newer lists
188       for (int i = 1; i < messagesInOrder.size(); i++) {
189         final String message = messagesInOrder.get(i);
190         final Collection<FilePath> currentFiles = filesToMove.get(message);
191
192         for (int j = 0; j < i; j++) {
193           final String previous = messagesInOrder.get(j);
194           final boolean somethingChanged = filesToMove.get(previous).removeAll(currentFiles);
195           if (somethingChanged) {
196             myHaveLostSomething.add(previous);
197           }
198         }
199       }
200     }
201
202     public Set<String> getHaveLostSomething() {
203       return myHaveLostSomething;
204     }
205   }
206 }