replaced <code></code> with more concise {@code}
[idea/community.git] / plugins / git4idea / src / git4idea / branch / GitBranchUtil.java
1 /*
2  * Copyright 2000-2014 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.branch;
17
18 import com.google.common.base.Function;
19 import com.google.common.collect.Collections2;
20 import com.intellij.dvcs.DvcsUtil;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.project.Project;
23 import com.intellij.openapi.util.io.FileUtil;
24 import com.intellij.openapi.vcs.VcsException;
25 import com.intellij.openapi.vfs.CharsetToolkit;
26 import com.intellij.openapi.vfs.VirtualFile;
27 import com.intellij.util.containers.ContainerUtil;
28 import git4idea.*;
29 import git4idea.commands.GitCommand;
30 import git4idea.commands.GitSimpleHandler;
31 import git4idea.config.GitConfigUtil;
32 import git4idea.config.GitVcsSettings;
33 import git4idea.repo.GitBranchTrackInfo;
34 import git4idea.repo.GitConfig;
35 import git4idea.repo.GitRemote;
36 import git4idea.repo.GitRepository;
37 import git4idea.ui.branch.GitMultiRootBranchConfig;
38 import git4idea.validators.GitNewBranchNameValidator;
39 import org.jetbrains.annotations.CalledInAwt;
40 import org.jetbrains.annotations.NotNull;
41 import org.jetbrains.annotations.Nullable;
42
43 import java.io.File;
44 import java.io.IOException;
45 import java.util.ArrayList;
46 import java.util.Collection;
47 import java.util.Collections;
48 import java.util.HashMap;
49
50 import static com.intellij.util.ObjectUtils.assertNotNull;
51
52 /**
53  * @author Kirill Likhodedov
54  */
55 public class GitBranchUtil {
56
57   private static final Logger LOG = Logger.getInstance(GitBranchUtil.class);
58
59   private static final Function<GitBranch,String> BRANCH_TO_NAME = input -> {
60     assert input != null;
61     return input.getName();
62   };
63   // The name that specifies that git is on specific commit rather then on some branch ({@value})
64  private static final String NO_BRANCH_NAME = "(no branch)";
65
66   private GitBranchUtil() {}
67
68   /**
69    * Returns the tracking information about the given branch in the given repository,
70    * or null if there is no such information (i.e. if the branch doesn't have a tracking branch).
71    */
72   @Nullable
73   public static GitBranchTrackInfo getTrackInfoForBranch(@NotNull GitRepository repository, @NotNull GitLocalBranch branch) {
74     for (GitBranchTrackInfo trackInfo : repository.getBranchTrackInfos()) {
75       if (trackInfo.getLocalBranch().equals(branch)) {
76         return trackInfo;
77       }
78     }
79     return null;
80   }
81
82   @Nullable
83   public static GitBranchTrackInfo getTrackInfo(@NotNull GitRepository repository, @NotNull String localBranchName) {
84     return ContainerUtil.find(repository.getBranchTrackInfos(), it -> it.getLocalBranch().getName().equals(localBranchName));
85   }
86
87   @NotNull
88   static String getCurrentBranchOrRev(@NotNull Collection<GitRepository> repositories) {
89     if (repositories.size() > 1) {
90       GitMultiRootBranchConfig multiRootBranchConfig = new GitMultiRootBranchConfig(repositories);
91       String currentBranch = multiRootBranchConfig.getCurrentBranch();
92       LOG.assertTrue(currentBranch != null, "Repositories have unexpectedly diverged. " + multiRootBranchConfig);
93       return currentBranch;
94     }
95     else {
96       assert !repositories.isEmpty() : "No repositories passed to GitBranchOperationsProcessor.";
97       GitRepository repository = repositories.iterator().next();
98       return getBranchNameOrRev(repository);
99     }
100   }
101
102   @NotNull
103   public static Collection<String> convertBranchesToNames(@NotNull Collection<? extends GitBranch> branches) {
104     return Collections2.transform(branches, BRANCH_TO_NAME);
105   }
106
107   /**
108    * Returns the current branch in the given repository, or null if either repository is not on the branch, or in case of error.
109    * @deprecated Use {@link GitRepository#getCurrentBranch()}
110    */
111   @Deprecated
112   @Nullable
113   public static GitLocalBranch getCurrentBranch(@NotNull Project project, @NotNull VirtualFile root) {
114     GitRepository repository = GitUtil.getRepositoryManager(project).getRepositoryForRoot(root);
115     if (repository != null) {
116       return repository.getCurrentBranch();
117     }
118     else {
119       LOG.info("getCurrentBranch: Repository is null for root " + root);
120       return getCurrentBranchFromGit(project, root);
121     }
122   }
123
124   @Nullable
125   private static GitLocalBranch getCurrentBranchFromGit(@NotNull Project project, @NotNull VirtualFile root) {
126     GitSimpleHandler handler = new GitSimpleHandler(project, root, GitCommand.REV_PARSE);
127     handler.addParameters("--abbrev-ref", "HEAD");
128     handler.setSilent(true);
129     try {
130       String name = handler.run();
131       if (!name.equals("HEAD")) {
132         return new GitLocalBranch(name);
133       }
134       else {
135         return null;
136       }
137     }
138     catch (VcsException e) {
139       LOG.info("git rev-parse --abbrev-ref HEAD", e);
140       return null;
141     }
142   }
143
144   /**
145    * Get tracked remote for the branch
146    */
147   @Nullable
148   public static String getTrackedRemoteName(Project project, VirtualFile root, String branchName) throws VcsException {
149     return GitConfigUtil.getValue(project, root, trackedRemoteKey(branchName));
150   }
151
152   /**
153    * Get tracked branch of the given branch
154    */
155   @Nullable
156   public static String getTrackedBranchName(Project project, VirtualFile root, String branchName) throws VcsException {
157     return GitConfigUtil.getValue(project, root, trackedBranchKey(branchName));
158   }
159
160   @NotNull
161   private static String trackedBranchKey(String branchName) {
162     return "branch." + branchName + ".merge";
163   }
164
165   @NotNull
166   private static String trackedRemoteKey(String branchName) {
167     return "branch." + branchName + ".remote";
168   }
169
170   /**
171    * Get the tracking branch for the given branch, or null if the given branch doesn't track anything.
172    * @deprecated Use {@link GitConfig#getBranchTrackInfos()}
173    */
174   @Deprecated
175   @Nullable
176   public static GitRemoteBranch tracked(@NotNull Project project, @NotNull VirtualFile root, @NotNull String branchName) throws VcsException {
177     final HashMap<String, String> result = new HashMap<>();
178     GitConfigUtil.getValues(project, root, null, result);
179     String remoteName = result.get(trackedRemoteKey(branchName));
180     if (remoteName == null) {
181       return null;
182     }
183     String branch = result.get(trackedBranchKey(branchName));
184     if (branch == null) {
185       return null;
186     }
187
188     if (".".equals(remoteName)) {
189       return new GitSvnRemoteBranch(branch);
190     }
191
192     GitRemote remote = findRemoteByNameOrLogError(project, root, remoteName);
193     if (remote == null) return null;
194     return new GitStandardRemoteBranch(remote, branch);
195   }
196
197   @Nullable
198   @Deprecated
199   public static GitRemote findRemoteByNameOrLogError(@NotNull Project project, @NotNull VirtualFile root, @NotNull String remoteName) {
200     GitRepository repository = GitUtil.getRepositoryForRootOrLogError(project, root);
201     if (repository == null) {
202       return null;
203     }
204
205     GitRemote remote = GitUtil.findRemoteByName(repository, remoteName);
206     if (remote == null) {
207       LOG.warn("Couldn't find remote with name " + remoteName);
208       return null;
209     }
210     return remote;
211   }
212
213   /**
214    * Convert {@link git4idea.GitRemoteBranch GitRemoteBranches} to their names, and remove remote HEAD pointers: origin/HEAD.
215    */
216   @NotNull
217   public static Collection<String> getBranchNamesWithoutRemoteHead(@NotNull Collection<GitRemoteBranch> remoteBranches) {
218     return Collections2.filter(convertBranchesToNames(remoteBranches), input -> {
219       assert input != null;
220       return !input.equals("HEAD");
221     });
222   }
223
224   @NotNull
225   public static String stripRefsPrefix(@NotNull String branchName) {
226     if (branchName.startsWith(GitBranch.REFS_HEADS_PREFIX)) {
227       return branchName.substring(GitBranch.REFS_HEADS_PREFIX.length());
228     }
229     else if (branchName.startsWith(GitBranch.REFS_REMOTES_PREFIX)) {
230       return branchName.substring(GitBranch.REFS_REMOTES_PREFIX.length());
231     }
232     else if (branchName.startsWith(GitTag.REFS_TAGS_PREFIX)) {
233       return branchName.substring(GitTag.REFS_TAGS_PREFIX.length());
234     }
235     return branchName;
236   }
237
238   /**
239    * Returns current branch name (if on branch) or current revision otherwise.
240    * For fresh repository returns an empty string.
241    */
242   @NotNull
243   public static String getBranchNameOrRev(@NotNull GitRepository repository) {
244     if (repository.isOnBranch()) {
245       GitBranch currentBranch = repository.getCurrentBranch();
246       assert currentBranch != null;
247       return currentBranch.getName();
248     } else {
249       String currentRevision = repository.getCurrentRevision();
250       return currentRevision != null ? currentRevision.substring(0, 7) : "";
251     }
252   }
253
254   /**
255    * <p>Shows a message dialog to enter the name of new branch.</p>
256    * <p>Optionally allows to not checkout this branch, and just create it.</p>
257    *
258    * @return the name of the new branch and whether it should be checked out, or {@code null} if user has cancelled the dialog.
259    */
260   @Nullable
261   public static GitNewBranchOptions getNewBranchNameFromUser(@NotNull Project project,
262                                                              @NotNull Collection<GitRepository> repositories,
263                                                              @NotNull String dialogTitle) {
264     return new GitNewBranchDialog(project, dialogTitle, GitNewBranchNameValidator.newInstance(repositories)).showAndGetOptions();
265   }
266
267   /**
268    * Returns the text that is displaying current branch.
269    * In the simple case it is just the branch name, but in detached HEAD state it displays the hash or "rebasing master".
270    */
271   @NotNull
272   public static String getDisplayableBranchText(@NotNull GitRepository repository) {
273     GitRepository.State state = repository.getState();
274     if (state == GitRepository.State.DETACHED) {
275       String currentRevision = repository.getCurrentRevision();
276       assert currentRevision != null : "Current revision can't be null in DETACHED state, only on the fresh repository.";
277       return DvcsUtil.getShortHash(currentRevision);
278     }
279
280     String prefix = "";
281     if (state == GitRepository.State.MERGING || state == GitRepository.State.REBASING) {
282       prefix = state.toString() + " ";
283     }
284
285     GitBranch branch = repository.getCurrentBranch();
286     String branchName = (branch == null ? "" : branch.getName());
287     return prefix + branchName;
288   }
289
290   /**
291    * Guesses the Git root on which a Git action is to be invoked.
292    * <ol>
293    *   <li>
294    *     Returns the root for the selected file. Selected file is determined by {@link DvcsUtil#getSelectedFile(Project)}.
295    *     If selected file is unknown (for example, no file is selected in the Project View or Changes View and no file is open in the editor),
296    *     continues guessing. Otherwise returns the Git root for the selected file. If the file is not under a known Git root,
297    *     but there is at least one git root,  continues guessing, otherwise
298    *     {@code null} will be returned - the file is definitely determined, but it is not under Git and no git roots exists in project.
299    *   </li>
300    *   <li>
301    *     Takes all Git roots registered in the Project. If there is only one, it is returned.
302    *   </li>
303    *   <li>
304    *     If there are several Git roots,
305    *   </li>
306    * </ol>
307    *
308    * <p>
309    *   NB: This method has to be accessed from the <b>read action</b>, because it may query
310    *   {@link com.intellij.openapi.fileEditor.FileEditorManager#getSelectedTextEditor()}.
311    * </p>
312    * @param project current project
313    * @return Git root that may be considered as "current".
314    *         {@code null} is returned if a file not under Git was explicitly selected, if there are no Git roots in the project,
315    *         or if the current Git root couldn't be determined.
316    */
317   @Nullable
318   @CalledInAwt
319   public static GitRepository getCurrentRepository(@NotNull Project project) {
320     return getRepositoryOrGuess(project, DvcsUtil.getSelectedFile(project));
321   }
322
323   @Nullable
324   public static GitRepository getRepositoryOrGuess(@NotNull Project project, @Nullable VirtualFile file) {
325     if (project.isDisposed()) return null;
326     return DvcsUtil.guessRepositoryForFile(project, GitUtil.getRepositoryManager(project), file,
327                                            GitVcsSettings.getInstance(project).getRecentRootPath());
328   }
329
330   @NotNull
331   public static Collection<String> getCommonBranches(Collection<GitRepository> repositories,
332                                                      boolean local) {
333     Collection<String> commonBranches = null;
334     for (GitRepository repository : repositories) {
335       GitBranchesCollection branchesCollection = repository.getBranches();
336
337       Collection<String> names = local
338                                  ? convertBranchesToNames(branchesCollection.getLocalBranches())
339                                  : getBranchNamesWithoutRemoteHead(branchesCollection.getRemoteBranches());
340       if (commonBranches == null) {
341         commonBranches = names;
342       }
343       else {
344         commonBranches = ContainerUtil.intersection(commonBranches, names);
345       }
346     }
347
348     if (commonBranches != null) {
349       ArrayList<String> common = new ArrayList<>(commonBranches);
350       Collections.sort(common);
351       return common;
352     }
353     else {
354       return Collections.emptyList();
355     }
356   }
357
358   /**
359    * List branches containing a commit. Specify null if no commit filtering is needed.
360    */
361   @NotNull
362   public static Collection<String> getBranches(@NotNull Project project, @NotNull VirtualFile root, boolean localWanted,
363                                                boolean remoteWanted, @Nullable String containingCommit) throws VcsException {
364     // preparing native command executor
365     final GitSimpleHandler handler = new GitSimpleHandler(project, root, GitCommand.BRANCH);
366     handler.setSilent(true);
367     handler.addParameters("--no-color");
368     boolean remoteOnly = false;
369     if (remoteWanted && localWanted) {
370       handler.addParameters("-a");
371       remoteOnly = false;
372     } else if (remoteWanted) {
373       handler.addParameters("-r");
374       remoteOnly = true;
375     }
376     if (containingCommit != null) {
377       handler.addParameters("--contains", containingCommit);
378     }
379     final String output = handler.run();
380
381     if (output.trim().length() == 0) {
382       // the case after git init and before first commit - there is no branch and no output, and we'll take refs/heads/master
383       String head;
384       try {
385         File headFile = assertNotNull(GitUtil.getRepositoryManager(project).getRepositoryForRoot(root)).getRepositoryFiles().getHeadFile();
386         head = FileUtil.loadFile(headFile, CharsetToolkit.UTF8_CHARSET).trim();
387         final String prefix = "ref: refs/heads/";
388         return head.startsWith(prefix) ?
389                Collections.singletonList(head.substring(prefix.length())) :
390                Collections.emptyList();
391       } catch (IOException e) {
392         LOG.info(e);
393         return Collections.emptyList();
394       }
395     }
396
397     Collection<String> branches = ContainerUtil.newArrayList();
398     // standard situation. output example:
399     //  master
400     //* my_feature
401     //  remotes/origin/HEAD -> origin/master
402     //  remotes/origin/eap
403     //  remotes/origin/feature
404     //  remotes/origin/master
405     // also possible:
406     //* (no branch)
407     // and if we call with -r instead of -a, remotes/ prefix is omitted:
408     // origin/HEAD -> origin/master
409     final String[] split = output.split("\n");
410     for (String b : split) {
411       b = b.substring(2).trim();
412       if (b.equals(NO_BRANCH_NAME)) { continue; }
413
414       String remotePrefix = null;
415       if (b.startsWith("remotes/")) {
416         remotePrefix = "remotes/";
417       } else if (b.startsWith(GitBranch.REFS_REMOTES_PREFIX)) {
418         remotePrefix = GitBranch.REFS_REMOTES_PREFIX;
419       }
420       boolean isRemote = remotePrefix != null || remoteOnly;
421       if (isRemote) {
422         if (! remoteOnly) {
423           b = b.substring(remotePrefix.length());
424         }
425         final int idx = b.indexOf("HEAD ->");
426         if (idx > 0) {
427           continue;
428         }
429       }
430       branches.add(b);
431     }
432     return branches;
433   }
434 }