da65f1d9741b0e5c7e9f0e71ad8dfced5795ae5a
[idea/community.git] / platform / vcs-impl / src / com / intellij / openapi / vcs / changes / shelf / ShelveChangesManager.java
1 /*
2  * Copyright 2000-2015 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
17 /*
18  * Created by IntelliJ IDEA.
19  * User: yole
20  * Date: 22.11.2006
21  * Time: 19:59:36
22  */
23 package com.intellij.openapi.vcs.changes.shelf;
24
25 import com.intellij.lifecycle.PeriodicalTasksCloser;
26 import com.intellij.openapi.application.ApplicationManager;
27 import com.intellij.openapi.application.ModalityState;
28 import com.intellij.openapi.application.PathManager;
29 import com.intellij.openapi.application.impl.LaterInvocator;
30 import com.intellij.openapi.components.AbstractProjectComponent;
31 import com.intellij.openapi.components.PathMacroManager;
32 import com.intellij.openapi.components.TrackingPathMacroSubstitutor;
33 import com.intellij.openapi.diagnostic.Logger;
34 import com.intellij.openapi.diff.impl.patch.*;
35 import com.intellij.openapi.diff.impl.patch.apply.ApplyFilePatchBase;
36 import com.intellij.openapi.diff.impl.patch.formove.CustomBinaryPatchApplier;
37 import com.intellij.openapi.diff.impl.patch.formove.PatchApplier;
38 import com.intellij.openapi.options.BaseSchemeProcessor;
39 import com.intellij.openapi.options.SchemesManager;
40 import com.intellij.openapi.options.SchemesManagerFactory;
41 import com.intellij.openapi.progress.AsynchronousExecution;
42 import com.intellij.openapi.progress.ProgressIndicator;
43 import com.intellij.openapi.progress.ProgressManager;
44 import com.intellij.openapi.project.Project;
45 import com.intellij.openapi.util.*;
46 import com.intellij.openapi.util.io.FileUtil;
47 import com.intellij.openapi.util.text.StringUtil;
48 import com.intellij.openapi.vcs.*;
49 import com.intellij.openapi.vcs.changes.*;
50 import com.intellij.openapi.vcs.changes.patch.ApplyPatchDefaultExecutor;
51 import com.intellij.openapi.vcs.changes.patch.PatchFileType;
52 import com.intellij.openapi.vcs.changes.patch.PatchNameChecker;
53 import com.intellij.openapi.vcs.changes.ui.RollbackChangesDialog;
54 import com.intellij.openapi.vcs.changes.ui.RollbackWorker;
55 import com.intellij.openapi.vfs.CharsetToolkit;
56 import com.intellij.openapi.vfs.VirtualFile;
57 import com.intellij.util.Consumer;
58 import com.intellij.util.PathUtil;
59 import com.intellij.util.SmartList;
60 import com.intellij.util.containers.ContainerUtil;
61 import com.intellij.util.messages.MessageBus;
62 import com.intellij.util.messages.Topic;
63 import com.intellij.util.text.CharArrayCharSequence;
64 import com.intellij.util.text.UniqueNameGenerator;
65 import com.intellij.util.ui.UIUtil;
66 import com.intellij.vcsUtil.FilesProgress;
67 import org.jdom.Element;
68 import org.jetbrains.annotations.CalledInAny;
69 import org.jdom.Parent;
70 import org.jetbrains.annotations.NonNls;
71 import org.jetbrains.annotations.NotNull;
72 import org.jetbrains.annotations.Nullable;
73
74 import javax.swing.event.ChangeEvent;
75 import javax.swing.event.ChangeListener;
76 import java.io.*;
77 import java.util.*;
78
79 public class ShelveChangesManager extends AbstractProjectComponent implements JDOMExternalizable {
80   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vcs.changes.shelf.ShelveChangesManager");
81   @NonNls private static final String ELEMENT_CHANGELIST = "changelist";
82   @NonNls private static final String ELEMENT_RECYCLED_CHANGELIST = "recycled_changelist";
83   @NonNls private static final String DEFAULT_PATCH_NAME = "shelved";
84
85   @NotNull private final TrackingPathMacroSubstitutor myPathMacroSubstitutor;
86   @NotNull private final SchemesManager<ShelvedChangeList, ShelvedChangeList> mySchemeManager;
87
88   public static ShelveChangesManager getInstance(Project project) {
89     return PeriodicalTasksCloser.getInstance().safeGetComponent(project, ShelveChangesManager.class);
90   }
91
92   private static final String SHELVE_MANAGER_DIR_PATH = "shelf";
93   private final MessageBus myBus;
94
95   @NonNls private static final String ATTRIBUTE_SHOW_RECYCLED = "show_recycled";
96   @NotNull private final CompoundShelfFileProcessor myFileProcessor;
97
98   public static final Topic<ChangeListener> SHELF_TOPIC = new Topic<ChangeListener>("shelf updates", ChangeListener.class);
99   private boolean myShowRecycled;
100
101   public ShelveChangesManager(final Project project, final MessageBus bus) {
102     super(project);
103     myPathMacroSubstitutor = PathMacroManager.getInstance(myProject).createTrackingSubstitutor();
104     myBus = bus;
105     mySchemeManager =
106       SchemesManagerFactory.getInstance(project).create(SHELVE_MANAGER_DIR_PATH, new BaseSchemeProcessor<ShelvedChangeList>() {
107         @Nullable
108         @Override
109         public ShelvedChangeList readScheme(@NotNull Element element) throws InvalidDataException {
110           return readOneShelvedChangeList(element);
111         }
112
113         @Override
114         public Parent writeScheme(@NotNull ShelvedChangeList scheme) throws WriteExternalException {
115           Element child = new Element(ELEMENT_CHANGELIST);
116           scheme.writeExternal(child);
117           myPathMacroSubstitutor.collapsePaths(child);
118           return child;
119         }
120       });
121     myFileProcessor = new CompoundShelfFileProcessor(mySchemeManager.getRootDirectory());
122     ChangeListManager.getInstance(project).addDirectoryToIgnoreImplicitly(mySchemeManager.getRootDirectory().getAbsolutePath());
123     mySchemeManager.loadSchemes();
124   }
125
126   @NotNull
127   private ShelvedChangeList readOneShelvedChangeList(@NotNull Element element) throws InvalidDataException {
128     ShelvedChangeList data = new ShelvedChangeList();
129     myPathMacroSubstitutor.expandPaths(element);
130     data.readExternal(element);
131     return data;
132   }
133
134   @Override
135   @NonNls
136   @NotNull
137   public String getComponentName() {
138     return "ShelveChangesManager";
139   }
140
141   @Override
142   public void readExternal(Element element) throws InvalidDataException {
143     final String showRecycled = element.getAttributeValue(ATTRIBUTE_SHOW_RECYCLED);
144     if (showRecycled != null) {
145       myShowRecycled = Boolean.parseBoolean(showRecycled);
146     }
147     else {
148       myShowRecycled = true;
149     }
150     migrateOldShelfInfo(element, true);
151     migrateOldShelfInfo(element, false);
152   }
153
154   //load old shelf information from workspace.xml without moving .patch and binary files into new directory
155   private void migrateOldShelfInfo(@NotNull Element element, boolean recycled) throws InvalidDataException {
156     for (Element changeSetElement : element.getChildren(recycled ? ELEMENT_RECYCLED_CHANGELIST : ELEMENT_CHANGELIST)) {
157       ShelvedChangeList list = readOneShelvedChangeList(changeSetElement);
158       File uniqueDir = generateUniqueSchemePatchDir(list.DESCRIPTION, false);
159       list.setName(uniqueDir.getName());
160       list.setRecycled(recycled);
161       mySchemeManager.addNewScheme(list, false);
162     }
163   }
164
165   /**
166    * Should be called only once: when Settings Repository plugin runs first time
167    *
168    * @return collection of non-migrated or not deleted files to show a error somewhere outside
169    */
170   @NotNull
171   public Collection<String> checkAndMigrateOldPatchResourcesToNewSchemeStorage() {
172     Collection<String> nonMigratedPaths = ContainerUtil.newArrayList();
173     for (ShelvedChangeList list : mySchemeManager.getAllSchemes()) {
174       File patchDir = new File(myFileProcessor.getBaseDir(), list.getName());
175       nonMigratedPaths.addAll(migrateIfNeededToSchemeDir(list, patchDir));
176     }
177     return nonMigratedPaths;
178   }
179
180   @NotNull
181   private static Collection<String> migrateIfNeededToSchemeDir(@NotNull ShelvedChangeList list, @NotNull File targetDirectory) {
182     // it should be enough for migration to check if resource directory exists. If any bugs appeared add isAncestor checks for each path
183     if (targetDirectory.exists() || !targetDirectory.mkdirs()) return ContainerUtil.emptyList();
184     Collection<String> nonMigratedPaths = ContainerUtil.newArrayList();
185     //try to move .patch file
186     File patchFile = new File(list.PATH);
187     if (patchFile.exists()) {
188       File newPatchFile = getPatchFileInConfigDir(targetDirectory);
189       try {
190         FileUtil.copy(patchFile, newPatchFile);
191         list.PATH = newPatchFile.getPath();
192         FileUtil.delete(patchFile);
193       }
194       catch (IOException e) {
195         nonMigratedPaths.add(list.PATH);
196       }
197     }
198
199     for (ShelvedBinaryFile file : list.getBinaryFiles()) {
200       if (file.SHELVED_PATH != null) {
201         File shelvedFile = new File(file.SHELVED_PATH);
202         if (!StringUtil.isEmptyOrSpaces(file.AFTER_PATH) && shelvedFile.exists()) {
203           File newShelvedFile = new File(targetDirectory, PathUtil.getFileName(file.AFTER_PATH));
204           try {
205             FileUtil.copy(shelvedFile, newShelvedFile);
206             file.SHELVED_PATH = newShelvedFile.getPath();
207             FileUtil.delete(shelvedFile);
208           }
209           catch (IOException e) {
210             nonMigratedPaths.add(shelvedFile.getPath());
211           }
212         }
213       }
214     }
215     return nonMigratedPaths;
216   }
217
218   @Override
219   public void writeExternal(Element element) throws WriteExternalException {
220     element.setAttribute(ATTRIBUTE_SHOW_RECYCLED, Boolean.toString(myShowRecycled));
221   }
222
223   public List<ShelvedChangeList> getShelvedChangeLists() {
224     return getRecycled(false);
225   }
226
227   @NotNull
228   private List<ShelvedChangeList> getRecycled(final boolean recycled) {
229     return ContainerUtil.newUnmodifiableList(ContainerUtil.filter(mySchemeManager.getAllSchemes(), new Condition<ShelvedChangeList>() {
230       @Override
231       public boolean value(ShelvedChangeList list) {
232         return recycled ? list.isRecycled() : !list.isRecycled();
233       }
234     }));
235   }
236
237   public ShelvedChangeList shelveChanges(final Collection<Change> changes, final String commitMessage, final boolean rollback)
238     throws IOException, VcsException {
239     final ProgressIndicator progressIndicator = ProgressManager.getInstance().getProgressIndicator();
240     if (progressIndicator != null) {
241       progressIndicator.setText(VcsBundle.message("shelve.changes.progress.title"));
242     }
243     File schemePatchDir = generateUniqueSchemePatchDir(commitMessage, true);
244     final List<Change> textChanges = new ArrayList<Change>();
245     final List<ShelvedBinaryFile> binaryFiles = new ArrayList<ShelvedBinaryFile>();
246     for (Change change : changes) {
247       if (ChangesUtil.getFilePath(change).isDirectory()) {
248         continue;
249       }
250       if (change.getBeforeRevision() instanceof BinaryContentRevision || change.getAfterRevision() instanceof BinaryContentRevision) {
251         binaryFiles.add(shelveBinaryFile(schemePatchDir, change));
252       }
253       else {
254         textChanges.add(change);
255       }
256     }
257
258     final ShelvedChangeList changeList;
259     try {
260       File patchPath = getPatchFileInConfigDir(schemePatchDir);
261       ProgressManager.checkCanceled();
262       final List<FilePatch> patches =
263         IdeaTextPatchBuilder.buildPatch(myProject, textChanges, myProject.getBaseDir().getPresentableUrl(), false);
264       ProgressManager.checkCanceled();
265
266       CommitContext commitContext = new CommitContext();
267       baseRevisionsOfDvcsIntoContext(textChanges, commitContext);
268       myFileProcessor.savePathFile(
269         new CompoundShelfFileProcessor.ContentProvider() {
270           @Override
271           public void writeContentTo(final Writer writer, CommitContext commitContext) throws IOException {
272             UnifiedDiffWriter.write(myProject, patches, writer, "\n", commitContext);
273           }
274         },
275         patchPath, commitContext);
276
277       changeList = new ShelvedChangeList(patchPath.toString(), commitMessage.replace('\n', ' '), binaryFiles);
278       changeList.setName(schemePatchDir.getName());
279       ProgressManager.checkCanceled();
280       mySchemeManager.addNewScheme(changeList, false);
281
282       if (rollback) {
283         final String operationName = UIUtil.removeMnemonic(RollbackChangesDialog.operationNameByChanges(myProject, changes));
284         boolean modalContext = ApplicationManager.getApplication().isDispatchThread() && LaterInvocator.isInModalContext();
285         if (progressIndicator != null) {
286           progressIndicator.startNonCancelableSection();
287         }
288         new RollbackWorker(myProject, operationName, modalContext).
289           doRollback(changes, true, null, VcsBundle.message("shelve.changes.action"));
290       }
291     }
292     finally {
293       notifyStateChanged();
294     }
295
296     return changeList;
297   }
298
299   @NotNull
300   private static File getPatchFileInConfigDir(@NotNull File schemePatchDir) {
301     return new File(schemePatchDir, DEFAULT_PATCH_NAME + "." + VcsConfiguration.PATCH);
302   }
303
304   private void baseRevisionsOfDvcsIntoContext(List<Change> textChanges, CommitContext commitContext) {
305     ProjectLevelVcsManager vcsManager = ProjectLevelVcsManager.getInstance(myProject);
306     if (vcsManager.dvcsUsedInProject() && VcsConfiguration.getInstance(myProject).INCLUDE_TEXT_INTO_SHELF) {
307       final Set<Change> big = SelectFilesToAddTextsToPatchPanel.getBig(textChanges);
308       final ArrayList<FilePath> toKeep = new ArrayList<FilePath>();
309       for (Change change : textChanges) {
310         if (change.getBeforeRevision() == null || change.getAfterRevision() == null) continue;
311         if (big.contains(change)) continue;
312         FilePath filePath = ChangesUtil.getFilePath(change);
313         final AbstractVcs vcs = vcsManager.getVcsFor(filePath);
314         if (vcs != null && VcsType.distributed.equals(vcs.getType())) {
315           toKeep.add(filePath);
316         }
317       }
318       commitContext.putUserData(BaseRevisionTextPatchEP.ourPutBaseRevisionTextKey, true);
319       commitContext.putUserData(BaseRevisionTextPatchEP.ourBaseRevisionPaths, toKeep);
320     }
321   }
322
323   public ShelvedChangeList importFilePatches(final String fileName, final List<FilePatch> patches, final PatchEP[] patchTransitExtensions)
324     throws IOException {
325     try {
326       File schemePatchDir = generateUniqueSchemePatchDir(fileName, true);
327       File patchPath = getPatchFileInConfigDir(schemePatchDir);
328       myFileProcessor.savePathFile(
329         new CompoundShelfFileProcessor.ContentProvider() {
330           @Override
331           public void writeContentTo(final Writer writer, CommitContext commitContext) throws IOException {
332             UnifiedDiffWriter.write(myProject, patches, writer, "\n", patchTransitExtensions, commitContext);
333           }
334         },
335         patchPath, new CommitContext());
336
337       final ShelvedChangeList changeList =
338         new ShelvedChangeList(patchPath.toString(), fileName.replace('\n', ' '), new SmartList<ShelvedBinaryFile>());
339       changeList.setName(schemePatchDir.getName());
340       mySchemeManager.addNewScheme(changeList, false);
341       return changeList;
342     }
343     finally {
344       notifyStateChanged();
345     }
346   }
347
348   public List<VirtualFile> gatherPatchFiles(final Collection<VirtualFile> files) {
349     final List<VirtualFile> result = new ArrayList<VirtualFile>();
350
351     final LinkedList<VirtualFile> filesQueue = new LinkedList<VirtualFile>(files);
352     while (!filesQueue.isEmpty()) {
353       ProgressManager.checkCanceled();
354       final VirtualFile file = filesQueue.removeFirst();
355       if (file.isDirectory()) {
356         filesQueue.addAll(Arrays.asList(file.getChildren()));
357         continue;
358       }
359       if (PatchFileType.NAME.equals(file.getFileType().getName())) {
360         result.add(file);
361       }
362     }
363
364     return result;
365   }
366
367   public List<ShelvedChangeList> importChangeLists(final Collection<VirtualFile> files,
368                                                    final Consumer<VcsException> exceptionConsumer) {
369     final List<ShelvedChangeList> result = new ArrayList<ShelvedChangeList>(files.size());
370     try {
371       final FilesProgress filesProgress = new FilesProgress(files.size(), "Processing ");
372       for (VirtualFile file : files) {
373         filesProgress.updateIndicator(file);
374         final String description = file.getNameWithoutExtension().replace('_', ' ');
375         File schemeNameDir = generateUniqueSchemePatchDir(description, true);
376         final File patchPath = getPatchFileInConfigDir(schemeNameDir);
377         final ShelvedChangeList list = new ShelvedChangeList(patchPath.getPath(), description, new SmartList<ShelvedBinaryFile>(),
378                                                              file.getTimeStamp());
379         list.setName(schemeNameDir.getName());
380         try {
381           final List<TextFilePatch> patchesList = loadPatches(myProject, file.getPath(), new CommitContext());
382           if (!patchesList.isEmpty()) {
383             FileUtil.copy(new File(file.getPath()), patchPath);
384             // add only if ok to read patch
385             mySchemeManager.addNewScheme(list, false);
386             result.add(list);
387           }
388         }
389         catch (IOException e) {
390           exceptionConsumer.consume(new VcsException(e));
391         }
392         catch (PatchSyntaxException e) {
393           exceptionConsumer.consume(new VcsException(e));
394         }
395       }
396     }
397     finally {
398       notifyStateChanged();
399     }
400     return result;
401   }
402
403   private ShelvedBinaryFile shelveBinaryFile(@NotNull File schemePatchDir, final Change change) throws IOException {
404     final ContentRevision beforeRevision = change.getBeforeRevision();
405     final ContentRevision afterRevision = change.getAfterRevision();
406     File beforeFile = beforeRevision == null ? null : beforeRevision.getFile().getIOFile();
407     File afterFile = afterRevision == null ? null : afterRevision.getFile().getIOFile();
408     String shelvedPath = null;
409     if (afterFile != null) {
410       File shelvedFile = new File(schemePatchDir, afterFile.getName());
411       FileUtil.copy(afterRevision.getFile().getIOFile(), shelvedFile);
412       shelvedPath = shelvedFile.getPath();
413     }
414     String beforePath = ChangesUtil.getProjectRelativePath(myProject, beforeFile);
415     String afterPath = ChangesUtil.getProjectRelativePath(myProject, afterFile);
416     return new ShelvedBinaryFile(beforePath, afterPath, shelvedPath);
417   }
418
419   private void notifyStateChanged() {
420     if (!myProject.isDisposed()) {
421       myBus.syncPublisher(SHELF_TOPIC).stateChanged(new ChangeEvent(this));
422     }
423   }
424
425   @NotNull
426   private File generateUniqueSchemePatchDir(@NotNull final String defaultName, boolean createResourceDirectory) {
427     String uniqueName = UniqueNameGenerator
428       .generateUniqueName(shortenAndSanitize(defaultName), mySchemeManager.getAllSchemeNames());
429     File dir = new File(myFileProcessor.getBaseDir(), uniqueName);
430     if (createResourceDirectory && !dir.exists()) {
431       //noinspection ResultOfMethodCallIgnored
432       dir.mkdirs();
433     }
434     return dir;
435   }
436
437   @NotNull
438   // for create patch only; todo move or unify with unique directory creation
439   public static File suggestPatchName(Project project, @NotNull final String commitMessage, final File file, String extension) {
440     @NonNls String defaultPath = shortenAndSanitize(commitMessage);
441     while (true) {
442       final File nonexistentFile = FileUtil.findSequentNonexistentFile(file, defaultPath,
443                                                                        extension == null
444                                                                        ? VcsConfiguration.getInstance(project).getPatchFileExtension()
445                                                                        : extension);
446       if (nonexistentFile.getName().length() >= PatchNameChecker.MAX) {
447         defaultPath = defaultPath.substring(0, defaultPath.length() - 1);
448         continue;
449       }
450       return nonexistentFile;
451     }
452   }
453
454   @NotNull
455   private static String shortenAndSanitize(@NotNull String commitMessage) {
456     @NonNls String defaultPath = FileUtil.sanitizeFileName(commitMessage);
457     if (defaultPath.isEmpty()) {
458       defaultPath = "unnamed";
459     }
460     if (defaultPath.length() > PatchNameChecker.MAX - 10) {
461       defaultPath = defaultPath.substring(0, PatchNameChecker.MAX - 10);
462     }
463     return defaultPath;
464   }
465
466   @CalledInAny
467   public void unshelveChangeList(final ShelvedChangeList changeList,
468                                  @Nullable final List<ShelvedChange> changes,
469                                  @Nullable final List<ShelvedBinaryFile> binaryFiles,
470                                  @Nullable final LocalChangeList targetChangeList,
471                                  boolean showSuccessNotification) {
472     unshelveChangeList(changeList, changes, binaryFiles, targetChangeList, showSuccessNotification, false, false, null, null);
473   }
474
475   @CalledInAny
476   public void unshelveChangeList(final ShelvedChangeList changeList,
477                                  @Nullable final List<ShelvedChange> changes,
478                                  @Nullable final List<ShelvedBinaryFile> binaryFiles,
479                                  @Nullable final LocalChangeList targetChangeList,
480                                  final boolean showSuccessNotification,
481                                  final boolean systemOperation,
482                                  final boolean reverse,
483                                  final String leftConflictTitle,
484                                  final String rightConflictTitle) {
485     final List<FilePatch> remainingPatches = new ArrayList<FilePatch>();
486
487     final CommitContext commitContext = new CommitContext();
488     final List<TextFilePatch> textFilePatches;
489     try {
490       textFilePatches = loadTextPatches(myProject, changeList, changes, remainingPatches, commitContext);
491     }
492     catch (IOException e) {
493       LOG.info(e);
494       PatchApplier.showError(myProject, "Cannot load patch(es): " + e.getMessage(), true);
495       return;
496     }
497     catch (PatchSyntaxException e) {
498       PatchApplier.showError(myProject, "Cannot load patch(es): " + e.getMessage(), true);
499       LOG.info(e);
500       return;
501     }
502
503     final List<FilePatch> patches = new ArrayList<FilePatch>(textFilePatches);
504
505     final List<ShelvedBinaryFile> remainingBinaries = new ArrayList<ShelvedBinaryFile>();
506     final List<ShelvedBinaryFile> binaryFilesToUnshelve = getBinaryFilesToUnshelve(changeList, binaryFiles, remainingBinaries);
507
508     for (final ShelvedBinaryFile shelvedBinaryFile : binaryFilesToUnshelve) {
509       patches.add(new ShelvedBinaryFilePatch(shelvedBinaryFile));
510     }
511
512     ApplicationManager.getApplication().invokeAndWait(new Runnable() {
513       @Override
514       public void run() {
515         final BinaryPatchApplier binaryPatchApplier = new BinaryPatchApplier();
516         final PatchApplier<ShelvedBinaryFilePatch> patchApplier =
517           new PatchApplier<ShelvedBinaryFilePatch>(myProject, myProject.getBaseDir(),
518                                                    patches, targetChangeList, binaryPatchApplier, commitContext, reverse, leftConflictTitle,
519                                                    rightConflictTitle);
520         patchApplier.setIsSystemOperation(systemOperation);
521
522         remainingPatches.addAll(patchApplier.getRemainingPatches());
523
524         if (remainingPatches.isEmpty() && remainingBinaries.isEmpty()) {
525           recycleChangeList(changeList);
526         }
527         else {
528           saveRemainingPatches(changeList, remainingPatches, remainingBinaries, commitContext);
529         }
530
531         patchApplier.execute(showSuccessNotification, systemOperation);
532       }
533     }, ModalityState.defaultModalityState());
534   }
535
536   private static List<TextFilePatch> loadTextPatches(final Project project,
537                                                      final ShelvedChangeList changeList,
538                                                      final List<ShelvedChange> changes,
539                                                      final List<FilePatch> remainingPatches,
540                                                      final CommitContext commitContext)
541     throws IOException, PatchSyntaxException {
542     final List<TextFilePatch> textFilePatches = loadPatches(project, changeList.PATH, commitContext);
543
544     if (changes != null) {
545       final Iterator<TextFilePatch> iterator = textFilePatches.iterator();
546       while (iterator.hasNext()) {
547         TextFilePatch patch = iterator.next();
548         if (!needUnshelve(patch, changes)) {
549           remainingPatches.add(patch);
550           iterator.remove();
551         }
552       }
553     }
554     return textFilePatches;
555   }
556
557   private class BinaryPatchApplier implements CustomBinaryPatchApplier<ShelvedBinaryFilePatch> {
558     private final List<FilePatch> myAppliedPatches;
559
560     private BinaryPatchApplier() {
561       myAppliedPatches = new ArrayList<FilePatch>();
562     }
563
564     @Override
565     @NotNull
566     public ApplyPatchStatus apply(final List<Pair<VirtualFile, ApplyFilePatchBase<ShelvedBinaryFilePatch>>> patches) throws IOException {
567       for (Pair<VirtualFile, ApplyFilePatchBase<ShelvedBinaryFilePatch>> patch : patches) {
568         final ShelvedBinaryFilePatch shelvedPatch = patch.getSecond().getPatch();
569         unshelveBinaryFile(shelvedPatch.getShelvedBinaryFile(), patch.getFirst());
570         myAppliedPatches.add(shelvedPatch);
571       }
572       return ApplyPatchStatus.SUCCESS;
573     }
574
575     @Override
576     @NotNull
577     public List<FilePatch> getAppliedPatches() {
578       return myAppliedPatches;
579     }
580   }
581
582   private static List<ShelvedBinaryFile> getBinaryFilesToUnshelve(final ShelvedChangeList changeList,
583                                                                   final List<ShelvedBinaryFile> binaryFiles,
584                                                                   final List<ShelvedBinaryFile> remainingBinaries) {
585     if (binaryFiles == null) {
586       return new ArrayList<ShelvedBinaryFile>(changeList.getBinaryFiles());
587     }
588     ArrayList<ShelvedBinaryFile> result = new ArrayList<ShelvedBinaryFile>();
589     for (ShelvedBinaryFile file : changeList.getBinaryFiles()) {
590       if (binaryFiles.contains(file)) {
591         result.add(file);
592       }
593       else {
594         remainingBinaries.add(file);
595       }
596     }
597     return result;
598   }
599
600   private void unshelveBinaryFile(final ShelvedBinaryFile file, @NotNull final VirtualFile patchTarget) throws IOException {
601     final Ref<IOException> ex = new Ref<IOException>();
602     final Ref<VirtualFile> patchedFileRef = new Ref<VirtualFile>();
603     final File shelvedFile = file.SHELVED_PATH == null ? null : new File(file.SHELVED_PATH);
604
605     ApplicationManager.getApplication().runWriteAction(new Runnable() {
606       @Override
607       public void run() {
608         try {
609           if (shelvedFile == null) {
610             patchTarget.delete(this);
611           }
612           else {
613             patchTarget.setBinaryContent(FileUtil.loadFileBytes(shelvedFile));
614             patchedFileRef.set(patchTarget);
615           }
616         }
617         catch (IOException e) {
618           ex.set(e);
619         }
620       }
621     });
622     if (!ex.isNull()) {
623       throw ex.get();
624     }
625   }
626
627   private static boolean needUnshelve(final FilePatch patch, final List<ShelvedChange> changes) {
628     for (ShelvedChange change : changes) {
629       if (Comparing.equal(patch.getBeforeName(), change.getBeforePath())) {
630         return true;
631       }
632     }
633     return false;
634   }
635
636   private static void writePatchesToFile(final Project project,
637                                          final String path,
638                                          final List<FilePatch> remainingPatches,
639                                          CommitContext commitContext) {
640     try {
641       OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(path), CharsetToolkit.UTF8_CHARSET);
642       try {
643         UnifiedDiffWriter.write(project, remainingPatches, writer, "\n", commitContext);
644       }
645       finally {
646         writer.close();
647       }
648     }
649     catch (IOException e) {
650       LOG.error(e);
651     }
652   }
653
654   void saveRemainingPatches(final ShelvedChangeList changeList, final List<FilePatch> remainingPatches,
655                             final List<ShelvedBinaryFile> remainingBinaries, CommitContext commitContext) {
656     final File newPatchDir = generateUniqueSchemePatchDir(changeList.DESCRIPTION, true);
657     final File newPath = getPatchFileInConfigDir(newPatchDir);
658     try {
659       FileUtil.copy(new File(changeList.PATH), newPath);
660     }
661     catch (IOException e) {
662       // do not delete if cannot recycle
663       return;
664     }
665     final ShelvedChangeList listCopy = new ShelvedChangeList(newPath.getAbsolutePath(), changeList.DESCRIPTION,
666                                                              new ArrayList<ShelvedBinaryFile>(changeList.getBinaryFiles()));
667     listCopy.setName(newPatchDir.getName());
668     listCopy.DATE = changeList.DATE == null ? null : new Date(changeList.DATE.getTime());
669
670     writePatchesToFile(myProject, changeList.PATH, remainingPatches, commitContext);
671
672     changeList.getBinaryFiles().retainAll(remainingBinaries);
673     changeList.clearLoadedChanges();
674     recycleChangeList(listCopy, changeList);
675     // all newly create ShelvedChangeList have to be added to SchemesManger as new scheme
676     mySchemeManager.addNewScheme(listCopy, false);
677     notifyStateChanged();
678   }
679
680   public void restoreList(@NotNull final ShelvedChangeList changeList) {
681     ShelvedChangeList list = mySchemeManager.findSchemeByName(changeList.getName());
682     if (list != null) {
683       list.setRecycled(false);
684     }
685     notifyStateChanged();
686   }
687
688   public List<ShelvedChangeList> getRecycledShelvedChangeLists() {
689     return getRecycled(true);
690   }
691
692   public void clearRecycled() {
693     for (ShelvedChangeList list : getRecycledShelvedChangeLists()) {
694       deleteListImpl(list);
695       mySchemeManager.removeScheme(list);
696     }
697     notifyStateChanged();
698   }
699
700   private void recycleChangeList(@NotNull final ShelvedChangeList listCopy, @Nullable final ShelvedChangeList newList) {
701     if (newList != null) {
702       for (Iterator<ShelvedBinaryFile> shelvedChangeListIterator = listCopy.getBinaryFiles().iterator();
703            shelvedChangeListIterator.hasNext(); ) {
704         final ShelvedBinaryFile binaryFile = shelvedChangeListIterator.next();
705         for (ShelvedBinaryFile newBinary : newList.getBinaryFiles()) {
706           if (Comparing.equal(newBinary.BEFORE_PATH, binaryFile.BEFORE_PATH)
707               && Comparing.equal(newBinary.AFTER_PATH, binaryFile.AFTER_PATH)) {
708             shelvedChangeListIterator.remove();
709           }
710         }
711       }
712       for (Iterator<ShelvedChange> iterator = listCopy.getChanges(myProject).iterator(); iterator.hasNext(); ) {
713         final ShelvedChange change = iterator.next();
714         for (ShelvedChange newChange : newList.getChanges(myProject)) {
715           if (Comparing.equal(change.getBeforePath(), newChange.getBeforePath()) &&
716               Comparing.equal(change.getAfterPath(), newChange.getAfterPath())) {
717             iterator.remove();
718           }
719         }
720       }
721
722       // needed only if partial unshelve
723       try {
724         final CommitContext commitContext = new CommitContext();
725         final List<FilePatch> patches = new ArrayList<FilePatch>();
726         for (ShelvedChange change : listCopy.getChanges(myProject)) {
727           patches.add(change.loadFilePatch(myProject, commitContext));
728         }
729         writePatchesToFile(myProject, listCopy.PATH, patches, commitContext);
730       }
731       catch (IOException e) {
732         LOG.info(e);
733         // left file as is
734       }
735       catch (PatchSyntaxException e) {
736         LOG.info(e);
737         // left file as is
738       }
739     }
740
741     if (!listCopy.getBinaryFiles().isEmpty() || !listCopy.getChanges(myProject).isEmpty()) {
742       listCopy.setRecycled(true);
743       notifyStateChanged();
744     }
745   }
746
747   private void recycleChangeList(@NotNull final ShelvedChangeList changeList) {
748     recycleChangeList(changeList, null);
749     notifyStateChanged();
750   }
751
752   public void deleteChangeList(@NotNull final ShelvedChangeList changeList) {
753     deleteListImpl(changeList);
754     mySchemeManager.removeScheme(changeList);
755     notifyStateChanged();
756   }
757
758   private void deleteListImpl(@NotNull final ShelvedChangeList changeList) {
759     FileUtil.delete(new File(myFileProcessor.getBaseDir(), changeList.getName()));
760   }
761
762   public void renameChangeList(final ShelvedChangeList changeList, final String newName) {
763     changeList.DESCRIPTION = newName;
764     notifyStateChanged();
765   }
766
767   @NotNull
768   public static List<TextFilePatch> loadPatches(Project project,
769                                                 final String patchPath,
770                                                 CommitContext commitContext) throws IOException, PatchSyntaxException {
771     return loadPatches(project, patchPath, commitContext, true);
772   }
773
774   @NotNull
775   static List<? extends FilePatch> loadPatchesWithoutContent(Project project,
776                                                              final String patchPath,
777                                                              CommitContext commitContext) throws IOException, PatchSyntaxException {
778     return loadPatches(project, patchPath, commitContext, false);
779   }
780
781   private static List<TextFilePatch> loadPatches(Project project,
782                                                  final String patchPath,
783                                                  CommitContext commitContext,
784                                                  boolean loadContent) throws IOException, PatchSyntaxException {
785     char[] text = FileUtil.loadFileText(new File(patchPath), CharsetToolkit.UTF8);
786     PatchReader reader = new PatchReader(new CharArrayCharSequence(text), loadContent);
787     final List<TextFilePatch> textFilePatches = reader.readAllPatches();
788     final TransparentlyFailedValueI<Map<String, Map<String, CharSequence>>, PatchSyntaxException> additionalInfo = reader.getAdditionalInfo(
789       null);
790     ApplyPatchDefaultExecutor.applyAdditionalInfoBefore(project, additionalInfo, commitContext);
791     return textFilePatches;
792   }
793
794   public static class ShelvedBinaryFilePatch extends FilePatch {
795     private final ShelvedBinaryFile myShelvedBinaryFile;
796
797     public ShelvedBinaryFilePatch(final ShelvedBinaryFile shelvedBinaryFile) {
798       myShelvedBinaryFile = shelvedBinaryFile;
799       setBeforeName(myShelvedBinaryFile.BEFORE_PATH);
800       setAfterName(myShelvedBinaryFile.AFTER_PATH);
801     }
802
803     public static ShelvedBinaryFilePatch patchCopy(@NotNull final ShelvedBinaryFilePatch patch) {
804       return new ShelvedBinaryFilePatch(patch.getShelvedBinaryFile());
805     }
806
807     @Override
808     public String getBeforeFileName() {
809       return getFileName(myShelvedBinaryFile.BEFORE_PATH);
810     }
811
812     @Override
813     public String getAfterFileName() {
814       return getFileName(myShelvedBinaryFile.AFTER_PATH);
815     }
816
817     @Nullable
818     private static String getFileName(String filePath) {
819       return filePath != null ? PathUtil.getFileName(filePath) : null;
820     }
821
822     @Override
823     public boolean isNewFile() {
824       return myShelvedBinaryFile.BEFORE_PATH == null;
825     }
826
827     @Override
828     public boolean isDeletedFile() {
829       return myShelvedBinaryFile.AFTER_PATH == null;
830     }
831
832     public ShelvedBinaryFile getShelvedBinaryFile() {
833       return myShelvedBinaryFile;
834     }
835   }
836
837   public boolean isShowRecycled() {
838     return myShowRecycled;
839   }
840
841   public void setShowRecycled(final boolean showRecycled) {
842     myShowRecycled = showRecycled;
843     notifyStateChanged();
844   }
845 }