b89af90174992c0d429b3d867144de1ac91357f4
[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     final File newPatchDir = generateUniqueSchemePatchDir(changeList.DESCRIPTION, true);
759     final File newPath = getPatchFileInConfigDir(newPatchDir);
760     try {
761       FileUtil.copy(new File(changeList.PATH), newPath);
762     }
763     catch (IOException e) {
764       // do not delete if cannot recycle
765       return;
766     }
767     final ShelvedChangeList listCopy = new ShelvedChangeList(newPath.getAbsolutePath(), changeList.DESCRIPTION,
768                                                              new ArrayList<ShelvedBinaryFile>(changeList.getBinaryFiles()));
769     listCopy.markToDelete(changeList.isMarkedToDelete());
770     listCopy.setName(newPatchDir.getName());
771
772     writePatchesToFile(myProject, changeList.PATH, remainingPatches, commitContext);
773
774     changeList.getBinaryFiles().retainAll(remainingBinaries);
775     changeList.clearLoadedChanges();
776     recycleChangeList(listCopy, changeList);
777     // all newly create ShelvedChangeList have to be added to SchemesManger as new scheme
778     mySchemeManager.addNewScheme(listCopy, false);
779     notifyStateChanged();
780   }
781
782   public void restoreList(@NotNull final ShelvedChangeList changeList) {
783     ShelvedChangeList list = mySchemeManager.findSchemeByName(changeList.getName());
784     if (list != null) {
785       list.setRecycled(false);
786       list.updateDate();
787     }
788     notifyStateChanged();
789   }
790
791   @NotNull
792   public List<ShelvedChangeList> getRecycledShelvedChangeLists() {
793     return getRecycled(true);
794   }
795
796   public void clearRecycled() {
797     clearShelvedLists(getRecycledShelvedChangeLists());
798   }
799
800   private void clearShelvedLists(@NotNull List<ShelvedChangeList> shelvedLists) {
801     if (shelvedLists.isEmpty()) return;
802     for (ShelvedChangeList list : shelvedLists) {
803       deleteListImpl(list);
804       mySchemeManager.removeScheme(list);
805     }
806     notifyStateChanged();
807   }
808
809   private void recycleChangeList(@NotNull final ShelvedChangeList listCopy, @Nullable final ShelvedChangeList newList) {
810     if (newList != null) {
811       for (Iterator<ShelvedBinaryFile> shelvedChangeListIterator = listCopy.getBinaryFiles().iterator();
812            shelvedChangeListIterator.hasNext(); ) {
813         final ShelvedBinaryFile binaryFile = shelvedChangeListIterator.next();
814         for (ShelvedBinaryFile newBinary : newList.getBinaryFiles()) {
815           if (Comparing.equal(newBinary.BEFORE_PATH, binaryFile.BEFORE_PATH)
816               && Comparing.equal(newBinary.AFTER_PATH, binaryFile.AFTER_PATH)) {
817             shelvedChangeListIterator.remove();
818           }
819         }
820       }
821       for (Iterator<ShelvedChange> iterator = listCopy.getChanges(myProject).iterator(); iterator.hasNext(); ) {
822         final ShelvedChange change = iterator.next();
823         for (ShelvedChange newChange : newList.getChanges(myProject)) {
824           if (Comparing.equal(change.getBeforePath(), newChange.getBeforePath()) &&
825               Comparing.equal(change.getAfterPath(), newChange.getAfterPath())) {
826             iterator.remove();
827           }
828         }
829       }
830
831       // needed only if partial unshelve
832       try {
833         final CommitContext commitContext = new CommitContext();
834         final List<FilePatch> patches = new ArrayList<FilePatch>();
835         for (ShelvedChange change : listCopy.getChanges(myProject)) {
836           patches.add(change.loadFilePatch(myProject, commitContext));
837         }
838         writePatchesToFile(myProject, listCopy.PATH, patches, commitContext);
839       }
840       catch (IOException e) {
841         LOG.info(e);
842         // left file as is
843       }
844       catch (PatchSyntaxException e) {
845         LOG.info(e);
846         // left file as is
847       }
848     }
849
850     if (!listCopy.getBinaryFiles().isEmpty() || !listCopy.getChanges(myProject).isEmpty()) {
851       listCopy.setRecycled(true);
852       listCopy.updateDate();
853       notifyStateChanged();
854     }
855   }
856
857   public void recycleChangeList(@NotNull final ShelvedChangeList changeList) {
858     recycleChangeList(changeList, null);
859     notifyStateChanged();
860   }
861
862   public void deleteChangeList(@NotNull final ShelvedChangeList changeList) {
863     deleteListImpl(changeList);
864     mySchemeManager.removeScheme(changeList);
865     notifyStateChanged();
866   }
867
868   private void deleteListImpl(@NotNull final ShelvedChangeList changeList) {
869     FileUtil.delete(new File(myFileProcessor.getBaseDir(), changeList.getName()));
870     //backward compatibility deletion: if we didn't preform resource migration
871     FileUtil.delete(new File(changeList.PATH));
872     for (ShelvedBinaryFile binaryFile : changeList.getBinaryFiles()) {
873       final String path = binaryFile.SHELVED_PATH;
874       if (path != null) {
875         FileUtil.delete(new File(path));
876       }
877     }
878   }
879
880   public void renameChangeList(final ShelvedChangeList changeList, final String newName) {
881     changeList.DESCRIPTION = newName;
882     notifyStateChanged();
883   }
884
885   @NotNull
886   public static List<TextFilePatch> loadPatches(Project project,
887                                                 final String patchPath,
888                                                 CommitContext commitContext) throws IOException, PatchSyntaxException {
889     return loadPatches(project, patchPath, commitContext, true);
890   }
891
892   @NotNull
893   static List<? extends FilePatch> loadPatchesWithoutContent(Project project,
894                                                              final String patchPath,
895                                                              CommitContext commitContext) throws IOException, PatchSyntaxException {
896     return loadPatches(project, patchPath, commitContext, false);
897   }
898
899   private static List<TextFilePatch> loadPatches(Project project,
900                                                  final String patchPath,
901                                                  CommitContext commitContext,
902                                                  boolean loadContent) throws IOException, PatchSyntaxException {
903     char[] text = FileUtil.loadFileText(new File(patchPath), CharsetToolkit.UTF8);
904     PatchReader reader = new PatchReader(new CharArrayCharSequence(text), loadContent);
905     final List<TextFilePatch> textFilePatches = reader.readAllPatches();
906     final TransparentlyFailedValueI<Map<String, Map<String, CharSequence>>, PatchSyntaxException> additionalInfo = reader.getAdditionalInfo(
907       null);
908     ApplyPatchDefaultExecutor.applyAdditionalInfoBefore(project, additionalInfo, commitContext);
909     return textFilePatches;
910   }
911
912   public boolean isShowRecycled() {
913     return myShowRecycled;
914   }
915
916   public void setShowRecycled(final boolean showRecycled) {
917     myShowRecycled = showRecycled;
918     notifyStateChanged();
919   }
920 }