[git] IDEA-127472 Correctly checkout from diverged branches to common branch + tests

* Before branch operation starts, remember current branches for each
  repository, without assuming that the repositories are on the same
  branch. Use proper branch for each repository during rollback.
* Simplify some user messages in a couple of places to avoid forming
  complex messages like "branch A in repository B", etc.
  However, use such complex message format to notify about merge result.
This commit is contained in:
Kirill Likhodedov
2014-07-23 13:14:08 +04:00
parent 797a61cc2c
commit f1c398141a
7 changed files with 114 additions and 24 deletions
@@ -25,6 +25,9 @@ import com.intellij.openapi.vcs.VcsNotifier;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import git4idea.GitLocalBranch;
import git4idea.GitPlatformFacade;
import git4idea.GitUtil;
import git4idea.commands.Git;
@@ -32,6 +35,7 @@ import git4idea.commands.GitMessageWithFilesDetector;
import git4idea.config.GitVcsSettings;
import git4idea.repo.GitRepository;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
@@ -50,7 +54,7 @@ abstract class GitBranchOperation {
@NotNull protected final Git myGit;
@NotNull protected final GitBranchUiHandler myUiHandler;
@NotNull private final Collection<GitRepository> myRepositories;
@NotNull protected final String myCurrentBranchOrRev;
@NotNull protected final Map<GitRepository, String> myCurrentHeads;
private final GitVcsSettings mySettings;
@NotNull private final Collection<GitRepository> mySuccessfulRepositories;
@@ -63,7 +67,13 @@ abstract class GitBranchOperation {
myGit = git;
myUiHandler = uiHandler;
myRepositories = repositories;
myCurrentBranchOrRev = GitBranchUtil.getCurrentBranchOrRev(repositories);
myCurrentHeads = ContainerUtil.map2Map(repositories, new Function<GitRepository, Pair<GitRepository, String>>() {
@Override
public Pair<GitRepository, String> fun(GitRepository repository) {
GitLocalBranch currentBranch = repository.getCurrentBranch();
return Pair.create(repository, currentBranch == null ? repository.getCurrentRevision() : currentBranch.getName());
}
});
mySuccessfulRepositories = new ArrayList<GitRepository>();
myRemainingRepositories = new ArrayList<GitRepository>(myRepositories);
mySettings = myFacade.getSettings(myProject);
@@ -220,13 +230,30 @@ abstract class GitBranchOperation {
protected void updateRecentBranch() {
if (getRepositories().size() == 1) {
GitRepository repository = myRepositories.iterator().next();
mySettings.setRecentBranchOfRepository(repository.getRoot().getPath(), myCurrentBranchOrRev);
mySettings.setRecentBranchOfRepository(repository.getRoot().getPath(), myCurrentHeads.get(repository));
}
else {
mySettings.setRecentCommonBranch(myCurrentBranchOrRev);
String recentCommonBranch = getRecentCommonBranch();
if (recentCommonBranch != null) {
mySettings.setRecentCommonBranch(recentCommonBranch);
}
}
}
@Nullable
private String getRecentCommonBranch() {
String recentCommonBranch = null;
for (String branch : myCurrentHeads.values()) {
if (recentCommonBranch == null) {
recentCommonBranch = branch;
}
else if (!recentCommonBranch.equals(branch)) {
return null;
}
}
return recentCommonBranch;
}
private void showUnmergedFilesDialogWithRollback() {
boolean ok = myUiHandler.showUnmergedFilesMessageWithRollback(getOperationName(), getRollbackProposal());
if (ok) {
@@ -340,4 +367,34 @@ abstract class GitBranchOperation {
return Pair.create(allConflictingRepositories, affectedChanges);
}
@NotNull
protected static String stringifyBranchesByRepos(@NotNull Map<GitRepository, String> heads) {
MultiMap<String, VirtualFile> grouped = groupByBranches(heads);
if (grouped.size() == 1) {
return grouped.keySet().iterator().next();
}
return StringUtil.join(grouped.entrySet(), new Function<Map.Entry<String, Collection<VirtualFile>>, String>() {
@Override
public String fun(Map.Entry<String, Collection<VirtualFile>> entry) {
String roots = StringUtil.join(entry.getValue(), new Function<VirtualFile, String>() {
@Override
public String fun(VirtualFile file) {
return file.getName();
}
}, ", ");
return entry.getKey() + " (in " + roots + ")";
}
}, "<br/>");
}
@NotNull
private static MultiMap<String, VirtualFile> groupByBranches(@NotNull Map<GitRepository, String> heads) {
MultiMap<String, VirtualFile> result = MultiMap.create();
for (Map.Entry<GitRepository, String> entry : heads.entrySet()) {
result.putValue(entry.getValue(), entry.getKey().getRoot());
}
return result;
}
}
@@ -89,7 +89,7 @@ class GitCheckoutNewBranchOperation extends GitBranchOperation {
protected String getRollbackProposal() {
return "However checkout has succeeded for the following " + repositories() + ":<br/>" +
successfulRepositoriesJoined() +
"<br/>You may rollback (checkout back to " + myCurrentBranchOrRev + " and delete " + myNewBranchName + ") not to let branches diverge.";
"<br/>You may rollback (checkout previous branch back, and delete " + myNewBranchName + ") not to let branches diverge.";
}
@NotNull
@@ -104,7 +104,7 @@ class GitCheckoutNewBranchOperation extends GitBranchOperation {
GitCompoundResult deleteResult = new GitCompoundResult(myProject);
Collection<GitRepository> repositories = getSuccessfulRepositories();
for (GitRepository repository : repositories) {
GitCommandResult result = myGit.checkout(repository, myCurrentBranchOrRev, null, true);
GitCommandResult result = myGit.checkout(repository, myCurrentHeads.get(repository), null, true);
checkoutResult.append(repository, result);
if (result.success()) {
deleteResult.append(repository, myGit.branchDelete(repository, myNewBranchName, false));
@@ -113,7 +113,7 @@ class GitCheckoutNewBranchOperation extends GitBranchOperation {
}
if (checkoutResult.totalSuccess() && deleteResult.totalSuccess()) {
VcsNotifier.getInstance(myProject).notifySuccess("Rollback successful", String
.format("Checked out %s and deleted %s on %s %s", code(myCurrentBranchOrRev), code(myNewBranchName),
.format("Checked out %s and deleted %s on %s %s", stringifyBranchesByRepos(myCurrentHeads), code(myNewBranchName),
StringUtil.pluralize("root", repositories.size()), successfulRepositoriesJoined()));
}
else {
@@ -47,7 +47,7 @@ import static git4idea.util.GitUIUtil.code;
*/
class GitCheckoutOperation extends GitBranchOperation {
public static final String ROLLBACK_PROPOSAL_FORMAT = "You may rollback (checkout back to %s) not to let branches diverge.";
public static final String ROLLBACK_PROPOSAL_FORMAT = "You may rollback (checkout back to previous branch) not to let branches diverge.";
@NotNull private final String myStartPointReference;
@Nullable private final String myNewBranch;
@@ -115,7 +115,8 @@ class GitCheckoutOperation extends GitBranchOperation {
private boolean smartCheckoutOrNotify(@NotNull GitRepository repository,
@NotNull GitMessageWithFilesDetector localChangesOverwrittenByCheckout) {
Pair<List<GitRepository>, List<Change>> conflictingRepositoriesAndAffectedChanges =
getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByCheckout, myCurrentBranchOrRev, myStartPointReference);
getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByCheckout, myCurrentHeads.get(repository),
myStartPointReference);
List<GitRepository> allConflictingRepositories = conflictingRepositoriesAndAffectedChanges.getFirst();
List<Change> affectedChanges = conflictingRepositoriesAndAffectedChanges.getSecond();
@@ -153,8 +154,7 @@ class GitCheckoutOperation extends GitBranchOperation {
@Override
protected String getRollbackProposal() {
return "However checkout has succeeded for the following " + repositories() + ":<br/>" +
successfulRepositoriesJoined() +
"<br/>" + String.format(ROLLBACK_PROPOSAL_FORMAT, myCurrentBranchOrRev);
successfulRepositoriesJoined() + "<br/>" + ROLLBACK_PROPOSAL_FORMAT;
}
@NotNull
@@ -168,7 +168,7 @@ class GitCheckoutOperation extends GitBranchOperation {
GitCompoundResult checkoutResult = new GitCompoundResult(myProject);
GitCompoundResult deleteResult = new GitCompoundResult(myProject);
for (GitRepository repository : getSuccessfulRepositories()) {
GitCommandResult result = myGit.checkout(repository, myCurrentBranchOrRev, null, true);
GitCommandResult result = myGit.checkout(repository, myCurrentHeads.get(repository), null, true);
checkoutResult.append(repository, result);
if (result.success() && myNewBranch != null) {
/*
@@ -183,7 +183,7 @@ class GitCheckoutOperation extends GitBranchOperation {
if (!checkoutResult.totalSuccess() || !deleteResult.totalSuccess()) {
StringBuilder message = new StringBuilder();
if (!checkoutResult.totalSuccess()) {
message.append("Errors during checking out ").append(myCurrentBranchOrRev).append(": ");
message.append("Errors during checkout: ");
message.append(checkoutResult.getErrorOutputWithReposIndication());
}
if (!deleteResult.totalSuccess()) {
@@ -68,7 +68,7 @@ class GitDeleteBranchOperation extends GitBranchOperation {
else if (notFullyMergedDetector.hasHappened()) {
String baseBranch = notMergedToUpstreamDetector.getBaseBranch();
if (baseBranch == null) { // GitBranchNotMergedToUpstreamDetector didn't happen
baseBranch = myCurrentBranchOrRev;
baseBranch = myCurrentHeads.get(repository);
}
Collection<GitRepository> remainingRepositories = getRemainingRepositories();
@@ -189,7 +189,8 @@ class GitMergeOperation extends GitBranchOperation {
private boolean proposeSmartMergePerformAndNotify(@NotNull GitRepository repository,
@NotNull GitMessageWithFilesDetector localChangesOverwrittenByMerge) {
Pair<List<GitRepository>, List<Change>> conflictingRepositoriesAndAffectedChanges =
getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByMerge, myCurrentBranchOrRev, myBranchToMerge);
getConflictingRepositoriesAndAffectedChanges(repository, localChangesOverwrittenByMerge, myCurrentHeads.get(repository),
myBranchToMerge);
List<GitRepository> allConflictingRepositories = conflictingRepositoriesAndAffectedChanges.getFirst();
List<Change> affectedChanges = conflictingRepositoriesAndAffectedChanges.getSecond();
@@ -339,7 +340,8 @@ class GitMergeOperation extends GitBranchOperation {
@NotNull
@Override
public String getSuccessMessage() {
return String.format("Merged <b><code>%s</code></b> to <b><code>%s</code></b>", myBranchToMerge, myCurrentBranchOrRev);
return String.format("Merged <b><code>%s</code></b> to <b><code>%s</code></b>",
myBranchToMerge, stringifyBranchesByRepos(myCurrentHeads));
}
@NotNull
@@ -14,16 +14,13 @@
* limitations under the License.
*/
package git4idea.branch
import com.intellij.dvcs.test.MockVirtualFile
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.util.ProgressIndicatorBase
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vcs.FilePathImpl
import com.intellij.openapi.vcs.changes.Change
import com.intellij.openapi.vcs.changes.CurrentContentRevision
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.Function
import com.intellij.util.LineSeparator
@@ -38,9 +35,7 @@ import org.jetbrains.annotations.NotNull
import java.util.regex.Matcher
import static com.intellij.openapi.vcs.Executor.*
import static git4idea.test.GitExecutor.cd
import static git4idea.test.GitExecutor.git
import static git4idea.test.GitExecutor.*
import static git4idea.test.GitScenarios.*
class GitBranchWorkerTest extends GitPlatformTest {
@@ -600,6 +595,39 @@ class GitBranchWorkerTest extends GitPlatformTest {
assertEquals "Merge in ultimate should have been reset", ultimateTipAfterMerge, tip(myUltimate)
}
public void test_checkout_in_detached_head() {
cd(myCommunity);
touch("file.txt", "some content");
add("file.txt");
commit("msg");
git(myCommunity, "checkout HEAD^");
checkoutBranch("master", []);
assertCurrentBranch("master");
}
// inspired by IDEA-127472
public void test_checkout_to_common_branch_when_branches_have_diverged() {
branchWithCommit(myUltimate, "feature", "feature-file.txt", "feature_content", false);
branchWithCommit(myCommunity, "newbranch", "newbranch-file.txt", "newbranch_content", false);
checkoutBranch("master", [])
assertCurrentBranch("master");
}
public void test_rollback_checkout_from_diverged_branches_should_return_to_proper_branches() {
branchWithCommit(myUltimate, "feature", "feature-file.txt", "feature_content", false);
branchWithCommit(myCommunity, "newbranch", "newbranch-file.txt", "newbranch_content", false);
unmergedFiles(myContrib)
checkoutBranch "master", [
showUnmergedFilesMessageWithRollback: { String s1, String s2 -> true },
]
assertCurrentBranch(myUltimate, "feature");
assertCurrentBranch(myCommunity, "newbranch");
assertCurrentBranch(myContrib, "master");
}
static def assertCurrentBranch(GitRepository repository, String name) {
def curBranch = git(repository, "branch").split("\n").find { it -> it.contains("*") }.replace('*', ' ').trim()
assertEquals("Current branch is incorrect in ${repository}", name, curBranch)
@@ -38,14 +38,17 @@ class GitScenarios {
/**
* Create a branch with a commit and return back to master.
*/
static def branchWithCommit(GitRepository repository, String name, String file = "branch_file.txt", String content = "branch content") {
static def branchWithCommit(GitRepository repository, String name, String file = "branch_file.txt", String content = "branch content",
boolean returnToMaster = true) {
cd repository
git("checkout -b $name")
touch(file, content)
git("add $file")
git("commit -m branch_content")
git("checkout master")
if (returnToMaster) {
git("checkout master")
}
}
/**