/* * Copyright 2000-2009 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * Created by IntelliJ IDEA. * User: yole * Date: 22.11.2006 * Time: 19:59:36 */ package com.intellij.openapi.vcs.changes.shelf; import com.intellij.lifecycle.PeriodicalTasksCloser; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.components.AbstractProjectComponent; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.diff.impl.patch.*; import com.intellij.openapi.diff.impl.patch.apply.ApplyFilePatchBase; import com.intellij.openapi.diff.impl.patch.formove.CustomBinaryPatchApplier; import com.intellij.openapi.diff.impl.patch.formove.PatchApplier; import com.intellij.openapi.progress.AsynchronousExecution; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.*; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.vcs.*; import com.intellij.openapi.vcs.changes.*; import com.intellij.openapi.vcs.changes.patch.ApplyPatchDefaultExecutor; import com.intellij.openapi.vcs.changes.patch.PatchFileType; import com.intellij.openapi.vcs.changes.patch.PatchNameChecker; import com.intellij.openapi.vcs.changes.ui.RollbackChangesDialog; import com.intellij.openapi.vcs.changes.ui.RollbackWorker; import com.intellij.openapi.vfs.CharsetToolkit; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.Consumer; import com.intellij.util.PathUtil; import com.intellij.util.SmartList; import com.intellij.util.continuation.*; import com.intellij.util.messages.MessageBus; import com.intellij.util.messages.Topic; import com.intellij.util.text.CharArrayCharSequence; import com.intellij.util.ui.UIUtil; import com.intellij.vcsUtil.FilesProgress; import org.jdom.Element; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import java.io.*; import java.util.*; public class ShelveChangesManager extends AbstractProjectComponent implements JDOMExternalizable { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vcs.changes.shelf.ShelveChangesManager"); public static ShelveChangesManager getInstance(Project project) { return PeriodicalTasksCloser.getInstance().safeGetComponent(project, ShelveChangesManager.class); } private final MessageBus myBus; private final List myShelvedChangeLists = new ArrayList(); private final List myRecycledShelvedChangeLists = new ArrayList(); @NonNls private static final String ATTRIBUTE_SHOW_RECYCLED = "show_recycled"; private final CompoundShelfFileProcessor myFileProcessor; public static final Topic SHELF_TOPIC = new Topic("shelf updates", ChangeListener.class); private boolean myShowRecycled; public ShelveChangesManager(final Project project, final MessageBus bus) { super(project); myBus = bus; if (project.isDefault()) { myFileProcessor = new CompoundShelfFileProcessor(null, PathManager.getConfigPath() + File.separator + "shelf"); } else { myFileProcessor = new CompoundShelfFileProcessor("shelf"); } } @Override @NonNls @NotNull public String getComponentName() { return "ShelveChangesManager"; } @Override public void readExternal(Element element) throws InvalidDataException { //noinspection unchecked final String showRecycled = element.getAttributeValue(ATTRIBUTE_SHOW_RECYCLED); if (showRecycled != null) { myShowRecycled = Boolean.parseBoolean(showRecycled); } else { myShowRecycled = true; } readExternal(element, myShelvedChangeLists, myRecycledShelvedChangeLists); } public static void readExternal(final Element element, final List changes, final List recycled) throws InvalidDataException { changes.addAll(ShelvedChangeList.readChanges(element, false, true)); recycled.addAll(ShelvedChangeList.readChanges(element, true, true)); } @Override public void writeExternal(Element element) throws WriteExternalException { element.setAttribute(ATTRIBUTE_SHOW_RECYCLED, Boolean.toString(myShowRecycled)); ShelvedChangeList.writeChanges(myShelvedChangeLists, myRecycledShelvedChangeLists, element); } public List getShelvedChangeLists() { return Collections.unmodifiableList(myShelvedChangeLists); } public ShelvedChangeList shelveChanges(final Collection changes, final String commitMessage, final boolean rollback) throws IOException, VcsException { final List textChanges = new ArrayList(); final List binaryFiles = new ArrayList(); for(Change change: changes) { if (ChangesUtil.getFilePath(change).isDirectory()) { continue; } if (change.getBeforeRevision() instanceof BinaryContentRevision || change.getAfterRevision() instanceof BinaryContentRevision) { binaryFiles.add(shelveBinaryFile(change)); } else { textChanges.add(change); } } final ShelvedChangeList changeList; try { File patchPath = getPatchPath(commitMessage); ProgressManager.checkCanceled(); final List patches = IdeaTextPatchBuilder.buildPatch(myProject, textChanges, myProject.getBaseDir().getPresentableUrl(), false); ProgressManager.checkCanceled(); CommitContext commitContext = new CommitContext(); baseRevisionsOfDvcsIntoContext(textChanges, commitContext); myFileProcessor.savePathFile( new CompoundShelfFileProcessor.ContentProvider(){ @Override public void writeContentTo(final Writer writer, CommitContext commitContext) throws IOException { UnifiedDiffWriter.write(myProject, patches, writer, "\n", commitContext); } }, patchPath, commitContext); changeList = new ShelvedChangeList(patchPath.toString(), commitMessage.replace('\n', ' '), binaryFiles); myShelvedChangeLists.add(changeList); ProgressManager.checkCanceled(); if (rollback) { final String operationName = UIUtil.removeMnemonic(RollbackChangesDialog.operationNameByChanges(myProject, changes)); new RollbackWorker(myProject, operationName).doRollback(changes, true, null, VcsBundle.message("shelve.changes.action")); } } finally { notifyStateChanged(); } return changeList; } private void baseRevisionsOfDvcsIntoContext(List textChanges, CommitContext commitContext) { ProjectLevelVcsManager vcsManager = ProjectLevelVcsManager.getInstance(myProject); if (vcsManager.dvcsUsedInProject() && VcsConfiguration.getInstance(myProject).INCLUDE_TEXT_INTO_SHELF) { final Set big = SelectFilesToAddTextsToPatchPanel.getBig(textChanges); final ArrayList toKeep = new ArrayList(); for (Change change : textChanges) { if (change.getBeforeRevision() == null || change.getAfterRevision() == null) continue; if (big.contains(change)) continue; FilePath filePath = ChangesUtil.getFilePath(change); final AbstractVcs vcs = vcsManager.getVcsFor(filePath); if (vcs != null && VcsType.distributed.equals(vcs.getType())) { toKeep.add(filePath); } } commitContext.putUserData(BaseRevisionTextPatchEP.ourPutBaseRevisionTextKey, true); commitContext.putUserData(BaseRevisionTextPatchEP.ourBaseRevisionPaths, toKeep); } } public ShelvedChangeList importFilePatches(final String fileName, final List patches, final PatchEP[] patchTransitExtensions) throws IOException { try { final File patchPath = getPatchPath(fileName); myFileProcessor.savePathFile( new CompoundShelfFileProcessor.ContentProvider(){ @Override public void writeContentTo(final Writer writer, CommitContext commitContext) throws IOException { UnifiedDiffWriter.write(myProject, patches, writer, "\n", patchTransitExtensions, commitContext); } }, patchPath, new CommitContext()); final ShelvedChangeList changeList = new ShelvedChangeList(patchPath.toString(), fileName.replace('\n', ' '), new SmartList()); myShelvedChangeLists.add(changeList); return changeList; } finally { notifyStateChanged(); } } public List gatherPatchFiles(final Collection files) { final List result = new ArrayList(); final LinkedList filesQueue = new LinkedList(files); while (! filesQueue.isEmpty()) { ProgressManager.checkCanceled(); final VirtualFile file = filesQueue.removeFirst(); if (file.isDirectory()) { filesQueue.addAll(Arrays.asList(file.getChildren())); continue; } if (PatchFileType.NAME.equals(file.getFileType().getName())) { result.add(file); } } return result; } public List importChangeLists(final Collection files, final Consumer exceptionConsumer) { final List result = new ArrayList(files.size()); try { final FilesProgress filesProgress = new FilesProgress(files.size(), "Processing "); for (VirtualFile file : files) { filesProgress.updateIndicator(file); final String description = file.getNameWithoutExtension().replace('_', ' '); final File patchPath = getPatchPath(description); final ShelvedChangeList list = new ShelvedChangeList(patchPath.getPath(), description, new SmartList(), file.getTimeStamp()); try { final List patchesList = loadPatches(myProject, file.getPath(), new CommitContext()); if (! patchesList.isEmpty()) { FileUtil.copy(new File(file.getPath()), patchPath); // add only if ok to read patch myShelvedChangeLists.add(list); result.add(list); } } catch (IOException e) { exceptionConsumer.consume(new VcsException(e)); } catch (PatchSyntaxException e) { exceptionConsumer.consume(new VcsException(e)); } } } finally { notifyStateChanged(); } return result; } private ShelvedBinaryFile shelveBinaryFile(final Change change) throws IOException { final ContentRevision beforeRevision = change.getBeforeRevision(); final ContentRevision afterRevision = change.getAfterRevision(); File beforeFile = beforeRevision == null ? null : beforeRevision.getFile().getIOFile(); File afterFile = afterRevision == null ? null : afterRevision.getFile().getIOFile(); String shelvedPath = null; if (afterFile != null) { String shelvedName = FileUtil.getNameWithoutExtension(afterFile.getName()); String shelvedExt = FileUtilRt.getExtension(afterFile.getName()); File shelvedFile = FileUtil.findSequentNonexistentFile(myFileProcessor.getBaseIODir(), shelvedName, shelvedExt); myFileProcessor.saveFile(afterRevision.getFile().getIOFile(), shelvedFile); shelvedPath = shelvedFile.getPath(); } String beforePath = ChangesUtil.getProjectRelativePath(myProject, beforeFile); String afterPath = ChangesUtil.getProjectRelativePath(myProject, afterFile); return new ShelvedBinaryFile(beforePath, afterPath, shelvedPath); } private void notifyStateChanged() { if (!myProject.isDisposed()) { myBus.syncPublisher(SHELF_TOPIC).stateChanged(new ChangeEvent(this)); } } private File getPatchPath(@NonNls final String commitMessage) { File file = myFileProcessor.getBaseIODir(); if (!file.exists()) { //noinspection ResultOfMethodCallIgnored file.mkdirs(); } return suggestPatchName(myProject, commitMessage.length() > PatchNameChecker.MAX ? commitMessage.substring(0, PatchNameChecker.MAX) : commitMessage, file, VcsConfiguration.PATCH); } public static File suggestPatchName(Project project, final String commitMessage, final File file, String extension) { @NonNls String defaultPath = PathUtil.suggestFileName(commitMessage); if (defaultPath.isEmpty()) { defaultPath = "unnamed"; } if (defaultPath.length() > PatchNameChecker.MAX - 10) { defaultPath = defaultPath.substring(0, PatchNameChecker.MAX - 10); } while (true) { final File nonexistentFile = FileUtil.findSequentNonexistentFile(file, defaultPath, extension == null ? VcsConfiguration.getInstance(project).getPatchFileExtension() : extension); if (nonexistentFile.getName().length() >= PatchNameChecker.MAX) { defaultPath = defaultPath.substring(0, defaultPath.length() - 1); continue; } return nonexistentFile; } } public void unshelveChangeList(final ShelvedChangeList changeList, @Nullable final List changes, @Nullable final List binaryFiles, final LocalChangeList targetChangeList) { unshelveChangeList(changeList, changes, binaryFiles, targetChangeList, true); } @AsynchronousExecution public void unshelveChangeList(final ShelvedChangeList changeList, @Nullable final List changes, @Nullable final List binaryFiles, @Nullable final LocalChangeList targetChangeList, boolean showSuccessNotification) { final Continuation continuation = Continuation.createForCurrentProgress(myProject, true, "Unshelve changes"); final GatheringContinuationContext initContext = new GatheringContinuationContext(); scheduleUnshelveChangeList(changeList, changes, binaryFiles, targetChangeList, showSuccessNotification, initContext, false, false, null, null); continuation.run(initContext.getList()); } @AsynchronousExecution public void scheduleUnshelveChangeList(final ShelvedChangeList changeList, @Nullable final List changes, @Nullable final List binaryFiles, @Nullable final LocalChangeList targetChangeList, final boolean showSuccessNotification, final ContinuationContext context, final boolean systemOperation, final boolean reverse, final String leftConflictTitle, final String rightConflictTitle) { context.next(new TaskDescriptor("", Where.AWT) { @Override public void run(ContinuationContext contextInner) { final List remainingPatches = new ArrayList(); final CommitContext commitContext = new CommitContext(); final List textFilePatches; try { textFilePatches = loadTextPatches(myProject, changeList, changes, remainingPatches, commitContext); } catch (IOException e) { LOG.info(e); PatchApplier.showError(myProject, "Cannot load patch(es): " + e.getMessage(), true); return; } catch (PatchSyntaxException e) { PatchApplier.showError(myProject, "Cannot load patch(es): " + e.getMessage(), true); LOG.info(e); return; } final List patches = new ArrayList(textFilePatches); final List remainingBinaries = new ArrayList(); final List binaryFilesToUnshelve = getBinaryFilesToUnshelve(changeList, binaryFiles, remainingBinaries); for (final ShelvedBinaryFile shelvedBinaryFile : binaryFilesToUnshelve) { patches.add(new ShelvedBinaryFilePatch(shelvedBinaryFile)); } final BinaryPatchApplier binaryPatchApplier = new BinaryPatchApplier(); final PatchApplier patchApplier = new PatchApplier(myProject, myProject.getBaseDir(), patches, targetChangeList, binaryPatchApplier, commitContext, reverse, leftConflictTitle, rightConflictTitle); patchApplier.setIsSystemOperation(systemOperation); // after patch applier part contextInner.next(new TaskDescriptor("", Where.AWT) { @Override public void run(ContinuationContext context) { remainingPatches.addAll(patchApplier.getRemainingPatches()); if (remainingPatches.isEmpty() && remainingBinaries.isEmpty()) { recycleChangeList(changeList); } else { saveRemainingPatches(changeList, remainingPatches, remainingBinaries, commitContext); } } }); patchApplier.scheduleSelf(showSuccessNotification, contextInner, systemOperation); } }); } private static List loadTextPatches(final Project project, final ShelvedChangeList changeList, final List changes, final List remainingPatches, final CommitContext commitContext) throws IOException, PatchSyntaxException { final List textFilePatches = loadPatches(project, changeList.PATH, commitContext); if (changes != null) { final Iterator iterator = textFilePatches.iterator(); while (iterator.hasNext()) { TextFilePatch patch = iterator.next(); if (!needUnshelve(patch, changes)) { remainingPatches.add(patch); iterator.remove(); } } } return textFilePatches; } private class BinaryPatchApplier implements CustomBinaryPatchApplier { private final List myAppliedPatches; private BinaryPatchApplier() { myAppliedPatches = new ArrayList(); } @Override @NotNull public ApplyPatchStatus apply(final List>> patches) throws IOException { for (Pair> patch : patches) { final ShelvedBinaryFilePatch shelvedPatch = patch.getSecond().getPatch(); unshelveBinaryFile(shelvedPatch.getShelvedBinaryFile(), patch.getFirst()); myAppliedPatches.add(shelvedPatch); } return ApplyPatchStatus.SUCCESS; } @Override @NotNull public List getAppliedPatches() { return myAppliedPatches; } } private static List getBinaryFilesToUnshelve(final ShelvedChangeList changeList, final List binaryFiles, final List remainingBinaries) { if (binaryFiles == null) { return new ArrayList(changeList.getBinaryFiles()); } ArrayList result = new ArrayList(); for(ShelvedBinaryFile file: changeList.getBinaryFiles()) { if (binaryFiles.contains(file)) { result.add(file); } else { remainingBinaries.add(file); } } return result; } @Nullable private FilePath unshelveBinaryFile(final ShelvedBinaryFile file, @NotNull final VirtualFile patchTarget) throws IOException { final Ref result = new Ref(); final Ref ex = new Ref(); final Ref patchedFileRef = new Ref(); final File shelvedFile = file.SHELVED_PATH == null ? null : new File(file.SHELVED_PATH); ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { try { result.set(new FilePathImpl(patchTarget)); if (shelvedFile == null) { patchTarget.delete(this); } else { patchTarget.setBinaryContent(FileUtil.loadFileBytes(shelvedFile)); patchedFileRef.set(patchTarget); } } catch (IOException e) { ex.set(e); } } }); if (!ex.isNull()) { throw ex.get(); } return result.get(); } private static boolean needUnshelve(final FilePatch patch, final List changes) { for(ShelvedChange change: changes) { if (Comparing.equal(patch.getBeforeName(), change.getBeforePath())) { return true; } } return false; } private static void writePatchesToFile(final Project project, final String path, final List remainingPatches, CommitContext commitContext) { try { OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(path), CharsetToolkit.UTF8_CHARSET); try { UnifiedDiffWriter.write(project, remainingPatches, writer, "\n", commitContext); } finally { writer.close(); } } catch (IOException e) { LOG.error(e); } } void saveRemainingPatches(final ShelvedChangeList changeList, final List remainingPatches, final List remainingBinaries, CommitContext commitContext) { final File newPath = getPatchPath(changeList.DESCRIPTION); try { FileUtil.copy(new File(changeList.PATH), newPath); } catch (IOException e) { // do not delete if cannot recycle return; } final ShelvedChangeList listCopy = new ShelvedChangeList(newPath.getAbsolutePath(), changeList.DESCRIPTION, new ArrayList(changeList.getBinaryFiles())); listCopy.DATE = changeList.DATE == null ? null : new Date(changeList.DATE.getTime()); writePatchesToFile(myProject, changeList.PATH, remainingPatches, commitContext); changeList.getBinaryFiles().retainAll(remainingBinaries); changeList.clearLoadedChanges(); recycleChangeList(listCopy, changeList); notifyStateChanged(); } public void restoreList(final ShelvedChangeList changeList) { myShelvedChangeLists.add(changeList); myRecycledShelvedChangeLists.remove(changeList); changeList.setRecycled(false); notifyStateChanged(); } public List getRecycledShelvedChangeLists() { return myRecycledShelvedChangeLists; } public void clearRecycled() { for (ShelvedChangeList list : myRecycledShelvedChangeLists) { deleteListImpl(list); } myRecycledShelvedChangeLists.clear(); notifyStateChanged(); } private void recycleChangeList(final ShelvedChangeList listCopy, final ShelvedChangeList newList) { if (newList != null) { for (Iterator shelvedChangeListIterator = listCopy.getBinaryFiles().iterator(); shelvedChangeListIterator.hasNext();) { final ShelvedBinaryFile binaryFile = shelvedChangeListIterator.next(); for (ShelvedBinaryFile newBinary : newList.getBinaryFiles()) { if (Comparing.equal(newBinary.BEFORE_PATH, binaryFile.BEFORE_PATH) && Comparing.equal(newBinary.AFTER_PATH, binaryFile.AFTER_PATH)) { shelvedChangeListIterator.remove(); } } } for (Iterator iterator = listCopy.getChanges(myProject).iterator(); iterator.hasNext();) { final ShelvedChange change = iterator.next(); for (ShelvedChange newChange : newList.getChanges(myProject)) { if (Comparing.equal(change.getBeforePath(), newChange.getBeforePath()) && Comparing.equal(change.getAfterPath(), newChange.getAfterPath())) { iterator.remove(); } } } // needed only if partial unshelve try { final CommitContext commitContext = new CommitContext(); final List patches = new ArrayList(); for (ShelvedChange change : listCopy.getChanges(myProject)) { patches.add(change.loadFilePatch(myProject, commitContext)); } writePatchesToFile(myProject, listCopy.PATH, patches, commitContext); } catch (IOException e) { LOG.info(e); // left file as is } catch (PatchSyntaxException e) { LOG.info(e); // left file as is } } if (! listCopy.getBinaryFiles().isEmpty() || ! listCopy.getChanges(myProject).isEmpty()) { listCopy.setRecycled(true); myRecycledShelvedChangeLists.add(listCopy); notifyStateChanged(); } } private void recycleChangeList(final ShelvedChangeList changeList) { recycleChangeList(changeList, null); myShelvedChangeLists.remove(changeList); notifyStateChanged(); } public void deleteChangeList(final ShelvedChangeList changeList) { deleteListImpl(changeList); if (! changeList.isRecycled()) { myShelvedChangeLists.remove(changeList); } else { myRecycledShelvedChangeLists.remove(changeList); } notifyStateChanged(); } private void deleteListImpl(final ShelvedChangeList changeList) { File file = new File(changeList.PATH); myFileProcessor.delete(file.getName()); for(ShelvedBinaryFile binaryFile: changeList.getBinaryFiles()) { final String path = binaryFile.SHELVED_PATH; if (path != null) { File binFile = new File(path); myFileProcessor.delete(binFile.getName()); } } } public void renameChangeList(final ShelvedChangeList changeList, final String newName) { changeList.DESCRIPTION = newName; notifyStateChanged(); } // todo problem: control usage public static List loadPatches(Project project, final String patchPath, CommitContext commitContext) throws IOException, PatchSyntaxException { char[] text = FileUtil.loadFileText(new File(patchPath), CharsetToolkit.UTF8); PatchReader reader = new PatchReader(new CharArrayCharSequence(text)); final List textFilePatches = reader.readAllPatches(); final TransparentlyFailedValueI>, PatchSyntaxException> additionalInfo = reader.getAdditionalInfo( null); ApplyPatchDefaultExecutor.applyAdditionalInfoBefore(project, additionalInfo, commitContext); return textFilePatches; } public static class ShelvedBinaryFilePatch extends FilePatch { private final ShelvedBinaryFile myShelvedBinaryFile; public ShelvedBinaryFilePatch(final ShelvedBinaryFile shelvedBinaryFile) { myShelvedBinaryFile = shelvedBinaryFile; setBeforeName(myShelvedBinaryFile.BEFORE_PATH); setAfterName(myShelvedBinaryFile.AFTER_PATH); } @Override public String getBeforeFileName() { String[] pathNameComponents = myShelvedBinaryFile.BEFORE_PATH.replace(File.separatorChar, '/').split("/"); return pathNameComponents [pathNameComponents.length-1]; } @Override public String getAfterFileName() { String[] pathNameComponents = myShelvedBinaryFile.AFTER_PATH.replace(File.separatorChar, '/').split("/"); return pathNameComponents [pathNameComponents.length-1]; } @Override public boolean isNewFile() { return myShelvedBinaryFile.BEFORE_PATH == null; } @Override public boolean isDeletedFile() { return myShelvedBinaryFile.AFTER_PATH == null; } public ShelvedBinaryFile getShelvedBinaryFile() { return myShelvedBinaryFile; } } public boolean isShowRecycled() { return myShowRecycled; } public void setShowRecycled(final boolean showRecycled) { myShowRecycled = showRecycled; notifyStateChanged(); } }