Allow to create Git branch without checking it out

Add a checkbox to the branch creation dialog.

Fixes IDEA-149886
This commit is contained in:
Kirill Likhodedov
2017-02-14 15:13:27 +00:00
parent 5dbdc68314
commit 5332f00a8d
10 changed files with 220 additions and 64 deletions
@@ -1688,7 +1688,7 @@ public class Messages {
}
}
protected static class InputDialog extends MessageDialog {
public static class InputDialog extends MessageDialog {
protected JTextComponent myField;
private final InputValidator myValidator;
@@ -18,22 +18,29 @@ package git4idea.actions;
import com.intellij.openapi.project.Project;
import com.intellij.vcs.log.Hash;
import git4idea.branch.GitBrancher;
import git4idea.branch.GitNewBranchOptions;
import git4idea.repo.GitRepository;
import org.jetbrains.annotations.NotNull;
import static git4idea.branch.GitBranchUtil.getNewBranchNameFromUser;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
public class GitCreateNewBranchAction extends GitLogSingleCommitAction {
@Override
protected void actionPerformed(@NotNull GitRepository repository, @NotNull Hash commit) {
Project project = repository.getProject();
String name = getNewBranchNameFromUser(project, singleton(repository), "Checkout New Branch From " + commit.toShortString());
if (name != null) {
GitNewBranchOptions options = getNewBranchNameFromUser(project, singleton(repository), "Checkout New Branch From " + commit.toShortString());
if (options != null) {
GitBrancher brancher = GitBrancher.getInstance(project);
brancher.checkoutNewBranchStartingFrom(name, commit.asString(), singletonList(repository), null);
if (options.shouldCheckout()) {
brancher.checkoutNewBranchStartingFrom(options.getName(), commit.asString(), singletonList(repository), null);
}
else {
brancher.createBranch(options.getName(), singletonMap(repository, commit.asString()));
}
}
}
}
@@ -21,7 +21,6 @@ import com.google.common.collect.Collections2;
import com.intellij.dvcs.DvcsUtil;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vfs.CharsetToolkit;
@@ -260,13 +259,16 @@ public class GitBranchUtil {
}
/**
* Shows a message dialog to enter the name of new branch.
* @return name of new branch or {@code null} if user has cancelled the dialog.
* <p>Shows a message dialog to enter the name of new branch.</p>
* <p>Optionally allows to not checkout this branch, and just create it.</p>
*
* @return the name of the new branch and whether it should be checked out, or {@code null} if user has cancelled the dialog.
*/
@Nullable
public static String getNewBranchNameFromUser(@NotNull Project project, @NotNull Collection<GitRepository> repositories, @NotNull String dialogTitle) {
return Messages.showInputDialog(project, "New branch name:", dialogTitle, null, "",
GitNewBranchNameValidator.newInstance(repositories));
public static GitNewBranchOptions getNewBranchNameFromUser(@NotNull Project project,
@NotNull Collection<GitRepository> repositories,
@NotNull String dialogTitle) {
return new GitNewBranchDialog(project, dialogTitle, GitNewBranchNameValidator.newInstance(repositories)).showAndGetOptions();
}
/**
@@ -38,6 +38,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* Executes the logic of git branch operations.
@@ -75,6 +76,11 @@ public final class GitBranchWorker {
}
}
public void createBranch(@NotNull String name, @NotNull Map<GitRepository, String> startPoints) {
updateInfo(startPoints.keySet());
new GitCreateBranchOperation(myProject, myGit, myUiHandler, name, startPoints).execute();
}
public void createNewTag(@NotNull final String name, @NotNull final String reference, @NotNull final List<GitRepository> repositories) {
for (GitRepository repository : repositories) {
myGit.createNewTag(repository, name, null, reference);
@@ -22,6 +22,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Map;
/**
* <p>Executes various operations on Git branches: checkout, create new branch, merge, delete, compare.</p>
@@ -29,7 +30,7 @@ import java.util.List;
* <p>It also takes care of analyzing results and notifying the user.</p>
* <p>All operations can be called for multiple repositories at once.</p>
*
* @author Kirill Likhodedov
* @see GitBranchWorker
*/
public interface GitBrancher {
static GitBrancher getInstance(@NotNull Project project) {
@@ -48,6 +49,15 @@ public interface GitBrancher {
*/
void checkoutNewBranch(@NotNull String name, @NotNull List<GitRepository> repositories);
/**
* Creates new branch without checking it out.
*
* @param name name of the new branch.
* @param startPoints position (commit hash) where the branch should be created, for each repository.
* Such position can be indicated by any valid Git reference (commit hash, branch name, etc.)
*/
void createBranch(@NotNull String name, @NotNull Map<GitRepository, String> startPoints);
/**
* <p>Creates new tag on the selected reference.</p>
*
@@ -27,6 +27,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Map;
/**
* @author Kirill Likhodedov
@@ -55,6 +56,15 @@ class GitBrancherImpl implements GitBrancher {
return new GitBranchWorker(myProject, myGit, new GitBranchUiHandlerImpl(myProject, myGit, indicator));
}
@Override
public void createBranch(@NotNull String name, @NotNull Map<GitRepository, String> startPoints) {
new CommonBackgroundTask(myProject, "Creating branch " + name, null) {
@Override public void execute(@NotNull ProgressIndicator indicator) {
newWorker(indicator).createBranch(name, startPoints);
}
}.runInBackground();
}
@Override
public void createNewTag(@NotNull final String name, @NotNull final String reference, @NotNull final List<GitRepository> repositories,
@Nullable Runnable callInAwtLater) {
@@ -0,0 +1,78 @@
/*
* Copyright 2000-2017 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.project.Project
import com.intellij.openapi.vcs.VcsNotifier
import git4idea.commands.Git
import git4idea.commands.GitCompoundResult
import git4idea.repo.GitRepository
internal class GitCreateBranchOperation(
project: Project,
git: Git,
uiHandler: GitBranchUiHandler,
private val branchName: String,
private val startPoints: Map<GitRepository, String>) : GitBranchOperation(project, git, uiHandler, startPoints.keys) {
public override fun execute() {
var fatalErrorHappened = false
while (hasMoreRepositories() && !fatalErrorHappened) {
val repository = next()
val result = myGit.branchCreate(repository, branchName, startPoints[repository]!!)
if (result.success()) {
repository.update()
markSuccessful(repository)
}
else {
fatalError("Couldn't create new branch $branchName", result.errorOutputAsJoinedString)
fatalErrorHappened = true
}
}
if (!fatalErrorHappened) {
notifySuccess()
updateRecentBranch()
}
}
override fun rollback() {
val repositories = successfulRepositories
val deleteResult = GitCompoundResult(myProject)
for (repository in repositories) {
deleteResult.append(repository, myGit.branchDelete(repository, branchName, false))
repository.update()
}
val vcsNotifier = VcsNotifier.getInstance(myProject)
if (deleteResult.totalSuccess()) {
vcsNotifier.notifySuccess("Rollback successful", "Deleted $branchName")
}
else {
vcsNotifier.notifyError("Error during rollback", deleteResult.errorOutputWithReposIndication)
}
}
override fun getSuccessMessage() = "Branch <b><code>$branchName</code></b> was created"
override fun getRollbackProposal() = """
However the branch was created in the following ${repositories()}:<br/>
${successfulRepositoriesJoined()}<br/>
You may rollback (delete $branchName) not to let branches diverge.""".trimIndent()
override fun getOperationName() = "create branch"
}
@@ -0,0 +1,48 @@
/*
* Copyright 2000-2017 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.project.Project
import com.intellij.openapi.ui.Messages
import com.intellij.ui.components.JBCheckBox
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil.DEFAULT_HGAP
import com.intellij.util.ui.UIUtil.DEFAULT_VGAP
import git4idea.validators.GitNewBranchNameValidator
import java.awt.BorderLayout
import java.awt.event.KeyEvent
import javax.swing.JComponent
internal data class GitNewBranchOptions(val name: String, @get:JvmName("shouldCheckout") val checkout: Boolean)
internal class GitNewBranchDialog(project: Project, dialogTitle: String, validator: GitNewBranchNameValidator) :
Messages.InputDialog(project, "New branch name:", dialogTitle, null, "", validator) {
private lateinit var checkoutCheckbox : JBCheckBox
fun showAndGetOptions(): GitNewBranchOptions? {
return if (showAndGet()) GitNewBranchOptions(inputString!!.trim(), checkoutCheckbox.isSelected) else null
}
override fun createCenterPanel(): JComponent? {
checkoutCheckbox = JBCheckBox("Checkout branch", true)
checkoutCheckbox.mnemonic = KeyEvent.VK_C
val panel = JBUI.Panels.simplePanel(DEFAULT_HGAP, DEFAULT_VGAP)
panel.add(checkoutCheckbox, BorderLayout.WEST)
return panel
}
}
@@ -29,8 +29,10 @@ import com.intellij.openapi.ui.Messages;
import com.intellij.util.containers.ContainerUtil;
import git4idea.branch.GitBranchUtil;
import git4idea.branch.GitBrancher;
import git4idea.branch.GitNewBranchOptions;
import git4idea.repo.GitRepository;
import git4idea.validators.GitNewBranchNameValidator;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -40,6 +42,7 @@ import java.util.List;
import static com.intellij.dvcs.ui.BranchActionGroupPopup.wrapWithMoreActionIfNeeded;
import static com.intellij.dvcs.ui.BranchActionUtil.*;
import static git4idea.GitStatisticsCollectorKt.reportUsage;
import static git4idea.GitUtil.HEAD;
import static git4idea.branch.GitBranchType.LOCAL;
import static git4idea.branch.GitBranchType.REMOTE;
import static java.util.stream.Collectors.toList;
@@ -100,11 +103,17 @@ class GitBranchPopupActions {
@Override
public void actionPerformed(AnActionEvent e) {
final String name = GitBranchUtil.getNewBranchNameFromUser(myProject, myRepositories, "Create New Branch");
if (name != null) {
GitNewBranchOptions options = GitBranchUtil.getNewBranchNameFromUser(myProject, myRepositories, "Create New Branch");
if (options != null) {
GitBrancher brancher = GitBrancher.getInstance(myProject);
brancher.checkoutNewBranch(name, myRepositories);
reportUsage("git.branch.create.new");
if (options.shouldCheckout()) {
reportUsage("git.branch.create.new");
brancher.checkoutNewBranch(options.getName(), myRepositories);
}
else {
reportUsage("git.branch.create.new.nocheckout");
brancher.createBranch(options.getName(), StreamEx.of(myRepositories).toMap(position -> HEAD));
}
}
}
}
@@ -23,7 +23,6 @@ import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.util.Ref
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vcs.Executor
import com.intellij.openapi.vcs.Executor.*
import com.intellij.openapi.vcs.changes.Change
import com.intellij.openapi.vfs.VirtualFile
@@ -56,13 +55,13 @@ class GitBranchWorkerTest : GitPlatformTest() {
super.setUp()
cd(myProjectRoot)
val community = Executor.mkdir("community")
val contrib = Executor.mkdir("contrib")
val community = mkdir("community")
val contrib = mkdir("contrib")
myUltimate = createRepository(myProjectPath)
myCommunity = createRepository(community.path)
myContrib = createRepository(contrib.path)
myRepositories = Arrays.asList<GitRepository>(myUltimate, myCommunity, myContrib)
myRepositories = listOf(myUltimate, myCommunity, myContrib)
cd(myProjectRoot)
touch(".gitignore", "community\ncontrib")
@@ -75,8 +74,22 @@ class GitBranchWorkerTest : GitPlatformTest() {
checkoutNewBranch("feature", TestUiHandler())
assertCurrentBranch("feature")
assertEquals("Notification about successful branch creation is incorrect",
"Branch " + bcode("feature") + " was created", myVcsNotifier.lastNotification.content)
assertSuccessfulNotification("Branch ${bcode("feature")} was created")
}
fun `test create new branch without checkout not at HEAD`() {
val hashMap = myRepositories.map { it to it.currentRevision!! }.toMap()
myRepositories.forEach { cd(it); tac("f.txt") }
GitBranchWorker(myProject, myGit, TestUiHandler()).createBranch("feature", myRepositories.map{ it to "HEAD^" }.toMap())
assertCurrentBranch("master")
myRepositories.forEach {
val branch = it.branches.findLocalBranch("feature")
assertNotNull("Branch not created in $it", branch)
assertEquals("Branch feature created at wrong point", hashMap[it], it.branches.getHash(branch!!)!!.asString())
}
assertSuccessfulNotification("Branch ${bcode("feature")} was created")
}
fun test_create_new_branch_with_unmerged_files_in_first_repo_should_show_notification() {
@@ -177,9 +190,7 @@ class GitBranchWorkerTest : GitPlatformTest() {
unmergedFiles(myCommunity)
checkoutBranch("feature", object : TestUiHandler() {
override fun showUnmergedFilesMessageWithRollback(operationName: String, rollbackProposal: String): Boolean {
return true
}
override fun showUnmergedFilesMessageWithRollback(operationName: String, rollbackProposal: String) = true
})
assertCurrentBranch("master")
@@ -190,9 +201,7 @@ class GitBranchWorkerTest : GitPlatformTest() {
unmergedFiles(myCommunity)
checkoutBranch("feature", object : TestUiHandler() {
override fun showUnmergedFilesMessageWithRollback(operationName: String, rollbackProposal: String): Boolean {
return false
}
override fun showUnmergedFilesMessageWithRollback(operationName: String, rollbackProposal: String) = false
})
assertCurrentBranch(myUltimate, "feature")
@@ -206,8 +215,7 @@ class GitBranchWorkerTest : GitPlatformTest() {
checkoutRevision("feature", TestUiHandler())
assertDetachedState("feature")
assertEquals("Notification about successful branch checkout is incorrect", "Checked out " + bcode("feature"),
myVcsNotifier.lastNotification.content)
assertSuccessfulNotification("Checked out ${bcode("feature")}")
}
fun test_checkout_revision_checkout_ref_with_complete_success() {
@@ -216,8 +224,7 @@ class GitBranchWorkerTest : GitPlatformTest() {
checkoutRevision("feature~1", TestUiHandler())
assertDetachedState("master")
assertEquals("Notification about successful branch checkout is incorrect", "Checked out " + bcode("feature~1"),
myVcsNotifier.lastNotification.content)
assertSuccessfulNotification("Checked out ${bcode("feature~1")}")
}
fun test_checkout_revision_checkout_ref_with_complete_failure() {
@@ -227,13 +234,11 @@ class GitBranchWorkerTest : GitPlatformTest() {
assertCurrentBranch("master")
assertCurrentRevision("master")
assertEquals("Notification about successful branch checkout is incorrect",
"Revision not found in project, community and contrib",
myVcsNotifier.lastNotification.content)
assertSuccessfulNotification("Revision not found in project, community and contrib")
}
fun test_checkout_revision_checkout_ref_with_partial_success() {
branchWithCommit(ContainerUtil.list<GitRepository>(myCommunity, myContrib), "feature")
branchWithCommit(listOf(myCommunity, myContrib), "feature")
checkoutRevision("feature", TestUiHandler())
@@ -241,10 +246,8 @@ class GitBranchWorkerTest : GitPlatformTest() {
assertDetachedState(myCommunity, "feature")
assertDetachedState(myContrib, "feature")
assertEquals("Notification about successful branch checkout is incorrect",
"Checked out " + bcode("feature") + " in community and contrib" + "<br>" +
"Revision not found in project" + "<br><a href='rollback'>Rollback</a>",
myVcsNotifier.lastNotification.content)
assertSuccessfulNotification("Checked out ${bcode("feature")} in community and contrib<br/>" +
"Revision not found in project<br><a href='rollback'>Rollback</a>")
}
fun test_checkout_with_untracked_files_overwritten_by_checkout_in_first_repo_should_show_notification() {
@@ -369,13 +372,8 @@ class GitBranchWorkerTest : GitPlatformTest() {
private fun agree_to_smart_operation(operation: String, expectedSuccessMessage: String): List<String> {
val localChanges = prepareLocalChangesOverwrittenBy(myUltimate)
val handler = TestUiHandler()
checkoutOrMerge(operation, "feature", handler)
assertNotNull("No success notification was shown", myVcsNotifier.lastNotification)
assertEquals("Success message is incorrect", expectedSuccessMessage, myVcsNotifier.lastNotification.content)
checkoutOrMerge(operation, "feature", TestUiHandler())
assertSuccessfulNotification(expectedSuccessMessage)
return localChanges
}
@@ -407,9 +405,7 @@ class GitBranchWorkerTest : GitPlatformTest() {
changes: List<Change>,
paths: Collection<String>,
operation: String,
forceButton: String?): Int {
return GitSmartOperationDialog.CANCEL_EXIT_CODE
}
forceButton: String?) = GitSmartOperationDialog.CANCEL_EXIT_CODE
})
assertNull("Notification was unexpectedly shown:" + myVcsNotifier.lastNotification, myVcsNotifier.lastNotification)
@@ -436,9 +432,7 @@ class GitBranchWorkerTest : GitPlatformTest() {
changes: List<Change>,
paths: Collection<String>,
operation: String,
forceButton: String?): Int {
return GitSmartOperationDialog.CANCEL_EXIT_CODE
}
forceButton: String?) = GitSmartOperationDialog.CANCEL_EXIT_CODE
override fun notifyErrorWithRollbackProposal(title: String,
message: String,
@@ -460,15 +454,11 @@ class GitBranchWorkerTest : GitPlatformTest() {
changes: List<Change>,
paths: Collection<String>,
operation: String,
forceButton: String?): Int {
return GitSmartOperationDialog.FORCE_EXIT_CODE
}
forceButton: String?) = GitSmartOperationDialog.FORCE_EXIT_CODE
})
brancher.checkoutNewBranchStartingFrom("new_branch", "feature", myRepositories)
assertEquals("Notification about successful branch creation is incorrect",
"Checked out new branch <b><code>new_branch</code></b> from <b><code>feature</code></b>",
myVcsNotifier.lastNotification.content)
assertSuccessfulNotification("Checked out new branch <b><code>new_branch</code></b> from <b><code>feature</code></b>")
assertCurrentBranch("new_branch")
}
@@ -547,9 +537,7 @@ class GitBranchWorkerTest : GitPlatformTest() {
override fun showBranchIsNotFullyMergedDialog(project: Project,
history: Map<GitRepository, List<GitCommit>>,
baseBranches: Map<GitRepository, String>,
removedBranch: String): Boolean {
return true
}
removedBranch: String) = true
})
assertBranchDeleted("todelete")
}
@@ -639,9 +627,7 @@ class GitBranchWorkerTest : GitPlatformTest() {
mergeBranch("master2", TestUiHandler())
assertNotNull("Success message wasn't shown", myVcsNotifier.lastNotification)
assertEquals("Success message is incorrect",
"Merged " + bcode("master2") + " to " + bcode("master") + "<br/><a href='delete'>Delete master2</a>",
assertSuccessfulNotification("Merged " + bcode("master2") + " to " + bcode("master") + "<br/><a href='delete'>Delete master2</a>",
myVcsNotifier.lastNotification.content)
assertFile(myUltimate, "branch_file.txt", "branch content")
assertFile(myCommunity, "branch_file.txt", "branch content")