GitBranch#getShortName to get "local" name of a remote branch
[idea/community.git] / plugins / git4idea / src / git4idea / GitBranch.java
1 /*
2  * Copyright 2000-2009 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;
17
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.project.Project;
20 import com.intellij.openapi.util.io.FileUtil;
21 import com.intellij.openapi.vcs.VcsException;
22 import com.intellij.openapi.vfs.VirtualFile;
23 import git4idea.commands.GitCommand;
24 import git4idea.commands.GitSimpleHandler;
25 import git4idea.config.GitConfigUtil;
26 import git4idea.history.GitHistoryUtils;
27 import org.jetbrains.annotations.NonNls;
28 import org.jetbrains.annotations.NotNull;
29 import org.jetbrains.annotations.Nullable;
30
31 import java.io.File;
32 import java.io.IOException;
33 import java.util.ArrayList;
34 import java.util.Collection;
35 import java.util.HashMap;
36
37 /**
38  * This data class represents a Git branch
39  */
40 public class GitBranch extends GitReference {
41   @NonNls public static final String NO_BRANCH_NAME = "(no branch)"; // The name that specifies that git is on specific commit rather then on some branch ({@value}) 
42   @NonNls public static final String REFS_HEADS_PREFIX = "refs/heads/"; // Prefix for local branches ({@value})
43   @NonNls public static final String REFS_REMOTES_PREFIX = "refs/remotes/"; // Prefix for remote branches ({@value})
44
45   private final boolean myRemote;
46   private boolean myActive;
47   private static final Logger LOG = Logger.getInstance(GitBranch.class);
48   private final String myHash;
49
50   public GitBranch(@NotNull String name, @NotNull String hash, boolean active, boolean remote) {
51     super(name);
52     myRemote = remote;
53     myActive = active;
54     myHash = new String(hash);
55   }
56   
57   @Deprecated
58   public GitBranch(@NotNull String name, boolean active, boolean remote) {
59     this(name, "", active, remote);
60   }
61
62   /**
63    * @return true if the branch is remote
64    */
65   public boolean isRemote() {
66     return myRemote;
67   }
68
69   /**
70    * @return true if the branch is active
71    */
72   public boolean isActive() {
73     return myActive;
74   }
75
76   @NotNull
77   public String getFullName() {
78     return (myRemote ? REFS_REMOTES_PREFIX : REFS_HEADS_PREFIX) + myName;
79   }
80
81   /**
82    * <p>
83    *   Returns the "local" name of a remote branch.
84    *   For example, for "origin/master" returns "master".
85    * </p>
86    * <p>
87    *   Note that slashes are not permitted in remote names, so if we know that a branch is a remote branch,
88    *   we know that local branch name is tha part after the slash.
89    * </p>
90    * @return "local" name of a remote branch, or just {@link #getName()} for local branches.
91    */
92   @NotNull
93   public String getShortName() {
94     String name = getName();
95     if (myRemote) {
96       return name.substring(name.indexOf('/') + 1);
97     }
98     return name;
99   }
100
101   /**
102    * Get tracked remote for the branch
103    *
104    * @param project the context project
105    * @param root    the VCS root to investigate
106    * @return the remote name for tracked branch, "." meaning the current repository, or null if no branch is tracked
107    * @throws VcsException if there is a problem with running Git
108    */
109   @Nullable
110   public String getTrackedRemoteName(Project project, VirtualFile root) throws VcsException {
111     return GitConfigUtil.getValue(project, root, trackedRemoteKey());
112   }
113
114   /**
115    * Get tracked the branch
116    *
117    * @param project the context project
118    * @param root    the VCS root to investigate
119    * @return the name of tracked branch
120    * @throws VcsException if there is a problem with running Git
121    */
122   @Nullable
123   public String getTrackedBranchName(Project project, VirtualFile root) throws VcsException {
124     return GitConfigUtil.getValue(project, root, trackedBranchKey());
125   }
126
127   /**
128    * Checks if the branch exists in the repository.
129    * @return true if the branch exists, false otherwise.
130    * @deprecated use {@link git4idea.repo.GitRepository#getBranches()}
131    */
132   public boolean exists(VirtualFile root) {
133     final VirtualFile remoteBranch = root.findFileByRelativePath(".git/refs/remotes/" + myName);
134     if (remoteBranch != null && remoteBranch.exists()) {
135       return true;
136     }
137     final VirtualFile packedRefs = root.findFileByRelativePath(".git/packed-refs");
138     if (packedRefs != null && packedRefs.exists()) {
139       final byte[] contents;
140       try {
141         contents = packedRefs.contentsToByteArray();
142         return new String(contents).contains(myName);
143       } catch (IOException e) {
144         LOG.info("exists ", e);
145         return false;
146       }
147     }
148     return false;
149   }
150
151   /**
152    * Returns the hash on which this branch is reference to.
153    * May be empty, if this information wasn't supplied to the GitBranch constructor.
154    */
155   @NotNull
156   public String getHash() {
157     return myHash;
158   }
159
160   /**
161    * Get current branch from Git.
162    *
163    * @param project a project
164    * @param root    vcs root
165    * @return the current branch or null if there is no current branch or if specific commit has been checked out.
166    * @deprecated Prefer {@link git4idea.repo.GitRepository#getCurrentBranch()} that caches the current branch value,
167    *             and for updating reads it from disk instead of spawning a Git process.
168    *             Note however, that {@link git4idea.repo.GitRepository#getCurrentBranch()} is updated asynchronously.
169    *             If you need to be absolutely sure, that you've got the right value at the moment,
170    *             call {@link git4idea.repo.GitRepository#update(git4idea.repo.GitRepository.TrackedTopic...)} before querying.
171    * @throws VcsException if there is a problem running git
172    */
173   @Deprecated
174   @Nullable
175   public static GitBranch current(Project project, VirtualFile root) throws VcsException {
176     return list(project, root, false, false, null, null);
177   }
178
179   /**
180    * List branches for the git root as strings.
181    * @deprecated Prefer {@link git4idea.repo.GitRepository#getBranches()} that caches branches,
182    *             and for updating reads them from disk instead of spawning a Git process.
183    *             Note however, that {@link git4idea.repo.GitRepository#getBranches()} is updated asynchronously.
184    *             If you need to be absolutely sure, that you've got the right value at the moment,
185    *             call {@link git4idea.repo.GitRepository#update(git4idea.repo.GitRepository.TrackedTopic...)} before querying.
186    * @see #list(com.intellij.openapi.project.Project, com.intellij.openapi.vfs.VirtualFile, boolean, boolean, java.util.Collection, String)
187    */
188   @Nullable
189   @Deprecated
190   public static GitBranch listAsStrings(final Project project, final VirtualFile root, final boolean remote, final boolean local,
191                                         final Collection<String> branches, @Nullable final String containingCommit) throws VcsException {
192     final Collection<GitBranch> gitBranches = new ArrayList<GitBranch>();
193     final GitBranch result = list(project, root, local, remote, gitBranches, containingCommit);
194     for (GitBranch b : gitBranches) {
195       branches.add(b.getName());
196     }
197     return result;
198   }
199
200   /**
201    * List branches in the repository. Supply a Collection to this method, and it will be filled by branches.
202    * @deprecated Prefer {@link git4idea.repo.GitRepository#getBranches()} that caches branches,
203    *             and for updating reads them from disk instead of spawning a Git process.
204    *             Note however, that {@link git4idea.repo.GitRepository#getBranches()} is updated asynchronously.
205    *             If you need to be absolutely sure, that you've got the right value at the moment,
206    *             call {@link git4idea.repo.GitRepository#update(git4idea.repo.GitRepository.TrackedTopic...)} before querying.
207    * @param project          the context project
208    * @param root             the git root
209    * @param localWanted      should local branches be collected.
210    * @param remoteWanted     should remote branches be collected.
211    * @param branches         the collection which will be used to store branches.
212    *                         Can be null - then the method does the same as {@link #current(com.intellij.openapi.project.Project, com.intellij.openapi.vfs.VirtualFile)}
213    * @param containingCommit show only branches which contain the specified commit. If null, no commit filtering is performed.
214    * @return current branch. May be null if no branch is active.
215    * @throws VcsException if there is a problem with running git
216    */
217   @Nullable
218   @Deprecated
219   public static GitBranch list(final Project project, final VirtualFile root, final boolean localWanted, final boolean remoteWanted,
220                                @Nullable final Collection<GitBranch> branches, @Nullable final String containingCommit) throws VcsException {
221     // preparing native command executor
222     final GitSimpleHandler handler = new GitSimpleHandler(project, root, GitCommand.BRANCH);
223     handler.setNoSSH(true);
224     handler.setSilent(true);
225     handler.addParameters("--no-color");
226     boolean remoteOnly = false;
227     if (remoteWanted && localWanted) {
228       handler.addParameters("-a");
229       remoteOnly = false;
230     } else if (remoteWanted) {
231       handler.addParameters("-r");
232       remoteOnly = true;
233     }
234     if (containingCommit != null) {
235       handler.addParameters("--contains", containingCommit);
236     }
237     final String output = handler.run();
238
239     if (output.trim().length() == 0) {
240       // the case after git init and before first commit - there is no branch and no output, and we'll take refs/heads/master
241       String head;
242       try {
243         head = FileUtil.loadFile(new File(root.getPath(), ".git/HEAD"), GitUtil.UTF8_ENCODING).trim();
244         final String prefix = "ref: refs/heads/";
245         return head.startsWith(prefix) ? new GitBranch(head.substring(prefix.length()), true, false) : null;
246       } catch (IOException e) {
247         return null;
248       }
249     }
250
251     // standard situation. output example:
252     //  master
253     //* my_feature
254     //  remotes/origin/HEAD -> origin/master
255     //  remotes/origin/eap
256     //  remotes/origin/feature
257     //  remotes/origin/master
258     // also possible:
259     //* (no branch)
260     // and if we call with -r instead of -a, remotes/ prefix is omitted:
261     // origin/HEAD -> origin/master
262     final String[] split = output.split("\n");
263     GitBranch currentBranch = null;
264     String activeRemoteName = null;
265     for (String b : split) {
266       boolean current = b.charAt(0) == '*';
267       b = b.substring(2).trim();
268       if (b.equals(NO_BRANCH_NAME)) { continue; }
269
270       String remotePrefix = null;
271       if (b.startsWith("remotes/")) {
272         remotePrefix = "remotes/";
273       } else if (b.startsWith(REFS_REMOTES_PREFIX)) {
274         remotePrefix = REFS_REMOTES_PREFIX;
275       }
276       boolean isRemote = remotePrefix != null || remoteOnly;
277       if (isRemote) {
278         if (! remoteOnly) {
279           b = b.substring(remotePrefix.length());
280         }
281         final int idx = b.indexOf("HEAD ->");
282         if (idx > 0) {
283           activeRemoteName = b.substring(idx + "HEAD ->".length() + (remotePrefix == null ? 0 : remotePrefix.length()));
284           continue;
285         }
286       }
287       final GitBranch branch = new GitBranch(b, current, isRemote);
288       if (current) {
289         currentBranch = branch;
290       }
291       if (branches != null && ((isRemote && remoteWanted) || (!isRemote && localWanted))) {
292         branches.add(branch);
293       }
294     }
295     if (activeRemoteName != null) {
296       for (GitBranch branch : branches) {
297         if (activeRemoteName.equals(branch.getName())) {
298           branch.setActive(true);
299           break;
300         }
301       }
302     }
303     return currentBranch;
304   }
305
306   /**
307    * Set tracked branch
308    *
309    * @param project the context project
310    * @param root    the git root
311    * @param remote  the remote to track (null, for do not track anything, "." for local repository)
312    * @param branch  the branch to track
313    */
314   public void setTrackedBranch(Project project, VirtualFile root, String remote, String branch) throws VcsException {
315     if (remote == null || branch == null) {
316       GitConfigUtil.unsetValue(project, root, trackedRemoteKey());
317       GitConfigUtil.unsetValue(project, root, trackedBranchKey());
318     }
319     else {
320       GitConfigUtil.setValue(project, root, trackedRemoteKey(), remote);
321       GitConfigUtil.setValue(project, root, trackedBranchKey(), branch);
322     }
323   }
324
325   /**
326    * @return the key for the remote of the tracked branch
327    */
328   private String trackedBranchKey() {
329     return "branch." + getName() + ".merge";
330   }
331
332   /**
333    * @return the key for the tracked branch
334    */
335   private String trackedRemoteKey() {
336     return "branch." + getName() + ".remote";
337   }
338
339   /**
340    * Get tracked branch for the current branch
341    *
342    * @param project the project
343    * @param root    the vcs root
344    * @return the tracked branch
345    * @throws VcsException if there is a problem with accessing configuration file
346    */
347   @Nullable
348   public GitBranch tracked(Project project, VirtualFile root) throws VcsException {
349     final HashMap<String, String> result = new HashMap<String, String>();
350     GitConfigUtil.getValues(project, root, null, result);
351     String remote = result.get(trackedRemoteKey());
352     if (remote == null) {
353       return null;
354     }
355     String branch = result.get(trackedBranchKey());
356     if (branch == null) {
357       return null;
358     }
359     if (branch.startsWith(REFS_HEADS_PREFIX)) {
360       branch = branch.substring(REFS_HEADS_PREFIX.length());
361     }
362     boolean remoteFlag;
363     if (!".".equals(remote)) {
364       branch = remote + "/" + branch;
365       remoteFlag = true;
366     }
367     else {
368       remoteFlag = false;
369     }
370     return new GitBranch(branch, false, remoteFlag);
371   }
372
373   /**
374    * Get a merge base between the current branch and specified branch.
375    *
376    * @param project the current project
377    * @param root    the vcs root
378    * @param branch  the branch
379    * @return the common commit or null if the there is no common commit
380    * @throws VcsException the exception
381    */
382   @Nullable
383   public GitRevisionNumber getMergeBase(@NotNull Project project, @NotNull VirtualFile root, @NotNull GitBranch branch)
384     throws VcsException {
385     return GitHistoryUtils.getMergeBase(project, root, this.getFullName(), branch.getFullName());
386   }
387
388   public void setActive(boolean active) {
389     myActive = active;
390   }
391 }