blob: 19b1420ddda97732628e1002b6dd166aae1261c4 [file] [log] [blame]
/*
* Copyright 2000-2011 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.ui;
import com.intellij.history.LocalHistory;
import com.intellij.history.LocalHistoryAction;
import com.intellij.ide.util.DelegatingProgressIndicator;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
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.ui.Messages;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vcs.changes.*;
import com.intellij.openapi.vcs.changes.actions.MoveChangesToAnotherListAction;
import com.intellij.openapi.vcs.changes.committed.CommittedChangesCache;
import com.intellij.openapi.vcs.checkin.CheckinEnvironment;
import com.intellij.openapi.vcs.checkin.CheckinHandler;
import com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier;
import com.intellij.openapi.vcs.update.RefreshVFsSynchronously;
import com.intellij.util.Consumer;
import com.intellij.util.Function;
import com.intellij.util.NullableFunction;
import com.intellij.util.WaitForProgressToShow;
import com.intellij.util.concurrency.Semaphore;
import com.intellij.util.ui.ConfirmationDialog;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
public class CommitHelper {
public static final Key<Object> DOCUMENT_BEING_COMMITTED_KEY = new Key<Object>("DOCUMENT_BEING_COMMITTED");
private final static Logger LOG = Logger.getInstance("#com.intellij.openapi.vcs.changes.ui.CommitHelper");
private final Project myProject;
private final ChangeList myChangeList;
private final List<Change> myIncludedChanges;
private final String myActionName;
private final String myCommitMessage;
private final List<CheckinHandler> myHandlers;
private final boolean myAllOfDefaultChangeListChangesIncluded;
private final boolean myForceSyncCommit;
private final NullableFunction<Object, Object> myAdditionalData;
@Nullable private final CommitResultHandler myCustomResultHandler;
private final List<Document> myCommittingDocuments = new ArrayList<Document>();
private final VcsConfiguration myConfiguration;
private final VcsDirtyScopeManager myDirtyScopeManager;
private final HashSet<String> myFeedback;
public CommitHelper(final Project project,
final ChangeList changeList,
final List<Change> includedChanges,
final String actionName,
final String commitMessage,
final List<CheckinHandler> handlers,
final boolean allOfDefaultChangeListChangesIncluded,
final boolean synchronously, final NullableFunction<Object, Object> additionalDataHolder,
@Nullable CommitResultHandler customResultHandler) {
myProject = project;
myChangeList = changeList;
myIncludedChanges = includedChanges;
myActionName = actionName;
myCommitMessage = commitMessage;
myHandlers = handlers;
myAllOfDefaultChangeListChangesIncluded = allOfDefaultChangeListChangesIncluded;
myForceSyncCommit = synchronously;
myAdditionalData = additionalDataHolder;
myCustomResultHandler = customResultHandler;
myConfiguration = VcsConfiguration.getInstance(myProject);
myDirtyScopeManager = VcsDirtyScopeManager.getInstance(myProject);
myFeedback = new HashSet<String>();
}
public boolean doCommit() {
return doCommit(new CommitProcessor());
}
public boolean doAlienCommit(final AbstractVcs vcs) {
return doCommit(new AlienCommitProcessor(vcs));
}
private boolean doCommit(final GeneralCommitProcessor processor) {
final Runnable action = new Runnable() {
public void run() {
delegateCommitToVcsThread(processor);
}
};
if (myForceSyncCommit) {
ProgressManager.getInstance().runProcessWithProgressSynchronously(action, myActionName, true, myProject);
boolean success = doesntContainErrors(processor.getVcsExceptions());
if (success) {
reportResult(processor);
}
return success;
}
else {
Task.Backgroundable task =
new Task.Backgroundable(myProject, myActionName, true, myConfiguration.getCommitOption()) {
public void run(@NotNull final ProgressIndicator indicator) {
final ProjectLevelVcsManager vcsManager = ProjectLevelVcsManager.getInstance(myProject);
vcsManager.startBackgroundVcsOperation();
try {
action.run();
}
finally {
vcsManager.stopBackgroundVcsOperation();
}
}
@Override
public NotificationInfo notifyFinished() {
if (myCustomResultHandler == null) {
String text = reportResult(processor);
return new NotificationInfo("VCS Commit", "VCS Commit Finished", text, true);
}
return null;
}
};
ProgressManager.getInstance().run(task);
return false;
}
}
private void delegateCommitToVcsThread(final GeneralCommitProcessor processor) {
final ProgressIndicator indicator = new DelegatingProgressIndicator();
final Semaphore endSemaphore = new Semaphore();
endSemaphore.down();
ChangeListManagerImpl.getInstanceImpl(myProject).executeOnUpdaterThread(new Runnable() {
@Override
public void run() {
indicator.setText("Performing VCS commit...");
try {
ProgressManager.getInstance().runProcess(new Runnable() {
@Override
public void run() {
indicator.checkCanceled();
generalCommit(processor);
}
}, indicator);
}
finally {
endSemaphore.up();
}
}
});
indicator.setText("Waiting for VCS background tasks to finish...");
while (!endSemaphore.waitFor(20)) {
indicator.checkCanceled();
}
}
private String reportResult(GeneralCommitProcessor processor) {
final List<Change> changesFailedToCommit = processor.getChangesFailedToCommit();
int failed = changesFailedToCommit.size();
int committed = myIncludedChanges.size() - failed;
String text = committed + " " + StringUtil.pluralize("file", committed) + " committed";
if (failed > 0) {
text += ", " + failed + " " + StringUtil.pluralize("file", failed) + " failed to commit";
}
StringBuilder content = new StringBuilder(StringUtil.isEmpty(myCommitMessage) ? text : text + ": " + escape(myCommitMessage));
for (String s : myFeedback) {
content.append("\n");
content.append(s);
}
NotificationType notificationType = resolveNotificationType(processor);
VcsBalloonProblemNotifier.NOTIFICATION_GROUP.createNotification(content.toString(), notificationType).notify(myProject);
return text;
}
private static NotificationType resolveNotificationType(@NotNull GeneralCommitProcessor processor) {
boolean hasExceptions = !processor.getVcsExceptions().isEmpty();
boolean hasOnlyWarnings = doesntContainErrors(processor.getVcsExceptions());
return hasExceptions ? (hasOnlyWarnings ? NotificationType.WARNING : NotificationType.ERROR) : NotificationType.INFORMATION;
}
/*
Commit message is passed to NotificationManagerImpl#doNotify and displayed as HTML.
Thus HTML tag braces (< and >) should be escaped,
but only they since the text is passed directly to HTML <BODY> tag and is not a part of an attribute or else.
*/
private static String escape(String s) {
final String[] FROM = {"<", ">"};
final String[] TO = {"&lt;", "&gt;"};
return StringUtil.replace(s, FROM, TO);
}
private static boolean doesntContainErrors(final List<VcsException> vcsExceptions) {
for (VcsException vcsException : vcsExceptions) {
if (!vcsException.isWarning()) return false;
}
return true;
}
private void generalCommit(final GeneralCommitProcessor processor) {
try {
final Application appManager = ApplicationManager.getApplication();
appManager.runReadAction(new Runnable() {
public void run() {
markCommittingDocuments();
}
});
try {
processor.callSelf();
}
finally {
appManager.runReadAction(new Runnable() {
public void run() {
unmarkCommittingDocuments();
}
});
}
processor.doBeforeRefresh();
AbstractVcsHelper.getInstance(myProject).showErrors(processor.getVcsExceptions(), myActionName);
}
catch (RuntimeException e) {
LOG.error(e);
processor.myVcsExceptions.add(new VcsException(e));
throw e;
}
catch (Throwable e) {
LOG.error(e);
processor.myVcsExceptions.add(new VcsException(e));
throw new RuntimeException(e);
}
finally {
commitCompleted(processor.getVcsExceptions(), processor);
processor.customRefresh();
WaitForProgressToShow.runOrInvokeLaterAboveProgress(new Runnable() {
public void run() {
final Runnable runnable = processor.postRefresh();
if (runnable != null) {
runnable.run();
}
}
}, null, myProject);
}
}
private class AlienCommitProcessor extends GeneralCommitProcessor {
private final AbstractVcs myVcs;
private AlienCommitProcessor(final AbstractVcs vcs) {
myVcs = vcs;
}
public void callSelf() {
ChangesUtil.processItemsByVcs(myIncludedChanges, new ChangesUtil.VcsSeparator<Change>() {
public AbstractVcs getVcsFor(final Change item) {
return myVcs;
}
}, this);
}
public void process(final AbstractVcs vcs, final List<Change> items) {
if (myVcs.getName().equals(vcs.getName())) {
final CheckinEnvironment environment = vcs.getCheckinEnvironment();
if (environment != null) {
Collection<FilePath> paths = ChangesUtil.getPaths(items);
myPathsToRefresh.addAll(paths);
final List<VcsException> exceptions = environment.commit(items, myCommitMessage, myAdditionalData, myFeedback);
if (exceptions != null && exceptions.size() > 0) {
myVcsExceptions.addAll(exceptions);
myChangesFailedToCommit.addAll(items);
}
}
}
}
public void afterSuccessfulCheckIn() {
}
public void afterFailedCheckIn() {
}
public void doBeforeRefresh() {
}
public void customRefresh() {
}
public Runnable postRefresh() {
return null;
}
public void doVcsRefresh() {
}
}
private abstract static class GeneralCommitProcessor implements ChangesUtil.PerVcsProcessor<Change>, ActionsAroundRefresh {
protected final List<FilePath> myPathsToRefresh;
protected final List<VcsException> myVcsExceptions;
protected final List<Change> myChangesFailedToCommit;
protected GeneralCommitProcessor() {
myPathsToRefresh = new ArrayList<FilePath>();
myVcsExceptions = new ArrayList<VcsException>();
myChangesFailedToCommit = new ArrayList<Change>();
}
public abstract void callSelf();
public abstract void afterSuccessfulCheckIn();
public abstract void afterFailedCheckIn();
public List<FilePath> getPathsToRefresh() {
return myPathsToRefresh;
}
public List<VcsException> getVcsExceptions() {
return myVcsExceptions;
}
public List<Change> getChangesFailedToCommit() {
return myChangesFailedToCommit;
}
}
private interface ActionsAroundRefresh {
void doBeforeRefresh();
void customRefresh();
void doVcsRefresh();
Runnable postRefresh();
}
private static enum ChangeListsModificationAfterCommit {
DELETE_LIST,
MOVE_OTHERS,
NOTHING
}
private class CommitProcessor extends GeneralCommitProcessor {
private boolean myKeepChangeListAfterCommit;
private LocalHistoryAction myAction;
private ChangeListsModificationAfterCommit myAfterVcsRefreshModification;
private boolean myCommitSuccess;
private CommitProcessor() {
myAfterVcsRefreshModification = ChangeListsModificationAfterCommit.NOTHING;
if (myChangeList instanceof LocalChangeList) {
final LocalChangeList localList = (LocalChangeList) myChangeList;
final boolean containsAll = new HashSet<Change>(myIncludedChanges).containsAll(new HashSet<Change>(myChangeList.getChanges()));
if (containsAll && !localList.isDefault() && !localList.isReadOnly()) {
myAfterVcsRefreshModification = ChangeListsModificationAfterCommit.DELETE_LIST;
}
else if (myConfiguration.OFFER_MOVE_TO_ANOTHER_CHANGELIST_ON_PARTIAL_COMMIT && (! containsAll) &&
localList.isDefault() && myAllOfDefaultChangeListChangesIncluded) {
myAfterVcsRefreshModification = ChangeListsModificationAfterCommit.MOVE_OTHERS;
}
}
}
public void callSelf() {
ChangesUtil.processChangesByVcs(myProject, myIncludedChanges, this);
}
public void process(final AbstractVcs vcs, final List<Change> items) {
final CheckinEnvironment environment = vcs.getCheckinEnvironment();
if (environment != null) {
Collection<FilePath> paths = ChangesUtil.getPaths(items);
myPathsToRefresh.addAll(paths);
if (environment.keepChangeListAfterCommit(myChangeList)) {
myKeepChangeListAfterCommit = true;
}
final List<VcsException> exceptions = environment.commit(items, myCommitMessage, myAdditionalData, myFeedback);
if (exceptions != null && exceptions.size() > 0) {
myVcsExceptions.addAll(exceptions);
myChangesFailedToCommit.addAll(items);
}
}
}
public void afterSuccessfulCheckIn() {
myCommitSuccess = true;
}
public void afterFailedCheckIn() {
moveToFailedList(myChangeList, myCommitMessage, getChangesFailedToCommit(),
VcsBundle.message("commit.dialog.failed.commit.template", myChangeList.getName()), myProject);
}
public void doBeforeRefresh() {
final ChangeListManagerImpl clManager = (ChangeListManagerImpl) ChangeListManager.getInstance(myProject);
clManager.showLocalChangesInvalidated();
myAction = ApplicationManager.getApplication().runReadAction(new Computable<LocalHistoryAction>() {
public LocalHistoryAction compute() {
return LocalHistory.getInstance().startAction(myActionName);
}
});
}
public void customRefresh() {
final List<Change> toRefresh = new ArrayList<Change>();
ChangesUtil.processChangesByVcs(myProject, myIncludedChanges, new ChangesUtil.PerVcsProcessor<Change>() {
@Override
public void process(AbstractVcs vcs, List<Change> items) {
CheckinEnvironment ce = vcs.getCheckinEnvironment();
if (ce != null && ce.isRefreshAfterCommitNeeded()) {
toRefresh.addAll(items);
}
}
});
if (toRefresh.isEmpty()) {
return;
}
final ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
if (indicator != null) {
indicator.setText(VcsBundle.message("commit.dialog.refresh.files"));
}
RefreshVFsSynchronously.updateChanges(toRefresh);
}
public Runnable postRefresh() {
return new Runnable() {
public void run() {
// to be completely sure
if (myAction != null) {
myAction.finish();
}
if (!myProject.isDisposed()) {
// after vcs refresh is completed, outdated notifiers should be removed if some exists...
final ChangeListManager clManager = ChangeListManager.getInstance(myProject);
clManager.invokeAfterUpdate(new Runnable() {
public void run() {
if (myCommitSuccess) {
// do delete/ move of change list if needed
if (ChangeListsModificationAfterCommit.DELETE_LIST.equals(myAfterVcsRefreshModification)) {
if (! myKeepChangeListAfterCommit) {
clManager.removeChangeList(myChangeList.getName());
}
} else if (ChangeListsModificationAfterCommit.MOVE_OTHERS.equals(myAfterVcsRefreshModification)) {
ChangelistMoveOfferDialog dialog = new ChangelistMoveOfferDialog(myConfiguration);
dialog.show();
if (dialog.isOK()) {
final Collection<Change> changes = clManager.getDefaultChangeList().getChanges();
MoveChangesToAnotherListAction.askAndMove(myProject, changes, null);
}
}
}
final CommittedChangesCache cache = CommittedChangesCache.getInstance(myProject);
// in background since commit must have authorized
cache.refreshAllCachesAsync(false, true);
cache.refreshIncomingChangesAsync();
}
}, InvokeAfterUpdateMode.SILENT, null, new Consumer<VcsDirtyScopeManager>() {
public void consume(final VcsDirtyScopeManager vcsDirtyScopeManager) {
for (FilePath path : myPathsToRefresh) {
vcsDirtyScopeManager.fileDirty(path);
}
}
}, null);
LocalHistory.getInstance().putSystemLabel(myProject, myActionName + ": " + myCommitMessage);
}
}
};
}
private void vcsRefresh() {
for (FilePath path : myPathsToRefresh) {
myDirtyScopeManager.fileDirty(path);
}
}
public void doVcsRefresh() {
ApplicationManager.getApplication().runReadAction(new Runnable() {
public void run() {
vcsRefresh();
}
});
}
}
private void markCommittingDocuments() {
myCommittingDocuments.addAll(markCommittingDocuments(myProject, myIncludedChanges));
}
private void unmarkCommittingDocuments() {
unmarkCommittingDocuments(myCommittingDocuments);
myCommittingDocuments.clear();
}
/**
* Marks {@link Document documents} related to the given changes as "being committed".
* @return documents which were marked that way.
* @see #unmarkCommittingDocuments(java.util.Collection)
* @see VetoSavingCommittingDocumentsAdapter
*/
@NotNull
public static Collection<Document> markCommittingDocuments(@NotNull Project project, @NotNull List<Change> changes) {
Collection<Document> committingDocs = new ArrayList<Document>();
for (Change change : changes) {
Document doc = ChangesUtil.getFilePath(change).getDocument();
if (doc != null) {
doc.putUserData(DOCUMENT_BEING_COMMITTED_KEY, project);
committingDocs.add(doc);
}
}
return committingDocs;
}
/**
* Removes the "being committed marker" from the given {@link Document documents}.
* @see #markCommittingDocuments(com.intellij.openapi.project.Project, java.util.List)
* @see VetoSavingCommittingDocumentsAdapter
*/
public static void unmarkCommittingDocuments(@NotNull Collection<Document> committingDocs) {
for (Document doc : committingDocs) {
doc.putUserData(DOCUMENT_BEING_COMMITTED_KEY, null);
}
}
private void commitCompleted(final List<VcsException> allExceptions, final GeneralCommitProcessor processor) {
final List<VcsException> errors = collectErrors(allExceptions);
final int errorsSize = errors.size();
final int warningsSize = allExceptions.size() - errorsSize;
if (errorsSize == 0) {
for (CheckinHandler handler : myHandlers) {
handler.checkinSuccessful();
}
processor.afterSuccessfulCheckIn();
if (myCustomResultHandler != null) {
myCustomResultHandler.onSuccess(myCommitMessage);
}
}
else {
for (CheckinHandler handler : myHandlers) {
handler.checkinFailed(errors);
}
}
if ((errorsSize == 0) && (warningsSize == 0)) {
final ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
if (indicator != null) {
indicator.setText(VcsBundle.message("commit.dialog.completed.successfully"));
}
}
else {
if (myCustomResultHandler == null) {
showErrorDialogAndMoveToAnotherList(processor, errorsSize, warningsSize, errors);
}
else {
myCustomResultHandler.onFailure();
}
}
}
private void showErrorDialogAndMoveToAnotherList(final GeneralCommitProcessor processor, final int errorsSize, final int warningsSize,
@NotNull final List<VcsException> errors) {
WaitForProgressToShow.runOrInvokeLaterAboveProgress(new Runnable() {
public void run() {
String message;
if (errorsSize > 0 && warningsSize > 0) {
message = VcsBundle.message("message.text.commit.failed.with.errors.and.warnings");
}
else if (errorsSize > 0) {
message = StringUtil.pluralize(VcsBundle.message("message.text.commit.failed.with.error"), errorsSize);
}
else {
message = StringUtil.pluralize(VcsBundle.message("message.text.commit.finished.with.warning"), warningsSize);
}
message += ":\n" + StringUtil.join(errors, new Function<VcsException, String>() {
@Override
public String fun(VcsException e) {
return e.getMessage();
}
}, "\n");
//new VcsBalloonProblemNotifier(myProject, message, MessageType.ERROR).run();
Messages.showErrorDialog(message, VcsBundle.message("message.title.commit"));
if (errorsSize > 0) {
processor.afterFailedCheckIn();
}
}
}, null, myProject);
}
public static void moveToFailedList(final ChangeList changeList,
final String commitMessage,
final List<Change> failedChanges,
final String newChangelistName,
final Project project) {
// No need to move since we'll get exactly the same changelist.
if (failedChanges.containsAll(changeList.getChanges())) return;
final VcsConfiguration configuration = VcsConfiguration.getInstance(project);
if (configuration.MOVE_TO_FAILED_COMMIT_CHANGELIST != VcsShowConfirmationOption.Value.DO_ACTION_SILENTLY) {
final VcsShowConfirmationOption option = new VcsShowConfirmationOption() {
public Value getValue() {
return configuration.MOVE_TO_FAILED_COMMIT_CHANGELIST;
}
public void setValue(final Value value) {
configuration.MOVE_TO_FAILED_COMMIT_CHANGELIST = value;
}
@Override
public boolean isPersistent() {
return true;
}
};
boolean result = ConfirmationDialog.requestForConfirmation(option, project, VcsBundle.message("commit.failed.confirm.prompt"),
VcsBundle.message("commit.failed.confirm.title"),
Messages.getQuestionIcon());
if (!result) return;
}
final ChangeListManager changeListManager = ChangeListManager.getInstance(project);
int index = 1;
String failedListName = newChangelistName;
while (changeListManager.findChangeList(failedListName) != null) {
index++;
failedListName = newChangelistName + " (" + index + ")";
}
final LocalChangeList failedList = changeListManager.addChangeList(failedListName, commitMessage);
changeListManager.moveChangesTo(failedList, failedChanges.toArray(new Change[failedChanges.size()]));
}
private static List<VcsException> collectErrors(final List<VcsException> vcsExceptions) {
final ArrayList<VcsException> result = new ArrayList<VcsException>();
for (VcsException vcsException : vcsExceptions) {
if (!vcsException.isWarning()) {
result.add(vcsException);
}
}
return result;
}
}