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