diff --git a/plugins/git4idea/src/META-INF/plugin.xml b/plugins/git4idea/src/META-INF/plugin.xml index c2c4a73b4463..400291ee60bc 100644 --- a/plugins/git4idea/src/META-INF/plugin.xml +++ b/plugins/git4idea/src/META-INF/plugin.xml @@ -104,6 +104,9 @@ + diff --git a/plugins/git4idea/src/git4idea/GitUtil.java b/plugins/git4idea/src/git4idea/GitUtil.java index a80573a5ef53..1be7586ac1c9 100644 --- a/plugins/git4idea/src/git4idea/GitUtil.java +++ b/plugins/git4idea/src/git4idea/GitUtil.java @@ -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 * diff --git a/plugins/git4idea/src/git4idea/GitVcs.java b/plugins/git4idea/src/git4idea/GitVcs.java index b286f4240811..e4fffdff6da5 100644 --- a/plugins/git4idea/src/git4idea/GitVcs.java +++ b/plugins/git4idea/src/git4idea/GitVcs.java @@ -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 { myReferenceTracker.activate(); GitUsersComponent.getInstance(myProject).activate(); GitProjectLogManager.getInstance(myProject).activate(); + GitBranchConfigurations.getInstance(myProject).activate(); } /** @@ -517,6 +519,7 @@ public class GitVcs extends AbstractVcs { */ @Override protected void deactivate() { + GitBranchConfigurations.getInstance(myProject).deactivate(); if (myRootTracker != null) { myRootTracker.dispose(); myRootTracker = null; diff --git a/plugins/git4idea/src/git4idea/checkout/branches/GitBranchConfiguration.java b/plugins/git4idea/src/git4idea/checkout/branches/GitBranchConfiguration.java new file mode 100644 index 000000000000..b6ac0cda1cec --- /dev/null +++ b/plugins/git4idea/src/git4idea/checkout/branches/GitBranchConfiguration.java @@ -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 myReferences = new HashMap(); + /** + * 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 getReferences() { + synchronized (myConfig.getStateLock()) { + return new HashMap(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 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; + } + } +} diff --git a/plugins/git4idea/src/git4idea/checkout/branches/GitBranchConfigurationChangedDialog.form b/plugins/git4idea/src/git4idea/checkout/branches/GitBranchConfigurationChangedDialog.form new file mode 100644 index 000000000000..3ea33f0a08d7 --- /dev/null +++ b/plugins/git4idea/src/git4idea/checkout/branches/GitBranchConfigurationChangedDialog.form @@ -0,0 +1,51 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/plugins/git4idea/src/git4idea/checkout/branches/GitBranchConfigurationChangedDialog.java b/plugins/git4idea/src/git4idea/checkout/branches/GitBranchConfigurationChangedDialog.java new file mode 100644 index 000000000000..717ae4931284 --- /dev/null +++ b/plugins/git4idea/src/git4idea/checkout/branches/GitBranchConfigurationChangedDialog.java @@ -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 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 branches, final Set 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 roots) + throws VcsException { + final Set names = settings.getConfigurationNames(); + final Ref name = new Ref(); + final Ref code = new Ref(); + final List 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 prepareBranchDescriptors(GitBranchConfigurations settings, + GitBranchConfiguration config, + List roots) + throws VcsException { + List rc = new ArrayList(); + Map 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 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; + } + } +} diff --git a/plugins/git4idea/src/git4idea/checkout/branches/GitBranchConfigurations.java b/plugins/git4idea/src/git4idea/checkout/branches/GitBranchConfigurations.java new file mode 100644 index 000000000000..35cdcb832a12 --- /dev/null +++ b/plugins/git4idea/src/git4idea/checkout/branches/GitBranchConfigurations.java @@ -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, Disposable { + /** + * The logger + */ + private static final Logger LOG = Logger.getInstance(GitBranchConfigurations.class.getName()); + /** + * The comparator for branch configuration by name + */ + private static final Comparator CONFIGURATION_COMPARATOR = new Comparator() { + @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 BRANCH_INFO_COMPARATOR = new Comparator() { + @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 myConfigurations = new HashMap(); + /** + * Create event dispatcher for configuration events + */ + private final EventDispatcher 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 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 changes, ChangeList fromList) { + GitBranchConfigurations.this.referencesChanged(); + } + + @Override + public void changesAdded(Collection 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 cs = new ArrayList(myConfigurations.size()); + for (GitBranchConfiguration ci : myConfigurations.values()) { + BranchConfiguration c = new BranchConfiguration(); + c.NAME = ci.getName(); + Map map = ci.getReferences(); + ArrayList bs = new ArrayList(map.size()); + for (Map.Entry 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 getRemotesCandidates() { + try { + final List 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 currents = new HashMap(); + for (VirtualFile root : myGitRoots) { + GitBranch current = GitBranch.current(myProject, root); + currents.put(root, current == null ? "" : current.getName()); + } + if (myConfigurations.isEmpty()) { + List 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 detectConfigurations(boolean local, final List roots) throws VcsException { + HashSet all = new HashSet(); + HashSet forRoot = new HashSet(); + 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 rc = new ArrayList(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 getConfigurationNames() { + synchronized (myStateLock) { + return new HashSet(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 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 + } +} diff --git a/plugins/git4idea/src/git4idea/checkout/branches/GitBranchConfigurationsListener.java b/plugins/git4idea/src/git4idea/checkout/branches/GitBranchConfigurationsListener.java new file mode 100644 index 000000000000..4014c2f049ad --- /dev/null +++ b/plugins/git4idea/src/git4idea/checkout/branches/GitBranchConfigurationsListener.java @@ -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(); +} diff --git a/plugins/git4idea/src/git4idea/checkout/branches/GitBranchesWidget.java b/plugins/git4idea/src/git4idea/checkout/branches/GitBranchesWidget.java new file mode 100644 index 000000000000..ca47ea25fc41 --- /dev/null +++ b/plugins/git4idea/src/git4idea/checkout/branches/GitBranchesWidget.java @@ -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 widget = new Ref(); + 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 rc = new ArrayList(); + 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 rc = new ArrayList(); + + 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 exceptions = new ArrayList(); + try { + for (VirtualFile root : GitUtil.getGitRoots(myProject, GitVcs.getInstance(myProject))) { + GitLineHandler h = new GitLineHandler(myProject, root, GitCommand.FETCH); + h.addParameters("--all", "-v"); + final Collection 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(); + } + }); + } + }); + } + } + +} diff --git a/plugins/git4idea/src/git4idea/checkout/branches/GitCheckoutProcess.java b/plugins/git4idea/src/git4idea/checkout/branches/GitCheckoutProcess.java new file mode 100644 index 000000000000..ee3f381920c9 --- /dev/null +++ b/plugins/git4idea/src/git4idea/checkout/branches/GitCheckoutProcess.java @@ -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 myRoots; + /** + * The vcs exceptions + */ + private List 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 myNewBranchMapping = Collections.emptyMap(); + /** + * The described roots + */ + private final Map myDescribedRoots = new HashMap(); + /** + * 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()); + 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 changes = collectChanges(); + Collection 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 checkoutRoots = rootsToCheckout(); + if (checkoutRoots.size() > 0) { + // TODO disable saving + myProgress.setText("Shelving changes..."); + Pair 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 startedRoots = new HashSet(); + 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 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 exceptions = GitHandlerUtil.doSynchronouslyWithExceptions(h, myProgress, h.printableCommandLine()); + myExceptions.addAll(exceptions); + } + // filter out implicitly included roots + ArrayList newRoots = new ArrayList(); + 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 checkoutRoots, HashSet startedRoots) throws VcsException { + boolean failed = false; + myProgress.setText("Checking out..."); + HashSet filesToRefresh = new HashSet(); + 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 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 rootsToCheckout() { + ArrayList rc = new ArrayList(); + 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 selectChangesToTransfer(final List changes) { + final Ref t = new Ref(); + 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 roots = new HashSet(myRoots); + Map branches = myNewConfiguration.getReferences(); + for (Map.Entry 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 collectChanges() { + List changeLists = myChangeManager.getChangeLists(); + ArrayList changes = new ArrayList(); + 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 branches = c.getReferences(); + if (branches.size() != myRoots.size()) { + return false; + } + HashSet realSet = new HashSet(); + for (VirtualFile root : myRoots) { + realSet.add(root.getPath()); + } + HashSet storedSet = new HashSet(); + for (Map.Entry 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, String> parsedChanges = new HashMap, 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 lists = new HashMap(); + 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 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 shelveChanges(ProgressIndicator progress, + String configurationName, + Collection selectedChanges) { + assert myExceptions.isEmpty() : "The method should not be called if there is already problems detected"; + List changeLists = myChangeManager.getChangeListsCopy(); + HashMap changes = new HashMap(); + for (LocalChangeList l : changeLists) { + for (Change change : l.getChanges()) { + changes.put(change, l); + } + } + HashSet selected = new HashSet(selectedChanges); + HashSet other = new HashSet(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 changes, + HashSet toShelve, String description + ) { + if (toShelve.isEmpty()) { + return null; + } + HashSet lists = new HashSet(); + ArrayList ci = new ArrayList(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 li = new ArrayList(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 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; + } +} diff --git a/plugins/git4idea/src/git4idea/checkout/branches/GitManageConfigurationsDialog.form b/plugins/git4idea/src/git4idea/checkout/branches/GitManageConfigurationsDialog.form new file mode 100644 index 000000000000..16bcbe702c4b --- /dev/null +++ b/plugins/git4idea/src/git4idea/checkout/branches/GitManageConfigurationsDialog.form @@ -0,0 +1,121 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/plugins/git4idea/src/git4idea/checkout/branches/GitManageConfigurationsDialog.java b/plugins/git4idea/src/git4idea/checkout/branches/GitManageConfigurationsDialog.java new file mode 100644 index 000000000000..4103b5a826de --- /dev/null +++ b/plugins/git4idea/src/git4idea/checkout/branches/GitManageConfigurationsDialog.java @@ -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. + *

+ * 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 myShelveNames = new HashMap(); + /** + * 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("No associated shelve"); + myShelveNameLabel.setToolTipText("This configuration has no associated shelve"); + } + else { + String d = myShelveNames.get(ch.SHELVE_PATH); + myShelveNameLabel.setText(d); + myShelveNameLabel.setToolTipText(""); + } + } + 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> myMapping = new ArrayList>(); + /** + * 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 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 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); + } + } + } +} diff --git a/plugins/git4idea/src/git4idea/checkout/branches/GitSwitchBranchesDialog.form b/plugins/git4idea/src/git4idea/checkout/branches/GitSwitchBranchesDialog.form new file mode 100644 index 000000000000..c4e5ddc02666 --- /dev/null +++ b/plugins/git4idea/src/git4idea/checkout/branches/GitSwitchBranchesDialog.form @@ -0,0 +1,80 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/git4idea/src/git4idea/checkout/branches/GitSwitchBranchesDialog.java b/plugins/git4idea/src/git4idea/checkout/branches/GitSwitchBranchesDialog.java new file mode 100644 index 000000000000..b7e2965e8596 --- /dev/null +++ b/plugins/git4idea/src/git4idea/checkout/branches/GitSwitchBranchesDialog.java @@ -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 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 myBranches; + /** + * The existing configuration names (used to simplify validation) + */ + private Set 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 allChanges, + List 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(myProject, + Collections.emptyList(), + !myModify, + true, + null, + RemoteRevisionsCache.getInstance(project).getChangesNodeDecorator()) { + protected DefaultTreeModel buildTreeModel(final List changes, ChangeNodeDecorator changeNodeDecorator) { + TreeModelBuilder builder = new TreeModelBuilder(myProject, false); + return builder.buildModel(changes, changeNodeDecorator); + } + + protected List getSelectedObjects(final ChangesBrowserNode 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.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 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 allChanges, + List 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(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 prepareBranchesForRemote(String remoteBranch, List roots) + throws VcsException { + assert roots.size() > 0; + List rc = new ArrayList(); + HashSet allBranches = new HashSet(); + 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 prepareBranchDescriptors( + GitBranchConfiguration target, + List roots) + throws VcsException { + Map map = target == null ? Collections.emptyMap() : target.getReferences(); + List rc = new ArrayList(); + 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 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 existingBranches = new HashSet(); + /** + * The existing branches + */ + TreeSet referencesToSelect = new TreeSet(); + + /** + * 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 p = (Pair)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 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 checkoutNeeded = new ArrayList(); + /** + * The set of selected changes to transfer to new configuration + */ + List changes; + /** + * References to use for new branches + */ + HashMap referencesToUse = new HashMap(); + /** + * The target configuration + */ + GitBranchConfiguration target; + } +} diff --git a/plugins/git4idea/src/git4idea/commands/GitCommand.java b/plugins/git4idea/src/git4idea/commands/GitCommand.java index d2792dc801ed..9082aaedaf66 100644 --- a/plugins/git4idea/src/git4idea/commands/GitCommand.java +++ b/plugins/git4idea/src/git4idea/commands/GitCommand.java @@ -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 diff --git a/plugins/git4idea/src/git4idea/ui/GitUIUtil.java b/plugins/git4idea/src/git4idea/ui/GitUIUtil.java index b91240bbd0c9..b12a98295cd5 100644 --- a/plugins/git4idea/src/git4idea/ui/GitUIUtil.java +++ b/plugins/git4idea/src/git4idea/ui/GitUIUtil.java @@ -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 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.
Shelve Path:" + + StringUtil.escapeXml(ch.SHELVE_PATH) + + "
Shelve Path:" + + StringUtil.escapeXml(d) + + "