/*
* Copyright 2000-2016 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.
*/
package com.intellij.openapi.vcs.changes.shelf;
import com.intellij.concurrency.JobScheduler;
import com.intellij.lifecycle.PeriodicalTasksCloser;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.impl.LaterInvocator;
import com.intellij.openapi.components.AbstractProjectComponent;
import com.intellij.openapi.components.PathMacroManager;
import com.intellij.openapi.components.RoamingType;
import com.intellij.openapi.components.TrackingPathMacroSubstitutor;
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.options.BaseSchemeProcessor;
import com.intellij.openapi.options.SchemesManager;
import com.intellij.openapi.options.SchemesManagerFactory;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
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.Processor;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.messages.MessageBus;
import com.intellij.util.messages.Topic;
import com.intellij.util.text.CharArrayCharSequence;
import com.intellij.util.text.UniqueNameGenerator;
import com.intellij.util.ui.UIUtil;
import com.intellij.vcsUtil.FilesProgress;
import org.jdom.Element;
import org.jdom.Parent;
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.*;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class ShelveChangesManager extends AbstractProjectComponent implements JDOMExternalizable {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vcs.changes.shelf.ShelveChangesManager");
@NonNls private static final String ELEMENT_CHANGELIST = "changelist";
@NonNls private static final String ELEMENT_RECYCLED_CHANGELIST = "recycled_changelist";
@NonNls private static final String DEFAULT_PATCH_NAME = "shelved";
@NonNls private static final String REMOVE_FILES_FROM_SHELF_STRATEGY = "remove_strategy";
@NotNull private final TrackingPathMacroSubstitutor myPathMacroSubstitutor;
@NotNull private final SchemesManager<ShelvedChangeList, ShelvedChangeList> mySchemeManager;
private ScheduledFuture<?> myCleaningFuture;
private boolean myRemoveFilesFromShelf;
public static ShelveChangesManager getInstance(@NotNull Project project) {
return PeriodicalTasksCloser.getInstance().safeGetComponent(project, ShelveChangesManager.class);
}
private static final String SHELVE_MANAGER_DIR_PATH = "shelf";
private final MessageBus myBus;
@NonNls private static final String ATTRIBUTE_SHOW_RECYCLED = "show_recycled";
@NotNull private final CompoundShelfFileProcessor myFileProcessor;
public static final Topic<ChangeListener> SHELF_TOPIC = new Topic<>("shelf updates", ChangeListener.class);
private boolean myShowRecycled;
public ShelveChangesManager(final Project project, final MessageBus bus) {
super(project);
myPathMacroSubstitutor = PathMacroManager.getInstance(myProject).createTrackingSubstitutor();
myBus = bus;
mySchemeManager = SchemesManagerFactory.getInstance().createSchemesManager(SHELVE_MANAGER_DIR_PATH, new BaseSchemeProcessor<ShelvedChangeList>() {
@Nullable
@Override
public ShelvedChangeList readScheme(@NotNull Element element, boolean duringLoad) throws InvalidDataException {
return readOneShelvedChangeList(element);
}
@NotNull
@Override
public Parent writeScheme(@NotNull ShelvedChangeList scheme) throws WriteExternalException {
Element child = new Element(ELEMENT_CHANGELIST);
scheme.writeExternal(child);
myPathMacroSubstitutor.collapsePaths(child);
return child;
}
}, RoamingType.PER_USER);
myCleaningFuture = JobScheduler.getScheduler().scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
cleanSystemUnshelvedOlderOneWeek();
}
}, 1, 1, TimeUnit.DAYS);
Disposer.register(project, new Disposable() {
@Override
public void dispose() {
stopCleanScheduler();
}
});
File shelfDirectory = mySchemeManager.getRootDirectory();
myFileProcessor = new CompoundShelfFileProcessor(shelfDirectory);
// do not try to ignore when new project created,
// because it may lead to predefined ignore creation conflict; see ConvertExcludedToIgnoredTest etc
if (shelfDirectory.exists()) {
ChangeListManager.getInstance(project).addDirectoryToIgnoreImplicitly(shelfDirectory.getAbsolutePath());
}
}
private void stopCleanScheduler() {
if (myCleaningFuture != null) {
myCleaningFuture.cancel(false);
myCleaningFuture = null;
}
}
@Override
public void projectOpened() {
try {
mySchemeManager.loadSchemes();
//workaround for ignoring not valid patches, because readScheme doesn't support nullable value as it should be
filterNonValidShelvedChangeLists();
cleanSystemUnshelvedOlderOneWeek();
}
catch (Exception e) {
LOG.error("Couldn't read shelf information", e);
}
}
private void filterNonValidShelvedChangeLists() {
final List<ShelvedChangeList> allSchemes = ContainerUtil.newArrayList(mySchemeManager.getAllSchemes());
ContainerUtil.process(allSchemes, new Processor<ShelvedChangeList>() {
@Override
public boolean process(ShelvedChangeList shelvedChangeList) {
if (!shelvedChangeList.isValid()) {
mySchemeManager.removeScheme(shelvedChangeList);
}
return true;
}
});
}
@NotNull
public File getShelfResourcesDirectory() {
return myFileProcessor.getBaseDir();
}
@NotNull
private ShelvedChangeList readOneShelvedChangeList(@NotNull Element element) throws InvalidDataException {
ShelvedChangeList data = new ShelvedChangeList();
myPathMacroSubstitutor.expandPaths(element);
data.readExternal(element);
return data;
}
@Override
@NonNls
@NotNull
public String getComponentName() {
return "ShelveChangesManager";
}
@Override
public void readExternal(Element element) throws InvalidDataException {
final String showRecycled = element.getAttributeValue(ATTRIBUTE_SHOW_RECYCLED);
myShowRecycled = showRecycled == null || Boolean.parseBoolean(showRecycled);
String removeFilesStrategy = JDOMExternalizerUtil.readField(element, REMOVE_FILES_FROM_SHELF_STRATEGY);
myRemoveFilesFromShelf = removeFilesStrategy != null && Boolean.parseBoolean(removeFilesStrategy);
migrateOldShelfInfo(element, true);
migrateOldShelfInfo(element, false);
}
//load old shelf information from workspace.xml without moving .patch and binary files into new directory
private void migrateOldShelfInfo(@NotNull Element element, boolean recycled) throws InvalidDataException {
for (Element changeSetElement : element.getChildren(recycled ? ELEMENT_RECYCLED_CHANGELIST : ELEMENT_CHANGELIST)) {
ShelvedChangeList list = readOneShelvedChangeList(changeSetElement);
if (!list.isValid()) break;
File uniqueDir = generateUniqueSchemePatchDir(list.DESCRIPTION, false);
list.setName(uniqueDir.getName());
list.setRecycled(recycled);
mySchemeManager.addNewScheme(list, false);
}
}
/**
* Should be called only once: when Settings Repository plugin runs first time
*
* @return collection of non-migrated or not deleted files to show a error somewhere outside
*/
@NotNull
public Collection<String> checkAndMigrateOldPatchResourcesToNewSchemeStorage() {
Collection<String> nonMigratedPaths = ContainerUtil.newArrayList();
for (ShelvedChangeList list : mySchemeManager.getAllSchemes()) {
File patchDir = new File(myFileProcessor.getBaseDir(), list.getName());
nonMigratedPaths.addAll(migrateIfNeededToSchemeDir(list, patchDir));
}
return nonMigratedPaths;
}
@NotNull
private static Collection<String> migrateIfNeededToSchemeDir(@NotNull ShelvedChangeList list, @NotNull File targetDirectory) {
// it should be enough for migration to check if resource directory exists. If any bugs appeared add isAncestor checks for each path
if (targetDirectory.exists() || !targetDirectory.mkdirs()) return ContainerUtil.emptyList();
Collection<String> nonMigratedPaths = ContainerUtil.newArrayList();
//try to move .patch file
File patchFile = new File(list.PATH);
if (patchFile.exists()) {
File newPatchFile = getPatchFileInConfigDir(targetDirectory);
try {
FileUtil.copy(patchFile, newPatchFile);
list.PATH = FileUtil.toSystemIndependentName(newPatchFile.getPath());
FileUtil.delete(patchFile);
}
catch (IOException e) {
nonMigratedPaths.add(list.PATH);
}
}
for (ShelvedBinaryFile file : list.getBinaryFiles()) {
if (file.SHELVED_PATH != null) {
File shelvedFile = new File(file.SHELVED_PATH);
if (!StringUtil.isEmptyOrSpaces(file.AFTER_PATH) && shelvedFile.exists()) {
File newShelvedFile = new File(targetDirectory, PathUtil.getFileName(file.AFTER_PATH));
try {
FileUtil.copy(shelvedFile, newShelvedFile);
file.SHELVED_PATH = FileUtil.toSystemIndependentName(newShelvedFile.getPath());
FileUtil.delete(shelvedFile);
}
catch (IOException e) {
nonMigratedPaths.add(shelvedFile.getPath());
}
}
}
}
return nonMigratedPaths;
}
@Override
public void writeExternal(Element element) throws WriteExternalException {
element.setAttribute(ATTRIBUTE_SHOW_RECYCLED, Boolean.toString(myShowRecycled));
JDOMExternalizerUtil.writeField(element, REMOVE_FILES_FROM_SHELF_STRATEGY, Boolean.toString(isRemoveFilesFromShelf()));
}
@NotNull
public List<ShelvedChangeList> getShelvedChangeLists() {
return getRecycled(false);
}
@NotNull
private List<ShelvedChangeList> getRecycled(final boolean recycled) {
return ContainerUtil.newUnmodifiableList(ContainerUtil.filter(mySchemeManager.getAllSchemes(), new Condition<ShelvedChangeList>() {
@Override
public boolean value(ShelvedChangeList list) {
return recycled == list.isRecycled();
}
}));
}
public ShelvedChangeList shelveChanges(final Collection<Change> changes, final String commitMessage, final boolean rollback)
throws IOException, VcsException {
return shelveChanges(changes, commitMessage, rollback, false);
}
public ShelvedChangeList shelveChanges(final Collection<Change> changes, final String commitMessage, final boolean rollback, boolean markToBeDeleted)
throws IOException, VcsException {
final ProgressIndicator progressIndicator = ProgressManager.getInstance().getProgressIndicator();
if (progressIndicator != null) {
progressIndicator.setText(VcsBundle.message("shelve.changes.progress.title"));
}
File schemePatchDir = generateUniqueSchemePatchDir(commitMessage, true);
final List<Change> textChanges = new ArrayList<>();
final List<ShelvedBinaryFile> 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(schemePatchDir, change));
}
else {
textChanges.add(change);
}
}
final ShelvedChangeList changeList;
try {
File patchPath = getPatchFileInConfigDir(schemePatchDir);
ProgressManager.checkCanceled();
final List<FilePatch> 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(@NotNull final Writer writer, @NotNull CommitContext commitContext) throws IOException {
UnifiedDiffWriter.write(myProject, patches, writer, "\n", commitContext);
}
}, patchPath, commitContext);
changeList = new ShelvedChangeList(patchPath.toString(), commitMessage.replace('\n', ' '), binaryFiles);
changeList.markToDelete(markToBeDeleted);
changeList.setName(schemePatchDir.getName());
ProgressManager.checkCanceled();
mySchemeManager.addNewScheme(changeList, false);
if (rollback) {
final String operationName = UIUtil.removeMnemonic(RollbackChangesDialog.operationNameByChanges(myProject, changes));
boolean modalContext = ApplicationManager.getApplication().isDispatchThread() && LaterInvocator.isInModalContext();
if (progressIndicator != null) {
progressIndicator.startNonCancelableSection();
}
new RollbackWorker(myProject, operationName, modalContext).
doRollback(changes, true, null, VcsBundle.message("shelve.changes.action"));
}
}
finally {
notifyStateChanged();
}
return changeList;
}
public void unshelveSilentlyAsynchronously(@NotNull final Project project,
@NotNull final List<ShelvedChangeList> selectedChangeLists,
@NotNull final List<ShelvedChange> selectedChanges,
@NotNull final List<ShelvedBinaryFile> selectedBinaryChanges,
@Nullable final LocalChangeList forcePredefinedOneChangelist) {
ProgressManager.getInstance().run(new Task.Backgroundable(project, VcsBundle.message("unshelve.changes.progress.title"), true) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
for (ShelvedChangeList changeList : selectedChangeLists) {
List<ShelvedChange> changesForChangelist =
ContainerUtil.newArrayList(ContainerUtil.intersection(changeList.getChanges(myProject), selectedChanges));
List<ShelvedBinaryFile> binariesForChangelist =
ContainerUtil.newArrayList(ContainerUtil.intersection(changeList.getBinaryFiles(), selectedBinaryChanges));
boolean shouldUnshelveAllList = changesForChangelist.isEmpty() && binariesForChangelist.isEmpty();
unshelveChangeList(changeList, shouldUnshelveAllList ? null : changesForChangelist,
shouldUnshelveAllList ? null : binariesForChangelist,
forcePredefinedOneChangelist != null ? forcePredefinedOneChangelist : getChangeListUnshelveTo(changeList),
true);
}
}
});
}
@NotNull
private LocalChangeList getChangeListUnshelveTo(@NotNull ShelvedChangeList list) {
String changeListName = list.DESCRIPTION;
ChangeListManager manager = ChangeListManager.getInstance(myProject);
LocalChangeList localChangeList = manager.findChangeList(changeListName);
if (localChangeList != null) return localChangeList;
if (list.isMarkedToDelete()) {
localChangeList = ChangeListUtil.getPredefinedChangeList(changeListName, manager);
}
return localChangeList != null ? localChangeList : manager.addChangeList(changeListName, "");
}
@NotNull
private static File getPatchFileInConfigDir(@NotNull File schemePatchDir) {
return new File(schemePatchDir, DEFAULT_PATCH_NAME + "." + VcsConfiguration.PATCH);
}
private void baseRevisionsOfDvcsIntoContext(List<Change> textChanges, CommitContext commitContext) {
ProjectLevelVcsManager vcsManager = ProjectLevelVcsManager.getInstance(myProject);
if (vcsManager.dvcsUsedInProject() && VcsConfiguration.getInstance(myProject).INCLUDE_TEXT_INTO_SHELF) {
final Set<Change> big = SelectFilesToAddTextsToPatchPanel.getBig(textChanges);
final ArrayList<FilePath> 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<FilePatch> patches, final PatchEP[] patchTransitExtensions) throws IOException {
try {
File schemePatchDir = generateUniqueSchemePatchDir(fileName, true);
File patchPath = getPatchFileInConfigDir(schemePatchDir);
myFileProcessor.savePathFile(new CompoundShelfFileProcessor.ContentProvider() {
@Override
public void writeContentTo(@NotNull final Writer writer, @NotNull 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<>());
changeList.setName(schemePatchDir.getName());
mySchemeManager.addNewScheme(changeList, false);
return changeList;
}
finally {
notifyStateChanged();
}
}
public List<VirtualFile> gatherPatchFiles(final Collection<VirtualFile> files) {
final List<VirtualFile> result = new ArrayList<>();
final LinkedList<VirtualFile> 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<ShelvedChangeList> importChangeLists(final Collection<VirtualFile> files, final Consumer<VcsException> exceptionConsumer) {
final List<ShelvedChangeList> 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('_', ' ');
File schemeNameDir = generateUniqueSchemePatchDir(description, true);
final File patchPath = getPatchFileInConfigDir(schemeNameDir);
final ShelvedChangeList list = new ShelvedChangeList(patchPath.getPath(), description, new SmartList<>(), file.getTimeStamp());
list.setName(schemeNameDir.getName());
try {
final List<TextFilePatch> patchesList = loadPatches(myProject, file.getPath(), new CommitContext());
if (!patchesList.isEmpty()) {
FileUtil.copy(new File(file.getPath()), patchPath);
// add only if ok to read patch
mySchemeManager.addNewScheme(list, false);
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(@NotNull File schemePatchDir, 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) {
File shelvedFile = new File(schemePatchDir, afterFile.getName());
FileUtil.copy(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));
}
}
@NotNull
private File generateUniqueSchemePatchDir(@NotNull final String defaultName, boolean createResourceDirectory) {
ignoreShelfDirectoryIfFirstShelf();
String uniqueName = UniqueNameGenerator.generateUniqueName(shortenAndSanitize(defaultName), mySchemeManager.getAllSchemeNames());
File dir = new File(myFileProcessor.getBaseDir(), uniqueName);
if (createResourceDirectory && !dir.exists()) {
//noinspection ResultOfMethodCallIgnored
dir.mkdirs();
}
return dir;
}
private void ignoreShelfDirectoryIfFirstShelf() {
File shelfDir = getShelfResourcesDirectory();
//check that shelf directory wasn't exist before that to ignore it only once
if (!shelfDir.exists()) {
ChangeListManager.getInstance(myProject).addDirectoryToIgnoreImplicitly(shelfDir.getAbsolutePath());
}
}
@NotNull
// for create patch only
public static File suggestPatchName(Project project, @NotNull final String commitMessage, final File file, String extension) {
@NonNls String defaultPath = shortenAndSanitize(commitMessage);
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;
}
}
@NotNull
private static String shortenAndSanitize(@NotNull String commitMessage) {
@NonNls String defaultPath = FileUtil.sanitizeFileName(commitMessage);
if (defaultPath.isEmpty()) {
defaultPath = "unnamed";
}
if (defaultPath.length() > PatchNameChecker.MAX - 10) {
defaultPath = defaultPath.substring(0, PatchNameChecker.MAX - 10);
}
return defaultPath;
}
@CalledInAny
public void unshelveChangeList(final ShelvedChangeList changeList,
@Nullable final List<ShelvedChange> changes,
@Nullable final List<ShelvedBinaryFile> binaryFiles,
@Nullable final LocalChangeList targetChangeList,
boolean showSuccessNotification) {
unshelveChangeList(changeList, changes, binaryFiles, targetChangeList, showSuccessNotification, false, false, null, null);
}
@CalledInAny
public void unshelveChangeList(final ShelvedChangeList changeList,
@Nullable final List<ShelvedChange> changes,
@Nullable final List<ShelvedBinaryFile> binaryFiles,
@Nullable final LocalChangeList targetChangeList,
final boolean showSuccessNotification,
final boolean systemOperation,
final boolean reverse,
final String leftConflictTitle,
final String rightConflictTitle) {
final List<FilePatch> remainingPatches = new ArrayList<>();
final CommitContext commitContext = new CommitContext();
final List<TextFilePatch> 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<FilePatch> patches = new ArrayList<>(textFilePatches);
final List<ShelvedBinaryFile> remainingBinaries = new ArrayList<>();
final List<ShelvedBinaryFile> binaryFilesToUnshelve = getBinaryFilesToUnshelve(changeList, binaryFiles, remainingBinaries);
for (final ShelvedBinaryFile shelvedBinaryFile : binaryFilesToUnshelve) {
patches.add(new ShelvedBinaryFilePatch(shelvedBinaryFile));
}
ApplicationManager.getApplication().invokeAndWait(new Runnable() {
@Override
public void run() {
final BinaryPatchApplier binaryPatchApplier = new BinaryPatchApplier();
final PatchApplier<ShelvedBinaryFilePatch> patchApplier =
new PatchApplier<>(myProject, myProject.getBaseDir(), patches, targetChangeList, binaryPatchApplier, commitContext, reverse, leftConflictTitle,
rightConflictTitle);
patchApplier.setIsSystemOperation(systemOperation);
patchApplier.execute(showSuccessNotification, systemOperation);
if (isRemoveFilesFromShelf() || systemOperation) {
remainingPatches.addAll(patchApplier.getRemainingPatches());
if (remainingPatches.isEmpty() && remainingBinaries.isEmpty()) {
recycleChangeList(changeList);
}
else {
saveRemainingPatches(changeList, remainingPatches, remainingBinaries, commitContext);
}
}
}
}, ModalityState.defaultModalityState());
}
private static List<TextFilePatch> loadTextPatches(final Project project,
final ShelvedChangeList changeList,
final List<ShelvedChange> changes,
final List<FilePatch> remainingPatches,
final CommitContext commitContext) throws IOException, PatchSyntaxException {
final List<TextFilePatch> textFilePatches = loadPatches(project, changeList.PATH, commitContext);
if (changes != null) {
final Iterator<TextFilePatch> iterator = textFilePatches.iterator();
while (iterator.hasNext()) {
TextFilePatch patch = iterator.next();
if (!needUnshelve(patch, changes)) {
remainingPatches.add(patch);
iterator.remove();
}
}
}
return textFilePatches;
}
public void setRemoveFilesFromShelf(boolean removeFilesFromShelf) {
myRemoveFilesFromShelf = removeFilesFromShelf;
}
public boolean isRemoveFilesFromShelf() {
return myRemoveFilesFromShelf;
}
private void cleanSystemUnshelvedOlderOneWeek() {
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_MONTH, -7);
cleanUnshelved(true, cal.getTimeInMillis());
}
public void cleanUnshelved(final boolean onlyMarkedToDelete, long timeBefore) {
final Date limitDate = new Date(timeBefore);
final List<ShelvedChangeList> toDelete = ContainerUtil.filter(mySchemeManager.getAllSchemes(), new Condition<ShelvedChangeList>() {
@Override
public boolean value(ShelvedChangeList list) {
return (list.isRecycled()) && list.DATE.before(limitDate) && (!onlyMarkedToDelete || list.isMarkedToDelete());
}
});
clearShelvedLists(toDelete);
}
private class BinaryPatchApplier implements CustomBinaryPatchApplier<ShelvedBinaryFilePatch> {
private final List<FilePatch> myAppliedPatches;
private BinaryPatchApplier() {
myAppliedPatches = new ArrayList<>();
}
@Override
@NotNull
public ApplyPatchStatus apply(final List<Pair<VirtualFile, ApplyFilePatchBase<ShelvedBinaryFilePatch>>> patches) throws IOException {
for (Pair<VirtualFile, ApplyFilePatchBase<ShelvedBinaryFilePatch>> patch : patches) {
final ShelvedBinaryFilePatch shelvedPatch = patch.getSecond().getPatch();
unshelveBinaryFile(shelvedPatch.getShelvedBinaryFile(), patch.getFirst());
myAppliedPatches.add(shelvedPatch);
}
return ApplyPatchStatus.SUCCESS;
}
@Override
@NotNull
public List<FilePatch> getAppliedPatches() {
return myAppliedPatches;
}
}
private static List<ShelvedBinaryFile> getBinaryFilesToUnshelve(final ShelvedChangeList changeList,
final List<ShelvedBinaryFile> binaryFiles,
final List<ShelvedBinaryFile> remainingBinaries) {
if (binaryFiles == null) {
return new ArrayList<>(changeList.getBinaryFiles());
}
ArrayList<ShelvedBinaryFile> result = new ArrayList<>();
for (ShelvedBinaryFile file : changeList.getBinaryFiles()) {
if (binaryFiles.contains(file)) {
result.add(file);
}
else {
remainingBinaries.add(file);
}
}
return result;
}
private void unshelveBinaryFile(final ShelvedBinaryFile file, @NotNull final VirtualFile patchTarget) throws IOException {
final Ref<IOException> ex = new Ref<>();
final Ref<VirtualFile> 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 {
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();
}
}
private static boolean needUnshelve(final FilePatch patch, final List<ShelvedChange> 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<FilePatch> 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);
}
}
public void saveRemainingPatches(final ShelvedChangeList changeList,
final List<FilePatch> remainingPatches,
final List<ShelvedBinaryFile> remainingBinaries,
CommitContext commitContext) {
ShelvedChangeList listCopy;
try {
listCopy = !changeList.isRecycled() ? createRecycledChangelist(changeList) : null;
}
catch (IOException e) {
// do not delete if cannot recycle
return;
}
writePatchesToFile(myProject, changeList.PATH, remainingPatches, commitContext);
changeList.getBinaryFiles().retainAll(remainingBinaries);
changeList.clearLoadedChanges();
if (listCopy != null) {
recycleChangeList(listCopy, changeList);
// all newly create ShelvedChangeList have to be added to SchemesManger as new scheme
mySchemeManager.addNewScheme(listCopy, false);
}
notifyStateChanged();
}
@Nullable
private ShelvedChangeList createRecycledChangelist(ShelvedChangeList changeList) throws IOException {
final File newPatchDir = generateUniqueSchemePatchDir(changeList.DESCRIPTION, true);
final File newPath = getPatchFileInConfigDir(newPatchDir);
FileUtil.copy(new File(changeList.PATH), newPath);
final ShelvedChangeList listCopy = new ShelvedChangeList(newPath.getAbsolutePath(), changeList.DESCRIPTION, new ArrayList<>(changeList.getBinaryFiles()));
listCopy.markToDelete(changeList.isMarkedToDelete());
listCopy.setName(newPatchDir.getName());
return listCopy;
}
public void restoreList(@NotNull final ShelvedChangeList changeList) {
ShelvedChangeList list = mySchemeManager.findSchemeByName(changeList.getName());
if (list != null) {
list.setRecycled(false);
list.updateDate();
}
notifyStateChanged();
}
@NotNull
public List<ShelvedChangeList> getRecycledShelvedChangeLists() {
return getRecycled(true);
}
public void clearRecycled() {
clearShelvedLists(getRecycledShelvedChangeLists());
}
private void clearShelvedLists(@NotNull List<ShelvedChangeList> shelvedLists) {
if (shelvedLists.isEmpty()) return;
for (ShelvedChangeList list : shelvedLists) {
deleteListImpl(list);
mySchemeManager.removeScheme(list);
}
notifyStateChanged();
}
private void recycleChangeList(@NotNull final ShelvedChangeList listCopy, @Nullable final ShelvedChangeList newList) {
if (newList != null) {
for (Iterator<ShelvedBinaryFile> 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<ShelvedChange> 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<FilePatch> 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);
listCopy.updateDate();
notifyStateChanged();
}
}
public void recycleChangeList(@NotNull final ShelvedChangeList changeList) {
recycleChangeList(changeList, null);
notifyStateChanged();
}
public void deleteChangeList(@NotNull final ShelvedChangeList changeList) {
deleteListImpl(changeList);
mySchemeManager.removeScheme(changeList);
notifyStateChanged();
}
private void deleteListImpl(@NotNull final ShelvedChangeList changeList) {
FileUtil.delete(new File(myFileProcessor.getBaseDir(), changeList.getName()));
//backward compatibility deletion: if we didn't preform resource migration
FileUtil.delete(new File(changeList.PATH));
for (ShelvedBinaryFile binaryFile : changeList.getBinaryFiles()) {
final String path = binaryFile.SHELVED_PATH;
if (path != null) {
FileUtil.delete(new File(path));
}
}
}
public void renameChangeList(final ShelvedChangeList changeList, final String newName) {
changeList.DESCRIPTION = newName;
notifyStateChanged();
}
@NotNull
public static List<TextFilePatch> loadPatches(Project project, final String patchPath, CommitContext commitContext) throws IOException, PatchSyntaxException {
return loadPatches(project, patchPath, commitContext, true);
}
@NotNull
static List<? extends FilePatch> loadPatchesWithoutContent(Project project, final String patchPath, CommitContext commitContext)
throws IOException, PatchSyntaxException {
return loadPatches(project, patchPath, commitContext, false);
}
private static List<TextFilePatch> loadPatches(Project project, final String patchPath, CommitContext commitContext, boolean loadContent)
throws IOException, PatchSyntaxException {
char[] text = FileUtil.loadFileText(new File(patchPath), CharsetToolkit.UTF8);
PatchReader reader = new PatchReader(new CharArrayCharSequence(text), loadContent);
final List<TextFilePatch> textFilePatches = reader.readTextPatches();
ApplyPatchDefaultExecutor.applyAdditionalInfoBefore(project, reader.getAdditionalInfo(null), commitContext);
return textFilePatches;
}
public boolean isShowRecycled() {
return myShowRecycled;
}
public void setShowRecycled(final boolean showRecycled) {
myShowRecycled = showRecycled;
notifyStateChanged();
}
}