diff --git a/plugins/hg4idea/resources/org/zmlx/hg4idea/HgVcsMessages.properties b/plugins/hg4idea/resources/org/zmlx/hg4idea/HgVcsMessages.properties index 804bc45776d9..7ba4790d25f3 100644 --- a/plugins/hg4idea/resources/org/zmlx/hg4idea/HgVcsMessages.properties +++ b/plugins/hg4idea/resources/org/zmlx/hg4idea/HgVcsMessages.properties @@ -127,3 +127,4 @@ hg4idea.history.copy.revision.number=Copy Revision Number hg4idea.changelist.column.branch=Branch hg4idea.annotation.tool.tip=commit {0}\nAuthor: {1}\nDate: {2}\n\n{3} +hg4idea.push.asNewBranch=push as &new remote branch diff --git a/plugins/hg4idea/src/META-INF/plugin.xml b/plugins/hg4idea/src/META-INF/plugin.xml index ef53adfcd3d5..e875f749273d 100644 --- a/plugins/hg4idea/src/META-INF/plugin.xml +++ b/plugins/hg4idea/src/META-INF/plugin.xml @@ -55,6 +55,7 @@ + diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/HgPusher.java b/plugins/hg4idea/src/org/zmlx/hg4idea/HgPusher.java index 6b91e826aff0..1ddff9b62423 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/HgPusher.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/HgPusher.java @@ -137,6 +137,7 @@ public class HgPusher { command.setRevision(dialog.getRevision()); command.setForce(dialog.isForce()); command.setBranch(dialog.getBranch()); + command.setIsNewBranch(dialog.isNewBranch()); return command; } diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/action/HgAbstractGlobalAction.java b/plugins/hg4idea/src/org/zmlx/hg4idea/action/HgAbstractGlobalAction.java index 84a9e8028b0e..930cae34318b 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/action/HgAbstractGlobalAction.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/action/HgAbstractGlobalAction.java @@ -62,7 +62,7 @@ abstract class HgAbstractGlobalAction extends AnAction { protected abstract void execute(Project project, Collection repositories, @Nullable VirtualFile selectedRepo); - protected static void handleException(Project project, Exception e) { + public static void handleException(Project project, Exception e) { LOG.info(e); new HgCommandResultNotifier(project).notifyError(null, "Error", e.getMessage()); } diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/action/HgBranchPopup.java b/plugins/hg4idea/src/org/zmlx/hg4idea/action/HgBranchPopup.java new file mode 100644 index 000000000000..7975847b8435 --- /dev/null +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/action/HgBranchPopup.java @@ -0,0 +1,244 @@ +/* + * Copyright 2000-2013 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 org.zmlx.hg4idea.action; + +import com.intellij.openapi.actionSystem.ActionGroup; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DefaultActionGroup; +import com.intellij.openapi.actionSystem.impl.SimpleDataContext; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.popup.ListPopup; +import com.intellij.openapi.ui.popup.PopupStep; +import com.intellij.openapi.util.Condition; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.ErrorLabel; +import com.intellij.ui.JBColor; +import com.intellij.ui.components.panels.OpaquePanel; +import com.intellij.ui.popup.PopupFactoryImpl; +import com.intellij.ui.popup.WizardPopup; +import com.intellij.ui.popup.list.ListPopupImpl; +import com.intellij.ui.popup.list.PopupListElementRenderer; +import com.intellij.util.PlatformIcons; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.zmlx.hg4idea.repo.HgRepository; +import org.zmlx.hg4idea.repo.HgRepositoryImpl; +import org.zmlx.hg4idea.util.HgUtil; + +import javax.swing.*; +import java.awt.*; + +/** + *

+ * The popup which allows to quickly switch and control Hg branches. + *

+ *

+ * Use {@link #asListPopup()} to achieve the {@link com.intellij.openapi.ui.popup.ListPopup} itself. + *

+ * + * @author NadyaZabrodina + */ +public class HgBranchPopup { + + private final Project myProject; + + private final HgRepository myCurrentRepository; + private final ListPopupImpl myPopup; + + public ListPopup asListPopup() { + return myPopup; + } + + /** + * @param currentRepository Current repository, which means the repository of the currently open or selected file. + */ + public static HgBranchPopup getInstance(@NotNull Project project, @NotNull HgRepository currentRepository) { + return new HgBranchPopup(project, currentRepository); + } + + private HgBranchPopup(@NotNull Project project, @NotNull HgRepository currentRepository) { + myProject = project; + myCurrentRepository = currentRepository; + String title = createPopupTitle(currentRepository); + + Condition preselectActionCondition = new Condition() { + @Override + public boolean value(AnAction action) { + return false; + } + }; + + myPopup = new HgBranchActionGroupPopup(title, project, preselectActionCondition); + + setCurrentBranchInfo(); + } + + + @NotNull + private static String createPopupTitle(@NotNull HgRepository currentRepository) { + String title = "Hg Branches"; + + title += " in " + currentRepository.getRoot().getName(); + + return title; + } + + private void setCurrentBranchInfo() { + String branchText = "Current branch : "; + myPopup.setAdText(branchText + HgUtil.getDisplayableBranchText(myCurrentRepository), SwingConstants.CENTER); + } + + + private ActionGroup createActions() { + DefaultActionGroup popupGroup = new DefaultActionGroup(null, false); + + fillPopupWithCurrentRepositoryActions(popupGroup, createRepositoriesActions()); + + popupGroup.addSeparator(); + return popupGroup; + } + + + private DefaultActionGroup createRepositoriesActions() { + DefaultActionGroup popupGroup = new DefaultActionGroup(null, false); + popupGroup.addSeparator("Repositories"); + for (VirtualFile repository : HgUtil.getHgRepositories(myProject)) { + HgRepository repo = HgRepositoryImpl.getFullInstance(repository, myProject, myProject); + popupGroup.add(new RootAction(repo, null)); + } + return popupGroup; + } + + + private void fillPopupWithCurrentRepositoryActions(@NotNull DefaultActionGroup popupGroup, @Nullable DefaultActionGroup actions) { + popupGroup.addAll(new HgBranchPopupActions(myProject, myCurrentRepository).createActions(actions)); + } + + private static class RootAction extends ActionGroup { + + private final HgRepository myRepository; + + /** + * @param currentRepository Pass null in the case of common repositories - none repository will be highlighted then. + */ + RootAction(@NotNull HgRepository repository, @Nullable HgRepository currentRepository) { + super(repository.getRoot().getName(), true); + myRepository = repository; + if (repository.equals(currentRepository)) { + getTemplatePresentation().setIcon(PlatformIcons.CHECK_ICON); + } + } + + @NotNull + @Override + public AnAction[] getChildren(@Nullable AnActionEvent e) { + ActionGroup group = new HgBranchPopupActions(myRepository.getProject(), myRepository).createActions(null); + return group.getChildren(e); + } + + @NotNull + public String getCaption() { + String captionText = "Current branch in " + myRepository.getRoot().getName() + ": "; + return captionText + HgUtil.getDisplayableBranchText(myRepository); + } + + @NotNull + public String getBranch() { + return myRepository.getCurrentBranch(); + } + } + + private class HgBranchActionGroupPopup extends PopupFactoryImpl.ActionGroupPopup { + public HgBranchActionGroupPopup(@NotNull String title, @NotNull Project project, + @NotNull Condition preselectActionCondition) { + super(title, HgBranchPopup.this.createActions(), SimpleDataContext.getProjectContext(project), false, false, false, true, null, -1, + preselectActionCondition, null); + } + + @Override + protected WizardPopup createPopup(WizardPopup parent, PopupStep step, Object parentValue) { + WizardPopup popup = super.createPopup(parent, step, parentValue); + RootAction rootAction = getRootAction(parentValue); + if (rootAction != null) { + popup.setAdText((rootAction).getCaption()); + } + return popup; + } + + @Nullable + private RootAction getRootAction(Object value) { + if (value instanceof PopupFactoryImpl.ActionItem) { + AnAction action = ((PopupFactoryImpl.ActionItem)value).getAction(); + if (action instanceof RootAction) { + return (RootAction)action; + } + } + return null; + } + + @Override + protected ListCellRenderer getListElementRenderer() { + return new PopupListElementRenderer(this) { + + private ErrorLabel myBranchLabel; + + @Override + protected void customizeComponent(JList list, Object value, boolean isSelected) { + super.customizeComponent(list, value, isSelected); + + RootAction rootAction = getRootAction(value); + if (rootAction != null) { + myBranchLabel.setVisible(true); + myBranchLabel.setText(String.format("[%s]", rootAction.getBranch())); + + if (isSelected) { + setSelected(myBranchLabel); + } + else { + myBranchLabel.setBackground(getBackground()); + myBranchLabel.setForeground(JBColor.GRAY); // different foreground than for other elements + } + + adjustOpacity(myBranchLabel, isSelected); + } + else { + myBranchLabel.setVisible(false); + } + } + + @Override + protected JComponent createItemComponent() { + myTextLabel = new ErrorLabel(); + myTextLabel.setOpaque(true); + myTextLabel.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); + + myBranchLabel = new ErrorLabel(); + myBranchLabel.setOpaque(true); + myBranchLabel.setBorder(BorderFactory.createEmptyBorder(1, UIUtil.DEFAULT_HGAP, 1, 1)); + + JPanel compoundPanel = new OpaquePanel(new BorderLayout(), Color.white); + compoundPanel.add(myTextLabel, BorderLayout.CENTER); + compoundPanel.add(myBranchLabel, BorderLayout.EAST); + + return layoutComponent(compoundPanel); + } + }; + } + } +} + diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/action/HgBranchPopupActions.java b/plugins/hg4idea/src/org/zmlx/hg4idea/action/HgBranchPopupActions.java new file mode 100644 index 000000000000..81eb20400cc8 --- /dev/null +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/action/HgBranchPopupActions.java @@ -0,0 +1,226 @@ +/* + * Copyright 2000-2013 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 org.zmlx.hg4idea.action; + +import com.intellij.openapi.actionSystem.ActionGroup; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DefaultActionGroup; +import com.intellij.openapi.project.DumbAwareAction; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vcs.VcsException; +import com.intellij.openapi.vcs.update.UpdatedFiles; +import com.intellij.util.IconUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.zmlx.hg4idea.HgRevisionNumber; +import org.zmlx.hg4idea.HgVcs; +import org.zmlx.hg4idea.command.HgBranchCreateCommand; +import org.zmlx.hg4idea.command.HgMergeCommand; +import org.zmlx.hg4idea.command.HgUpdateCommand; +import org.zmlx.hg4idea.execution.HgCommandException; +import org.zmlx.hg4idea.execution.HgCommandResult; +import org.zmlx.hg4idea.execution.HgCommandResultHandler; +import org.zmlx.hg4idea.provider.update.HgConflictResolver; +import org.zmlx.hg4idea.provider.update.HgHeadMerger; +import org.zmlx.hg4idea.repo.HgRepository; +import org.zmlx.hg4idea.util.HgErrorUtil; +import org.zmlx.hg4idea.util.HgUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author Nadya Zabrodina + */ +public class HgBranchPopupActions { + + private final Project myProject; + private final HgRepository myRepository; + + HgBranchPopupActions(Project project, HgRepository repository) { + myProject = project; + myRepository = repository; + } + + ActionGroup createActions(@Nullable DefaultActionGroup toInsert) { + DefaultActionGroup popupGroup = new DefaultActionGroup(null, false); + popupGroup.addAction(new NewBranchAction(myProject, Collections.singletonList(myRepository))); + + if (toInsert != null) { + popupGroup.addAll(toInsert); + } + + popupGroup.addSeparator("Branches"); + List branches = new ArrayList(myRepository.getBranches()); + Collections.sort(branches); + for (String branch : branches) { + if (!branch.equals(myRepository.getCurrentBranch())) { // don't show current branch in the list + popupGroup.add(new BranchActions(myProject, branch, myRepository)); + } + } + return popupGroup; + } + + static class NewBranchAction extends DumbAwareAction { + private final Project myProject; + private final List myRepositories; + + NewBranchAction(@NotNull Project project, @NotNull List repositories) { + super("New Branch", "Create and checkout new branch", IconUtil.getAddIcon()); + myProject = project; + myRepositories = repositories; + } + + @Override + public void actionPerformed(AnActionEvent e) { + final String name = HgUtil.getNewBranchNameFromUser(myProject, "Create New Branch"); + + try { + new HgBranchCreateCommand(myProject, HgUtil.getRootForSelectedFile(myProject), name).execute(new HgCommandResultHandler() { + @Override + public void process(@Nullable HgCommandResult result) { + myProject.getMessageBus().syncPublisher(HgVcs.BRANCH_TOPIC).update(myProject, null); + if (HgErrorUtil.hasErrorsInCommandExecution(result)) { + new HgCommandResultNotifier(myProject) + .notifyError(result, "Creation failed", "Branch creation [" + name + "] failed"); + } + } + }); + } + catch (HgCommandException exception) { + HgAbstractGlobalAction.handleException(myProject, exception); + } + } + + @Override + public void update(AnActionEvent e) { + if (anyRepositoryIsFresh()) { + e.getPresentation().setEnabled(false); + e.getPresentation().setDescription("Checkout of a new branch is not possible before the first commit."); + } + } + + private boolean anyRepositoryIsFresh() { + for (HgRepository repository : myRepositories) { + if (repository.isFresh()) { + return true; + } + } + return false; + } + } + + + /** + * Actions available for branches. + */ + static class BranchActions extends ActionGroup { + + private final Project myProject; + private String myBranchName; + @NotNull private final HgRepository mySelectedRepository; + + BranchActions(@NotNull Project project, @NotNull String branchName, + @NotNull HgRepository selectedRepository) { + super("", true); + myProject = project; + myBranchName = branchName; + mySelectedRepository = selectedRepository; + getTemplatePresentation().setText(calcBranchText(), false); // no mnemonics + } + + @NotNull + private String calcBranchText() { + return myBranchName; + } + + @NotNull + @Override + public AnAction[] getChildren(@Nullable AnActionEvent e) { + return new AnAction[]{ + new UpdateToAction(myProject, mySelectedRepository, myBranchName), + new MergeAction(myProject, mySelectedRepository, myBranchName) + }; + } + + private static class MergeAction extends DumbAwareAction { + + private final Project myProject; + private final HgRepository mySelectedRepository; + private final String myBranchName; + + public MergeAction(@NotNull Project project, + @NotNull HgRepository selectedRepository, + @NotNull String branchName) { + super("Merge"); + myProject = project; + mySelectedRepository = selectedRepository; + myBranchName = branchName; + } + + @Override + public void actionPerformed(AnActionEvent e) { + UpdatedFiles updatedFiles = UpdatedFiles.create(); + HgMergeCommand hgMergeCommand = new HgMergeCommand(myProject, mySelectedRepository.getRoot()); + hgMergeCommand.setBranch(myBranchName); + //hgMergeCommand.setRevision(myBranchName.getHead().getChangeset()); + HgCommandResultNotifier notifier = new HgCommandResultNotifier(myProject); + try { + new HgHeadMerger(myProject, hgMergeCommand) + .merge(mySelectedRepository.getRoot(), updatedFiles, HgRevisionNumber.NULL_REVISION_NUMBER); + new HgConflictResolver(myProject, updatedFiles).resolve(mySelectedRepository.getRoot()); + } + catch (VcsException exception) { + if (exception.isWarning()) { + notifier.notifyWarning("Warning during merge", exception.getMessage()); + } + else { + notifier.notifyError(null, "Exception during merge", exception.getMessage()); + } + } + } + } + + private static class UpdateToAction extends DumbAwareAction { + + private final Project myProject; + private final HgRepository mySelectedRepository; + private final String myBranch; + + public UpdateToAction(@NotNull Project project, + @NotNull HgRepository selectedRepository, + @NotNull String branch) { + super("Update To"); + myProject = project; + mySelectedRepository = selectedRepository; + myBranch = branch; + } + + @Override + public void actionPerformed(AnActionEvent e) { + HgUpdateCommand hgUpdateCommand = new HgUpdateCommand(myProject, mySelectedRepository.getRoot()); + hgUpdateCommand.setBranch(myBranch); + HgCommandResult result = hgUpdateCommand.execute(); + if (HgErrorUtil.hasErrorsInCommandExecution(result)) { + new HgCommandResultNotifier(myProject).notifyError(result, "", "Update failed"); + } + myProject.getMessageBus().syncPublisher(HgVcs.BRANCH_TOPIC).update(myProject, null); + } + } + } +} diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/action/HgBranchesAction.java b/plugins/hg4idea/src/org/zmlx/hg4idea/action/HgBranchesAction.java new file mode 100644 index 000000000000..0debdaba692d --- /dev/null +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/action/HgBranchesAction.java @@ -0,0 +1,50 @@ +/* + * Copyright 2000-2013 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 org.zmlx.hg4idea.action; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.Nullable; +import org.zmlx.hg4idea.repo.HgRepository; +import org.zmlx.hg4idea.repo.HgRepositoryImpl; +import org.zmlx.hg4idea.util.HgUtil; + +import java.util.Collection; + +/** + * @author Nadya Zabrodina + */ + +public class HgBranchesAction extends HgAbstractGlobalAction { + + @Override + protected void execute(Project project, Collection repositories, @Nullable VirtualFile selectedRepo) { + HgRepository repository; + if (selectedRepo != null) { + repository = HgRepositoryImpl.getFullInstance(selectedRepo, project, project); + } + else { + VirtualFile selectedRoot = HgUtil.getRootForSelectedFile(project); + if (selectedRoot != null) { + repository = HgRepositoryImpl.getFullInstance(selectedRoot, project, project); + } + else { + return; + } + } + HgBranchPopup.getInstance(project, repository).asListPopup().showInFocusCenter(); + } +} diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgBranchCreateCommand.java b/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgBranchCreateCommand.java new file mode 100644 index 000000000000..759c0613ac29 --- /dev/null +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgBranchCreateCommand.java @@ -0,0 +1,50 @@ +/* + * Copyright 2000-2013 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 org.zmlx.hg4idea.command; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.zmlx.hg4idea.execution.HgCommandException; +import org.zmlx.hg4idea.execution.HgCommandExecutor; +import org.zmlx.hg4idea.execution.HgCommandResultHandler; + +import java.util.Arrays; + +/** + * @author Nadya Zabrodina + */ +public class HgBranchCreateCommand { + + private final Project project; + private final VirtualFile repo; + private final String branchName; + + public HgBranchCreateCommand(@NotNull Project project, @NotNull VirtualFile repo, @Nullable String branchName) { + this.project = project; + this.repo = repo; + this.branchName = branchName; + } + + public void execute(@Nullable HgCommandResultHandler resultHandler) throws HgCommandException { + if (StringUtil.isEmptyOrSpaces(branchName)) { + throw new HgCommandException("branch name is empty"); + } + new HgCommandExecutor(project).execute(repo, "branch", Arrays.asList(branchName), resultHandler); + } +} diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgPushCommand.java b/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgPushCommand.java index 37f37917f6b3..357fa891201a 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgPushCommand.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/command/HgPushCommand.java @@ -34,6 +34,7 @@ public class HgPushCommand { private String myRevision; private boolean myForce; private HgTagBranch myBranch; + private boolean myIsNewBranch; public HgPushCommand(Project project, @NotNull VirtualFile repo, String destination) { myProject = project; @@ -53,6 +54,10 @@ public class HgPushCommand { myBranch = branch; } + public void setIsNewBranch(boolean isNewBranch) { + myIsNewBranch = isNewBranch; + } + public void execute(final HgCommandResultHandler resultHandler) { final List arguments = new LinkedList(); if (!StringUtil.isEmptyOrSpaces(myRevision)) { @@ -60,8 +65,13 @@ public class HgPushCommand { arguments.add(myRevision); } if (myBranch != null) { - arguments.add("-b"); - arguments.add(myBranch.getName()); + if (myIsNewBranch) { + arguments.add("--new-branch"); + } + else { + arguments.add("-b"); + arguments.add(myBranch.getName()); + } } if (myForce) { arguments.add("-f"); diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepoStateException.java b/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepoStateException.java new file mode 100644 index 000000000000..6bcc816053bb --- /dev/null +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepoStateException.java @@ -0,0 +1,30 @@ +/* + * Copyright 2000-2013 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 org.zmlx.hg4idea.repo; + +/** + * @author Nadya Zabrodina + */ +public class HgRepoStateException extends RuntimeException { + + public HgRepoStateException(String message) { + super(message); + } + + public HgRepoStateException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepository.java b/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepository.java new file mode 100644 index 000000000000..b7feda1d6246 --- /dev/null +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepository.java @@ -0,0 +1,119 @@ +/* + * Copyright 2000-2013 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 org.zmlx.hg4idea.repo; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; + + +/** + *

+ * HgRepository is a representation of a Hg repository stored under the specified directory. + * It stores the information about the repository, which is frequently requested by other plugin components. + * All get-methods (like {@link #getCurrentRevision()}) are just getters of the correspondent fields and thus are very fast. + *

+ *

+ * The HgRepository is updated "externally" by the {@link org.zmlx.hg4idea.repo.HgRepositoryUpdater}, when correspondent {@code .hg/} service files + * change. + *

+ * Other components may subscribe to HgRepository changes via the STATUS_TOPIC {@link com.intellij.util.messages.Topic} + *

+ * + * @author Nadya Zabrodina + */ + +public interface HgRepository { + + String DEFAULT_BRANCH = "default"; + + /** + * Current state of the repository. + */ + + enum State { + /** + * HEAD is on branch, no merge process is in progress. + */ + + NORMAL, + /** + * During merge (for instance, merge failed with conflicts that weren't immediately resolved). + */ + + MERGING { + @Override + public String toString() { + return "Merging"; + } + }, + + } + + @NotNull + VirtualFile getRoot(); + + @NotNull + VirtualFile getHgDir(); + + @NotNull + String getPresentableUrl(); + + @NotNull + Project getProject(); + + + @NotNull + State getState(); + + + /** + * Returns the hash of the revision, which HEAD currently points to. + * Returns null only in the case of a fresh repository, when no commit have been made. + */ + + @Nullable + String getCurrentRevision(); + + /** + * Returns the current branch of this Hg repository. + * If the repository is being rebased, then the current branch is the branch being rebased (which was current before the rebase + * operation has started). + * Returns null, if the repository is not on a branch and not in the REBASING state. + */ + + @NotNull + String getCurrentBranch(); + + @NotNull + Collection getBranches(); + + /** + * @return true if current repository is "fresh", i.e. if no commits have been made yet. + */ + + boolean isFresh(); + + /** + * Updates the HgRepository by reading information from .hg dir... + */ + + void update(); +} diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepositoryFiles.java b/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepositoryFiles.java new file mode 100644 index 000000000000..2ccdbd016d42 --- /dev/null +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepositoryFiles.java @@ -0,0 +1,82 @@ +/* + * Copyright 2000-2013 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 org.zmlx.hg4idea.repo; + +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.Collection; + + +/** + * Stores paths to Hg service files (from .hg/ directory) that are used by IDEA, and provides methods to check if a file + * matches once of them. + * + * @author Nadya Zabrodina + */ +public class HgRepositoryFiles { + + public static final String BRANCHHEADS = "cache/branchheads"; + public static final String MERGE = "merge"; + public static final String BRANCH = "branch"; + + + private final String myBranchHeadsPath; + private final String myMergePath; + private final String myBranchPath; + + + public static HgRepositoryFiles getInstance(@NotNull VirtualFile hgDir) { + return new HgRepositoryFiles(hgDir); + } + + private HgRepositoryFiles(@NotNull VirtualFile hgDir) { + myBranchHeadsPath = hgDir.getPath() + slash(BRANCHHEADS); + myBranchPath = hgDir.getPath() + slash(BRANCH); + myMergePath = hgDir.getPath() + slash(MERGE); + } + + @NotNull + private static String slash(@NotNull String s) { + return "/" + s; + } + + /** + * Returns subdirectories of .hg which we are interested in - they should be watched by VFS. + */ + @NotNull + static Collection getSubDirRelativePaths() { + return Arrays.asList(slash(BRANCHHEADS), slash(MERGE)); + } + + public String getBranchHeadsPath() { + return myBranchHeadsPath; + } + + + public boolean isbranchHeadsFile(String filePath) { + return filePath.equals(myBranchHeadsPath); + } + + public boolean isBranchFile(String filePath) { + return filePath.equals(myBranchPath); + } + + public boolean isMergeFile(String filePath) { + return filePath.startsWith(myMergePath); + } +} diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepositoryImpl.java b/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepositoryImpl.java new file mode 100644 index 000000000000..75b8c3fe4345 --- /dev/null +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepositoryImpl.java @@ -0,0 +1,215 @@ +/* + * Copyright 2000-2013 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 org.zmlx.hg4idea.repo; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.Consumer; +import com.intellij.util.concurrency.QueueProcessor; +import com.intellij.util.messages.MessageBus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.zmlx.hg4idea.HgVcs; +import org.zmlx.hg4idea.util.HgUtil; + +import java.util.Collections; +import java.util.List; + + +/** + * @author Nadya Zabrodina + */ + +public class HgRepositoryImpl implements HgRepository, Disposable { + + private static final Object STUB_OBJECT = new Object(); + + private final Project myProject; + private final VirtualFile myRootDir; + private final HgRepositoryReader myReader; + private final VirtualFile myHgDir; + private final QueueProcessor myNotifier; + + @NotNull private volatile State myState = State.NORMAL; + @Nullable private volatile String myCurrentRevision = null; + @NotNull private volatile String myCurrentBranch = DEFAULT_BRANCH; + @NotNull private volatile List myBranches = Collections.emptyList(); + + + /* + * Get the HgRepository instance from the {@link HgRepositoryManager}. + * If you need to have an instance of HgRepository for a repository outside the project, use + * {@link #getLightInstance(com.intellij.openapi.vfs.VirtualFile, com.intellij.openapi.project.Project, PlatformFacade, com.intellij.openapi.Disposable)}. + + */ + protected HgRepositoryImpl(@NotNull VirtualFile rootDir, @NotNull Project project, + @NotNull Disposable parentDisposable) { + myRootDir = rootDir; + myProject = project; + Disposer.register(parentDisposable, this); + + myHgDir = myRootDir.findChild(HgUtil.DOT_HG); + assert myHgDir != null : ".hg directory wasn't found under " + rootDir.getPresentableUrl(); + + myReader = new HgRepositoryReader(VfsUtilCore.virtualToIoFile(myHgDir)); + + MessageBus messageBus = project.getMessageBus(); + myNotifier = new QueueProcessor(new NotificationConsumer(myProject, messageBus, this), myProject.getDisposed()); + update(); + } + + + public static HgRepository getFullInstance(@NotNull VirtualFile root, @NotNull Project project, + @NotNull Disposable parentDisposable) { + HgRepositoryImpl repository = new HgRepositoryImpl(root, project, parentDisposable); + repository.setupUpdater(); + return repository; + } + + private void setupUpdater() { + HgRepositoryUpdater updater = new HgRepositoryUpdater(this); + Disposer.register(this, updater); + } + + @Override + public void dispose() { + } + + @Override + @NotNull + public VirtualFile getRoot() { + return myRootDir; + } + + @NotNull + @Override + public VirtualFile getHgDir() { + return myHgDir; + } + + + @Override + @NotNull + public String getPresentableUrl() { + return getRoot().getPresentableUrl(); + } + + @Override + @NotNull + public Project getProject() { + return myProject; + } + + + @Override + @NotNull + public State getState() { + return myState; + } + + @Override + @Nullable + public String getCurrentRevision() { + return myCurrentRevision; + } + + @Override + @NotNull + public String getCurrentBranch() { + return myCurrentBranch; + } + + + @Override + @NotNull + public List getBranches() { + return myBranches; + } + + + @Override + public boolean isFresh() { + return !myReader.headExist(); + } + + @Override + public void update() { + readRepository(); + notifyListeners(); + } + + private void readRepository() { + if (!isFresh()) { + myState = myReader.readState(); + myCurrentRevision = myReader.readCurrentRevision(); + myCurrentBranch = myReader.readCurrentBranch(); + myBranches = myReader.readBranches(); + } + } + + protected void notifyListeners() { + myNotifier.add(STUB_OBJECT); // we don't have parameters for listeners + } + + private static class NotificationConsumer implements Consumer { + + private final Project myProject; + private final MessageBus myMessageBus; + private final HgRepository myRepository; + + NotificationConsumer(Project project, MessageBus messageBus, HgRepository repository) { + myProject = project; + myMessageBus = messageBus; + myRepository = repository; + } + + @Override + public void consume(Object o) { + if (!Disposer.isDisposed(myProject)) { + myMessageBus.syncPublisher(HgVcs.STATUS_TOPIC).update(myProject, myRepository.getRoot()); + } + } + } + + @Override + public String toString() { + return getPresentableUrl(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + HgRepositoryImpl that = (HgRepositoryImpl)o; + + if (!myProject.equals(that.myProject)) return false; + if (!myRootDir.equals(that.myRootDir)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = myProject.hashCode(); + result = 31 * result + (myRootDir.hashCode()); + return result; + } +} diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepositoryReader.java b/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepositoryReader.java new file mode 100644 index 000000000000..e6d8820d28e0 --- /dev/null +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepositoryReader.java @@ -0,0 +1,165 @@ +/* + * Copyright 2000-2013 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 org.zmlx.hg4idea.repo; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.util.io.FileUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Reads information about the Hg repository from Hg service files located in the {@code .hg} folder. + * NB: works with {@link java.io.File}, i.e. reads from disk. Consider using caching. + * Throws a {@link HgRepoStateException} in the case of incorrect Hg file format. + * + * @author Nadya Zabrodina + */ +class HgRepositoryReader { + + private static final Logger LOG = Logger.getInstance(HgRepositoryReader.class); + private static Pattern BRANCH_PATTERN = Pattern.compile("\\s*(.+)\\s+(.+)"); + + private static final int IO_RETRIES = 3; // number of retries before fail if an IOException happens during file read. + + private final File myHgDir; // .hg/ + private final File myBranchHeadsFile; // .hg/cache/branchheads - this file does not exist before first commit + private final File myCurrentBranch; // .hg/branch + + HgRepositoryReader(@NotNull File hgDir) { + myHgDir = hgDir; + assertFileExists(myHgDir, ".hg directory not found in " + hgDir); + myBranchHeadsFile = new File(new File(myHgDir, "cache"), "branchheads"); + myCurrentBranch = new File(myHgDir, "branch"); + } + + + /** + * Finds current revision value. + * + * @return The current revision hash, or {@code null} if current revision is unknown - it is the initial repository state. + */ + @Nullable + String readCurrentRevision() { + String[] branchesWithHeads = tryLoadFile(myBranchHeadsFile).trim().split("\n"); + String head = branchesWithHeads[0]; + Matcher matcher = BRANCH_PATTERN.matcher(head); + if (matcher.matches()) { + return (matcher.group(1)); + } + return null; + } + + /** + * Return current branch + */ + @NotNull + String readCurrentBranch() { + if (branchExist()) { + String rev = tryLoadFile(myCurrentBranch); + return rev.trim(); + } + return HgRepository.DEFAULT_BRANCH; + } + + + List readBranches() { + List branches = new ArrayList(); + String[] branchesWithHeads = tryLoadFile(myBranchHeadsFile).trim().split("\n"); + // first one - is a head revision: head hash + head number; + for (int i = 1; i < branchesWithHeads.length; ++i) { + Matcher matcher = BRANCH_PATTERN.matcher(branchesWithHeads[i]); + if (matcher.matches()) { + branches.add(matcher.group(2)); + } + } + return branches; + } + + + private static void assertFileExists(File file, String message) { + if (!file.exists()) { + throw new HgRepoStateException(message); + } + } + + /** + * Loads the file content. + * Tries 3 times, then a {@link HgRepoStateException} is thrown. + * + * @param file File to read. + * @return file content. + */ + @NotNull + private static String tryLoadFile(final File file) { + return tryOrThrow(new Callable() { + @Override + public String call() throws Exception { + return FileUtil.loadFile(file); + } + }, file); + } + + /** + * Tries to execute the given action. + * If an IOException happens, tries again up to 3 times, and then throws a {@link HgRepoStateException}. + * If an other exception happens, rethrows it as a {@link HgRepoStateException}. + * In the case of success returns the result of the task execution. + */ + private static String tryOrThrow(Callable actionToTry, File fileToLoad) { + IOException cause = null; + for (int i = 0; i < IO_RETRIES; i++) { + try { + return actionToTry.call(); + } + catch (IOException e) { + LOG.info("IOException while loading " + fileToLoad, e); + cause = e; + } + catch (Exception e) { // this shouldn't happen since only IOExceptions are thrown in clients. + throw new HgRepoStateException("Couldn't load file " + fileToLoad, e); + } + } + throw new HgRepoStateException("Couldn't load file " + fileToLoad, cause); + } + + public HgRepository.State readState() { + if (isMergeInProgress()) { + return HgRepository.State.MERGING; + } + return HgRepository.State.NORMAL; + } + + private boolean isMergeInProgress() { + File mergeFile = new File(myHgDir, "merge"); + return mergeFile.exists(); + } + + public boolean headExist() { + return myBranchHeadsFile.exists(); + } + + public boolean branchExist() { + return myCurrentBranch.exists(); + } +} diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepositoryUpdater.java b/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepositoryUpdater.java new file mode 100644 index 000000000000..e44213af5fe2 --- /dev/null +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/repo/HgRepositoryUpdater.java @@ -0,0 +1,146 @@ +/* + * Copyright 2000-2013 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 org.zmlx.hg4idea.repo; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileManager; +import com.intellij.openapi.vfs.newvfs.BulkFileListener; +import com.intellij.openapi.vfs.newvfs.events.VFileEvent; +import com.intellij.util.Consumer; +import com.intellij.util.Processor; +import com.intellij.util.concurrency.QueueProcessor; +import com.intellij.util.messages.MessageBusConnection; +import com.intellij.vcsUtil.VcsUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * Listens to .hg service files changes and updates {@link HgRepository} when needed. + * + * @author Nadya Zabrodina + */ +final class HgRepositoryUpdater implements Disposable, BulkFileListener { + private final HgRepositoryFiles myRepositoryFiles; + private final MessageBusConnection myMessageBusConnection; + private final QueueProcessor myUpdateQueue; + private final Object DUMMY_UPDATE_OBJECT = new Object(); + private final VirtualFile myBranchHeadsDir; + private final LocalFileSystem.WatchRequest myWatchRequest; + + + HgRepositoryUpdater(HgRepository repository) { + VirtualFile hgDir = repository.getHgDir(); + myWatchRequest = LocalFileSystem.getInstance().addRootToWatch(hgDir.getPath(), true); + myRepositoryFiles = HgRepositoryFiles.getInstance(hgDir); + visitHgDirVfs(hgDir); + + myBranchHeadsDir = VcsUtil.getVirtualFile(myRepositoryFiles.getBranchHeadsPath()); + + Project project = repository.getProject(); + myUpdateQueue = new QueueProcessor(new Updater(repository), project.getDisposed()); + if (!project.isDisposed()) { + myMessageBusConnection = project.getMessageBus().connect(); + myMessageBusConnection.subscribe(VirtualFileManager.VFS_CHANGES, this); + } + else { + myMessageBusConnection = null; + } + } + + private static void visitHgDirVfs(@NotNull VirtualFile hgDir) { + hgDir.getChildren(); + for (String subdir : HgRepositoryFiles.getSubDirRelativePaths()) { + VirtualFile dir = hgDir.findFileByRelativePath(subdir); + // process recursively, because we need to visit all branches under cache/branchheads + visitAllChildrenRecursively(dir); + } + } + + private static void visitAllChildrenRecursively(@Nullable VirtualFile dir) { + if (dir == null) { + return; + } + VfsUtil.processFilesRecursively(dir, new Processor() { + @Override + public boolean process(VirtualFile virtualFile) { + return true; + } + }); + } + + @Override + public void dispose() { + if (myWatchRequest != null) { + LocalFileSystem.getInstance().removeWatchedRoot(myWatchRequest); + } + if (myMessageBusConnection != null) { + myMessageBusConnection.disconnect(); + } + } + + @Override + public void before(@NotNull List events) { + // everything is handled in #after() + } + + @Override + public void after(@NotNull List events) { + // which files in .hg were changed + boolean branchHeadsChanged = false; + boolean branchFileChanged = false; + boolean mergeFileChanged = false; + for (VFileEvent event : events) { + String filePath = event.getPath(); + if (filePath == null) { + continue; + } + if (myRepositoryFiles.isbranchHeadsFile(filePath)) { + branchHeadsChanged = true; + } + else if (myRepositoryFiles.isBranchFile(filePath)) { + branchFileChanged = true; + visitAllChildrenRecursively(myBranchHeadsDir); + } + else if (myRepositoryFiles.isMergeFile(filePath)) { + mergeFileChanged = true; + } + } + + + if (branchHeadsChanged || branchFileChanged || mergeFileChanged) { + myUpdateQueue.add(DUMMY_UPDATE_OBJECT); + } + } + + private static class Updater implements Consumer { + private final HgRepository myRepository; + + public Updater(HgRepository repository) { + myRepository = repository; + } + + @Override + public void consume(Object dummy) { + myRepository.update(); + } + } +} diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/status/ui/HgStatusWidget.java b/plugins/hg4idea/src/org/zmlx/hg4idea/status/ui/HgStatusWidget.java index ab2d2ed5515f..547ccc0530d8 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/status/ui/HgStatusWidget.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/status/ui/HgStatusWidget.java @@ -20,6 +20,7 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.FileEditorManagerEvent; import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.popup.ListPopup; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.wm.StatusBarWidget; @@ -31,22 +32,25 @@ import org.jetbrains.annotations.Nullable; import org.zmlx.hg4idea.HgProjectSettings; import org.zmlx.hg4idea.HgUpdater; import org.zmlx.hg4idea.HgVcs; +import org.zmlx.hg4idea.action.HgBranchPopup; +import org.zmlx.hg4idea.repo.HgRepositoryImpl; import org.zmlx.hg4idea.status.HgCurrentBranchStatus; +import org.zmlx.hg4idea.util.HgUtil; -import java.awt.*; import java.awt.event.MouseEvent; /** * Widget to display basic hg status in the IJ status bar. */ -public class HgStatusWidget extends EditorBasedWidget implements StatusBarWidget.TextPresentation, StatusBarWidget.Multiframe, HgUpdater { +public class HgStatusWidget extends EditorBasedWidget + implements StatusBarWidget.MultipleTextValuesPresentation, + StatusBarWidget.Multiframe, HgUpdater { private static final String MAX_STRING = "hg: default branch (128)"; @NotNull private final HgVcs myVcs; @NotNull private final HgProjectSettings myProjectSettings; @NotNull private final HgCurrentBranchStatus myCurrentBranchStatus; - private MessageBusConnection myBusConnection; private volatile String myText = ""; private volatile String myTooltip = ""; @@ -91,13 +95,20 @@ public class HgStatusWidget extends EditorBasedWidget implements StatusBarWidget } @Override - public String getTooltipText() { - return myTooltip; + public ListPopup getPopupStep() { + Project project = getProject(); + if (project == null) { + return null; + } + VirtualFile root = HgUtil.getRootForSelectedFile(project); + if (root != null) { + return HgBranchPopup.getInstance(project, HgRepositoryImpl.getFullInstance(root, project, project)).asListPopup(); + } + return null; } - @NotNull @Override - public String getText() { + public String getSelectedValue() { final String text = myText; return StringUtil.isEmpty(text) ? "" : "hg: " + text; } @@ -105,15 +116,17 @@ public class HgStatusWidget extends EditorBasedWidget implements StatusBarWidget @NotNull @Override @Deprecated - public String getMaxPossibleText() { + public String getMaxValue() { return MAX_STRING; } + @Override - public float getAlignment() { - return Component.LEFT_ALIGNMENT; + public String getTooltipText() { + return myTooltip; } + @Override // Updates branch information on click public Consumer getClickConsumer() { @@ -161,8 +174,8 @@ public class HgStatusWidget extends EditorBasedWidget implements StatusBarWidget return; } - myBusConnection = project.getMessageBus().connect(); - myBusConnection.subscribe(HgVcs.STATUS_TOPIC, this); + MessageBusConnection busConnection = project.getMessageBus().connect(); + busConnection.subscribe(HgVcs.STATUS_TOPIC, this); DvcsUtil.installStatusBarWidget(myProject, this); } diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/ui/HgPushDialog.form b/plugins/hg4idea/src/org/zmlx/hg4idea/ui/HgPushDialog.form index 546b3c2d8ef0..a11d251812ad 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/ui/HgPushDialog.form +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/ui/HgPushDialog.form @@ -21,7 +21,7 @@ - + @@ -55,12 +55,12 @@ - + - + @@ -80,6 +80,20 @@ + + + + + + + + + + + + + + diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/ui/HgPushDialog.java b/plugins/hg4idea/src/org/zmlx/hg4idea/ui/HgPushDialog.java index f7ee65c06d52..a76c7872391c 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/ui/HgPushDialog.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/ui/HgPushDialog.java @@ -51,6 +51,7 @@ public class HgPushDialog extends DialogWrapper { private JCheckBox branchCheckBox; private JComboBox branchComboBox; private EditorComboBox myRepositoryURL; + private JCheckBox newBranchCheckBox; private String myCurrentRepositoryUrl; public HgPushDialog(Project project, Collection repos, List branches, @Nullable VirtualFile selectedRepo) { @@ -114,6 +115,10 @@ public class HgPushDialog extends DialogWrapper { return forceCheckBox.isSelected(); } + public boolean isNewBranch() { + return newBranchCheckBox.isSelected(); + } + protected JComponent createCenterPanel() { return contentPanel; } @@ -159,6 +164,7 @@ public class HgPushDialog extends DialogWrapper { setOKActionEnabled(validateOptions()); revisionTxt.setEnabled(revisionCbx.isSelected()); branchComboBox.setEnabled(branchCheckBox.isSelected()); + newBranchCheckBox.setEnabled(branchCheckBox.isSelected()); } private boolean validateOptions() { diff --git a/plugins/hg4idea/src/org/zmlx/hg4idea/util/HgUtil.java b/plugins/hg4idea/src/org/zmlx/hg4idea/util/HgUtil.java index a76e1517ecad..0f355b2b4271 100644 --- a/plugins/hg4idea/src/org/zmlx/hg4idea/util/HgUtil.java +++ b/plugins/hg4idea/src/org/zmlx/hg4idea/util/HgUtil.java @@ -14,7 +14,14 @@ package org.zmlx.hg4idea.util; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.TextEditor; import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.ShutDownTracker; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vcs.*; @@ -24,6 +31,9 @@ import com.intellij.openapi.vcs.history.VcsFileRevisionEx; import com.intellij.openapi.vcs.vfs.VcsVirtualFile; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.wm.StatusBar; +import com.intellij.openapi.wm.WindowManager; +import com.intellij.openapi.wm.impl.status.StatusBarUtil; import com.intellij.ui.GuiUtils; import com.intellij.vcsUtil.VcsUtil; import org.jetbrains.annotations.NotNull; @@ -33,6 +43,7 @@ import org.zmlx.hg4idea.command.HgRemoveCommand; import org.zmlx.hg4idea.command.HgStatusCommand; import org.zmlx.hg4idea.command.HgWorkingCopyRevisionsCommand; import org.zmlx.hg4idea.provider.HgChangeProvider; +import org.zmlx.hg4idea.repo.HgRepository; import java.awt.*; import java.io.*; @@ -50,6 +61,7 @@ public abstract class HgUtil { public static final Pattern URL_WITH_PASSWORD = Pattern.compile("(?:.+)://(?:.+)(:.+)@(?:.+)"); //http(s)://username:password@url public static final int MANY_FILES = 100; private static final Logger LOG = Logger.getInstance(HgUtil.class); + public static final String DOT_HG = ".hg"; public static File copyResourceToTempFile(String basename, String extension) throws IOException { final InputStream in = HgUtil.class.getClassLoader().getResourceAsStream("python/" + basename + extension); @@ -203,7 +215,7 @@ public abstract class HgUtil { * Checks if the given directory is an hg root. */ public static boolean isHgRoot(VirtualFile dir) { - return dir.findChild(".hg") != null; + return dir.findChild(DOT_HG) != null; } /** @@ -249,6 +261,52 @@ public abstract class HgUtil { return getHgRootOrThrow(project, VcsUtil.getFilePath(file.getPath())); } + /** + * Returns the currently selected file, based on which HgBranch components will identify the current repository root. + */ + @Nullable + public static VirtualFile getSelectedFile(@NotNull Project project) { + StatusBar statusBar = WindowManager.getInstance().getStatusBar(project); + final FileEditor fileEditor = StatusBarUtil.getCurrentFileEditor(project, statusBar); + VirtualFile result = null; + if (fileEditor != null) { + if (fileEditor instanceof TextEditor) { + Document document = ((TextEditor)fileEditor).getEditor().getDocument(); + result = FileDocumentManager.getInstance().getFile(document); + } + } + + if (result == null) { + final FileEditorManager manager = FileEditorManager.getInstance(project); + if (manager != null) { + Editor editor = manager.getSelectedTextEditor(); + if (editor != null) { + result = FileDocumentManager.getInstance().getFile(editor.getDocument()); + } + } + } + return result; + } + + @Nullable + public static VirtualFile getRootForSelectedFile(@NotNull Project project) { + VirtualFile selectedFile = getSelectedFile(project); + if (selectedFile != null) { + return getHgRootOrNull(project, selectedFile); + } + return null; + } + + + /** + * 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. + */ + @Nullable + public static String getNewBranchNameFromUser(@NotNull Project project, + @NotNull String dialogTitle) { + return Messages.showInputDialog(project, "Enter the name of new branch:", dialogTitle, Messages.getQuestionIcon()); + } /** * Checks is a merge operation is in progress on the given repository. * Actually gets the number of parents of the current revision. If there are 2 parents, then a merge is going on. Otherwise there is @@ -481,4 +539,13 @@ public abstract class HgUtil { } return path; } + + public static String getDisplayableBranchText(HgRepository repository) { + HgRepository.State state = repository.getState(); + String branchText = ""; + if (state == HgRepository.State.MERGING) { + branchText += state.toString() + " "; + } + return branchText + repository.getCurrentBranch(); + } }