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