git4idea: IDEA-55724: initial support for checking out multiple branches added

This commit is contained in:
Constantine Plotnikov
2010-07-31 04:17:06 +04:00
parent beef84ae5f
commit 5c08b021c2
16 changed files with 4247 additions and 1 deletions
+3
View File
@@ -104,6 +104,9 @@
<projectService
serviceInterface="git4idea.history.browser.GitProjectLogManager"
serviceImplementation="git4idea.history.browser.GitProjectLogManager"/>
<projectService
serviceInterface="git4idea.checkout.branches.GitBranchConfigurations"
serviceImplementation="git4idea.checkout.branches.GitBranchConfigurations"/>
<applicationService
serviceInterface="git4idea.config.GitVcsApplicationSettings"
serviceImplementation="git4idea.config.GitVcsApplicationSettings"/>
@@ -491,6 +491,21 @@ public class GitUtil {
return relativePath(VfsUtil.virtualToIoFile(root), VfsUtil.virtualToIoFile(file));
}
/**
* Get relative path
*
* @param root a root file
* @param file a virtual file
* @return a relative path
* @throws IllegalArgumentException if path is not under root.
*/
public static String relativeOrFullPath(final VirtualFile root, VirtualFile file) {
if (root == null) {
file.getPath();
}
return relativePath(VfsUtil.virtualToIoFile(root), VfsUtil.virtualToIoFile(file));
}
/**
* Get relative path
*
@@ -51,6 +51,7 @@ import git4idea.changes.GitCommittedChangeListProvider;
import git4idea.changes.GitOutgoingChangesProvider;
import git4idea.checkin.GitCheckinEnvironment;
import git4idea.checkin.GitCommitAndPushExecutor;
import git4idea.checkout.branches.GitBranchConfigurations;
import git4idea.commands.GitCommand;
import git4idea.commands.GitSimpleHandler;
import git4idea.config.GitVcsConfigurable;
@@ -510,6 +511,7 @@ public class GitVcs extends AbstractVcs<CommittedChangeList> {
myReferenceTracker.activate();
GitUsersComponent.getInstance(myProject).activate();
GitProjectLogManager.getInstance(myProject).activate();
GitBranchConfigurations.getInstance(myProject).activate();
}
/**
@@ -517,6 +519,7 @@ public class GitVcs extends AbstractVcs<CommittedChangeList> {
*/
@Override
protected void deactivate() {
GitBranchConfigurations.getInstance(myProject).deactivate();
if (myRootTracker != null) {
myRootTracker.dispose();
myRootTracker = null;
@@ -0,0 +1,170 @@
/*
* Copyright 2000-2010 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.checkout.branches;
import com.intellij.util.xmlb.XmlSerializerUtil;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
/**
* The branch configuration wrapper
*/
public class GitBranchConfiguration {
/**
* The configuration
*/
final GitBranchConfigurations myConfig;
/**
* The name of configuration
*/
private String myName;
/**
* The auto-detected flag
*/
private boolean myAutoDetected;
/**
* The root to reference mapping
*/
private HashMap<String, String> myReferences = new HashMap<String, String>();
/**
* The
*/
@Nullable private GitBranchConfigurations.BranchChanges myChanges;
/**
* The configuration with the specified name
*
* @param config
* @param name
*/
GitBranchConfiguration(GitBranchConfigurations config, String name) {
myConfig = config;
myName = name;
}
/**
* @return the name of configuration
*/
public String getName() {
synchronized (myConfig.getStateLock()) {
return myName;
}
}
/**
* @return the name of configuration
*/
public void setName(String name) {
synchronized (myConfig.getStateLock()) {
assert name != null;
if (name.equals(myName)) {
return;
}
assert myConfig.configurationRenamed(this, myName, name) : "Configuration should have existed";
myName = name;
}
}
/**
* @return the copy of list of branches for configuration
*/
public Map<String, String> getReferences() {
synchronized (myConfig.getStateLock()) {
return new HashMap<String, String>(myReferences);
}
}
public void setBranch(String root, String reference) {
synchronized (myConfig.getStateLock()) {
myReferences.put(root, reference);
}
}
public String getReference(String root) {
synchronized (myConfig.getStateLock()) {
return myReferences.get(root);
}
}
public void clearReferences() {
synchronized (myConfig.getStateLock()) {
myReferences.clear();
}
}
/**
* Set changes to the configuration. Note that changes are assumed not to change
*
* @param changes
*/
void setChanges(@Nullable GitBranchConfigurations.BranchChanges changes) {
synchronized (myConfig.getStateLock()) {
myChanges = changes;
}
}
/**
* @return get copy of stored changes descriptor
*/
@Nullable
GitBranchConfigurations.BranchChanges getChanges() {
synchronized (myConfig.getStateLock()) {
if (myChanges == null) {
return null;
}
GitBranchConfigurations.BranchChanges rc = new GitBranchConfigurations.BranchChanges();
XmlSerializerUtil.copyBean(myChanges, rc);
return rc;
}
}
/**
* Set mapping for roots
*
* @param references the new mapping
*/
public void setReferences(Map<String, String> references) {
synchronized (myConfig.getStateLock()) {
myReferences.clear();
myReferences.putAll(references);
}
}
/**
* @return true if the configuration was auto-detected
*/
public boolean isAutoDetected() {
synchronized (myConfig.getStateLock()) {
return myAutoDetected;
}
}
/**
* Indicate if configuration was auto-detected
*
* @param autoDetected new value
*/
public void setAutoDetected(boolean autoDetected) {
synchronized (myConfig.getStateLock()) {
myAutoDetected = autoDetected;
}
}
}
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="git4idea.checkout.branches.GitBranchConfigurationChangedDialog">
<grid id="27dc6" binding="myRootPanel" layout-manager="GridLayoutManager" row-count="3" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="738" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="9c9f2" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="2" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="&lt;html&gt;The current branch configuration has been changed.&lt;br/&gt;Do you want to update the current branch configuration, rename it, or save it as a new branch configuration?&lt;/hml&gt;"/>
</properties>
</component>
<component id="d959d" class="javax.swing.JLabel">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<labelFor value="b591c"/>
<text value="&amp;Name"/>
</properties>
</component>
<scrollpane id="14dad" class="com.intellij.ui.components.JBScrollPane">
<constraints>
<grid row="2" column="0" row-span="1" col-span="2" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="b7cc6" class="com.intellij.ui.table.JBTable" binding="myTable">
<constraints/>
<properties/>
</component>
</children>
</scrollpane>
<component id="b591c" class="javax.swing.JTextField" binding="myNameTextField" default-binding="true">
<constraints>
<grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
</children>
</grid>
</form>
@@ -0,0 +1,319 @@
/*
* Copyright 2000-2010 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.checkout.branches;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.DocumentAdapter;
import com.intellij.ui.table.JBTable;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.table.AbstractTableModel;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* The dialog shown to inform of changes in the current configuration
*/
public class GitBranchConfigurationChangedDialog extends DialogWrapper {
/**
* The new configuration exit code
*/
private static final int NEW_CONFIGURATION = NEXT_USER_EXIT_CODE;
/**
* The name text field
*/
private JTextField myNameTextField;
/**
* The table that describes configuration changes
*/
private JBTable myTable;
/**
* The root panel
*/
private JPanel myRootPanel;
/**
* The base directory for the project
*/
private final File myBaseFile;
/**
* The branch descriptors to show in the table
*/
private final List<BranchDescriptor> myBranches;
/**
* The new configuration action
*/
private DialogWrapperExitAction myNewAction;
/**
* The constructor from project
*
* @param project the project to use to display window
* @param config
* @param branches
* @param names
*/
protected GitBranchConfigurationChangedDialog(Project project,
final GitBranchConfiguration config,
List<BranchDescriptor> branches, final Set<String> names) {
super(project, true);
setTitle("Git Branch Configuration Changed");
myBranches = branches;
VirtualFile baseDir = project.getBaseDir();
myBaseFile = baseDir == null ? null : new File(baseDir.getPath());
myTable.setModel(new DescriptorTableModel());
myNameTextField.setText(config.getName());
myNewAction = new DialogWrapperExitAction("New Configuration", NEW_CONFIGURATION);
myNameTextField.getDocument().addDocumentListener(new DocumentAdapter() {
@Override
protected void textChanged(DocumentEvent e) {
String text = myNameTextField.getText().trim();
if (text.length() == 0) {
setError("Empty configuration name is not allowed.");
}
else if (text.equals(config.getName())) {
setError(null);
myNewAction.setEnabled(false);
}
else if (names.contains(text)) {
setError("There is another configuration with the same name");
}
else {
setError(null);
}
}
private void setError(String s) {
setErrorText(s);
setOKActionEnabled(s == null);
myNewAction.setEnabled(s == null);
}
});
setOKButtonText("Update");
init();
}
/**
* {@inheritDoc}
*/
@Override
protected JComponent createCenterPanel() {
return myRootPanel;
}
/**
* Show the dialog in AWT thread and wait for its completion
*
* @param settings the settings to use
* @param config the configuration to check
* @param roots the roots collection
* @return null if project is cancelled, or configuration that user has selected for storing the current state (created or updated)
*/
@Nullable
static GitBranchConfiguration showDialog(final GitBranchConfigurations settings,
final GitBranchConfiguration config,
final List<VirtualFile> roots)
throws VcsException {
final Set<String> names = settings.getConfigurationNames();
final Ref<String> name = new Ref<String>();
final Ref<Integer> code = new Ref<Integer>();
final List<BranchDescriptor> list = prepareBranchDescriptors(settings, config, roots);
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
GitBranchConfigurationChangedDialog d = new GitBranchConfigurationChangedDialog(settings.getProject(), config, list, names);
d.show();
code.set(d.getExitCode());
name.set(d.myNameTextField.getText());
}
});
if (code.get() == CANCEL_EXIT_CODE) {
return null;
}
GitBranchConfiguration updateConfig;
synchronized (settings.getStateLock()) {
if (code.get() == OK_EXIT_CODE) {
updateConfig = config;
updateConfig.setName(name.get());
}
else if (code.get() == NEW_CONFIGURATION) {
updateConfig = settings.createConfiguration(name.get());
}
else {
throw new RuntimeException("Unexpected exit code: " + code.get());
}
updateConfig.clearReferences();
for (BranchDescriptor d : list) {
if (d.root != null) {
updateConfig.setBranch(d.root.getPath(), d.actualRef);
}
}
}
return updateConfig;
}
/**
* {@inheritDoc}
*/
@Override
protected Action[] createActions() {
return new Action[]{getOKAction(), myNewAction, getCancelAction()};
}
/**
* {@inheritDoc}
*/
@Override
protected String getDimensionServiceKey() {
return getClass().getName();
}
static List<BranchDescriptor> prepareBranchDescriptors(GitBranchConfigurations settings,
GitBranchConfiguration config,
List<VirtualFile> roots)
throws VcsException {
List<BranchDescriptor> rc = new ArrayList<BranchDescriptor>();
Map<String, String> map = config.getReferences();
for (VirtualFile root : roots) {
BranchDescriptor d = new BranchDescriptor();
d.root = root;
d.actualRef = settings.describeRoot(root);
d.storedRef = map.remove(root.getPath());
rc.add(d);
}
for (Map.Entry<String, String> m : map.entrySet()) {
BranchDescriptor d = new BranchDescriptor();
d.storedRoot = m.getKey();
d.storedRef = m.getValue();
rc.add(d);
}
return rc;
}
/**
* The table model that describes roots
*/
private class DescriptorTableModel extends AbstractTableModel {
/**
* The relative path for the root
*/
private static final int ROOT_COLUMN = 0;
/**
* The actual reference
*/
private static final int ACTUAL_REF_COLUMN = 1;
/**
* The reference stored in the configuration
*/
private static final int STORED_REF_COLUMN = 2;
/**
* {@inheritDoc}
*/
@Override
public int getRowCount() {
return myBranches.size();
}
/**
* {@inheritDoc}
*/
@Override
public int getColumnCount() {
return STORED_REF_COLUMN + 1;
}
/**
* {@inheritDoc}
*/
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
BranchDescriptor d = myBranches.get(rowIndex);
switch (columnIndex) {
case ROOT_COLUMN:
return d.getRoot(myBaseFile);
case ACTUAL_REF_COLUMN:
return d.actualRef == null ? "" : d.actualRef;
case STORED_REF_COLUMN:
return d.storedRef == null ? "" : d.storedRef;
default:
throw new IllegalStateException("Unexpected column");
}
}
/**
* {@inheritDoc}
*/
@Override
public String getColumnName(int column) {
switch (column) {
case ROOT_COLUMN:
return "Vcs Root";
case ACTUAL_REF_COLUMN:
return "Actual";
case STORED_REF_COLUMN:
return "Configured";
default:
throw new IllegalStateException("Unexpected column");
}
}
}
/**
* The descriptor for the branch
*/
private static class BranchDescriptor {
/**
* The vcs root
*/
VirtualFile root;
/**
* The branch information stored in the configuration
*/
String storedRoot;
/**
* The stored reference
*/
String storedRef;
/**
* The actual reference
*/
String actualRef;
public String getRoot(final File baseFile) {
String path = root == null ? storedRoot : root.getPath();
String relative = baseFile == null ? path : FileUtil.getRelativePath(baseFile, new File(path));
return relative == null ? path : relative;
}
}
}
@@ -0,0 +1,898 @@
/*
* Copyright 2000-2010 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.checkout.branches;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ex.ProjectManagerEx;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.vcs.FileStatus;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.*;
import com.intellij.openapi.vcs.changes.shelf.ShelveChangesManager;
import com.intellij.openapi.vcs.changes.ui.ChangesViewContentManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.util.EventDispatcher;
import git4idea.GitBranch;
import git4idea.GitUtil;
import git4idea.GitVcs;
import git4idea.commands.GitCommand;
import git4idea.commands.GitSimpleHandler;
import git4idea.merge.GitMergeUtil;
import git4idea.rebase.GitRebaseUtils;
import git4idea.ui.GitUIUtil;
import git4idea.vfs.GitReferenceListener;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* Git branch configurations settings and project level state
*/
@State(
name = "Git.Branch.Configurations",
storages = {@Storage(
id = "ws",
file = "$WORKSPACE_FILE$")})
public class GitBranchConfigurations implements PersistentStateComponent<GitBranchConfigurations.State>, Disposable {
/**
* The logger
*/
private static final Logger LOG = Logger.getInstance(GitBranchConfigurations.class.getName());
/**
* The comparator for branch configuration by name
*/
private static final Comparator<BranchConfiguration> CONFIGURATION_COMPARATOR = new Comparator<BranchConfiguration>() {
@Override
public int compare(BranchConfiguration o1, BranchConfiguration o2) {
return o1.NAME.compareTo(o2.NAME);
}
};
/**
* The comparator for branch information by root
*/
private static final Comparator<BranchInfo> BRANCH_INFO_COMPARATOR = new Comparator<BranchInfo>() {
@Override
public int compare(BranchInfo o1, BranchInfo o2) {
return o1.ROOT.compareTo(o2.ROOT);
}
};
/**
* The project
*/
private final Project myProject;
/**
* The git vcs
*/
private final GitVcs myVcs;
/**
* The shelve manager instance
*/
private final ShelveChangesManager myShelveManager;
/**
* The dirty scope manager
*/
private final VcsDirtyScopeManager myDirtyScopeManager;
/**
* Change manager
*/
private final ChangeListManagerEx myChangeManager;
/**
* Project manager
*/
private final ProjectManagerEx myProjectManager;
/**
* The state lock
*/
private final Object myStateLock = new Object();
/**
* The set of configurations
*/
private final HashMap<String, GitBranchConfiguration> myConfigurations = new HashMap<String, GitBranchConfiguration>();
/**
* Create event dispatcher for configuration events
*/
private final EventDispatcher<GitBranchConfigurationsListener> myListeners =
EventDispatcher.create(GitBranchConfigurationsListener.class);
/**
* The current configuration
*/
private GitBranchConfiguration myCurrentConfiguration;
/**
* The reference listener
*/
private final GitReferenceListener myReferenceListener;
/**
* The current status (cached, invalidated when references change)
*/
private SpecialStatus myCurrentStatus;
/**
* The collection of git roots
*/
private List<VirtualFile> myGitRoots = Collections.emptyList();
/**
* If true, checkout background process is in progress
*/
private boolean myCheckoutIsInProgress = false;
/**
* The widget uninstall action (on deactivate)
*/
private Runnable myWidgetUninstall;
/**
* Listener for changes
*/
private final ChangeListAdapter myChangesListener;
/**
* The constructor used to dependency injection
*
* @param project the project
* @param shelveManager the shelve manager
* @param dirtyScopeManager the dirty scope manager
* @param changeManager the change manager
* @param projectManager the project manager
*/
public GitBranchConfigurations(Project project, ShelveChangesManager shelveManager,
VcsDirtyScopeManager dirtyScopeManager,
ChangeListManagerEx changeManager,
ProjectManagerEx projectManager) {
myProject = project;
myVcs = GitVcs.getInstance(project);
myShelveManager = shelveManager;
myDirtyScopeManager = dirtyScopeManager;
myChangeManager = changeManager;
myProjectManager = projectManager;
myReferenceListener = new GitReferenceListener() {
@Override
public void referencesChanged(VirtualFile root) {
GitBranchConfigurations.this.referencesChanged();
}
};
Disposer.register(myProject, this);
myChangesListener = new ChangeListAdapter() {
@Override
public void changesRemoved(Collection<Change> changes, ChangeList fromList) {
GitBranchConfigurations.this.referencesChanged();
}
@Override
public void changesAdded(Collection<Change> changes, ChangeList toList) {
GitBranchConfigurations.this.referencesChanged();
}
};
}
/**
* Add listener
*
* @param l the listener
*/
public void addConfigurationListener(GitBranchConfigurationsListener l) {
myListeners.addListener(l);
}
/**
* Remove listener
*
* @param l the listener
*/
public void removeConfigurationListener(GitBranchConfigurationsListener l) {
myListeners.removeListener(l);
}
/**
* Handle reference change, also notified when roots changed.
*/
public void referencesChanged() {
synchronized (myStateLock) {
updateRootCollection();
updateSpecialStatus();
fireReferencesChanged();
}
}
/**
* Fire that references changed
*/
private void fireReferencesChanged() {
myListeners.getMulticaster().referencesChanged();
}
/**
* Update collections of roots
*/
private void updateRootCollection() {
try {
myGitRoots = GitUtil.getGitRoots(myProject, myVcs);
}
catch (VcsException e) {
LOG.warn("Empty list of roots is detected", e);
myGitRoots = Collections.emptyList();
}
}
/**
* Update special status
*/
private void updateSpecialStatus() {
SpecialStatus p = myCurrentStatus;
myCurrentStatus = calculateSpecialStatus();
if (p != myCurrentStatus) {
myListeners.getMulticaster().specialStatusChanged();
}
}
/**
* Activate component
*/
public void activate() {
myVcs.addGitReferenceListener(myReferenceListener);
myChangeManager.addChangeListListener(myChangesListener);
synchronized (myStateLock) {
updateRootCollection();
if (myCurrentConfiguration == null) {
if (calculateSpecialStatus() == SpecialStatus.NORMAL) {
try {
detectLocals();
}
catch (VcsException e) {
LOG.error("Exception during detecting local configurations", e);
}
}
}
referencesChanged();
}
if (!ApplicationManager.getApplication().isHeadlessEnvironment()) {
myWidgetUninstall = GitBranchesWidget.install(myProject, this);
}
}
/**
* Deactivate component
*/
public void deactivate() {
myVcs.removeGitReferenceListener(myReferenceListener);
myChangeManager.removeChangeListListener(myChangesListener);
if (myWidgetUninstall != null) {
myWidgetUninstall.run();
myWidgetUninstall = null;
}
}
/**
* Get component instance
*
* @param project a context project
* @return the git settings
*/
public static GitBranchConfigurations getInstance(Project project) {
return ServiceManager.getService(project, GitBranchConfigurations.class);
}
@Override
public void dispose() {
deactivate();
}
/**
* {@inheritDoc}
*/
@SuppressWarnings({"NonPrivateFieldAccessedInSynchronizedContext"})
@Override
public State getState() {
synchronized (myStateLock) {
State rc = new State();
rc.CURRENT = myCurrentConfiguration == null ? null : myCurrentConfiguration.getName();
ArrayList<BranchConfiguration> cs = new ArrayList<BranchConfiguration>(myConfigurations.size());
for (GitBranchConfiguration ci : myConfigurations.values()) {
BranchConfiguration c = new BranchConfiguration();
c.NAME = ci.getName();
Map<String, String> map = ci.getReferences();
ArrayList<BranchInfo> bs = new ArrayList<BranchInfo>(map.size());
for (Map.Entry<String, String> m : map.entrySet()) {
BranchInfo b = new BranchInfo();
b.ROOT = m.getKey();
b.REFERENCE = m.getValue();
bs.add(b);
}
c.BRANCHES = bs.toArray(new BranchInfo[bs.size()]);
Arrays.sort(c.BRANCHES, BRANCH_INFO_COMPARATOR);
c.CHANGES = ci.getChanges();
c.IS_AUTO_DETECTED = ci.isAutoDetected();
cs.add(c);
}
rc.CONFIGURATIONS = cs.toArray(new BranchConfiguration[cs.size()]);
Arrays.sort(rc.CONFIGURATIONS, CONFIGURATION_COMPARATOR);
return rc;
}
}
/**
* {@inheritDoc}
*/
@SuppressWarnings({"NonPrivateFieldAccessedInSynchronizedContext"})
@Override
public void loadState(State state) {
synchronized (myStateLock) {
myConfigurations.clear();
for (BranchConfiguration bc : state.CONFIGURATIONS) {
GitBranchConfiguration n = new GitBranchConfiguration(this, bc.NAME);
myConfigurations.put(n.getName(), n);
for (BranchInfo bi : bc.BRANCHES) {
n.setBranch(bi.ROOT, bi.REFERENCE);
}
myConfigurations.put(bc.NAME, n);
n.setAutoDetected(bc.IS_AUTO_DETECTED);
}
if (myCurrentConfiguration == null) {
myCurrentConfiguration = myConfigurations.get(state.CURRENT);
}
else {
myCurrentConfiguration = myConfigurations.get(myCurrentConfiguration.getName());
if (myCurrentConfiguration == null) {
myCurrentConfiguration = myConfigurations.get(state.CURRENT);
}
}
fireCurrentConfigurationChanged();
fireConfigurationsChanged();
}
}
/**
* @return the candidate remote configurations
*/
List<String> getRemotesCandidates() {
try {
final List<VirtualFile> roots;
synchronized (myStateLock) {
roots = myGitRoots;
}
return detectConfigurations(false, roots);
}
catch (VcsException e) {
return Collections.emptyList();
}
}
/**
* @return the overall special status
*/
public SpecialStatus getSpecialStatus() {
synchronized (myStateLock) {
return myCurrentStatus;
}
}
/**
* @return the calculated overall special status
*/
private SpecialStatus calculateSpecialStatus() {
synchronized (myStateLock) {
if (myCheckoutIsInProgress) {
return SpecialStatus.CHECKOUT_IN_PROGRESS;
}
for (VirtualFile root : myGitRoots) {
if (GitRebaseUtils.isRebaseInTheProgress(root)) {
return SpecialStatus.REBASING;
}
if (GitMergeUtil.isMergeInTheProgress(root)) {
return SpecialStatus.MERGING;
}
if (root.findChild(".gitmodules") != null) {
return SpecialStatus.SUBMODULES;
}
}
for (LocalChangeList changeList : myChangeManager.getChangeListsCopy()) {
for (Change change : changeList.getChanges()) {
if (change.getFileStatus() == FileStatus.MERGED_WITH_CONFLICTS) {
return SpecialStatus.MERGING;
}
}
}
return myGitRoots.size() == 0 ? SpecialStatus.NON_GIT : SpecialStatus.NORMAL;
}
}
/**
* Detect local branch configurations
*
* @throws VcsException if there is a problem with detecting
*/
private void detectLocals() throws VcsException {
synchronized (myStateLock) {
HashMap<VirtualFile, String> currents = new HashMap<VirtualFile, String>();
for (VirtualFile root : myGitRoots) {
GitBranch current = GitBranch.current(myProject, root);
currents.put(root, current == null ? "" : current.getName());
}
if (myConfigurations.isEmpty()) {
List<String> locals = detectConfigurations(true, myGitRoots);
if (locals.isEmpty()) {
// no commits
locals.add("master");
}
for (String localName : locals) {
GitBranchConfiguration c = createConfiguration(localName);
c.setAutoDetected(true);
boolean currentsMatched = true;
for (VirtualFile root : myGitRoots) {
c.setBranch(root.getPath(), localName);
currentsMatched &= currents.get(root).equals(localName);
}
if (currentsMatched) {
myCurrentConfiguration = c;
}
}
if (myCurrentConfiguration == null) {
// the configuration does not matches any standard, there could be no configurations with spaces at this point
// since it is not allowed branch name.
String name = "Unknown 1";
GitBranchConfiguration c = createConfiguration(name);
for (VirtualFile root : myGitRoots) {
c.setBranch(root.getPath(), describeRoot(root));
}
}
}
fireCurrentConfigurationChanged();
fireConfigurationsChanged();
}
}
/**
* The configurations changed
*/
private void fireConfigurationsChanged() {
myListeners.getMulticaster().configurationsChanged();
}
/**
* The current configuration changed
*/
private void fireCurrentConfigurationChanged() {
myListeners.getMulticaster().currentConfigurationChanged();
}
/**
* Describe vcs root
*
* @param root the root to describe
* @return the current reference
* @throws VcsException if there is a problem with describing the root
*/
String describeRoot(VirtualFile root) throws VcsException {
GitBranch current = GitBranch.current(myProject, root);
if (current == null) {
// It is on the tag or specific commit. In future, support for submodules should be added.
return detectTag(root);
}
else {
return current.getName();
}
}
/**
* Get tag name for the head
*
* @param root the root to describe
* @return the commit expression that describes root state
*/
private String detectTag(VirtualFile root) {
try {
GitSimpleHandler h = new GitSimpleHandler(myProject, root, GitCommand.DESCRIBE);
h.addParameters("--tags", "--exact", "HEAD");
h.setNoSSH(true);
return h.run().trim();
}
catch (VcsException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("describe HEAD failed for root: " + root.getPath());
}
GitSimpleHandler h = new GitSimpleHandler(myProject, root, GitCommand.SHOW);
h.setNoSSH(true);
h.addParameters("--pretty=format:%H", "HEAD");
try {
return h.run().trim();
}
catch (VcsException e1) {
throw new RuntimeException("Unexpected exception at this time, the failure should have been detected at current(): ", e1);
}
}
}
/**
* Detect possible configurations
*
* @param roots
* @return a sorted list of branches
* @throws VcsException if there is a problem with running git
*/
private List<String> detectConfigurations(boolean local, final List<VirtualFile> roots) throws VcsException {
HashSet<String> all = new HashSet<String>();
HashSet<String> forRoot = new HashSet<String>();
boolean isFirst = true;
for (VirtualFile root : roots) {
forRoot.clear();
GitBranch.listAsStrings(myProject, root, !local, local, forRoot, null);
if (isFirst) {
isFirst = false;
all.addAll(forRoot);
}
else {
all.retainAll(forRoot);
}
}
ArrayList<String> rc = new ArrayList<String>(all);
Collections.sort(rc);
return rc;
}
/**
* @return the used vcs
*/
GitVcs getVcs() {
return myVcs;
}
/**
* @return the context project
*/
Project getProject() {
return myProject;
}
/**
* @return the current configuration
* @throws VcsException if there is a problem with configurations
*/
GitBranchConfiguration getCurrentConfiguration() throws VcsException {
synchronized (myStateLock) {
if (myCurrentConfiguration == null) {
throw new VcsException("The current configuration is not yet detected");
}
return myCurrentConfiguration;
}
}
/**
* @return the configuration names
*/
Set<String> getConfigurationNames() {
synchronized (myStateLock) {
return new HashSet<String>(myConfigurations.keySet());
}
}
/**
* Create new branch configuration
*
* @param name the configuration name
* @return the created configuration
*/
@NotNull
GitBranchConfiguration createConfiguration(String name) {
synchronized (myStateLock) {
if (myConfigurations.containsKey(name)) {
throw new IllegalStateException("The name " + name + " is already used");
}
GitBranchConfiguration c = new GitBranchConfiguration(this, name);
myConfigurations.put(name, c);
fireConfigurationsChanged();
return c;
}
}
/**
* Find configuration by name
*
* @param name the name to use
* @return the configuration by name
* @throws VcsException if there is an error in the state
*/
@Nullable
GitBranchConfiguration getConfiguration(String name) throws VcsException {
synchronized (myStateLock) {
return myConfigurations.get(name);
}
}
/**
* @return the state lock for branch configurations
*/
Object getStateLock() {
return myStateLock;
}
/**
* Check if configuration with specified name already exists
*
* @param name the name to check
* @return true if the configuration exists
*/
boolean hasConfigurationName(String name) {
synchronized (myStateLock) {
return myConfigurations.containsKey(name);
}
}
/**
* Set current configuration
*
* @param newConfiguration the new current configuration
*/
void setCurrentConfiguration(GitBranchConfiguration newConfiguration) {
synchronized (myStateLock) {
assert myConfigurations.get(newConfiguration.getName()) == newConfiguration;
myCurrentConfiguration = newConfiguration;
fireCurrentConfigurationChanged();
}
}
/**
* @return the shelve manager from project
*/
ShelveChangesManager getShelveManager() {
return myShelveManager;
}
/**
* Remove configuration
*
* @param toRemove the removed configuration
*/
public void removeConfiguration(@NotNull GitBranchConfiguration toRemove) {
synchronized (myStateLock) {
if (toRemove == myCurrentConfiguration) {
throw new IllegalArgumentException("Unable to remove the current configuration");
}
myConfigurations.remove(toRemove.getName());
fireConfigurationsChanged();
}
}
/**
* Start checkout process for selected configuration in the background
*
* @param configuration the selected configuration or null if new configuration is needed.
* @param remote the remote pseudo configuration name
* @param quick the quick checkout
*/
public void startCheckout(final GitBranchConfiguration configuration, final String remote, final boolean quick) {
if (remote != null && configuration != null) {
throw new IllegalArgumentException("Either remote or configuration to checkout must be null");
}
synchronized (myStateLock) {
final SpecialStatus status = calculateSpecialStatus();
if (status != SpecialStatus.NORMAL) {
throw new IllegalStateException("Checkout cannot be started due to special status (it must have been checked in UI): " + status);
}
myCheckoutIsInProgress = true;
updateSpecialStatus();
}
final String name = configuration != null ? configuration.getName() : remote == null ? "new configuration" : remote;
final String title = "Checking out " + name;
ProgressManager.getInstance().run(new Task.Backgroundable(myProject, title, false) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
try {
final GitCheckoutProcess process =
new GitCheckoutProcess(GitBranchConfigurations.this, myProject, myShelveManager, myDirtyScopeManager, myChangeManager,
myProjectManager, indicator, configuration, remote, quick);
process.run();
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
final List<VcsException> exceptions = process.getExceptions();
String op = (process.isModify() ? "Modification" : "Checkout") + " of " + name;
if (!exceptions.isEmpty()) {
GitUIUtil.showTabErrors(myProject, title, exceptions);
ToolWindowManager.getInstance(myProject).notifyByBalloon(
ChangesViewContentManager.TOOLWINDOW_ID, MessageType.ERROR, op + " failed.");
}
else if (process.isCancelled()) {
ToolWindowManager.getInstance(myProject).notifyByBalloon(
ChangesViewContentManager.TOOLWINDOW_ID, MessageType.WARNING, op + " was cancelled by user.");
}
else {
ToolWindowManager.getInstance(myProject).notifyByBalloon(
ChangesViewContentManager.TOOLWINDOW_ID, MessageType.INFO, op + " complete.");
}
}
});
}
catch (Throwable t) {
LOG.error("Unexpected exception from checkout: ", t);
}
finally {
synchronized (myStateLock) {
myCheckoutIsInProgress = false;
updateSpecialStatus();
}
}
}
});
}
/**
* Internal notification about renamed configuration
*
* @param toRename configuration to rename
* @param oldName the old name
* @param newName the new name @return true if configuration actually renamed.
*/
boolean configurationRenamed(GitBranchConfiguration toRename, String oldName, String newName) {
synchronized (myStateLock) {
final GitBranchConfiguration c = myConfigurations.get(oldName);
if (c == toRename) {
myConfigurations.remove(oldName);
myConfigurations.put(newName, c);
}
return c == toRename;
}
}
/**
* The configuration state
*/
public static class State {
/**
* The current configuration
*/
public String CURRENT;
/**
* The branch configuration
*/
public BranchConfiguration[] CONFIGURATIONS = new BranchConfiguration[0];
}
/**
* The branch configuration
*/
public static class BranchConfiguration {
/**
* If true, the configuration was auto-detected
*/
public boolean IS_AUTO_DETECTED;
/**
* The configuration name
*/
public String NAME;
/**
* The branch information
*/
public BranchInfo[] BRANCHES = new BranchInfo[0];
/**
* The branch changes
*/
public BranchChanges CHANGES;
}
/**
* Branch mapping information
*/
public static class BranchInfo {
/**
* The vcs root for which information is stored
*/
public String ROOT;
/**
* The local branch or specific commit
*/
public String REFERENCE;
}
/**
* The changes associated with the branch state
*/
public static class BranchChanges {
/**
* The path to shelve that keeps changes
*/
public String SHELVE_PATH;
/**
* Change list information
*/
public ChangeListInfo[] CHANGE_LISTS = new ChangeListInfo[0];
/**
* Information about distribution of changes among change lists
*/
public ChangeInfo[] CHANGES = new ChangeInfo[0];
}
/**
* The change list information
*/
public static class ChangeListInfo {
/**
* If true, the change list was a default change list
*/
public boolean IS_DEFAULT = false;
/**
* The change list name
*/
public String NAME;
/**
* The change list comment
*/
public String COMMENT;
}
/**
* Change information. The change is identified by before path and after path (for deleted)
*/
public static class ChangeInfo {
/**
* The before path
*/
public String BEFORE_PATH;
/**
* The after path
*/
public String AFTER_PATH;
/**
* The name of change list to which change belong
*/
public String CHANGE_LIST_NAME;
}
/**
* The special status for the roots
*/
public enum SpecialStatus {
/**
* Normal work tree, checkout is possible
*/
NORMAL,
/**
* Rebasing
*/
REBASING,
/**
* Merging
*/
MERGING,
/**
* Non git project
*/
NON_GIT,
/**
* The submodules are detected in the project
*/
SUBMODULES,
/**
* The background checkout process is in progress
*/
CHECKOUT_IN_PROGRESS
}
}
@@ -0,0 +1,31 @@
/*
* Copyright 2000-2010 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.checkout.branches;
import java.util.EventListener;
/**
* The listener for changes in the branch configurations. The events listed here are needed for the current UI.
*/
public interface GitBranchConfigurationsListener extends EventListener {
void configurationsChanged();
void specialStatusChanged();
void currentConfigurationChanged();
void referencesChanged();
}
@@ -0,0 +1,553 @@
/*
* Copyright 2000-2010 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.checkout.branches;
import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.impl.SimpleDataContext;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.ListPopup;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.IconLoader;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.ui.ChangesViewContentManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.CustomStatusBarWidget;
import com.intellij.openapi.wm.StatusBar;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.openapi.wm.WindowManager;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.util.ui.UIUtil;
import git4idea.GitUtil;
import git4idea.GitVcs;
import git4idea.commands.GitCommand;
import git4idea.commands.GitHandlerUtil;
import git4idea.commands.GitLineHandler;
import git4idea.ui.GitUIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collection;
/**
* The git branches widget
*/
public class GitBranchesWidget extends JLabel implements CustomStatusBarWidget {
/**
* The logger
*/
private static final Logger LOG = Logger.getInstance(GitBranchesWidget.class.getName());
/**
* The ID of the widget
*/
public static final String ID = "git4idea.BranchConfigurations";
/**
* The listener
*/
final GitBranchConfigurationsListener myConfigurationsListener;
/**
* The project
*/
final Project myProject;
/**
* The configurations instance
*/
private final GitBranchConfigurations myConfigurations;
/**
* The selectable configurations. Null if invalidated or non-initialized
*/
private AnAction[] mySelectableConfigurations;
/**
* The candidate remote configurations. Null if invalidated or non-initialized
*/
private AnAction[] myRemoveConfigurations;
/**
* The status bar
*/
private StatusBar myStatusBar;
/**
* If true, the popup is enabled
*/
private boolean myPopupEnabled = false;
/**
* The action group for pop up
*/
private DefaultActionGroup myPopupActionGroup;
/**
* The current popup
*/
private ListPopup myPopup;
/**
* The default foreground color
*/
private final Color myDefaultForeground;
/**
* The constructor
*
* @param project the project instance
*/
public GitBranchesWidget(Project project, GitBranchConfigurations configurations) {
myProject = project;
setBorder(WidgetBorder.INSTANCE);
//setBorder(BorderFactory.createEtchedBorder());
myConfigurations = configurations;
myDefaultForeground = getForeground();
myConfigurationsListener = new MyGitBranchConfigurationsListener();
myConfigurations.addConfigurationListener(myConfigurationsListener);
setIcon(IconLoader.findIcon("/icons/branch.png", getClass()));
Disposer.register(myConfigurations, this);
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
showPopup();
}
});
updateLabel();
}
/**
* Create and install widget
*
* @param project the context project
* @param configurations the configurations to use
* @return the action that uninstalls widget
*/
static Runnable install(final Project project, final GitBranchConfigurations configurations) {
final Ref<GitBranchesWidget> widget = new Ref<GitBranchesWidget>();
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
StatusBar statusBar = WindowManager.getInstance().getStatusBar(project);
if (statusBar != null) {
final GitBranchesWidget w = new GitBranchesWidget(project, configurations);
statusBar.addWidget(w, "after InsertOverwrite", project);
widget.set(w);
}
}
});
return new Runnable() {
@Override
public void run() {
if (widget.get() != null) {
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
Disposer.dispose(widget.get());
}
});
}
}
};
}
/**
* {@inheritDoc}
*/
@Override
public JComponent getComponent() {
return this;
}
/**
* {@inheritDoc}
*/
@NotNull
@Override
public String ID() {
return ID;
}
/**
* {@inheritDoc}
*/
@Override
public WidgetPresentation getPresentation(@NotNull Type type) {
return null;
}
/**
* {@inheritDoc}
*/
@Override
public void install(@NotNull StatusBar statusBar) {
myStatusBar = statusBar;
}
/**
* {@inheritDoc}
*/
@Override
public void dispose() {
myConfigurations.removeConfigurationListener(myConfigurationsListener);
myStatusBar.removeWidget(ID());
}
/**
* @return get or create remotes group
*/
private AnAction[] getRemotes() {
assert myPopupEnabled : "pop should be enabled";
if (myRemoveConfigurations == null) {
ArrayList<AnAction> rc = new ArrayList<AnAction>();
for (final String c : myConfigurations.getRemotesCandidates()) {
rc.add(new AnAction(c) {
@Override
public void actionPerformed(AnActionEvent e) {
myConfigurations.startCheckout(null, c, false);
}
});
}
rc.add(new RefreshRemotesAction());
myRemoveConfigurations = rc.toArray(new AnAction[rc.size()]);
}
return myRemoveConfigurations;
}
void showPopup() {
if (!myPopupEnabled) {
return;
}
final DataContext parent = DataManager.getInstance().getDataContext((Component)myStatusBar);
final DataContext dataContext = SimpleDataContext.getSimpleContext(PlatformDataKeys.PROJECT.getName(), myProject, parent);
myPopup = JBPopupFactory.getInstance()
.createActionGroupPopup("Checkout", getPopupActionGroup(), dataContext, JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, true,
new Runnable() {
@Override
public void run() {
myPopup = null;
}
}, 20);
final Dimension dimension = myPopup.getContent().getPreferredSize();
final Point at = new Point(0, -dimension.height);
myPopup.show(new RelativePoint(getComponent(), at));
}
/**
* @return get or create selectable configuration group
*/
private AnAction[] getSelectable() {
assert myPopupEnabled : "pop should be enabled";
if (mySelectableConfigurations == null) {
ArrayList<AnAction> rc = new ArrayList<AnAction>();
GitBranchConfiguration current;
try {
current = myConfigurations.getCurrentConfiguration();
}
catch (VcsException e) {
LOG.error("Unexpected error at this point", e);
mySelectableConfigurations = new AnAction[0];
return mySelectableConfigurations;
}
String name = current == null ? "" : current.getName();
for (final String c : myConfigurations.getConfigurationNames()) {
if (name.equals(c)) {
// skip current config
continue;
}
rc.add(new AnAction(c) {
@Override
public void actionPerformed(AnActionEvent e) {
try {
final GitBranchConfiguration toCheckout = myConfigurations.getConfiguration(c);
if (toCheckout == null) {
throw new VcsException("The configuration " + c + " cannot be found.");
}
myConfigurations.startCheckout(toCheckout, null, true);
}
catch (VcsException e1) {
GitUIUtil.showOperationError(myProject, e1, "Unable to load: " + c);
}
}
});
}
mySelectableConfigurations = rc.toArray(new AnAction[rc.size()]);
}
return mySelectableConfigurations;
}
/**
* @return the action group for popup
*/
ActionGroup getPopupActionGroup() {
if (myPopupActionGroup == null) {
myPopupActionGroup = new DefaultActionGroup(null, false);
myPopupActionGroup.addAction(new AnAction("Manage Configurations ...") {
@Override
public void actionPerformed(AnActionEvent e) {
GitManageConfigurationsDialog.showDialog(myProject, myConfigurations);
}
});
myPopupActionGroup.addAction(new AnAction("Modify Current Configuration ...") {
@Override
public void actionPerformed(AnActionEvent e) {
try {
final GitBranchConfiguration current = myConfigurations.getCurrentConfiguration();
myConfigurations.startCheckout(current, null, false);
}
catch (VcsException e1) {
LOG.error("The current configuration must exists at this point", e1);
}
}
});
myPopupActionGroup.addAction(new AnAction("New Configuration ...") {
@Override
public void actionPerformed(AnActionEvent e) {
myConfigurations.startCheckout(null, null, false);
}
});
myPopupActionGroup.add(new MyRemotesActionGroup());
myPopupActionGroup.addSeparator("Branch Configurations");
myPopupActionGroup.add(new MySelectableActionGroup());
}
return myPopupActionGroup;
}
/**
* Update label on the widget
*/
private void updateLabel() {
cancelPopup();
final GitBranchConfigurations.SpecialStatus status = myConfigurations.getSpecialStatus();
String t;
myPopupEnabled = false;
switch (status) {
case CHECKOUT_IN_PROGRESS:
t = "Checkout in progress...";
break;
case MERGING:
t = "Merging...";
break;
case REBASING:
t = "Rebasing...";
break;
case NON_GIT:
t = "Non-Git project";
break;
case SUBMODULES:
t = "Submodules unsupported";
break;
case NORMAL:
GitBranchConfiguration current;
try {
current = myConfigurations.getCurrentConfiguration();
}
catch (VcsException e) {
current = null;
}
if (current == null) {
t = "Detection in progress";
}
else {
myPopupEnabled = true;
t = current.getName();
}
break;
default:
t = "Unknown status: " + status;
}
setForeground(myPopupEnabled ? myDefaultForeground : Color.RED);
setText(t);
}
/**
* Cancel popup if it is shown
*/
private void cancelPopup() {
if (myPopup != null) {
myPopup.cancel();
}
}
/**
* Remotes action group
*/
class MySelectableActionGroup extends ActionGroup {
/**
* The constructor
*/
public MySelectableActionGroup() {
super(null, false);
}
/**
* {@inheritDoc}
*/
@NotNull
@Override
public AnAction[] getChildren(@Nullable AnActionEvent e) {
return getSelectable();
}
}
/**
* Remotes action group
*/
class MyRemotesActionGroup extends ActionGroup {
/**
* The constructor
*/
public MyRemotesActionGroup() {
super("Remotes", true);
}
/**
* {@inheritDoc}
*/
@NotNull
@Override
public AnAction[] getChildren(@Nullable AnActionEvent e) {
return getRemotes();
}
}
/**
* The configuration listener
*/
class MyGitBranchConfigurationsListener implements GitBranchConfigurationsListener {
/**
* {@inheritDoc}
*/
@Override
public void configurationsChanged() {
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
mySelectableConfigurations = null;
}
});
}
/**
* {@inheritDoc}
*/
@Override
public void specialStatusChanged() {
refreshLabel();
}
private void refreshLabel() {
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
updateLabel();
}
});
}
/**
* {@inheritDoc}
*/
@Override
public void currentConfigurationChanged() {
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
mySelectableConfigurations = null;
updateLabel();
}
});
}
/**
* {@inheritDoc}
*/
@Override
public void referencesChanged() {
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
myRemoveConfigurations = null;
updateLabel();
}
});
}
}
/**
* Refresh git remotes
*/
class RefreshRemotesAction extends AnAction {
/**
* The constructor
*/
public RefreshRemotesAction() {
super("Refresh Remotes...", "Fetch all references for git vcs roots", IconLoader.findIcon("/vcs/refresh.png"));
}
/**
* {@inheritDoc}
*/
@Override
public void actionPerformed(AnActionEvent e) {
final String title = "Refreshing remotes";
ProgressManager.getInstance().run(new Task.Backgroundable(myProject, title, false) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
final ArrayList<VcsException> exceptions = new ArrayList<VcsException>();
try {
for (VirtualFile root : GitUtil.getGitRoots(myProject, GitVcs.getInstance(myProject))) {
GitLineHandler h = new GitLineHandler(myProject, root, GitCommand.FETCH);
h.addParameters("--all", "-v");
final Collection<VcsException> e = GitHandlerUtil
.doSynchronouslyWithExceptions(h, indicator, "Fetching all for " + GitUtil.relativePath(myProject.getBaseDir(), root));
exceptions.addAll(e);
}
}
catch (VcsException e1) {
exceptions.add(e1);
}
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
if (!exceptions.isEmpty()) {
GitUIUtil.showTabErrors(myProject, title, exceptions);
ToolWindowManager.getInstance(myProject).notifyByBalloon(
ChangesViewContentManager.TOOLWINDOW_ID, MessageType.ERROR, "Refreshing remotes failed.");
}
else {
ToolWindowManager.getInstance(myProject).notifyByBalloon(
ChangesViewContentManager.TOOLWINDOW_ID, MessageType.INFO, "Refreshing remotes complete.");
}
myRemoveConfigurations = null;
cancelPopup();
}
});
}
});
}
}
}
@@ -0,0 +1,756 @@
/*
* Copyright 2000-2010 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.checkout.branches;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ex.ProjectManagerEx;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.*;
import com.intellij.openapi.vcs.changes.shelf.ShelveChangesManager;
import com.intellij.openapi.vcs.changes.shelf.ShelvedChangeList;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ui.UIUtil;
import com.intellij.vcsUtil.VcsUtil;
import git4idea.GitRevisionNumber;
import git4idea.GitUtil;
import git4idea.checkout.branches.GitBranchConfigurations.BranchChanges;
import git4idea.checkout.branches.GitBranchConfigurations.ChangeInfo;
import git4idea.checkout.branches.GitBranchConfigurations.ChangeListInfo;
import git4idea.commands.*;
import git4idea.update.GitStashUtils;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.*;
import java.util.concurrent.Semaphore;
/**
* The checkout branch process. It used to organize the entire checkout process
*/
public class GitCheckoutProcess {
/**
* The logger
*/
private static final Logger LOG = Logger.getInstance(GitCheckoutProcess.class.getName());
/**
* The configuration process
*/
final GitBranchConfigurations myConfig;
/**
* The vcs roots
*/
private List<VirtualFile> myRoots;
/**
* The vcs exceptions
*/
private List<VcsException> myExceptions;
/**
* The project
*/
private final Project myProject;
/**
* The shelve manager
*/
private final ShelveChangesManager myShelveManager;
/**
* The dirty scope manager
*/
private final VcsDirtyScopeManager myDirtyScopeManager;
/**
* The changes manager
*/
private final ChangeListManagerEx myChangeManager;
/**
* The project manager
*/
private final ProjectManagerEx myProjectManager;
/**
* The progress indicator
*/
private final ProgressIndicator myProgress;
/**
* The name of remote pseudo-configuration
*/
@Nullable private final String myRemoteConfiguration;
/**
* If true, the quick process is being used, and user is not offered to select changes
*/
private boolean myQuick;
/**
* The checkout process is run in the modify mode. Branch name or roots are changed.
*/
private final boolean myIsModify;
/**
* The new configuration
*/
private GitBranchConfiguration myNewConfiguration;
/**
* The new branch mapping
*/
private Map<VirtualFile, String> myNewBranchMapping = Collections.emptyMap();
/**
* The described roots
*/
private final Map<VirtualFile, String> myDescribedRoots = new HashMap<VirtualFile, String>();
/**
* If true, the checkout process was cancelled
*/
private boolean myCancelled;
/**
* The constructor
*
* @param config the configuration object
* @param project the context project
* @param shelveManager the shelve manager
* @param dirtyScopeManager the dirty scope manager
* @param changeManager the change manager
* @param projectManager the project manager
* @param progress the progress indicator for the process
* @param newConfiguration the new configuration, the current if modify
* @param quick if true, the no changes are assumed to be selected, so dialog need not be shown
*/
public GitCheckoutProcess(GitBranchConfigurations config,
Project project,
ShelveChangesManager shelveManager,
VcsDirtyScopeManager dirtyScopeManager,
ChangeListManagerEx changeManager,
ProjectManagerEx projectManager,
ProgressIndicator progress,
@Nullable GitBranchConfiguration newConfiguration,
@Nullable String remoteConfiguration,
boolean quick) {
myExceptions = Collections.synchronizedList(new ArrayList<VcsException>());
myConfig = config;
myProject = project;
myShelveManager = shelveManager;
myDirtyScopeManager = dirtyScopeManager;
myChangeManager = changeManager;
myProjectManager = projectManager;
myProgress = progress;
myNewConfiguration = newConfiguration;
myRemoteConfiguration = remoteConfiguration;
boolean f = false;
try {
f = myNewConfiguration == config.getCurrentConfiguration();
}
catch (VcsException e) {
myExceptions.add(e);
}
myIsModify = f;
myQuick = quick & !myIsModify;
}
/**
* Start the checkout process. The all activity is done in this thread. When needed, the activity is scheduled in awt or other threads.
*/
public void run() {
if (!myExceptions.isEmpty()) {
return;
}
myProgress.setText("Staring checkout...");
try {
myRoots = GitUtil.getGitRoots(myConfig.getProject(), myConfig.getVcs());
saveAll();
waitForChanges();
myProjectManager.blockReloadingProjectOnExternalChanges();
try {
GitBranchConfiguration oldConfiguration = checkCurrentConfiguration();
if (oldConfiguration == null) {
myCancelled = true;
return;
}
for (VirtualFile root : myRoots) {
myDescribedRoots.put(root, myConfig.describeRoot(root));
}
ensureNoChangesInCurrent(oldConfiguration);
if (!myExceptions.isEmpty()) {
return;
}
List<Change> changes = collectChanges();
Collection<Change> selected;
myProgress.setText("Verifying the current configuration...");
if (myQuick && checkRoots()) {
// show switch dialog with new configuration that allows selecting changes (if not quick switch)
selected = Collections.emptyList();
}
else {
myProgress.setText("Selecting changes to transfer...");
selected = selectChangesToTransfer(changes);
if (selected == null) {
// the process was cancelled, or no changes that require checkout were made to the current configuration
myCancelled = true;
return;
}
}
// preparation phase finished. do actual checkout.
assert myNewConfiguration != null;
List<VirtualFile> checkoutRoots = rootsToCheckout();
if (checkoutRoots.size() > 0) {
// TODO disable saving
myProgress.setText("Shelving changes...");
Pair<BranchChanges, BranchChanges> changesPair = shelveChanges(myProgress, oldConfiguration.getName(), selected);
try {
// save changes in old root, it also may be aliased with new root
oldConfiguration.setChanges(changesPair.first);
try {
HashSet<VirtualFile> startedRoots = new HashSet<VirtualFile>();
boolean failed = !checkoutAndRefreshRoots(checkoutRoots, startedRoots);
myProgress.setText2("");
if (!failed) {
myConfig.setCurrentConfiguration(myNewConfiguration);
}
else {
myNewConfiguration = oldConfiguration;
rollbackRootCheckout(startedRoots);
}
}
finally {
final BranchChanges branchChanges = myNewConfiguration.getChanges();
myNewConfiguration.setChanges(null);
restoreChanges(myProgress, branchChanges);
}
}
finally {
// restore transient shelve
restoreChanges(myProgress, changesPair.second);
}
// TODO enable saving
}
else {
myConfig.setCurrentConfiguration(myNewConfiguration);
}
}
finally {
myProjectManager.unblockReloadingProjectOnExternalChanges();
}
// launch project update?
}
catch (VcsException e) {
myExceptions.add(e);
}
finally {
saveAll(); // saves configuration changes
}
}
/**
* Ensure that current configuration contains no changes. This is a bug condition.
*
* @param oldConfiguration the old configuration
*/
private void ensureNoChangesInCurrent(GitBranchConfiguration oldConfiguration) {
final BranchChanges oldChanges = oldConfiguration.getChanges();
if (oldChanges != null) {
String name = oldChanges.SHELVE_PATH;
for (ShelvedChangeList changeList : myShelveManager.getShelvedChangeLists()) {
if (changeList.PATH.equals(oldChanges.SHELVE_PATH)) {
name = changeList.DESCRIPTION;
break;
}
}
final VcsException ex = new VcsException("The current configuration contains shelve: " + name);
LOG.error(ex);
myExceptions.add(ex);
oldConfiguration.setChanges(null);
}
}
/**
* Rollback the root checkout
*
* @param startedRoots that roots that has been started and need to be rolled back
*/
private void rollbackRootCheckout(HashSet<VirtualFile> startedRoots) {
myProgress.setText("Rolling back...");
for (VirtualFile root : startedRoots) {
myProgress.setText2(root.getPath());
startedRoots.add(root);
GitLineHandler h = new GitLineHandler(myProject, root, GitCommand.CHECKOUT);
h.setNoSSH(true);
h.addParameters("-f", myDescribedRoots.get(root));
Collection<VcsException> exceptions = GitHandlerUtil.doSynchronouslyWithExceptions(h, myProgress, h.printableCommandLine());
myExceptions.addAll(exceptions);
}
// filter out implicitly included roots
ArrayList<VirtualFile> newRoots = new ArrayList<VirtualFile>();
loop:
for (VirtualFile root : startedRoots) {
for (VirtualFile other : startedRoots) {
if (other != root && VfsUtil.isAncestor(other, root, true)) {
continue loop;
}
}
newRoots.add(root);
}
for (VirtualFile root : newRoots) {
root.refresh(false, true);
}
myProgress.setText2("");
}
/**
* Checkout needed vcs roots and do vcs refresh on diff files
*
* @param checkoutRoots the roots to checkout
* @param startedRoots the roots for which checkout actually started (Modified by this method)
* @return true if checkout is successful
* @throws VcsException
*/
private boolean checkoutAndRefreshRoots(List<VirtualFile> checkoutRoots, HashSet<VirtualFile> startedRoots) throws VcsException {
boolean failed = false;
myProgress.setText("Checking out...");
HashSet<File> filesToRefresh = new HashSet<File>();
for (VirtualFile root : checkoutRoots) {
myProgress.setText2(root.getPath());
startedRoots.add(root);
GitRevisionNumber prev = GitRevisionNumber.resolve(myProject, root, "HEAD");
GitLineHandler h = new GitLineHandler(myProject, root, GitCommand.CHECKOUT);
h.addParameters("-f");
String branchedRef = myNewBranchMapping.get(root);
String ref = myNewConfiguration.getReference(root.getPath());
if (branchedRef != null) {
h.addParameters("-l", "-t", "-b", ref, branchedRef);
}
else {
h.addParameters(ref);
}
Collection<VcsException> exceptions = GitHandlerUtil.doSynchronouslyWithExceptions(h, myProgress, h.printableCommandLine());
if (!exceptions.isEmpty()) {
myExceptions.addAll(exceptions);
failed = true;
break;
}
GitSimpleHandler d = new GitSimpleHandler(myProject, root, GitCommand.DIFF);
d.addParameters("--name-only", prev.asString() + "..HEAD");
d.setNoSSH(true);
d.setSilent(true);
d.endOptions();
try {
File base = new File(root.getPath());
for (StringScanner s = new StringScanner(d.run()); s.hasMoreData();) {
String l = s.line();
if (l.length() > 0) {
filesToRefresh.add(new File(base, GitUtil.unescapePath(l)));
}
}
}
catch (VcsException e) {
LOG.error("Unexpected diff failure", e);
myExceptions.add(e);
failed = true;
break;
}
}
if (!failed) {
LocalFileSystem.getInstance().refreshIoFiles(filesToRefresh);
}
return !failed;
}
/**
* @return set of roots to checkout
*/
private List<VirtualFile> rootsToCheckout() {
ArrayList<VirtualFile> rc = new ArrayList<VirtualFile>();
for (VirtualFile root : myRoots) {
String current = myDescribedRoots.get(root);
String newRef = myNewConfiguration.getReference(root.getPath());
if (!current.equals(newRef)) {
rc.add(root);
}
}
return rc;
}
/**
* @param changes all changes
* @return this method allows renaming and updating, the branch configuration and to select changes to transfer to new configuration.
* null if process is cancelled, or only name changed in the current configuration and no checkout is required.
*/
@Nullable
private Collection<Change> selectChangesToTransfer(final List<Change> changes) {
final Ref<GitSwitchBranchesDialog.Result> t = new Ref<GitSwitchBranchesDialog.Result>();
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
try {
t.set(GitSwitchBranchesDialog
.showDialog(myProject, myNewConfiguration, changes, myRoots, myRemoteConfiguration, myConfig, myIsModify));
}
catch (VcsException e) {
myExceptions.add(e);
}
catch (Throwable e) {
LOG.error("Unexpected error", e);
myExceptions.add(new VcsException("Selecting changes failed", e));
}
}
});
if (t.get() == null) {
return null;
}
else {
myNewBranchMapping = t.get().referencesToUse;
myNewConfiguration = t.get().target;
return t.get().changes;
}
}
/**
* @return true if roots in the new configuration are configured correctly
*/
private boolean checkRoots() {
if (myNewConfiguration == null) {
return false;
}
LocalFileSystem lfs = LocalFileSystem.getInstance();
HashSet<VirtualFile> roots = new HashSet<VirtualFile>(myRoots);
Map<String, String> branches = myNewConfiguration.getReferences();
for (Map.Entry<String, String> m : branches.entrySet()) {
VirtualFile root = lfs.findFileByPath(m.getKey());
if (root == null || root.findChild(".git") == null || !roots.contains(root)) {
return false;
}
roots.remove(root);
try {
GitRevisionNumber.resolve(myProject, root, m.getValue());
}
catch (VcsException e) {
return false;
}
}
return roots.isEmpty();
}
/**
* @return collect changes from project
*/
private List<Change> collectChanges() {
List<LocalChangeList> changeLists = myChangeManager.getChangeLists();
ArrayList<Change> changes = new ArrayList<Change>();
for (LocalChangeList l : changeLists) {
changes.addAll(l.getChanges());
}
return changes;
}
/**
* @return the current configuration, or null if process was cancelled
* @throws VcsException if there is a problem with checking configuration
*/
@Nullable
private GitBranchConfiguration checkCurrentConfiguration() throws VcsException {
GitBranchConfiguration c = myConfig.getCurrentConfiguration();
if (!myIsModify && !rootsMatchConfiguration(c)) {
// when the current configuration is modified, there is no need to show duplicate dialogs
c = GitBranchConfigurationChangedDialog.showDialog(myConfig, c, myRoots);
}
return c;
}
/**
* Check of the root mapping has been changed for the current vcs root
*
* @param c the configuration to check
* @return true if nothing shoudlbe checked out
* @throws VcsException
*/
private boolean rootsMatchConfiguration(GitBranchConfiguration c) throws VcsException {
LocalFileSystem lfs = LocalFileSystem.getInstance();
Map<String, String> branches = c.getReferences();
if (branches.size() != myRoots.size()) {
return false;
}
HashSet<String> realSet = new HashSet<String>();
for (VirtualFile root : myRoots) {
realSet.add(root.getPath());
}
HashSet<String> storedSet = new HashSet<String>();
for (Map.Entry<String, String> m : branches.entrySet()) {
String rootPath = m.getKey();
storedSet.add(rootPath);
VirtualFile root = lfs.findFileByPath(rootPath);
String ref = myConfig.describeRoot(root);
if (!ref.equals(m.getValue())) {
return false;
}
}
return storedSet.equals(realSet);
}
private void waitForChanges() throws VcsException {
final Semaphore s = new Semaphore(0);
waitForChangesRefresh("Preparing for the checkout: ", new Runnable() {
@Override
public void run() {
s.release();
}
});
try {
s.acquire();
}
catch (InterruptedException e) {
throw new VcsException("Waiting for changes was interrupted: ", e);
}
}
/**
* Save all changes before start of update process
*/
private static void saveAll() {
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
public void run() {
FileDocumentManager.getInstance().saveAllDocuments();
}
});
}
});
}
/**
* Restore changes.
*
* @param progress the progress indicator
* @param changes the changes to restore, null means no changes to restore
* @return true if changes has been restored successfully
*/
private boolean restoreChanges(ProgressIndicator progress,
final BranchChanges changes) {
if (changes == null) {
return true;
}
ShelvedChangeList shelve = null;
for (ShelvedChangeList changeList : myShelveManager.getShelvedChangeLists()) {
if (changeList.PATH.equals(changes.SHELVE_PATH)) {
shelve = changeList;
}
}
if (shelve == null) {
//noinspection ThrowableInstanceNeverThrown
myExceptions.add(new VcsException("Failed to find shelve with path" + changes.SHELVE_PATH));
return false;
}
progress.setText("Refreshing files before restoring shelve: " + shelve.DESCRIPTION);
GitStashUtils.doSystemUnshelve(myProject, shelve, myShelveManager, myChangeManager, myExceptions);
// dirty files and parse changes
final HashMap<Pair<String, String>, String> parsedChanges = new HashMap<Pair<String, String>, String>();
for (ChangeInfo changeInfo : changes.CHANGES) {
String before = changeInfo.BEFORE_PATH;
String after = changeInfo.AFTER_PATH;
parsedChanges.put(Pair.create(before, after), changeInfo.CHANGE_LIST_NAME);
if (after != null) {
myDirtyScopeManager.fileDirty(VcsUtil.getFilePath(after));
}
if (before != null) {
myDirtyScopeManager.fileDirty(VcsUtil.getFilePath(before));
}
}
final ShelvedChangeList finalShelve = shelve;
try {
waitForChanges();
HashMap<String, LocalChangeList> lists = new HashMap<String, LocalChangeList>();
for (LocalChangeList localChangeList : myChangeManager.getChangeLists()) {
lists.put(localChangeList.getName(), localChangeList);
}
LocalChangeList defaultList = myChangeManager.getDefaultChangeList();
for (ChangeListInfo changeListInfo : changes.CHANGE_LISTS) {
LocalChangeList changeList = lists.get(changeListInfo.NAME);
if (changeList != null) {
myChangeManager.setReadOnly(changeList.getName(), false);
}
else {
changeList = myChangeManager.addChangeList(changeListInfo.NAME, changeListInfo.COMMENT);
lists.put(changeListInfo.NAME, changeList);
}
if (changeListInfo.IS_DEFAULT) {
myChangeManager.setDefaultChangeList(changeList);
}
}
for (Change change : defaultList.getChanges()) {
ContentRevision beforeRevision = change.getBeforeRevision();
String before = beforeRevision == null ? null : beforeRevision.getFile().getPath();
ContentRevision afterRevision = change.getAfterRevision();
String after = afterRevision == null ? null : afterRevision.getFile().getPath();
Pair<String, String> key = Pair.create(before, after);
String listName = parsedChanges.get(key);
assert listName != null : "List name should be found: " + key;
if (!listName.equals(defaultList.getName())) {
LocalChangeList changeList = lists.get(listName);
assert changeList != null : "Change List should be found: " + listName;
myChangeManager.moveChangesTo(changeList, new Change[]{change});
}
}
return true;
}
catch (Throwable t) {
//noinspection ThrowableInstanceNeverThrown
myExceptions.add(
new VcsException("Failed to process restore shelved change list: " + finalShelve.DESCRIPTION + ". Please restore it manually.", t));
return false;
}
}
/**
* Wait until changes are refreshed
*
* @param title the title of the operation
* @param runnable the process that awaits changes
*/
void waitForChangesRefresh(final String title, final Runnable runnable) {
UIUtil.invokeLaterIfNeeded(new Runnable() {
public void run() {
myChangeManager.invokeAfterUpdate(runnable, InvokeAfterUpdateMode.BACKGROUND_NOT_CANCELLABLE, title,
ModalityState.NON_MODAL);
}
});
}
/**
* Shelve selected and transient changes. If creation of one of shelves fails, the other shelve is rolled back
*
* @param configurationName the current configuration name
* @param selectedChanges the selected changes
* @return null if operation fails or pair if operation completed successfully. The first is changes to be stored in old configuration, the second is trainsient changes.
*/
@Nullable
private Pair<BranchChanges, BranchChanges> shelveChanges(ProgressIndicator progress,
String configurationName,
Collection<Change> selectedChanges) {
assert myExceptions.isEmpty() : "The method should not be called if there is already problems detected";
List<LocalChangeList> changeLists = myChangeManager.getChangeListsCopy();
HashMap<Change, LocalChangeList> changes = new HashMap<Change, LocalChangeList>();
for (LocalChangeList l : changeLists) {
for (Change change : l.getChanges()) {
changes.put(change, l);
}
}
HashSet<Change> selected = new HashSet<Change>(selectedChanges);
HashSet<Change> other = new HashSet<Change>(changes.keySet());
other.removeAll(selected);
Date now = new Date();
BranchChanges storedChanges = shelveChanges(progress, changes, other,
"Shelved changes for configuration " + configurationName + " (created on " + now + ")");
if (!myExceptions.isEmpty()) {
return null;
}
BranchChanges transientChanges = shelveChanges(progress, changes, selected,
"Transferred changes from configuration " +
configurationName +
" (created on " +
now +
")");
if (!myExceptions.isEmpty()) {
restoreChanges(progress, storedChanges);
return null;
}
return Pair.create(storedChanges, transientChanges);
}
/**
* Shelve changes remembering change configuration
*
* @param progress the progress
* @param changes the all changes to process
* @param toShelve the shelved change subset
* @param description the description of the shelve
* @return branch change set or null if shelve is not created (there will be exceptions in list in case of errors)
*/
@Nullable
private BranchChanges shelveChanges(ProgressIndicator progress,
HashMap<Change, LocalChangeList> changes,
HashSet<Change> toShelve, String description
) {
if (toShelve.isEmpty()) {
return null;
}
HashSet<LocalChangeList> lists = new HashSet<LocalChangeList>();
ArrayList<ChangeInfo> ci = new ArrayList<ChangeInfo>(toShelve.size());
for (Change c : toShelve) {
LocalChangeList l = changes.get(c);
lists.add(l);
ChangeInfo i = new ChangeInfo();
ContentRevision after = c.getAfterRevision();
if (after != null) {
i.AFTER_PATH = after.getFile().getPath();
}
ContentRevision before = c.getBeforeRevision();
if (before != null) {
i.BEFORE_PATH = before.getFile().getPath();
}
i.CHANGE_LIST_NAME = l.getName();
ci.add(i);
}
ArrayList<ChangeListInfo> li = new ArrayList<ChangeListInfo>(lists.size());
for (LocalChangeList l : lists) {
ChangeListInfo i = new ChangeListInfo();
i.IS_DEFAULT = l.isDefault();
i.NAME = l.getName();
i.COMMENT = l.getComment();
li.add(i);
}
if (progress != null) {
progress.setText("Creating shelve: " + description);
}
ShelvedChangeList shelved = GitStashUtils.shelveChanges(myProject, myShelveManager, toShelve, description, myExceptions);
if (shelved == null) {
return null;
}
BranchChanges b = new BranchChanges();
b.SHELVE_PATH = shelved.PATH;
b.CHANGE_LISTS = li.toArray(new ChangeListInfo[li.size()]);
b.CHANGES = ci.toArray(new ChangeInfo[ci.size()]);
return b;
}
/**
* @return the list of problems
*/
public List<VcsException> getExceptions() {
return myExceptions;
}
/**
* @return true if the process was cancelled
*/
public boolean isCancelled() {
return myCancelled;
}
/**
* @return true if the process was modification process
*/
public boolean isModify() {
return myIsModify;
}
}
@@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="git4idea.checkout.branches.GitManageConfigurationsDialog">
<grid id="27dc6" binding="myRootPanel" layout-manager="GridLayoutManager" row-count="3" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="488" height="476"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<scrollpane id="1ca7c" class="com.intellij.ui.components.JBScrollPane">
<constraints>
<grid row="1" column="0" row-span="2" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="5ef88" class="com.intellij.ui.components.JBList" binding="myNamesList">
<constraints/>
<properties>
<selectionMode value="0"/>
</properties>
</component>
</children>
</scrollpane>
<component id="39006" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<labelFor value="1ca7c"/>
<text value="&amp;Branches"/>
</properties>
</component>
<grid id="eb1ed" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="1" column="1" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="8" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="87360" class="javax.swing.JButton" binding="myDeleteButton" default-binding="true">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="&amp;Delete"/>
</properties>
</component>
</children>
</grid>
<grid id="f0a1f" layout-manager="GridLayoutManager" row-count="4" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="2" column="1" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="etched" title="Branch Information"/>
<children>
<scrollpane id="a4091" class="com.intellij.ui.components.JBScrollPane">
<constraints>
<grid row="3" column="0" row-span="1" col-span="2" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="618c9" class="com.intellij.ui.table.JBTable" binding="myBranchesTable">
<constraints/>
<properties>
<autoCreateRowSorter value="true"/>
</properties>
</component>
</children>
</scrollpane>
<component id="79958" class="javax.swing.JLabel">
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<labelFor value="a4091"/>
<text value="Branch &amp;Mapping:"/>
</properties>
</component>
<component id="24ff5" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Name:"/>
</properties>
</component>
<component id="269d" class="javax.swing.JLabel" binding="myNameLabel">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value=""/>
</properties>
</component>
<component id="72d60" class="javax.swing.JLabel">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Shelve:"/>
</properties>
</component>
<component id="18235" class="javax.swing.JLabel" binding="myShelveNameLabel">
<constraints>
<grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="4" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value=""/>
</properties>
</component>
</children>
</grid>
</children>
</grid>
</form>
@@ -0,0 +1,350 @@
/*
* Copyright 2000-2010 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.checkout.branches;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.shelf.ShelvedChangeList;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.components.JBList;
import com.intellij.ui.table.JBTable;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.AbstractTableModel;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/**
* The manage configurations dialog. Currently only delete and limited view works.
* <p/>
* TODO auto-detection of configurations
*/
public class GitManageConfigurationsDialog extends DialogWrapper {
/**
* The new configuration exit code
*/
private static final int NEW_CONFIGURATION_EXIT_CODE = NEXT_USER_EXIT_CODE;
/**
* The delete configuration button
*/
private JButton myDeleteButton;
/**
* The configuration name list
*/
private JBList myNamesList;
/**
* The table with branches
*/
private JBTable myBranchesTable;
/**
* The root panel
*/
private JPanel myRootPanel;
/**
* The configuration name label
*/
private JLabel myNameLabel;
/**
* The name label
*/
private JLabel myShelveNameLabel;
/**
* The project to use
*/
private final Project myProject;
/**
* The git configurations to use
*/
private final GitBranchConfigurations myConfigurations;
/**
* Map from shelve path to shelve name
*/
private final HashMap<String, String> myShelveNames = new HashMap<String, String>();
/**
* The table model
*/
private final MyBranchMappingModel myBranchMappingModel;
/**
* The list model with names
*/
private final DefaultListModel myNamesModel;
/**
* The constructor
*
* @param project the project
* @param configurations the configuration service
*/
protected GitManageConfigurationsDialog(Project project, GitBranchConfigurations configurations) {
super(project, true);
setTitle("Manage Branch Configurations");
setOKButtonText("Checkout");
myProject = project;
myConfigurations = configurations;
myBranchMappingModel = new MyBranchMappingModel();
myBranchesTable.setModel(myBranchMappingModel);
for (ShelvedChangeList shelvedChangeList : configurations.getShelveManager().getShelvedChangeLists()) {
myShelveNames.put(shelvedChangeList.PATH, shelvedChangeList.DESCRIPTION);
}
myNamesList.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
updateOnSelection();
}
});
myNamesModel = new DefaultListModel();
myNamesList.setModel(myNamesModel);
for (String n : myConfigurations.getConfigurationNames()) {
myNamesModel.addElement(n);
}
myDeleteButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
GitBranchConfiguration current = getCurrentConfiguration();
GitBranchConfiguration selected = getSelectedConfiguration();
assert selected != null : "The configuration must be selected (the button should be disabled)";
assert current != selected : "The current must not be the same as selected (the button should be disabled)";
int i = myNamesList.getSelectedIndex();
assert i != -1 && myNamesModel.elementAt(i).equals(selected.getName());
myConfigurations.removeConfiguration(selected);
myNamesModel.removeElementAt(i);
if (i >= myNamesModel.size()) {
i = myNamesModel.size() - 1;
}
myNamesList.setSelectedIndex(i);
}
});
init();
if (myNamesModel.size() > 0) {
myNamesList.setSelectedIndex(0);
}
updateOnSelection();
}
/**
* Update dialog on selection
*/
private void updateOnSelection() {
GitBranchConfiguration current = getCurrentConfiguration();
GitBranchConfiguration selected = getSelectedConfiguration();
myBranchMappingModel.set(selected);
if (selected == null) {
myNameLabel.setText("");
myShelveNameLabel.setText("");
}
else {
myNameLabel.setText(selected.getName());
GitBranchConfigurations.BranchChanges ch = selected.getChanges();
if (ch == null) {
myShelveNameLabel.setText("<html><i>No associated shelve</i></html>");
myShelveNameLabel.setToolTipText("<html><i>This configuration has no associated shelve</i></html>");
}
else {
String d = myShelveNames.get(ch.SHELVE_PATH);
myShelveNameLabel.setText(d);
myShelveNameLabel.setToolTipText("<html><table><tr><td>Shelve Path:</td><td>" +
StringUtil.escapeXml(ch.SHELVE_PATH) +
"</td></tr><tr><td>Shelve Path:</td><td>" +
StringUtil.escapeXml(d) +
"</td></tr></html>");
}
}
boolean isNonCurrent = selected != null || selected != current;
myDeleteButton.setEnabled(isNonCurrent);
setOKActionEnabled(isNonCurrent && myConfigurations.getSpecialStatus() == GitBranchConfigurations.SpecialStatus.NORMAL);
}
/**
* @return the selected configuration
*/
@Nullable
private GitBranchConfiguration getSelectedConfiguration() {
String value = (String)myNamesList.getSelectedValue();
GitBranchConfiguration selected;
try {
selected = value == null ? null : myConfigurations.getConfiguration(value);
}
catch (VcsException e1) {
selected = null;
}
return selected;
}
/**
* @return the current configuration
*/
@Nullable
private GitBranchConfiguration getCurrentConfiguration() {
GitBranchConfiguration current;
try {
current = myConfigurations.getCurrentConfiguration();
}
catch (VcsException e1) {
current = null;
}
return current;
}
/**
* {@inheritDoc}
*/
@Override
protected JComponent createCenterPanel() {
return myRootPanel;
}
/**
* {@inheritDoc}
*/
@Override
protected String getDimensionServiceKey() {
return getClass().getName();
}
/**
* {@inheritDoc}
*/
@Override
protected Action[] createActions() {
return new Action[]{getOKAction(), new DialogWrapperExitAction("New Configuration", NEW_CONFIGURATION_EXIT_CODE), getCancelAction()};
}
/**
* Show dialog and if ok (checkout) pressed, checkout the respective model.
*
* @param project the context project
* @param configurations the configuration service
*/
public static void showDialog(Project project, GitBranchConfigurations configurations) {
GitManageConfigurationsDialog d = new GitManageConfigurationsDialog(project, configurations);
d.show();
if (d.getExitCode() == OK_EXIT_CODE) {
configurations.startCheckout(d.getSelectedConfiguration(), null, false);
}
else if (d.getExitCode() == NEW_CONFIGURATION_EXIT_CODE) {
configurations.startCheckout(null, null, false);
}
}
/**
* The branch mapping model
*/
private class MyBranchMappingModel extends AbstractTableModel {
/**
* The root column
*/
private static final int ROOT = 0;
/**
* The reference column (commit, tag, or branch)
*/
private static final int REFERENCE = 1;
/**
* Total count of columns
*/
private static final int COLUMNS = REFERENCE + 1;
/**
* The mapping in branches first is root the second is reference
*/
final ArrayList<Pair<String, String>> myMapping = new ArrayList<Pair<String, String>>();
/**
* Base project file
*/
private final File myBaseFile;
/**
* The constructor
*/
MyBranchMappingModel() {
VirtualFile base = myProject.getBaseDir();
myBaseFile = base == null ? null : new File(base.getPath());
}
/**
* Update table model
*
* @param c the configuration to use to update table model (null means nothing to update)
*/
void set(GitBranchConfiguration c) {
myMapping.clear();
if (c != null) {
for (Map.Entry<String, String> e : c.getReferences().entrySet()) {
String root = e.getKey();
String relative = myBaseFile == null ? null : FileUtil.getRelativePath(myBaseFile, new File(root));
myMapping.add(Pair.create(relative == null ? root : relative, e.getValue()));
}
}
fireTableDataChanged();
}
/**
* {@inheritDoc}
*/
@Override
public int getRowCount() {
return myMapping.size();
}
/**
* {@inheritDoc}
*/
@Override
public int getColumnCount() {
return COLUMNS;
}
/**
* {@inheritDoc}
*/
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
Pair<String, String> d = myMapping.get(rowIndex);
switch (columnIndex) {
case ROOT:
return d.first;
case REFERENCE:
return d.second;
default:
throw new IllegalStateException("The invalid column number: " + columnIndex);
}
}
/**
* {@inheritDoc}
*/
@Override
public String getColumnName(int column) {
switch (column) {
case ROOT:
return "Vcs Root";
case REFERENCE:
return "Reference";
default:
throw new IllegalStateException("The invalid column number: " + column);
}
}
}
}
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="git4idea.checkout.branches.GitSwitchBranchesDialog">
<grid id="27dc6" binding="myRoot" layout-manager="GridLayoutManager" row-count="4" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="604" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<grid id="df0f4" binding="myChangesPanel" layout-manager="BorderLayout" hgap="0" vgap="0">
<constraints>
<grid row="1" column="1" row-span="3" col-span="1" vsize-policy="3" hsize-policy="7" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</grid>
<grid id="9e5c7" layout-manager="GridLayoutManager" row-count="3" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="0" column="0" row-span="4" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="59a7b" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<labelFor value="d3bf1"/>
<text value="Branch Configuration &amp;Name"/>
</properties>
</component>
<component id="d3bf1" class="javax.swing.JTextField" binding="myNameTextField">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties>
<toolTipText value="The branch configuration name"/>
</properties>
</component>
<component id="54c58" class="javax.swing.JLabel">
<constraints>
<grid row="1" column="0" row-span="1" col-span="2" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<labelFor value="5d3d5"/>
<text value="&amp;Branches"/>
</properties>
</component>
<scrollpane id="5d3d5" class="com.intellij.ui.components.JBScrollPane">
<constraints>
<grid row="2" column="0" row-span="1" col-span="2" vsize-policy="3" hsize-policy="7" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="84a18" class="com.intellij.ui.table.JBTable" binding="myBranchesTable">
<constraints/>
<properties/>
</component>
</children>
</scrollpane>
</children>
</grid>
<component id="9416e" class="javax.swing.JLabel" binding="myChangesLabel">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="&amp;Changes to transfer to new configuration"/>
</properties>
</component>
</children>
</grid>
</form>
@@ -0,0 +1,879 @@
/*
* Copyright 2000-2010 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.checkout.branches;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.RemoteRevisionsCache;
import com.intellij.openapi.vcs.changes.ui.ChangeNodeDecorator;
import com.intellij.openapi.vcs.changes.ui.ChangesBrowserNode;
import com.intellij.openapi.vcs.changes.ui.ChangesTreeList;
import com.intellij.openapi.vcs.changes.ui.TreeModelBuilder;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.ColoredTableCellRenderer;
import com.intellij.ui.DocumentAdapter;
import com.intellij.ui.SimpleTextAttributes;
import com.intellij.ui.table.JBTable;
import com.intellij.util.ui.AbstractTableCellEditor;
import git4idea.GitBranch;
import git4idea.GitRevisionNumber;
import git4idea.validators.GitBranchNameValidator;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.plaf.basic.BasicComboBoxRenderer;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.tree.DefaultTreeModel;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.*;
import java.util.List;
/**
* The switch branches dialog
*/
public class GitSwitchBranchesDialog extends DialogWrapper {
/**
* The branch configuration name text field
*/
private JTextField myNameTextField;
/**
* The changes panel
*/
private JPanel myChangesPanel;
/**
* The root panel
*/
private JPanel myRoot;
/**
* The branches table
*/
private JBTable myBranchesTable;
/**
* Changes to transfer to new configurations label
*/
private JLabel myChangesLabel;
/**
* The changes tree
*/
private final ChangesTreeList<Change> myChangesTree;
/**
* The project to use
*/
private final Project myProject;
/**
* The target branch configuration
*/
private final GitBranchConfiguration myTarget;
/**
* The configuration settings object
*/
private final GitBranchConfigurations myConfig;
/**
* If true, the dialog was invoked to modify the current configuration
*/
private final boolean myModify;
/**
* The list of branches to use
*/
private final List<BranchDescriptor> myBranches;
/**
* The existing configuration names (used to simplify validation)
*/
private Set<String> myExistingConfigNames;
/**
* Base project directory
*/
private File myBaseFile;
/**
* The constructor
*
* @param project the project
* @param target the target configuration
* @param allChanges the all changes
* @param roots the collection of roots
* @param remoteBranch the remote branch
* @param config the configuration
* @param isModify the modify flag
* @throws VcsException if there is a problem with detecting the current state
*/
protected GitSwitchBranchesDialog(Project project,
final GitBranchConfiguration target,
final List<Change> allChanges,
List<VirtualFile> roots,
String remoteBranch, final GitBranchConfigurations config, boolean isModify) throws VcsException {
super(project, true);
setTitle(isModify ? "Modify Branch Configuration" : "Checkout Branch Configuration");
assert (remoteBranch == null) || (target == null) : "There should be no target for remote branch";
myTarget = target;
myConfig = config;
myModify = isModify;
myProject = project;
VirtualFile baseDir = project.getBaseDir();
myBaseFile = baseDir == null ? null : new File(baseDir.getPath());
myExistingConfigNames = myConfig.getConfigurationNames();
myChangesTree = new ChangesTreeList<Change>(myProject,
Collections.<Change>emptyList(),
!myModify,
true,
null,
RemoteRevisionsCache.getInstance(project).getChangesNodeDecorator()) {
protected DefaultTreeModel buildTreeModel(final List<Change> changes, ChangeNodeDecorator changeNodeDecorator) {
TreeModelBuilder builder = new TreeModelBuilder(myProject, false);
return builder.buildModel(changes, changeNodeDecorator);
}
protected List<Change> getSelectedObjects(final ChangesBrowserNode<Change> node) {
return node.getAllChangesUnder();
}
@Nullable
protected Change getLeadSelectedObject(final ChangesBrowserNode node) {
final Object o = node.getUserObject();
if (o instanceof Change) {
return (Change)o;
}
return null;
}
};
if (remoteBranch != null) {
myBranches = prepareBranchesForRemote(remoteBranch, roots);
}
else {
myBranches = prepareBranchDescriptors(target, roots);
}
if (target == null) {
myNameTextField.setText(generateNewConfigurationName());
}
else {
myNameTextField.setText(target.getName());
}
myChangesTree.setChangesToDisplay(allChanges);
myChangesTree.setIncludedChanges(Collections.<Change>emptyList());
myChangesPanel.add(myChangesTree, BorderLayout.CENTER);
myChangesLabel.setLabelFor(myChangesTree);
if (myModify) {
myChangesLabel.setText("Changes in the current configuration");
}
RootTableModel tableModel = new RootTableModel();
myBranchesTable.setModel(tableModel);
myBranchesTable.setDefaultRenderer(Pair.class, new PairTableRenderer());
final TableColumnModel columns = myBranchesTable.getColumnModel();
final PairTableRenderer renderer = new PairTableRenderer();
for (Enumeration<TableColumn> cs = columns.getColumns(); cs.hasMoreElements();) {
cs.nextElement().setCellRenderer(renderer);
}
TableColumn revisionColumn = columns.getColumn(RootTableModel.REVISION_COLUMN);
revisionColumn.setCellEditor(new ReferenceEditor());
TableColumn branchColumn = columns.getColumn(RootTableModel.NEW_BRANCH_COLUMN);
branchColumn.setCellEditor(new BranchNameEditor());
myNameTextField.getDocument().addDocumentListener(new DocumentAdapter() {
@Override
protected void textChanged(DocumentEvent e) {
verify();
}
});
tableModel.addTableModelListener(new TableModelListener() {
@Override
public void tableChanged(TableModelEvent e) {
verify();
}
});
init();
}
/**
* The show configuration dialog
*
* @param project the project to use
* @param target the target configuration
* @param allChanges the collection of changes
* @param roots the vcs roots
* @param remoteBranch the remote branch
* @param config the configuration
* @param isModify the modify mode flag
* @return the pair of selected changes and
* @throws VcsException if there is a problem with accessing git
*/
@Nullable
public static Result showDialog(Project project,
@Nullable GitBranchConfiguration target,
final List<Change> allChanges,
List<VirtualFile> roots,
@Nullable String remoteBranch,
final GitBranchConfigurations config,
boolean isModify) throws VcsException {
GitSwitchBranchesDialog d = new GitSwitchBranchesDialog(project, target, allChanges, roots, remoteBranch, config, isModify);
d.show();
if (d.isOK()) {
return d.createResult();
}
else {
return null;
}
}
/**
* @return create dialog result object basing on the dialog state
*/
private Result createResult() {
Result rc = new Result();
String name = myNameTextField.getText().trim();
if (myTarget == null) {
rc.target = myConfig.createConfiguration(name);
}
else {
rc.target = myTarget;
rc.target.setName(name);
}
rc.changes = new ArrayList<Change>(myChangesTree.getIncludedChanges());
for (BranchDescriptor d : myBranches) {
if (d.root != null) {
if (!StringUtil.isEmpty(d.newBranchName)) {
rc.referencesToUse.put(d.root, d.referenceToCheckout.trim());
rc.target.setBranch(d.root.getPath(), d.newBranchName.trim());
rc.checkoutNeeded.add(d.root);
}
else {
rc.target.setBranch(d.root.getPath(), d.referenceToCheckout.trim());
if (!d.referenceToCheckout.equals(d.currentReference)) {
rc.checkoutNeeded.add(d.root);
}
}
}
}
return rc;
}
/**
* Verify dialog state
*/
private void verify() {
String text = myNameTextField.getText().trim();
if (text.length() == 0) {
setError("Empty configuration name is not allowed.");
return;
}
else if (myTarget != null && text.equals(myTarget.getName())) {
}
else if (myExistingConfigNames.contains(text)) {
setError("There is another configuration with the same name");
return;
}
for (BranchDescriptor d : myBranches) {
switch (d.status) {
case BRANCH_NAME_EXISTS:
setError("Duplicate branch name for root " + d.getRoot());
return;
case INVALID_BRANCH_NAME:
setError("Invalid branch name for root " + d.getRoot());
return;
case BAD_REVISION:
setError("Invalid revision for root " + d.getRoot());
return;
case MISSING_REVISION:
setError("The revision must be specified for root " + d.getRoot());
return;
case CHECKOUT_NEEDED:
case NO_ACTION:
case REMOVED_ROOT:
break;
default:
throw new RuntimeException("Unexpected status: " + d.status);
}
}
setError(null);
}
private void setError(String s) {
setErrorText(s);
setOKActionEnabled(s == null);
}
/**
* Generate new configuration name basing on descriptor
*
* @return the generated configuration name
*/
private String generateNewConfigurationName() {
String name = null;
for (BranchDescriptor d : myBranches) {
if (d.newBranchName != null) {
name = d.newBranchName;
break;
}
if (d.existingBranches.contains(d.currentReference)) {
name = d.currentReference;
}
}
if (name == null) {
name = "Unnamed";
}
if (myExistingConfigNames.contains(name)) {
for (int i = 2; i < Integer.MAX_VALUE; i++) {
String t = name + " " + i;
if (!myExistingConfigNames.contains(t)) {
name = t;
break;
}
}
}
return name;
}
/**
* Prepare branches for the case of remote checkout
*
* @param remoteBranch the remote branch to checkout
* @param roots the collection of vcs roots
* @return the list of descriptors for the remote
* @throws VcsException if git failed
*/
private List<BranchDescriptor> prepareBranchesForRemote(String remoteBranch, List<VirtualFile> roots)
throws VcsException {
assert roots.size() > 0;
List<BranchDescriptor> rc = new ArrayList<BranchDescriptor>();
HashSet<String> allBranches = new HashSet<String>();
for (VirtualFile root : roots) {
BranchDescriptor d = new BranchDescriptor();
d.root = root;
d.currentReference = myConfig.describeRoot(root);
d.referenceToCheckout = "remotes/" + remoteBranch;
GitBranch.listAsStrings(myProject, root, false, true, d.existingBranches, null);
GitBranch.listAsStrings(myProject, root, true, true, d.referencesToSelect, null);
allBranches.addAll(d.existingBranches);
rc.add(d);
}
int p = remoteBranch.indexOf('/');
assert p > 0 && p < remoteBranch.length() - 1 : "Unexpected format for remote branch: " + remoteBranch;
String candidate = remoteBranch.substring(p + 1);
String actual = null;
if (!allBranches.contains(candidate)) {
actual = candidate;
}
else {
for (int i = 2; i < Integer.MAX_VALUE; i++) {
String t = candidate + i;
if (!allBranches.contains(t)) {
actual = t;
break;
}
}
assert actual != null : "Unexpected number of branches: " + remoteBranch;
}
for (BranchDescriptor d : rc) {
d.newBranchName = actual;
d.updateStatus();
}
return rc;
}
/**
* Prepare branch descriptors for existing configuration
*
* @param target the target
* @param roots the vcs root
* @return the list of branch descriptors
* @throws VcsException in case of git error
*/
private List<BranchDescriptor> prepareBranchDescriptors(
GitBranchConfiguration target,
List<VirtualFile> roots)
throws VcsException {
Map<String, String> map = target == null ? Collections.<String, String>emptyMap() : target.getReferences();
List<BranchDescriptor> rc = new ArrayList<BranchDescriptor>();
for (VirtualFile root : roots) {
BranchDescriptor d = new BranchDescriptor();
d.root = root;
d.storedReference = map.remove(root.getPath());
if (d.storedReference != null) {
d.storedRoot = d.root.getPath();
}
d.currentReference = myConfig.describeRoot(root);
if (d.storedReference != null && !myModify) {
d.referenceToCheckout = d.storedReference;
}
else {
d.referenceToCheckout = d.currentReference;
}
GitBranch.listAsStrings(myProject, root, false, true, d.existingBranches, null);
GitBranch.listAsStrings(myProject, root, true, true, d.referencesToSelect, null);
d.updateStatus();
rc.add(d);
}
for (Map.Entry<String, String> m : map.entrySet()) {
String root = m.getKey();
String ref = m.getValue();
BranchDescriptor d = new BranchDescriptor();
d.storedReference = ref;
d.storedRoot = root;
d.referenceToCheckout = ref;
d.updateStatus();
rc.add(d);
}
return rc;
}
@Override
protected JComponent createCenterPanel() {
return myRoot;
}
/**
* The table model that displays mapping for the vcs roots
*/
class RootTableModel extends AbstractTableModel {
/**
* The vcs root
*/
static final int ROOT_COLUMN = 0;
/**
* The revision
*/
static final int REVISION_COLUMN = 1;
/**
* The name of branch to checkout
*/
static final int NEW_BRANCH_COLUMN = 2;
/**
* The status
*/
static final int STATUS_COLUMN = 3;
/**
* The total number of columns
*/
static final int COLUMNS = STATUS_COLUMN + 1;
@Override
public int getRowCount() {
return myBranches.size();
}
@Override
public int getColumnCount() {
return COLUMNS;
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
BranchDescriptor d = myBranches.get(rowIndex);
if (d.root == null) {
return false;
}
return columnIndex == REVISION_COLUMN || columnIndex == NEW_BRANCH_COLUMN;
}
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
String t = (String)aValue;
BranchDescriptor d = myBranches.get(rowIndex);
if (d.root == null) {
return;
}
if (columnIndex == REVISION_COLUMN) {
d.referenceToCheckout = t;
String remotesPrefix = "remotes/";
if (StringUtil.isEmpty(d.newBranchName) && t.startsWith(remotesPrefix) && d.referencesToSelect.contains(t)) {
int p = t.indexOf(t.indexOf('/'), remotesPrefix.length() + 1);
if (p != -1) {
String c = t.substring(p);
if (!d.existingBranches.contains(c)) {
d.newBranchName = c;
}
else {
for (int i = 2; i < Integer.MAX_VALUE; i++) {
String candidate = c + i;
if (!d.existingBranches.contains(c)) {
d.newBranchName = candidate;
break;
}
}
}
}
}
}
else if (columnIndex == NEW_BRANCH_COLUMN) {
d.newBranchName = t;
}
d.updateStatus();
fireTableRowsUpdated(rowIndex, rowIndex);
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
BranchDescriptor d = myBranches.get(rowIndex);
switch (columnIndex) {
case ROOT_COLUMN:
return Pair.create(d.getRoot(), d.root != null);
case REVISION_COLUMN:
return Pair.create(d.referenceToCheckout, d.isReferenceValid);
case NEW_BRANCH_COLUMN:
return Pair.create(d.newBranchName == null ? "" : d.newBranchName, d.isNewBranchValid);
case STATUS_COLUMN:
switch (d.status) {
case INVALID_BRANCH_NAME:
return Pair.create("Invalid new branch name", false);
case BAD_REVISION:
return Pair.create("Invalid revision", false);
case MISSING_REVISION:
return Pair.create("Missing revision", false);
case CHECKOUT_NEEDED:
return Pair.create("Checkout", true);
case REMOVED_ROOT:
return Pair.create("Removed root", true);
case NO_ACTION:
return Pair.create("", true);
case BRANCH_NAME_EXISTS:
return Pair.create("Branch name exists", false);
default:
throw new IllegalStateException("Unknown status:" + d.status);
}
default:
throw new IllegalStateException("Unknown column: " + columnIndex);
}
}
@Override
public String getColumnName(int column) {
switch (column) {
case ROOT_COLUMN:
return "Vcs Root";
case REVISION_COLUMN:
return "Checkout";
case NEW_BRANCH_COLUMN:
return "As New Branch";
case STATUS_COLUMN:
return "Status";
default:
throw new IllegalStateException("Unknown column: " + column);
}
}
}
/**
* The object representing row entry
*/
class BranchDescriptor {
/**
* The root to checkout, if null means that the old root is missing.
*/
VirtualFile root;
/**
* Stored root path
*/
String storedRoot;
/**
* Stored reference
*/
String storedReference;
/**
* The commit expression to checkout
*/
String referenceToCheckout;
/**
* if true, branch name is valid
*/
boolean isReferenceValid;
/**
* True if commit expression is valid
*/
RootStatus status;
/**
* The name of of branch
*/
String newBranchName;
/**
* if true, branch name is valid
*/
boolean isNewBranchValid;
/**
* The current type
*/
String currentReference;
/**
* The existing branches
*/
HashSet<String> existingBranches = new HashSet<String>();
/**
* The existing branches
*/
TreeSet<String> referencesToSelect = new TreeSet<String>();
/**
* Update status of the entry
*/
void updateStatus() {
if (root == null) {
status = RootStatus.REMOVED_ROOT;
return;
}
status = branchNameStatus(newBranchName);
isNewBranchValid = status == null;
isReferenceValid = true;
if (referenceToCheckout == null) {
status = RootStatus.MISSING_REVISION;
isReferenceValid = false;
return;
}
try {
GitRevisionNumber.resolve(myProject, root, referenceToCheckout);
if (status == null) {
status = StringUtil.isEmpty(newBranchName) && currentReference.equals(storedReference)
? RootStatus.NO_ACTION
: RootStatus.CHECKOUT_NEEDED;
}
}
catch (VcsException e) {
isReferenceValid = false;
status = RootStatus.BAD_REVISION;
}
}
/**
* Get branch name status
*
* @param name the name to check
* @return null if branch name is ok, or status describing the problem
*/
@Nullable
private RootStatus branchNameStatus(final String name) {
RootStatus b = null;
if (!StringUtil.isEmpty(name)) {
if (!GitBranchNameValidator.INSTANCE.checkInput(name)) {
b = RootStatus.INVALID_BRANCH_NAME;
}
if (existingBranches.contains(name)) {
b = RootStatus.BRANCH_NAME_EXISTS;
}
}
return b;
}
public String getRoot() {
String path = root == null ? storedRoot : root.getPath();
String relative = myBaseFile == null ? path : FileUtil.getRelativePath(myBaseFile, new File(path));
return relative == null ? path : relative;
}
}
/**
* The root status
*/
enum RootStatus {
/**
* The checkout is needed for the vcs root. Non-error status.
*/
CHECKOUT_NEEDED,
/**
* No action needed for vcs root. Non-error status.
*/
NO_ACTION,
/**
* The branch information does not represents a vcs root anymore. Non-error status.
*/
REMOVED_ROOT,
/**
* The bad revision expression
*/
MISSING_REVISION,
/**
* The revision expression is invalid or could not be evaluated
*/
BAD_REVISION,
/**
* The bad name for the branch
*/
INVALID_BRANCH_NAME,
/**
* The bad name for the branch
*/
BRANCH_NAME_EXISTS
}
/**
* Pair text renderer
*/
static class PairTableRenderer extends ColoredTableCellRenderer {
@Override
protected void customizeCellRenderer(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) {
@SuppressWarnings({"unchecked"}) Pair<String, Boolean> p = (Pair<String, Boolean>)value;
String t = p.first == null ? "" : p.first;
if (p.second) {
append(t);
}
else {
append(t, SimpleTextAttributes.ERROR_ATTRIBUTES);
}
}
}
/**
* The editor for references
*/
class ReferenceEditor extends AbstractTableCellEditor {
/**
* The root panel
*/
private final JPanel myPanel = new JPanel(new GridBagLayout());
/**
* Combobox for the panel
*/
private final JComboBox myComboBox = new JComboBox();
/**
* The constructor
*/
private ReferenceEditor() {
myComboBox.setEditable(true);
myComboBox.setRenderer(new BasicComboBoxRenderer());
myComboBox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
stopCellEditing();
}
});
myPanel.add(myComboBox,
new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0),
0,
0));
}
/**
* {@inheritDoc}
*/
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
BranchDescriptor d = myBranches.get(row);
myComboBox.removeAllItems();
for (String s : d.referencesToSelect) {
myComboBox.addItem(s);
}
myComboBox.setSelectedItem(d.referenceToCheckout);
return myPanel;
}
/**
* {@inheritDoc}
*/
public Object getCellEditorValue() {
return myComboBox.getSelectedItem();
}
}
/**
* The editor for branch names
*/
class BranchNameEditor extends AbstractTableCellEditor {
/**
* The root panel
*/
private final JPanel myPanel = new JPanel(new GridBagLayout());
/**
* Combobox for the panel
*/
private final JTextField myTextField = new JTextField();
/**
* The values that considered invalid
*/
private Set<String> myInvalidValues;
/**
* Default foregorund color (likely black one)
*/
private Color myDefaultForeground;
/**
* The constructor
*/
private BranchNameEditor() {
myDefaultForeground = myTextField.getForeground();
myTextField.getDocument().addDocumentListener(new DocumentAdapter() {
@Override
protected void textChanged(DocumentEvent e) {
String s = myTextField.getText();
if (s.length() == 0 &&
(myInvalidValues == null || !myInvalidValues.contains(s)) &&
(s.length() == 0 || GitBranchNameValidator.INSTANCE.checkInput(s))) {
myTextField.setForeground(myDefaultForeground);
}
else {
myTextField.setForeground(Color.RED);
}
}
});
myPanel.add(myTextField,
new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0),
0,
0));
}
/**
* {@inheritDoc}
*/
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
BranchDescriptor d = myBranches.get(row);
myInvalidValues = d.existingBranches;
myTextField.setText(d.newBranchName == null ? "" : d.newBranchName);
return myPanel;
}
/**
* {@inheritDoc}
*/
@Nullable
public Object getCellEditorValue() {
String s = myTextField.getText().trim();
return s.length() == 0 ? null : s;
}
}
/**
* The result of the dialog
*/
public static class Result {
/**
* The roots for which checkout is needed
*/
Collection<VirtualFile> checkoutNeeded = new ArrayList<VirtualFile>();
/**
* The set of selected changes to transfer to new configuration
*/
List<Change> changes;
/**
* References to use for new branches
*/
HashMap<VirtualFile, String> referencesToUse = new HashMap<VirtualFile, String>();
/**
* The target configuration
*/
GitBranchConfiguration target;
}
}
@@ -137,6 +137,10 @@ public class GitCommand {
* Check attributes command
*/
public static final GitCommand CHECK_ATTR = read("check-attr");
/**
* The constant for git command
*/
public static final GitCommand DESCRIBE = meta("describe");
/**
* Name of environment variable that specifies editor for the git
*/
@@ -251,7 +255,8 @@ public class GitCommand {
/**
* Metadata read/write command
*/
META, }
META,
}
/**
* Thread policy for command
@@ -17,6 +17,7 @@ package git4idea.ui;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.vcs.AbstractVcsHelper;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vfs.VirtualFile;
import git4idea.GitBranch;
@@ -218,6 +219,17 @@ public class GitUIUtil {
Messages.showErrorDialog(project, message, GitBundle.message("error.occurred.during", operation));
}
/**
* Show errors on the tab
*
* @param project the context project
* @param title the operation title
* @param errors the errors to display
*/
public static void showTabErrors(Project project, String title, List<VcsException> errors) {
AbstractVcsHelper.getInstance(project).showErrors(errors, title);
}
/**
* Setup remotes combobox. The default remote for the current branch is selected by default.
* This method gets current branch for the project.