blob: 1d105eaea0f15666f5c781ddc33dea5b6df64c68 [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 git4idea.push;
import com.intellij.history.Label;
import com.intellij.history.LocalHistory;
import com.intellij.ide.BrowserUtil;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationListener;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.VcsNotifier;
import com.intellij.openapi.vcs.ex.ProjectLevelVcsManagerEx;
import com.intellij.openapi.vcs.update.ActionInfo;
import com.intellij.openapi.vcs.update.UpdateInfoTree;
import com.intellij.openapi.vcs.update.UpdatedFiles;
import git4idea.GitBranch;
import git4idea.GitRevisionNumber;
import git4idea.GitUtil;
import git4idea.branch.GitBranchUtil;
import git4idea.merge.MergeChangeCollector;
import git4idea.repo.GitRepository;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.event.HyperlinkEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
class GitPushResult {
private static final Logger LOG = Logger.getInstance(GitPushResult.class);
private final Project myProject;
private final Map<GitRepository, GitPushRepoResult> myResults = new HashMap<GitRepository, GitPushRepoResult>();
private final Map<GitRepository, GitRevisionNumber> myUpdateStarts = new HashMap<GitRepository, GitRevisionNumber>();
private Label myBeforeUpdateLabel;
GitPushResult(@NotNull Project project) {
this(project, null, new HashMap<GitRepository, GitRevisionNumber>());
}
private GitPushResult(@NotNull Project project, @Nullable Label beforeUpdateLabel, @NotNull Map<GitRepository, GitRevisionNumber> updateStarts) {
myProject = project;
myBeforeUpdateLabel = beforeUpdateLabel;
for (Map.Entry<GitRepository, GitRevisionNumber> entry : updateStarts.entrySet()) {
myUpdateStarts.put(entry.getKey(), entry.getValue());
}
}
void append(@NotNull GitRepository repository, @NotNull GitPushRepoResult result) {
myResults.put(repository, result);
}
/**
* For the specified repositories remembers the revision when update process (actually, auto-update inside the push process) has started.
*/
void markUpdateStartIfNotMarked(@NotNull Collection<GitRepository> repositories) {
if (myUpdateStarts.isEmpty()) {
myBeforeUpdateLabel = LocalHistory.getInstance().putSystemLabel(myProject, "Before push");
}
for (GitRepository repository : repositories) {
if (!myUpdateStarts.containsKey(repository)) {
String currentRevision = repository.getCurrentRevision();
if (currentRevision != null) {
myUpdateStarts.put(repository, new GitRevisionNumber(currentRevision));
}
}
}
}
boolean wasErrorCancelOrNotAuthorized() {
for (GitPushRepoResult repoResult : myResults.values()) {
if (repoResult.isOneOfErrors()) {
return true;
}
}
return false;
}
@NotNull
private GroupedResult group() {
final Map<GitRepository, GitPushRepoResult> successfulResults = new HashMap<GitRepository, GitPushRepoResult>();
final Map<GitRepository, GitPushRepoResult> errorResults = new HashMap<GitRepository, GitPushRepoResult>();
final Map<GitRepository, GitPushRepoResult> rejectedResults = new HashMap<GitRepository, GitPushRepoResult>();
for (Map.Entry<GitRepository, GitPushRepoResult> entry : myResults.entrySet()) {
GitRepository repository = entry.getKey();
GitPushRepoResult repoResult = entry.getValue();
switch (repoResult.getType()) {
case SUCCESS:
successfulResults.put(repository, repoResult);
break;
case ERROR:
case CANCEL:
case NOT_AUTHORIZED:
errorResults.put(repository, repoResult);
break;
case NOT_PUSHING:
break;
case SOME_REJECTED:
rejectedResults.put(repository, repoResult);
break;
}
}
return new GroupedResult(successfulResults, errorResults, rejectedResults);
}
boolean isEmpty() {
return myResults.isEmpty();
}
@NotNull
GitPushResult remove(@NotNull Map<GitRepository, GitBranch> repoBranchPairs) {
GitPushResult result = new GitPushResult(myProject, myBeforeUpdateLabel, myUpdateStarts);
for (Map.Entry<GitRepository, GitPushRepoResult> entry : myResults.entrySet()) {
GitRepository repository = entry.getKey();
GitPushRepoResult repoResult = entry.getValue();
if (repoBranchPairs.containsKey(repository)) {
GitPushRepoResult adjustedResult = repoResult.remove(repoBranchPairs.get(repository));
if (!repoResult.isEmpty()) {
result.append(repository, adjustedResult);
}
} else {
result.append(repository, repoResult);
}
}
return result;
}
/**
* Merges the given results to this result.
* In the case of conflict (i.e. different results for a repository-branch pair), current result is preferred over the previous one.
*/
void mergeFrom(@Nullable GitPushResult previousResult) {
if (previousResult == null) {
return;
}
for (Map.Entry<GitRepository, GitPushRepoResult> entry : previousResult.myResults.entrySet()) {
GitRepository repository = entry.getKey();
GitPushRepoResult repoResult = entry.getValue();
if (myResults.containsKey(repository)) {
myResults.get(repository).mergeFrom(previousResult.myResults.get(repository));
} else {
append(repository, repoResult);
}
}
for (Map.Entry<GitRepository, GitRevisionNumber> entry : previousResult.myUpdateStarts.entrySet()) {
myUpdateStarts.put(entry.getKey(), entry.getValue());
}
myBeforeUpdateLabel = previousResult.myBeforeUpdateLabel;
}
@NotNull
Map<GitRepository, GitBranch> getRejectedPushesFromCurrentBranchToTrackedBranch(GitPushInfo pushInfo) {
final Map<GitRepository, GitBranch> rejectedPushesForCurrentBranch = new HashMap<GitRepository, GitBranch>();
for (Map.Entry<GitRepository, GitPushRepoResult> entry : group().myRejectedResults.entrySet()) {
GitRepository repository = entry.getKey();
GitBranch currentBranch = repository.getCurrentBranch();
if (currentBranch == null) {
continue;
}
GitPushRepoResult repoResult = entry.getValue();
GitPushBranchResult curBranchResult = repoResult.getBranchResults().get(currentBranch);
if (curBranchResult == null) {
continue;
}
String trackedBranchName;
try {
String simpleName = GitBranchUtil.getTrackedBranchName(myProject, repository.getRoot(), currentBranch.getName());
if (simpleName == null) {
continue;
}
if (simpleName.startsWith(GitBranch.REFS_HEADS_PREFIX)) {
simpleName = simpleName.substring(GitBranch.REFS_HEADS_PREFIX.length());
}
String remote = GitBranchUtil.getTrackedRemoteName(myProject, repository.getRoot(), currentBranch.getName());
if (remote == null) {
continue;
}
trackedBranchName = remote + "/" + simpleName;
}
catch (VcsException e) {
LOG.info("Couldn't get tracked branch for branch " + currentBranch, e);
continue;
}
if (!pushInfo.getPushSpecs().get(repository).getDest().getName().equals(trackedBranchName)) {
// push from current branch was rejected, but it was a push not to the tracked branch => ignore
continue;
}
if (curBranchResult.isRejected()) {
rejectedPushesForCurrentBranch.put(repository, currentBranch);
}
}
return rejectedPushesForCurrentBranch;
}
/**
* Constructs the HTML-formatted message from error outputs of failed repositories and notify.
* If there is only 1 repository in the project, just returns the error without writing the repository url (to avoid confusion for people
* with only 1 root ever).
* Otherwise adds repository URL to the error that repository produced.
*
* The procedure also includes collecting for updated files (if an auto-update was performed during the push), which may be lengthy.
*/
void createPushNotificationAndNotify() {
final UpdatedFiles updatedFiles = collectUpdatedFiles();
GroupedResult groupedResult = group();
boolean error = !groupedResult.myErrorResults.isEmpty();
boolean rejected = !groupedResult.myRejectedResults.isEmpty();
boolean success = !groupedResult.mySuccessfulResults.isEmpty();
boolean onlyError = error && !rejected && !success;
boolean onlyRejected = rejected && !error && !success;
final boolean onlySuccess = success && !rejected && !error;
int pushedCommitsNumber = calcPushedCommitTotalNumber(myResults);
String title;
if (error) {
if (onlyError) {
title = "Push failed";
} else {
title = "Push partially failed";
if (success) {
title += ", " + commits(pushedCommitsNumber) + " pushed";
}
}
} else if (rejected) {
if (onlyRejected) {
title = "Push rejected";
} else {
title = "Push partially rejected, " + commits(pushedCommitsNumber) + " pushed";
}
} else {
title = "Push successful";
}
String errorReport = reportForGroup(groupedResult.myErrorResults, GroupedResult.Type.ERROR);
String successReport = reportForGroup(groupedResult.mySuccessfulResults, GroupedResult.Type.SUCCESS);
String rejectedReport = reportForGroup(groupedResult.myRejectedResults, GroupedResult.Type.REJECT);
StringBuilder sb = new StringBuilder();
sb.append(errorReport);
sb.append(rejectedReport);
sb.append(successReport);
if (!updatedFiles.isEmpty()) {
sb.append("<a href='UpdatedFiles'>View files updated during the push</a>");
}
NotificationListener viewUpdateFilesListener = new ViewUpdatedFilesNotificationListener(updatedFiles);
VcsNotifier vcsNotifier = VcsNotifier.getInstance(myProject);
if (onlySuccess) {
vcsNotifier.notifySuccess(title, sb.toString(), viewUpdateFilesListener);
}
else if (error) {
vcsNotifier.notifyError(title, sb.toString(), viewUpdateFilesListener);
}
else {
vcsNotifier.notifyImportantWarning(title, sb.toString(), viewUpdateFilesListener);
}
}
@NotNull
private UpdatedFiles collectUpdatedFiles() {
UpdatedFiles updatedFiles = UpdatedFiles.create();
for (Map.Entry<GitRepository, GitRevisionNumber> updatedRepository : myUpdateStarts.entrySet()) {
GitRepository repository = updatedRepository.getKey();
final MergeChangeCollector collector = new MergeChangeCollector(myProject, repository.getRoot(), updatedRepository.getValue());
final ArrayList<VcsException> exceptions = new ArrayList<VcsException>();
collector.collect(updatedFiles, exceptions);
for (VcsException exception : exceptions) {
LOG.info(exception);
}
}
return updatedFiles;
}
private static int calcPushedCommitTotalNumber(@NotNull Map<GitRepository, GitPushRepoResult> successfulResults) {
int sum = 0;
for (GitPushRepoResult pushRepoResult : successfulResults.values()) {
for (GitPushBranchResult branchResult : pushRepoResult.getBranchResults().values()) {
sum += branchResult.getNumberOfPushedCommits();
}
}
return sum;
}
@NotNull
private String reportForGroup(@NotNull Map<GitRepository, GitPushRepoResult> groupResult, @NotNull GroupedResult.Type resultType) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<GitRepository, GitPushRepoResult> entry : groupResult.entrySet()) {
GitRepository repository = entry.getKey();
GitPushRepoResult result = entry.getValue();
if (!GitUtil.justOneGitRepository(myProject)) {
sb.append("<code>" + repository.getPresentableUrl() + "</code>:<br/>");
}
if (resultType == GroupedResult.Type.SUCCESS || resultType == GroupedResult.Type.REJECT) {
sb.append(result.getPerBranchesNonErrorReport());
} else {
sb.append(result.getOutput());
}
sb.append("<br/>");
}
return sb.toString();
}
private static class GroupedResult {
enum Type {
SUCCESS,
REJECT,
ERROR
}
private final Map<GitRepository, GitPushRepoResult> mySuccessfulResults;
private final Map<GitRepository, GitPushRepoResult> myErrorResults;
private final Map<GitRepository, GitPushRepoResult> myRejectedResults;
GroupedResult(@NotNull Map<GitRepository, GitPushRepoResult> successfulResults,
@NotNull Map<GitRepository, GitPushRepoResult> errorResults,
@NotNull Map<GitRepository, GitPushRepoResult> rejectedResults) {
mySuccessfulResults = successfulResults;
myErrorResults = errorResults;
myRejectedResults = rejectedResults;
}
}
@NotNull
private static String commits(int commitNum) {
return commitNum + " " + StringUtil.pluralize("commit", commitNum);
}
private class ViewUpdatedFilesNotificationListener implements NotificationListener {
private final UpdatedFiles myUpdatedFiles;
public ViewUpdatedFilesNotificationListener(UpdatedFiles updatedFiles) {
myUpdatedFiles = updatedFiles;
}
@Override
public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) {
if (event.getEventType().equals(HyperlinkEvent.EventType.ACTIVATED)) {
if (event.getDescription().equals("UpdatedFiles")) {
ProjectLevelVcsManagerEx vcsManager = ProjectLevelVcsManagerEx.getInstanceEx(myProject);
UpdateInfoTree tree = vcsManager.showUpdateProjectInfo(myUpdatedFiles, "Update", ActionInfo.UPDATE, false);
tree.setBefore(myBeforeUpdateLabel);
tree.setAfter(LocalHistory.getInstance().putSystemLabel(myProject, "After push"));
}
else {
BrowserUtil.browse(event.getDescription());
}
}
}
}
}