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