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