blob: 1dba1e07f6acf6cabdc13fd9aabd93298a9a487f [file] [log] [blame]
/*
* Copyright 2000-2012 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.branch;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.vcs.VcsNotifier;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
import git4idea.GitCommit;
import git4idea.GitPlatformFacade;
import git4idea.commands.*;
import git4idea.repo.GitRepository;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Deletes a branch.
* If branch is not fully merged to the current branch, shows a dialog with the list of unmerged commits and with a list of branches
* current branch are merged to, and makes force delete, if wanted.
*
* @author Kirill Likhodedov
*/
class GitDeleteBranchOperation extends GitBranchOperation {
private static final Logger LOG = Logger.getInstance(GitDeleteBranchOperation.class);
private final String myBranchName;
GitDeleteBranchOperation(@NotNull Project project, GitPlatformFacade facade, @NotNull Git git, @NotNull GitBranchUiHandler uiHandler,
@NotNull Collection<GitRepository> repositories, @NotNull String branchName) {
super(project, facade, git, uiHandler, repositories);
myBranchName = branchName;
}
@Override
public void execute() {
boolean fatalErrorHappened = false;
while (hasMoreRepositories() && !fatalErrorHappened) {
final GitRepository repository = next();
GitSimpleEventDetector notFullyMergedDetector = new GitSimpleEventDetector(GitSimpleEventDetector.Event.BRANCH_NOT_FULLY_MERGED);
GitBranchNotMergedToUpstreamDetector notMergedToUpstreamDetector = new GitBranchNotMergedToUpstreamDetector();
GitCommandResult result = myGit.branchDelete(repository, myBranchName, false, notFullyMergedDetector, notMergedToUpstreamDetector);
if (result.success()) {
refresh(repository);
markSuccessful(repository);
}
else if (notFullyMergedDetector.hasHappened()) {
String baseBranch = notMergedToUpstreamDetector.getBaseBranch();
if (baseBranch == null) { // GitBranchNotMergedToUpstreamDetector didn't happen
baseBranch = myCurrentHeads.get(repository);
}
Collection<GitRepository> remainingRepositories = getRemainingRepositories();
boolean forceDelete = showNotFullyMergedDialog(myBranchName, baseBranch, remainingRepositories);
if (forceDelete) {
GitCompoundResult compoundResult = forceDelete(myBranchName, remainingRepositories);
if (compoundResult.totalSuccess()) {
GitRepository[] remainingRepositoriesArray = ArrayUtil.toObjectArray(remainingRepositories, GitRepository.class);
markSuccessful(remainingRepositoriesArray);
refresh(remainingRepositoriesArray);
}
else {
fatalError(getErrorTitle(), compoundResult.getErrorOutputWithReposIndication());
return;
}
}
else {
if (wereSuccessful()) {
showFatalErrorDialogWithRollback(getErrorTitle(), "This branch is not fully merged to " + baseBranch + ".");
}
fatalErrorHappened = true;
}
}
else {
fatalError(getErrorTitle(), result.getErrorOutputAsJoinedString());
fatalErrorHappened = true;
}
}
if (!fatalErrorHappened) {
notifySuccess();
}
}
private static void refresh(@NotNull GitRepository... repositories) {
for (GitRepository repository : repositories) {
repository.update();
}
}
@Override
protected void rollback() {
GitCompoundResult result = new GitCompoundResult(myProject);
for (GitRepository repository : getSuccessfulRepositories()) {
GitCommandResult res = myGit.branchCreate(repository, myBranchName);
result.append(repository, res);
refresh(repository);
}
if (!result.totalSuccess()) {
VcsNotifier.getInstance(myProject).notifyError("Error during rollback of branch deletion",
result.getErrorOutputWithReposIndication());
}
}
@NotNull
private String getErrorTitle() {
return String.format("Branch %s wasn't deleted", myBranchName);
}
@NotNull
public String getSuccessMessage() {
return String.format("Deleted branch <b><code>%s</code></b>", myBranchName);
}
@NotNull
@Override
protected String getRollbackProposal() {
return "However branch deletion has succeeded for the following " + repositories() + ":<br/>" +
successfulRepositoriesJoined() +
"<br/>You may rollback (recreate " + myBranchName + " in these roots) not to let branches diverge.";
}
@NotNull
@Override
protected String getOperationName() {
return "branch deletion";
}
@NotNull
private GitCompoundResult forceDelete(@NotNull String branchName, @NotNull Collection<GitRepository> possibleFailedRepositories) {
GitCompoundResult compoundResult = new GitCompoundResult(myProject);
for (GitRepository repository : possibleFailedRepositories) {
GitCommandResult res = myGit.branchDelete(repository, branchName, true);
compoundResult.append(repository, res);
}
return compoundResult;
}
/**
* Shows a dialog "the branch is not fully merged" with the list of unmerged commits.
* User may still want to force delete the branch.
* In multi-repository setup collects unmerged commits for all given repositories.
* @return true if the branch should be force deleted.
*/
private boolean showNotFullyMergedDialog(@NotNull final String unmergedBranch, @NotNull final String baseBranch,
@NotNull Collection<GitRepository> repositories) {
final List<String> mergedToBranches = getMergedToBranches(unmergedBranch);
final Map<GitRepository, List<GitCommit>> history = new HashMap<GitRepository, List<GitCommit>>();
// note getRepositories() instead of getRemainingRepositories() here:
// we don't confuse user with the absence of repositories that have succeeded, just show no commits for them (and don't query for log)
for (GitRepository repository : getRepositories()) {
if (repositories.contains(repository)) {
history.put(repository, getUnmergedCommits(repository, unmergedBranch, baseBranch));
}
else {
history.put(repository, Collections.<GitCommit>emptyList());
}
}
return myUiHandler.showBranchIsNotFullyMergedDialog(myProject, history, unmergedBranch, mergedToBranches, baseBranch);
}
@NotNull
private List<GitCommit> getUnmergedCommits(@NotNull GitRepository repository, @NotNull String branchName, @NotNull String baseBranch) {
return myGit.history(repository, baseBranch + ".." + branchName);
}
@NotNull
private List<String> getMergedToBranches(String branchName) {
List<String> mergedToBranches = null;
for (GitRepository repository : getRemainingRepositories()) {
List<String> branches = getMergedToBranches(repository, branchName);
if (mergedToBranches == null) {
mergedToBranches = branches;
}
else {
mergedToBranches = new ArrayList<String>(ContainerUtil.intersection(mergedToBranches, branches));
}
}
return mergedToBranches != null ? mergedToBranches : new ArrayList<String>();
}
/**
* Branches which the given branch is merged to ({@code git branch --merged},
* except the given branch itself.
*/
@NotNull
private List<String> getMergedToBranches(@NotNull GitRepository repository, @NotNull String branchName) {
String tip = tip(repository, branchName);
if (tip == null) {
return Collections.emptyList();
}
return branchContainsCommit(repository, tip, branchName);
}
@Nullable
private String tip(GitRepository repository, @NotNull String branchName) {
GitCommandResult result = myGit.tip(repository, branchName);
if (result.success() && result.getOutput().size() == 1) {
return result.getOutput().get(0).trim();
}
// failing in this method is not critical - it is just additional information. So we just log the error
LOG.info("Failed to get [git rev-list -1] for branch [" + branchName + "]. " + result);
return null;
}
@NotNull
private List<String> branchContainsCommit(@NotNull GitRepository repository, @NotNull String tip, @NotNull String branchName) {
GitCommandResult result = myGit.branchContains(repository, tip);
if (result.success()) {
List<String> branches = new ArrayList<String>();
for (String s : result.getOutput()) {
s = s.trim();
if (s.startsWith("*")) {
s = s.substring(2);
}
if (!s.equals(branchName)) { // this branch contains itself - not interesting
branches.add(s);
}
}
return branches;
}
// failing in this method is not critical - it is just additional information. So we just log the error
LOG.info("Failed to get [git branch --contains] for hash [" + tip + "]. " + result);
return Collections.emptyList();
}
// warning: not deleting branch 'feature' that is not yet merged to
// 'refs/remotes/origin/feature', even though it is merged to HEAD.
// error: The branch 'feature' is not fully merged.
// If you are sure you want to delete it, run 'git branch -D feature'.
private static class GitBranchNotMergedToUpstreamDetector implements GitLineHandlerListener {
private static final Pattern PATTERN = Pattern.compile(".*'(.*)', even though it is merged to.*");
@Nullable private String myBaseBranch;
@Override
public void onLineAvailable(String line, Key outputType) {
Matcher matcher = PATTERN.matcher(line);
if (matcher.matches()) {
myBaseBranch = matcher.group(1);
}
}
@Override
public void processTerminated(int exitCode) {
}
@Override
public void startFailed(Throwable exception) {
}
@Nullable
public String getBaseBranch() {
return myBaseBranch;
}
}
}