2 * Copyright 2000-2011 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package git4idea.push;
18 import com.intellij.openapi.application.ApplicationManager;
19 import com.intellij.openapi.diagnostic.Logger;
20 import com.intellij.openapi.progress.EmptyProgressIndicator;
21 import com.intellij.openapi.project.Project;
22 import com.intellij.openapi.ui.DialogWrapper;
23 import com.intellij.openapi.util.Pair;
24 import com.intellij.openapi.vcs.VcsException;
25 import com.intellij.ui.components.JBLoadingPanel;
26 import com.intellij.util.Consumer;
27 import com.intellij.util.ui.UIUtil;
28 import git4idea.GitBranch;
29 import git4idea.GitUtil;
30 import git4idea.repo.GitRemote;
31 import git4idea.repo.GitRepository;
32 import git4idea.repo.GitRepositoryManager;
33 import org.jetbrains.annotations.NotNull;
34 import org.jetbrains.annotations.Nullable;
38 import java.util.Collection;
39 import java.util.HashMap;
41 import java.util.concurrent.atomic.AtomicReference;
44 * @author Kirill Likhodedov
46 public class GitPushDialog extends DialogWrapper {
48 private static final Logger LOG = Logger.getInstance(GitPushDialog.class);
49 private static final String DEFAULT_REMOTE = "origin";
51 private Project myProject;
52 private final GitPusher myPusher;
53 private final GitPushLog myListPanel;
54 private GitCommitsByRepoAndBranch myGitCommitsToPush;
55 private Map<GitRepository, GitPushSpec> myPushSpecs;
56 private final Collection<GitRepository> myRepositories;
57 private final JBLoadingPanel myLoadingPanel;
58 private final Object COMMITS_LOADING_LOCK = new Object();
59 private final GitManualPushToBranch myRefspecPanel;
60 private final AtomicReference<String> myDestBranchInfoOnRefresh = new AtomicReference<String>();
62 private final boolean myPushPossible;
64 public GitPushDialog(@NotNull Project project) {
67 myPusher = new GitPusher(myProject, new EmptyProgressIndicator());
69 myRepositories = GitRepositoryManager.getInstance(myProject).getRepositories();
71 myLoadingPanel = new JBLoadingPanel(new BorderLayout(), this.getDisposable());
73 myListPanel = new GitPushLog(myProject, myRepositories, new RepositoryCheckboxListener());
74 myRefspecPanel = new GitManualPushToBranch(myRepositories, new RefreshButtonListener());
76 if (GitManualPushToBranch.getRemotesWithCommonNames(myRepositories).isEmpty()) {
77 myRefspecPanel.setVisible(false);
78 setErrorText("Can't push, because no remotes are defined");
79 setOKActionEnabled(false);
80 myPushPossible = false;
82 myPushPossible = true;
86 setOKButtonText("Push");
91 protected JComponent createCenterPanel() {
92 JPanel optionsPanel = new JPanel(new BorderLayout());
93 optionsPanel.add(myRefspecPanel);
95 JComponent rootPanel = new JPanel(new BorderLayout(0, 15));
96 rootPanel.add(createCommitListPanel(), BorderLayout.CENTER);
97 rootPanel.add(optionsPanel, BorderLayout.SOUTH);
102 private JComponent createCommitListPanel() {
103 myLoadingPanel.add(myListPanel, BorderLayout.CENTER);
104 if (myPushPossible) {
105 loadCommitsInBackground();
107 myLoadingPanel.startLoading();
108 myLoadingPanel.stopLoading();
111 JPanel commitListPanel = new JPanel(new BorderLayout());
112 commitListPanel.add(myLoadingPanel, BorderLayout.CENTER);
113 return commitListPanel;
116 private void loadCommitsInBackground() {
117 myLoadingPanel.startLoading();
119 ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
121 final AtomicReference<String> error = new AtomicReference<String>();
122 synchronized (COMMITS_LOADING_LOCK) {
123 error.set(collectInfoToPush());
126 final Pair<String, String> remoteAndBranch = getRemoteAndTrackedBranchForCurrentBranch();
127 UIUtil.invokeLaterIfNeeded(new Runnable() {
130 if (error.get() != null) {
131 myListPanel.displayError(error.get());
133 myListPanel.setCommits(myGitCommitsToPush);
135 if (!myRefspecPanel.turnedOn()) {
136 myRefspecPanel.selectRemote(remoteAndBranch.getFirst());
137 myRefspecPanel.setBranchToPushIfNotSet(remoteAndBranch.getSecond());
139 myLoadingPanel.stopLoading();
147 private Pair<String, String> getRemoteAndTrackedBranchForCurrentBranch() {
148 if (myGitCommitsToPush != null) {
149 Collection<GitRepository> repositories = myGitCommitsToPush.getRepositories();
150 if (!repositories.isEmpty()) {
151 GitRepository repository = repositories.iterator().next();
152 GitBranch currentBranch = repository.getCurrentBranch();
153 assert currentBranch != null;
154 if (myGitCommitsToPush.get(repository).get(currentBranch).getDestBranch() == GitPusher.NO_TARGET_BRANCH) { // push to branch with the same name
155 return Pair.create(DEFAULT_REMOTE, currentBranch.getName());
159 remoteName = currentBranch.getTrackedRemoteName(myProject, repository.getRoot());
160 if (remoteName == null) {
161 remoteName = DEFAULT_REMOTE;
164 catch (VcsException e) {
165 LOG.info("Couldn't retrieve tracked branch for current branch " + currentBranch, e);
166 remoteName = DEFAULT_REMOTE;
168 String targetBranch = getNameWithoutRemote(myGitCommitsToPush.get(repository).get(currentBranch).getDestBranch(), remoteName);
169 return Pair.create(remoteName, targetBranch);
172 return Pair.create(DEFAULT_REMOTE, "");
176 private static String getNameWithoutRemote(@NotNull GitBranch remoteBranch, @NotNull String remoteName) {
178 String branchName = remoteBranch.getName();
179 if (branchName.startsWith(remoteName)) {
180 return branchName.substring(remoteName.length());
183 // we are taking the current branch of the first repository
184 // it is possible (though unlikely), that this branch has other remote than the common remote selected in the refspec panel
185 // then we return the full branch name.
186 // the push won't work absolutely correct, if the remote doesn't have this branch, but it is not our problem in the case of
187 // several repositories with different remotes sets and different branches.
188 return remoteBranch.getFullName();
193 private String collectInfoToPush() {
195 myPushSpecs = pushSpecsForCurrentOrEnteredBranches();
196 myGitCommitsToPush = myPusher.collectCommitsToPush(myPushSpecs);
199 catch (VcsException e) {
200 myGitCommitsToPush = GitCommitsByRepoAndBranch.empty();
201 LOG.error("Couldn't collect commits to push. Push spec: " + myPushSpecs, e);
202 return e.getMessage();
206 private Map<GitRepository, GitPushSpec> pushSpecsForCurrentOrEnteredBranches() throws VcsException {
207 Map<GitRepository, GitPushSpec> defaultSpecs = new HashMap<GitRepository, GitPushSpec>();
208 for (GitRepository repository : myRepositories) {
209 GitBranch currentBranch = repository.getCurrentBranch();
210 if (currentBranch == null) {
213 String remoteName = currentBranch.getTrackedRemoteName(repository.getProject(), repository.getRoot());
214 String trackedBranchName = currentBranch.getTrackedBranchName(repository.getProject(), repository.getRoot());
215 GitRemote remote = GitUtil.findRemoteByName(repository, remoteName);
216 GitBranch tracked = findRemoteBranchByName(repository, remote, trackedBranchName);
217 if (remote == null || tracked == null) {
218 Pair<GitRemote,GitBranch> remoteAndBranch = GitUtil.findMatchingRemoteBranch(repository, currentBranch);
219 if (remoteAndBranch == null) {
220 remote = myRefspecPanel.getSelectedRemote();
221 tracked = GitPusher.NO_TARGET_BRANCH;
223 remote = remoteAndBranch.getFirst();
224 tracked = remoteAndBranch.getSecond();
228 if (myRefspecPanel.turnedOn()) {
229 String manualBranchName = myRefspecPanel.getBranchToPush();
230 remote = myRefspecPanel.getSelectedRemote();
231 GitBranch manualBranch = findRemoteBranchByName(repository, remote, manualBranchName);
232 if (manualBranch == null) {
233 if (!manualBranchName.startsWith("refs/remotes/")) {
234 manualBranchName = myRefspecPanel.getSelectedRemote().getName() + "/" + manualBranchName;
236 manualBranch = new GitBranch(manualBranchName, false, true);
238 tracked = manualBranch;
241 GitPushSpec pushSpec = new GitPushSpec(remote, currentBranch, tracked);
242 defaultSpecs.put(repository, pushSpec);
248 private static GitBranch findRemoteBranchByName(@NotNull GitRepository repository, @Nullable GitRemote remote, @Nullable String name) {
249 if (name == null || remote == null) {
252 final String BRANCH_PREFIX = "refs/heads/";
253 if (name.startsWith(BRANCH_PREFIX)) {
254 name = name.substring(BRANCH_PREFIX.length());
257 for (GitBranch branch : repository.getBranches().getRemoteBranches()) {
258 if (branch.getName().equals(remote.getName() + "/" + name)) {
266 public JComponent getPreferredFocusedComponent() {
267 return myListPanel.getPreferredFocusComponent();
271 protected String getDimensionServiceKey() {
272 return GitPushDialog.class.getName();
276 public GitPushInfo getPushInfo() {
277 // waiting for commit list loading, because this information is needed to correctly handle rejected push situation and correctly
278 // notify about pushed commits
279 // TODO optimize: don't refresh: information about pushed commits can be achieved from the successful push output
280 synchronized (COMMITS_LOADING_LOCK) {
281 GitCommitsByRepoAndBranch selectedCommits;
282 if (myGitCommitsToPush == null) {
284 selectedCommits = myGitCommitsToPush;
287 if (refreshNeeded()) {
290 Collection<GitRepository> selectedRepositories = myListPanel.getSelectedRepositories();
291 selectedCommits = myGitCommitsToPush.retainAll(selectedRepositories);
293 return new GitPushInfo(selectedCommits, myPushSpecs);
297 private boolean refreshNeeded() {
298 String currentDestBranchValue = myRefspecPanel.turnedOn() ? myRefspecPanel.getBranchToPush(): null;
299 String savedValue = myDestBranchInfoOnRefresh.get();
300 if (savedValue == null) {
301 return currentDestBranchValue != null;
303 return !savedValue.equals(myDestBranchInfoOnRefresh.get());
306 private class RepositoryCheckboxListener implements Consumer<Boolean> {
307 @Override public void consume(Boolean checked) {
309 setOKActionEnabled(true);
311 Collection<GitRepository> repositories = myListPanel.getSelectedRepositories();
312 if (repositories.isEmpty()) {
313 setOKActionEnabled(false);
315 setOKActionEnabled(true);
321 private class RefreshButtonListener implements Runnable {
324 myDestBranchInfoOnRefresh.set(myRefspecPanel.turnedOn() ? myRefspecPanel.getBranchToPush(): null);
325 loadCommitsInBackground();