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