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