IDEA-89881 git push to an alternative branch pushed to the tracked branch instead
[idea/community.git] / plugins / git4idea / src / git4idea / push / GitPushDialog.java
1 /*
2  * Copyright 2000-2011 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 package git4idea.push;
17
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.branch.GitBranchUtil;
31 import git4idea.history.browser.GitCommit;
32 import git4idea.repo.GitRemote;
33 import git4idea.repo.GitRepository;
34 import git4idea.repo.GitRepositoryManager;
35 import org.jetbrains.annotations.NotNull;
36 import org.jetbrains.annotations.Nullable;
37
38 import javax.swing.*;
39 import java.awt.*;
40 import java.util.*;
41 import java.util.List;
42 import java.util.concurrent.atomic.AtomicReference;
43
44 /**
45  * @author Kirill Likhodedov
46  */
47 public class GitPushDialog extends DialogWrapper {
48
49   private static final Logger LOG = Logger.getInstance(GitPushDialog.class);
50   private static final String DEFAULT_REMOTE = "origin";
51
52   private Project myProject;
53   private final GitRepositoryManager myRepositoryManager;
54   private final GitPusher myPusher;
55   private final GitPushLog myListPanel;
56   private GitCommitsByRepoAndBranch myGitCommitsToPush;
57   private Map<GitRepository, GitPushSpec> myPushSpecs;
58   private final Collection<GitRepository> myRepositories;
59   private final JBLoadingPanel myLoadingPanel;
60   private final Object COMMITS_LOADING_LOCK = new Object();
61   private final GitManualPushToBranch myRefspecPanel;
62   private final AtomicReference<String> myDestBranchInfoOnRefresh = new AtomicReference<String>();
63
64   private final boolean myPushPossible;
65
66   public GitPushDialog(@NotNull Project project) {
67     super(project);
68     myProject = project;
69     myPusher = new GitPusher(myProject, new EmptyProgressIndicator());
70     myRepositoryManager = GitUtil.getRepositoryManager(myProject);
71
72     myRepositories = getRepositoriesWithRemotes();
73
74     myLoadingPanel = new JBLoadingPanel(new BorderLayout(), this.getDisposable());
75
76     myListPanel = new GitPushLog(myProject, myRepositories, new RepositoryCheckboxListener());
77     myRefspecPanel = new GitManualPushToBranch(myRepositories, new RefreshButtonListener());
78
79     if (GitManualPushToBranch.getRemotesWithCommonNames(myRepositories).isEmpty()) {
80       myRefspecPanel.setVisible(false);
81       setErrorText("Can't push, because no remotes are defined");
82       setOKActionEnabled(false);
83       myPushPossible = false;
84     } else {
85       myPushPossible = true;
86     }
87
88     init();
89     setOKButtonText("Push");
90     setOKButtonMnemonic('P');
91     setTitle("Git Push");
92   }
93
94   @NotNull
95   private List<GitRepository> getRepositoriesWithRemotes() {
96     List<GitRepository> repositories = new ArrayList<GitRepository>();
97     for (GitRepository repository : myRepositoryManager.getRepositories()) {
98       if (!repository.getRemotes().isEmpty()) {
99         repositories.add(repository);
100       }
101     }
102     return repositories;
103   }
104
105   @Override
106   protected JComponent createCenterPanel() {
107     JPanel optionsPanel = new JPanel(new BorderLayout());
108     optionsPanel.add(myRefspecPanel);
109
110     JComponent rootPanel = new JPanel(new BorderLayout(0, 15));
111     rootPanel.add(createCommitListPanel(), BorderLayout.CENTER);
112     rootPanel.add(optionsPanel, BorderLayout.SOUTH);
113     return rootPanel;
114   }
115
116   @Override
117   protected String getHelpId() {
118     return "reference.VersionControl.Git.PushDialog";
119   }
120
121   private JComponent createCommitListPanel() {
122     myLoadingPanel.add(myListPanel, BorderLayout.CENTER);
123     if (myPushPossible) {
124       loadCommitsInBackground();
125     } else {
126       myLoadingPanel.startLoading();
127       myLoadingPanel.stopLoading();
128     }
129
130     JPanel commitListPanel = new JPanel(new BorderLayout());
131     commitListPanel.add(myLoadingPanel, BorderLayout.CENTER);
132     return commitListPanel;
133   }
134
135   private void loadCommitsInBackground() {
136     myLoadingPanel.startLoading();
137
138     ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
139       public void run() {
140         final AtomicReference<String> error = new AtomicReference<String>();
141         synchronized (COMMITS_LOADING_LOCK) {
142           error.set(collectInfoToPush());
143         }
144
145         final Pair<String, String> remoteAndBranch = getRemoteAndTrackedBranchForCurrentBranch();
146         UIUtil.invokeLaterIfNeeded(new Runnable() {
147           @Override
148           public void run() {
149             if (error.get() != null) {
150               myListPanel.displayError(error.get());
151             } else {
152               myListPanel.setCommits(myGitCommitsToPush);
153             }
154             if (!myRefspecPanel.turnedOn()) {
155               myRefspecPanel.selectRemote(remoteAndBranch.getFirst());
156               myRefspecPanel.setBranchToPushIfNotSet(remoteAndBranch.getSecond());
157             }
158             myLoadingPanel.stopLoading();
159           }
160         });
161       }
162     });
163   }
164
165   @NotNull
166   private Pair<String, String> getRemoteAndTrackedBranchForCurrentBranch() {
167     if (myGitCommitsToPush != null) {
168       Collection<GitRepository> repositories = myGitCommitsToPush.getRepositories();
169       if (!repositories.isEmpty()) {
170         GitRepository repository = repositories.iterator().next();
171         GitBranch currentBranch = repository.getCurrentBranch();
172         assert currentBranch != null;
173         if (myGitCommitsToPush.get(repository).get(currentBranch).getDestBranch() == GitPusher.NO_TARGET_BRANCH) { // push to branch with the same name
174           return Pair.create(DEFAULT_REMOTE, currentBranch.getName());
175         }
176         String remoteName;
177         try {
178           remoteName = currentBranch.getTrackedRemoteName(myProject, repository.getRoot());
179           if (remoteName == null) {
180             remoteName = DEFAULT_REMOTE;
181           }
182         }
183         catch (VcsException e) {
184           LOG.info("Couldn't retrieve tracked branch for current branch " + currentBranch, e);
185           remoteName = DEFAULT_REMOTE;
186         }
187         String targetBranch = myGitCommitsToPush.get(repository).get(currentBranch).getDestBranch().getShortName();
188         return Pair.create(remoteName, targetBranch);
189       }
190     }
191     return Pair.create(DEFAULT_REMOTE, "");
192   }
193
194   @Nullable
195   private String collectInfoToPush() {
196     try {
197       LOG.info("collectInfoToPush...");
198       myPushSpecs = pushSpecsForCurrentOrEnteredBranches();
199       myGitCommitsToPush = myPusher.collectCommitsToPush(myPushSpecs);
200       LOG.info(String.format("collectInfoToPush | Collected commits to push. Push spec: %s, commits: %s",
201                              myPushSpecs, logMessageForCommits(myGitCommitsToPush)));
202       return null;
203     }
204     catch (VcsException e) {
205       myGitCommitsToPush = GitCommitsByRepoAndBranch.empty();
206       LOG.error("collectInfoToPush | Couldn't collect commits to push. Push spec: " + myPushSpecs, e);
207       return e.getMessage();
208     }
209   }
210
211   private static String logMessageForCommits(GitCommitsByRepoAndBranch commitsToPush) {
212     StringBuilder logMessage = new StringBuilder();
213     for (GitCommit commit : commitsToPush.getAllCommits()) {
214       logMessage.append(commit.getShortHash());
215     }
216     return logMessage.toString();
217   }
218
219   private Map<GitRepository, GitPushSpec> pushSpecsForCurrentOrEnteredBranches() throws VcsException {
220     Map<GitRepository, GitPushSpec> defaultSpecs = new HashMap<GitRepository, GitPushSpec>();
221     for (GitRepository repository : myRepositories) {
222       GitBranch currentBranch = repository.getCurrentBranch();
223       if (currentBranch == null) {
224         continue;
225       }
226       String remoteName = currentBranch.getTrackedRemoteName(repository.getProject(), repository.getRoot());
227       String trackedBranchName = currentBranch.getTrackedBranchName(repository.getProject(), repository.getRoot());
228       GitRemote remote = GitUtil.findRemoteByName(repository, remoteName);
229       GitBranch targetBranch = GitBranchUtil.findRemoteBranchByName(repository, remote, trackedBranchName);
230       if (remote == null || targetBranch == null) {
231         Pair<GitRemote,GitBranch> remoteAndBranch = GitUtil.findMatchingRemoteBranch(repository, currentBranch);
232         if (remoteAndBranch == null) {
233           remote = myRefspecPanel.getSelectedRemote();
234           targetBranch = GitPusher.NO_TARGET_BRANCH;
235         } else {
236           remote = remoteAndBranch.getFirst();
237           targetBranch = remoteAndBranch.getSecond();
238         }
239       }
240
241       if (myRefspecPanel.turnedOn()) {
242         String manualBranchName = myRefspecPanel.getBranchToPush();
243         remote = myRefspecPanel.getSelectedRemote();
244         GitBranch manualBranch = GitBranchUtil.findRemoteBranchByName(repository, remote, manualBranchName);
245         if (manualBranch == null) {
246           if (!manualBranchName.startsWith("refs/remotes/")) {
247             manualBranchName = myRefspecPanel.getSelectedRemote().getName() + "/" + manualBranchName;
248           }
249           manualBranch = new GitBranch(manualBranchName, false, true);
250         }
251         targetBranch = manualBranch;
252       }
253
254       GitPushSpec pushSpec = new GitPushSpec(remote, currentBranch, targetBranch);
255       defaultSpecs.put(repository, pushSpec);
256     }
257     return defaultSpecs;
258   }
259
260   @Override
261   public JComponent getPreferredFocusedComponent() {
262     return myListPanel.getPreferredFocusComponent();
263   }
264
265   @Override
266   protected String getDimensionServiceKey() {
267     return GitPushDialog.class.getName();
268   }
269
270   @NotNull
271   public GitPushInfo getPushInfo() {
272     // waiting for commit list loading, because this information is needed to correctly handle rejected push situation and correctly
273     // notify about pushed commits
274     // TODO optimize: don't refresh: information about pushed commits can be achieved from the successful push output
275     LOG.info("getPushInfo start");
276     synchronized (COMMITS_LOADING_LOCK) {
277       GitCommitsByRepoAndBranch selectedCommits;
278       if (myGitCommitsToPush == null) {
279         LOG.info("getPushInfo | myGitCommitsToPush == null. collecting...");
280         collectInfoToPush();
281         selectedCommits = myGitCommitsToPush;
282       }
283       else {
284         if (refreshNeeded()) {
285           LOG.info("getPushInfo | refresh is needed, collecting...");
286           collectInfoToPush();
287         }
288         Collection<GitRepository> selectedRepositories = myListPanel.getSelectedRepositories();
289         selectedCommits = myGitCommitsToPush.retainAll(selectedRepositories);
290       }
291       LOG.info("getPushInfo | selectedCommits: " + logMessageForCommits(selectedCommits));
292       return new GitPushInfo(selectedCommits, myPushSpecs);
293     }
294   }
295
296   private boolean refreshNeeded() {
297     String currentDestBranchValue = myRefspecPanel.turnedOn() ? myRefspecPanel.getBranchToPush(): null;
298     String savedValue = myDestBranchInfoOnRefresh.get();
299     if (savedValue == null) {
300       return currentDestBranchValue != null;
301     }
302     return !savedValue.equals(currentDestBranchValue);
303   }
304
305   private class RepositoryCheckboxListener implements Consumer<Boolean> {
306     @Override public void consume(Boolean checked) {
307       if (checked) {
308         setOKActionEnabled(true);
309       } else {
310         Collection<GitRepository> repositories = myListPanel.getSelectedRepositories();
311         if (repositories.isEmpty()) {
312           setOKActionEnabled(false);
313         } else {
314           setOKActionEnabled(true);
315         }
316       }
317     }
318   }
319
320   private class RefreshButtonListener implements Runnable {
321     @Override
322     public void run() {
323       myDestBranchInfoOnRefresh.set(myRefspecPanel.turnedOn() ? myRefspecPanel.getBranchToPush(): null);
324       loadCommitsInBackground();
325     }
326   }
327
328 }