GitPushDialog: better logging, rename variable to more correct
[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.history.browser.GitCommit;
31 import git4idea.repo.GitRemote;
32 import git4idea.repo.GitRepository;
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 = GitUtil.getRepositoryManager(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   @Override
102   protected String getHelpId() {
103     return "reference.VersionControl.Git.PushDialog";
104   }
105
106   private JComponent createCommitListPanel() {
107     myLoadingPanel.add(myListPanel, BorderLayout.CENTER);
108     if (myPushPossible) {
109       loadCommitsInBackground();
110     } else {
111       myLoadingPanel.startLoading();
112       myLoadingPanel.stopLoading();
113     }
114
115     JPanel commitListPanel = new JPanel(new BorderLayout());
116     commitListPanel.add(myLoadingPanel, BorderLayout.CENTER);
117     return commitListPanel;
118   }
119
120   private void loadCommitsInBackground() {
121     myLoadingPanel.startLoading();
122
123     ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
124       public void run() {
125         final AtomicReference<String> error = new AtomicReference<String>();
126         synchronized (COMMITS_LOADING_LOCK) {
127           error.set(collectInfoToPush());
128         }
129
130         final Pair<String, String> remoteAndBranch = getRemoteAndTrackedBranchForCurrentBranch();
131         UIUtil.invokeLaterIfNeeded(new Runnable() {
132           @Override
133           public void run() {
134             if (error.get() != null) {
135               myListPanel.displayError(error.get());
136             } else {
137               myListPanel.setCommits(myGitCommitsToPush);
138             }
139             if (!myRefspecPanel.turnedOn()) {
140               myRefspecPanel.selectRemote(remoteAndBranch.getFirst());
141               myRefspecPanel.setBranchToPushIfNotSet(remoteAndBranch.getSecond());
142             }
143             myLoadingPanel.stopLoading();
144           }
145         });
146       }
147     });
148   }
149
150   @NotNull
151   private Pair<String, String> getRemoteAndTrackedBranchForCurrentBranch() {
152     if (myGitCommitsToPush != null) {
153       Collection<GitRepository> repositories = myGitCommitsToPush.getRepositories();
154       if (!repositories.isEmpty()) {
155         GitRepository repository = repositories.iterator().next();
156         GitBranch currentBranch = repository.getCurrentBranch();
157         assert currentBranch != null;
158         if (myGitCommitsToPush.get(repository).get(currentBranch).getDestBranch() == GitPusher.NO_TARGET_BRANCH) { // push to branch with the same name
159           return Pair.create(DEFAULT_REMOTE, currentBranch.getName());
160         }
161         String remoteName;
162         try {
163           remoteName = currentBranch.getTrackedRemoteName(myProject, repository.getRoot());
164           if (remoteName == null) {
165             remoteName = DEFAULT_REMOTE;
166           }
167         }
168         catch (VcsException e) {
169           LOG.info("Couldn't retrieve tracked branch for current branch " + currentBranch, e);
170           remoteName = DEFAULT_REMOTE;
171         }
172         String targetBranch = myGitCommitsToPush.get(repository).get(currentBranch).getDestBranch().getShortName();
173         return Pair.create(remoteName, targetBranch);
174       }
175     }
176     return Pair.create(DEFAULT_REMOTE, "");
177   }
178
179   @Nullable
180   private String collectInfoToPush() {
181     try {
182       LOG.info("collectInfoToPush...");
183       myPushSpecs = pushSpecsForCurrentOrEnteredBranches();
184       myGitCommitsToPush = myPusher.collectCommitsToPush(myPushSpecs);
185       LOG.info(String.format("collectInfoToPush | Collected commits to push. Push spec: %s, commits: %s",
186                              myPushSpecs, logMessageForCommits(myGitCommitsToPush)));
187       return null;
188     }
189     catch (VcsException e) {
190       myGitCommitsToPush = GitCommitsByRepoAndBranch.empty();
191       LOG.error("collectInfoToPush | Couldn't collect commits to push. Push spec: " + myPushSpecs, e);
192       return e.getMessage();
193     }
194   }
195
196   private static String logMessageForCommits(GitCommitsByRepoAndBranch commitsToPush) {
197     StringBuilder logMessage = new StringBuilder();
198     for (GitCommit commit : commitsToPush.getAllCommits()) {
199       logMessage.append(commit.getShortHash());
200     }
201     return logMessage.toString();
202   }
203
204   private Map<GitRepository, GitPushSpec> pushSpecsForCurrentOrEnteredBranches() throws VcsException {
205     Map<GitRepository, GitPushSpec> defaultSpecs = new HashMap<GitRepository, GitPushSpec>();
206     for (GitRepository repository : myRepositories) {
207       GitBranch currentBranch = repository.getCurrentBranch();
208       if (currentBranch == null) {
209         continue;
210       }
211       String remoteName = currentBranch.getTrackedRemoteName(repository.getProject(), repository.getRoot());
212       String trackedBranchName = currentBranch.getTrackedBranchName(repository.getProject(), repository.getRoot());
213       GitRemote remote = GitUtil.findRemoteByName(repository, remoteName);
214       GitBranch targetBranch = findRemoteBranchByName(repository, remote, trackedBranchName);
215       if (remote == null || targetBranch == null) {
216         Pair<GitRemote,GitBranch> remoteAndBranch = GitUtil.findMatchingRemoteBranch(repository, currentBranch);
217         if (remoteAndBranch == null) {
218           remote = myRefspecPanel.getSelectedRemote();
219           targetBranch = GitPusher.NO_TARGET_BRANCH;
220         } else {
221           remote = remoteAndBranch.getFirst();
222           targetBranch = remoteAndBranch.getSecond();
223         }
224       }
225
226       if (myRefspecPanel.turnedOn()) {
227         String manualBranchName = myRefspecPanel.getBranchToPush();
228         remote = myRefspecPanel.getSelectedRemote();
229         GitBranch manualBranch = findRemoteBranchByName(repository, remote, manualBranchName);
230         if (manualBranch == null) {
231           if (!manualBranchName.startsWith("refs/remotes/")) {
232             manualBranchName = myRefspecPanel.getSelectedRemote().getName() + "/" + manualBranchName;
233           }
234           manualBranch = new GitBranch(manualBranchName, false, true);
235         }
236         targetBranch = manualBranch;
237       }
238
239       GitPushSpec pushSpec = new GitPushSpec(remote, currentBranch, targetBranch);
240       defaultSpecs.put(repository, pushSpec);
241     }
242     return defaultSpecs;
243   }
244
245   @Nullable
246   private static GitBranch findRemoteBranchByName(@NotNull GitRepository repository, @Nullable GitRemote remote, @Nullable String name) {
247     if (name == null || remote == null) {
248       return null;
249     }
250     final String BRANCH_PREFIX = "refs/heads/";
251     if (name.startsWith(BRANCH_PREFIX)) {
252       name = name.substring(BRANCH_PREFIX.length());
253     }
254
255     for (GitBranch branch : repository.getBranches().getRemoteBranches()) {
256       if (branch.getName().equals(remote.getName() + "/" + name)) {
257         return branch;
258       }
259     }
260     return null;
261   }
262
263   @Override
264   public JComponent getPreferredFocusedComponent() {
265     return myListPanel.getPreferredFocusComponent();
266   }
267
268   @Override
269   protected String getDimensionServiceKey() {
270     return GitPushDialog.class.getName();
271   }
272
273   @NotNull
274   public GitPushInfo getPushInfo() {
275     // waiting for commit list loading, because this information is needed to correctly handle rejected push situation and correctly
276     // notify about pushed commits
277     // TODO optimize: don't refresh: information about pushed commits can be achieved from the successful push output
278     LOG.info("getPushInfo start");
279     synchronized (COMMITS_LOADING_LOCK) {
280       GitCommitsByRepoAndBranch selectedCommits;
281       if (myGitCommitsToPush == null) {
282         LOG.info("getPushInfo | myGitCommitsToPush == null. collecting...");
283         collectInfoToPush();
284         selectedCommits = myGitCommitsToPush;
285       }
286       else {
287         if (refreshNeeded()) {
288           LOG.info("getPushInfo | refresh is needed, collecting...");
289           collectInfoToPush();
290         }
291         Collection<GitRepository> selectedRepositories = myListPanel.getSelectedRepositories();
292         selectedCommits = myGitCommitsToPush.retainAll(selectedRepositories);
293       }
294       LOG.info("getPushInfo | selectedCommits: " + logMessageForCommits(selectedCommits));
295       return new GitPushInfo(selectedCommits, myPushSpecs);
296     }
297   }
298
299   private boolean refreshNeeded() {
300     String currentDestBranchValue = myRefspecPanel.turnedOn() ? myRefspecPanel.getBranchToPush(): null;
301     String savedValue = myDestBranchInfoOnRefresh.get();
302     if (savedValue == null) {
303       return currentDestBranchValue != null;
304     }
305     return !savedValue.equals(currentDestBranchValue);
306   }
307
308   private class RepositoryCheckboxListener implements Consumer<Boolean> {
309     @Override public void consume(Boolean checked) {
310       if (checked) {
311         setOKActionEnabled(true);
312       } else {
313         Collection<GitRepository> repositories = myListPanel.getSelectedRepositories();
314         if (repositories.isEmpty()) {
315           setOKActionEnabled(false);
316         } else {
317           setOKActionEnabled(true);
318         }
319       }
320     }
321   }
322
323   private class RefreshButtonListener implements Runnable {
324     @Override
325     public void run() {
326       myDestBranchInfoOnRefresh.set(myRefspecPanel.turnedOn() ? myRefspecPanel.getBranchToPush(): null);
327       loadCommitsInBackground();
328     }
329   }
330
331 }