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