bf17f84d4c274d08837ce26ba91594d01b9aa92b
[idea/community.git] / plugins / git4idea / src / git4idea / GitUtil.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.components.ServiceManager;
19 import com.intellij.openapi.diagnostic.Logger;
20 import com.intellij.openapi.project.Project;
21 import com.intellij.openapi.util.Pair;
22 import com.intellij.openapi.util.io.FileUtil;
23 import com.intellij.openapi.util.text.StringUtil;
24 import com.intellij.openapi.vcs.FilePath;
25 import com.intellij.openapi.vcs.ProjectLevelVcsManager;
26 import com.intellij.openapi.vcs.VcsException;
27 import com.intellij.openapi.vcs.changes.Change;
28 import com.intellij.openapi.vcs.changes.ChangeListManager;
29 import com.intellij.openapi.vcs.changes.FilePathsHelper;
30 import com.intellij.openapi.vcs.vfs.AbstractVcsVirtualFile;
31 import com.intellij.openapi.vfs.LocalFileSystem;
32 import com.intellij.openapi.vfs.VirtualFile;
33 import com.intellij.util.Consumer;
34 import com.intellij.util.Function;
35 import com.intellij.vcsUtil.VcsFileUtil;
36 import com.intellij.vcsUtil.VcsUtil;
37 import git4idea.changes.GitChangeUtils;
38 import git4idea.changes.GitCommittedChangeList;
39 import git4idea.commands.GitCommand;
40 import git4idea.commands.GitHandler;
41 import git4idea.commands.GitSimpleHandler;
42 import git4idea.config.GitConfigUtil;
43 import git4idea.i18n.GitBundle;
44 import git4idea.repo.GitRemote;
45 import git4idea.repo.GitRepository;
46 import git4idea.repo.GitRepositoryManager;
47 import git4idea.util.StringScanner;
48 import org.jetbrains.annotations.NotNull;
49 import org.jetbrains.annotations.Nullable;
50
51 import java.io.File;
52 import java.io.IOException;
53 import java.io.UnsupportedEncodingException;
54 import java.nio.charset.Charset;
55 import java.util.*;
56
57 /**
58  * Git utility/helper methods
59  */
60 public class GitUtil {
61   /**
62    * Comparator for virtual files by name
63    */
64   public static final Comparator<VirtualFile> VIRTUAL_FILE_COMPARATOR = new Comparator<VirtualFile>() {
65     public int compare(final VirtualFile o1, final VirtualFile o2) {
66       if (o1 == null && o2 == null) {
67         return 0;
68       }
69       if (o1 == null) {
70         return -1;
71       }
72       if (o2 == null) {
73         return 1;
74       }
75       return o1.getPresentableUrl().compareTo(o2.getPresentableUrl());
76     }
77   };
78   /**
79    * The UTF-8 encoding name
80    */
81   public static final String UTF8_ENCODING = "UTF-8";
82   /**
83    * The UTF8 charset
84    */
85   public static final Charset UTF8_CHARSET = Charset.forName(UTF8_ENCODING);
86   public static final String DOT_GIT = ".git";
87
88   private final static Logger LOG = Logger.getInstance(GitUtil.class);
89   private static final int SHORT_HASH_LENGTH = 8;
90
91   /**
92    * A private constructor to suppress instance creation
93    */
94   private GitUtil() {
95     // do nothing
96   }
97
98   @Nullable
99   public static VirtualFile findGitDir(@NotNull VirtualFile rootDir) {
100     VirtualFile child = rootDir.findChild(DOT_GIT);
101     if (child == null) {
102       return null;
103     }
104     if (child.isDirectory()) {
105       return child;
106     }
107
108     // this is standard for submodules, although probably it can
109     String content;
110     try {
111       content = readFile(child);
112     }
113     catch (IOException e) {
114       throw new RuntimeException("Couldn't read " + child, e);
115     }
116     String pathToDir;
117     String prefix = "gitdir:";
118     if (content.startsWith(prefix)) {
119       pathToDir = content.substring(prefix.length()).trim();
120     }
121     else {
122       pathToDir = content;
123     }
124
125     if (!FileUtil.isAbsolute(pathToDir)) {
126       String canonicalPath = FileUtil.toCanonicalPath(FileUtil.join(rootDir.getPath(), pathToDir));
127       if (canonicalPath == null) {
128         return null;
129       }
130       pathToDir = FileUtil.toSystemIndependentName(canonicalPath);
131     }
132     return VcsUtil.getVirtualFile(pathToDir);
133   }
134
135   /**
136    * Makes 3 attempts to get the contents of the file. If all 3 fail with an IOException, rethrows the exception.
137    */
138   @NotNull
139   public static String readFile(@NotNull VirtualFile file) throws IOException {
140     final int ATTEMPTS = 3;
141     for (int attempt = 0; attempt < ATTEMPTS; attempt++) {
142       try {
143         return new String(file.contentsToByteArray());
144       }
145       catch (IOException e) {
146         LOG.info(String.format("IOException while reading %s (attempt #%s)", file, attempt));
147         if (attempt >= ATTEMPTS - 1) {
148           throw e;
149         }
150       }
151     }
152     throw new AssertionError("Shouldn't get here. Couldn't read " + file);
153   }
154
155   /**
156    * Sort files by Git root
157    *
158    * @param virtualFiles files to sort
159    * @return sorted files
160    * @throws VcsException if non git files are passed
161    */
162   @NotNull
163   public static Map<VirtualFile, List<VirtualFile>> sortFilesByGitRoot(@NotNull Collection<VirtualFile> virtualFiles) throws VcsException {
164     return sortFilesByGitRoot(virtualFiles, false);
165   }
166
167   /**
168    * Sort files by Git root
169    *
170    * @param virtualFiles files to sort
171    * @param ignoreNonGit if true, non-git files are ignored
172    * @return sorted files
173    * @throws VcsException if non git files are passed when {@code ignoreNonGit} is false
174    */
175   public static Map<VirtualFile, List<VirtualFile>> sortFilesByGitRoot(Collection<VirtualFile> virtualFiles, boolean ignoreNonGit)
176     throws VcsException {
177     Map<VirtualFile, List<VirtualFile>> result = new HashMap<VirtualFile, List<VirtualFile>>();
178     for (VirtualFile file : virtualFiles) {
179       final VirtualFile vcsRoot = gitRootOrNull(file);
180       if (vcsRoot == null) {
181         if (ignoreNonGit) {
182           continue;
183         }
184         else {
185           throw new VcsException("The file " + file.getPath() + " is not under Git");
186         }
187       }
188       List<VirtualFile> files = result.get(vcsRoot);
189       if (files == null) {
190         files = new ArrayList<VirtualFile>();
191         result.put(vcsRoot, files);
192       }
193       files.add(file);
194     }
195     return result;
196   }
197
198   /**
199    * Sort files by vcs root
200    *
201    * @param files files to sort.
202    * @return the map from root to the files under the root
203    * @throws VcsException if non git files are passed
204    */
205   public static Map<VirtualFile, List<FilePath>> sortFilePathsByGitRoot(final Collection<FilePath> files) throws VcsException {
206     return sortFilePathsByGitRoot(files, false);
207   }
208
209   /**
210    * Sort files by vcs root
211    *
212    * @param files files to sort.
213    * @return the map from root to the files under the root
214    */
215   public static Map<VirtualFile, List<FilePath>> sortGitFilePathsByGitRoot(Collection<FilePath> files) {
216     try {
217       return sortFilePathsByGitRoot(files, true);
218     }
219     catch (VcsException e) {
220       throw new RuntimeException("Unexpected exception:", e);
221     }
222   }
223
224
225   /**
226    * Sort files by vcs root
227    *
228    * @param files        files to sort.
229    * @param ignoreNonGit if true, non-git files are ignored
230    * @return the map from root to the files under the root
231    * @throws VcsException if non git files are passed when {@code ignoreNonGit} is false
232    */
233   public static Map<VirtualFile, List<FilePath>> sortFilePathsByGitRoot(Collection<FilePath> files, boolean ignoreNonGit)
234     throws VcsException {
235     Map<VirtualFile, List<FilePath>> rc = new HashMap<VirtualFile, List<FilePath>>();
236     for (FilePath p : files) {
237       VirtualFile root = getGitRootOrNull(p);
238       if (root == null) {
239         if (ignoreNonGit) {
240           continue;
241         }
242         else {
243           throw new VcsException("The file " + p.getPath() + " is not under Git");
244         }
245       }
246       List<FilePath> l = rc.get(root);
247       if (l == null) {
248         l = new ArrayList<FilePath>();
249         rc.put(root, l);
250       }
251       l.add(p);
252     }
253     return rc;
254   }
255
256   /**
257    * Parse UNIX timestamp as it is returned by the git
258    *
259    * @param value a value to parse
260    * @return timestamp as {@link Date} object
261    */
262   private static Date parseTimestamp(String value) {
263     final long parsed;
264     parsed = Long.parseLong(value.trim());
265     return new Date(parsed * 1000);
266   }
267
268   /**
269    * Parse UNIX timestamp returned from Git and handle {@link NumberFormatException} if one happens: return new {@link Date} and
270    * log the error properly.
271    * In some cases git output gets corrupted and this method is intended to catch the reason, why.
272    * @param value      Value to parse.
273    * @param handler    Git handler that was called to received the output.
274    * @param gitOutput  Git output.
275    * @return Parsed Date or <code>new Date</code> in the case of error.
276    */
277   public static Date parseTimestampWithNFEReport(String value, GitHandler handler, String gitOutput) {
278     try {
279       return parseTimestamp(value);
280     } catch (NumberFormatException e) {
281       LOG.error("annotate(). NFE. Handler: " + handler + ". Output: " + gitOutput, e);
282       return  new Date();
283     }
284   }
285
286   /**
287    * Get git roots from content roots
288    *
289    * @param roots git content roots
290    * @return a content root
291    */
292   public static Set<VirtualFile> gitRootsForPaths(final Collection<VirtualFile> roots) {
293     HashSet<VirtualFile> rc = new HashSet<VirtualFile>();
294     for (VirtualFile root : roots) {
295       VirtualFile f = root;
296       do {
297         if (f.findFileByRelativePath(DOT_GIT) != null) {
298           rc.add(f);
299           break;
300         }
301         f = f.getParent();
302       }
303       while (f != null);
304     }
305     return rc;
306   }
307
308   /**
309    * Return a git root for the file path (the parent directory with ".git" subdirectory)
310    *
311    * @param filePath a file path
312    * @return git root for the file
313    * @throws IllegalArgumentException if the file is not under git
314    * @throws VcsException             if the file is not under git
315    *
316    * @deprecated because uses the java.io.File.
317    * @use GitRepositoryManager#getRepositoryForFile().
318    */
319   public static VirtualFile getGitRoot(final FilePath filePath) throws VcsException {
320     VirtualFile root = getGitRootOrNull(filePath);
321     if (root != null) {
322       return root;
323     }
324     throw new VcsException("The file " + filePath + " is not under git.");
325   }
326
327   /**
328    * Return a git root for the file path (the parent directory with ".git" subdirectory)
329    *
330    * @param filePath a file path
331    * @return git root for the file or null if the file is not under git
332    *
333    * @deprecated because uses the java.io.File.
334    * @use GitRepositoryManager#getRepositoryForFile().
335    */
336   @Deprecated
337   @Nullable
338   public static VirtualFile getGitRootOrNull(final FilePath filePath) {
339     return getGitRootOrNull(filePath.getIOFile());
340   }
341
342   public static boolean isGitRoot(final File file) {
343     return file != null && file.exists() && file.isDirectory() && new File(file, DOT_GIT).exists();
344   }
345
346   /**
347    * @deprecated because uses the java.io.File.
348    * @use GitRepositoryManager#getRepositoryForFile().
349    */
350   @Deprecated
351   @Nullable
352   public static VirtualFile getGitRootOrNull(final File file) {
353     File root = file;
354     while (root != null && (!root.exists() || !root.isDirectory() || !new File(root, DOT_GIT).exists())) {
355       root = root.getParentFile();
356     }
357     return root == null ? null : LocalFileSystem.getInstance().findFileByIoFile(root);
358   }
359
360   /**
361    * Return a git root for the file (the parent directory with ".git" subdirectory)
362    *
363    * @param file the file to check
364    * @return git root for the file
365    * @throws VcsException if the file is not under git
366    *
367    * @deprecated because uses the java.io.File.
368    * @use GitRepositoryManager#getRepositoryForFile().
369    */
370   public static VirtualFile getGitRoot(@NotNull final VirtualFile file) throws VcsException {
371     final VirtualFile root = gitRootOrNull(file);
372     if (root != null) {
373       return root;
374     }
375     else {
376       throw new VcsException("The file " + file.getPath() + " is not under git.");
377     }
378   }
379
380   /**
381    * Return a git root for the file (the parent directory with ".git" subdirectory)
382    *
383    * @param file the file to check
384    * @return git root for the file or null if the file is not not under Git
385    *
386    * @deprecated because uses the java.io.File.
387    * @use GitRepositoryManager#getRepositoryForFile().
388    */
389   @Nullable
390   public static VirtualFile gitRootOrNull(final VirtualFile file) {
391     if (file instanceof AbstractVcsVirtualFile) {
392       return getGitRootOrNull(VcsUtil.getFilePath(file.getPath()));
393     }
394     VirtualFile root = file;
395     while (root != null) {
396       if (root.findFileByRelativePath(DOT_GIT) != null) {
397         return root;
398       }
399       root = root.getParent();
400     }
401     return root;
402   }
403
404   /**
405    * Get git roots for the project. The method shows dialogs in the case when roots cannot be retrieved, so it should be called
406    * from the event dispatch thread.
407    *
408    * @param project the project
409    * @param vcs     the git Vcs
410    * @return the list of the roots
411    *
412    * @deprecated because uses the java.io.File.
413    * @use GitRepositoryManager#getRepositoryForFile().
414    */
415   @NotNull
416   public static List<VirtualFile> getGitRoots(Project project, GitVcs vcs) throws VcsException {
417     final VirtualFile[] contentRoots = ProjectLevelVcsManager.getInstance(project).getRootsUnderVcs(vcs);
418     if (contentRoots == null || contentRoots.length == 0) {
419       throw new VcsException(GitBundle.getString("repository.action.missing.roots.unconfigured.message"));
420     }
421     final List<VirtualFile> roots = new ArrayList<VirtualFile>(gitRootsForPaths(Arrays.asList(contentRoots)));
422     if (roots.size() == 0) {
423       throw new VcsException(GitBundle.getString("repository.action.missing.roots.misconfigured"));
424     }
425     Collections.sort(roots, VIRTUAL_FILE_COMPARATOR);
426     return roots;
427   }
428
429
430   /**
431    * Check if the virtual file under git
432    *
433    * @param vFile a virtual file
434    * @return true if the file is under git
435    */
436   public static boolean isUnderGit(final VirtualFile vFile) {
437     return gitRootOrNull(vFile) != null;
438   }
439
440
441   /**
442    * Return committer name based on author name and committer name
443    *
444    * @param authorName    the name of author
445    * @param committerName the name of committer
446    * @return just a name if they are equal, or name that includes both author and committer
447    */
448   public static String adjustAuthorName(final String authorName, String committerName) {
449     if (!authorName.equals(committerName)) {
450       //noinspection HardCodedStringLiteral
451       committerName = authorName + ", via " + committerName;
452     }
453     return committerName;
454   }
455
456   /**
457    * Check if the file path is under git
458    *
459    * @param path the path
460    * @return true if the file path is under git
461    */
462   public static boolean isUnderGit(final FilePath path) {
463     return getGitRootOrNull(path) != null;
464   }
465
466   /**
467    * Get git roots for the selected paths
468    *
469    * @param filePaths the context paths
470    * @return a set of git roots
471    */
472   public static Set<VirtualFile> gitRoots(final Collection<FilePath> filePaths) {
473     HashSet<VirtualFile> rc = new HashSet<VirtualFile>();
474     for (FilePath path : filePaths) {
475       final VirtualFile root = getGitRootOrNull(path);
476       if (root != null) {
477         rc.add(root);
478       }
479     }
480     return rc;
481   }
482
483   /**
484    * Get git time (UNIX time) basing on the date object
485    *
486    * @param time the time to convert
487    * @return the time in git format
488    */
489   public static String gitTime(Date time) {
490     long t = time.getTime() / 1000;
491     return Long.toString(t);
492   }
493
494   /**
495    * Format revision number from long to 16-digit abbreviated revision
496    *
497    * @param rev the abbreviated revision number as long
498    * @return the revision string
499    */
500   public static String formatLongRev(long rev) {
501     return String.format("%015x%x", (rev >>> 4), rev & 0xF);
502   }
503
504   public static void getLocalCommittedChanges(final Project project,
505                                               final VirtualFile root,
506                                               final Consumer<GitSimpleHandler> parametersSpecifier,
507                                               final Consumer<GitCommittedChangeList> consumer, boolean skipDiffsForMerge) throws VcsException {
508     GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.LOG);
509     h.setSilent(true);
510     h.setNoSSH(true);
511     h.addParameters("--pretty=format:%x04%x01" + GitChangeUtils.COMMITTED_CHANGELIST_FORMAT, "--name-status");
512     parametersSpecifier.consume(h);
513
514     String output = h.run();
515     LOG.debug("getLocalCommittedChanges output: '" + output + "'");
516     StringScanner s = new StringScanner(output);
517     final StringBuilder sb = new StringBuilder();
518     boolean firstStep = true;
519     while (s.hasMoreData()) {
520       final String line = s.line();
521       final boolean lineIsAStart = line.startsWith("\u0004\u0001");
522       if ((!firstStep) && lineIsAStart) {
523         final StringScanner innerScanner = new StringScanner(sb.toString());
524         sb.setLength(0);
525         consumer.consume(GitChangeUtils.parseChangeList(project, root, innerScanner, skipDiffsForMerge, h, false, false));
526       }
527       sb.append(lineIsAStart ? line.substring(2) : line).append('\n');
528       firstStep = false;
529     }
530     if (sb.length() > 0) {
531       final StringScanner innerScanner = new StringScanner(sb.toString());
532       sb.setLength(0);
533       consumer.consume(GitChangeUtils.parseChangeList(project, root, innerScanner, skipDiffsForMerge, h, false, false));
534     }
535     if (s.hasMoreData()) {
536       throw new IllegalStateException("More input is avaialble: " + s.line());
537     }
538   }
539
540   public static List<GitCommittedChangeList> getLocalCommittedChanges(final Project project,
541                                                                    final VirtualFile root,
542                                                                    final Consumer<GitSimpleHandler> parametersSpecifier)
543     throws VcsException {
544     final List<GitCommittedChangeList> rc = new ArrayList<GitCommittedChangeList>();
545
546     getLocalCommittedChanges(project, root, parametersSpecifier, new Consumer<GitCommittedChangeList>() {
547       public void consume(GitCommittedChangeList committedChangeList) {
548         rc.add(committedChangeList);
549       }
550     }, false);
551
552     return rc;
553   }
554
555   /**
556    * <p>Unescape path returned by Git.</p>
557    * <p>
558    *   If there are quotes in the file name, Git not only escapes them, but also encloses the file name into quotes:
559    *   <code>"\"quote"</code>
560    * </p>
561    * <p>
562    *   If there are spaces in the file name, Git displays the name as is, without escaping spaces and without enclosing name in quotes.
563    * </p>
564    *
565    * @param path a path to unescape
566    * @return unescaped path ready to be searched in the VFS or file system.
567    * @throws com.intellij.openapi.vcs.VcsException if the path in invalid
568    */
569   @NotNull
570   public static String unescapePath(@NotNull String path) throws VcsException {
571     final String QUOTE = "\"";
572     if (path.startsWith(QUOTE) && path.endsWith(QUOTE)) {
573       path = path.substring(1, path.length() - 1);
574     }
575
576     final int l = path.length();
577     StringBuilder rc = new StringBuilder(l);
578     for (int i = 0; i < path.length(); i++) {
579       char c = path.charAt(i);
580       if (c == '\\') {
581         //noinspection AssignmentToForLoopParameter
582         i++;
583         if (i >= l) {
584           throw new VcsException("Unterminated escape sequence in the path: " + path);
585         }
586         final char e = path.charAt(i);
587         switch (e) {
588           case '\\':
589             rc.append('\\');
590             break;
591           case 't':
592             rc.append('\t');
593             break;
594           case 'n':
595             rc.append('\n');
596             break;
597           case '"':
598             rc.append('"');
599             break;
600           default:
601             if (VcsFileUtil.isOctal(e)) {
602               // collect sequence of characters as a byte array.
603               // count bytes first
604               int n = 0;
605               for (int j = i; j < l;) {
606                 if (VcsFileUtil.isOctal(path.charAt(j))) {
607                   n++;
608                   for (int k = 0; k < 3 && j < l && VcsFileUtil.isOctal(path.charAt(j)); k++) {
609                     //noinspection AssignmentToForLoopParameter
610                     j++;
611                   }
612                 }
613                 if (j + 1 >= l || path.charAt(j) != '\\' || !VcsFileUtil.isOctal(path.charAt(j + 1))) {
614                   break;
615                 }
616                 //noinspection AssignmentToForLoopParameter
617                 j++;
618               }
619               // convert to byte array
620               byte[] b = new byte[n];
621               n = 0;
622               while (i < l) {
623                 if (VcsFileUtil.isOctal(path.charAt(i))) {
624                   int code = 0;
625                   for (int k = 0; k < 3 && i < l && VcsFileUtil.isOctal(path.charAt(i)); k++) {
626                     code = code * 8 + (path.charAt(i) - '0');
627                     //noinspection AssignmentToForLoopParameter
628                     i++;
629                   }
630                   b[n++] = (byte)code;
631                 }
632                 if (i + 1 >= l || path.charAt(i) != '\\' || !VcsFileUtil.isOctal(path.charAt(i + 1))) {
633                   break;
634                 }
635                 //noinspection AssignmentToForLoopParameter
636                 i++;
637               }
638               assert n == b.length;
639               // add them to string
640               final String encoding = GitConfigUtil.getFileNameEncoding();
641               try {
642                 rc.append(new String(b, encoding));
643               }
644               catch (UnsupportedEncodingException e1) {
645                 throw new IllegalStateException("The file name encoding is unsuported: " + encoding);
646               }
647             }
648             else {
649               throw new VcsException("Unknown escape sequence '\\" + path.charAt(i) + "' in the path: " + path);
650             }
651         }
652       }
653       else {
654         rc.append(c);
655       }
656     }
657     return rc.toString();
658   }
659   
660   public static boolean justOneGitRepository(Project project) {
661     if (project.isDisposed()) {
662       return true;
663     }
664     GitRepositoryManager manager = getRepositoryManager(project);
665     if (manager == null) {
666       return true;
667     }
668     return !manager.moreThanOneRoot();
669   }
670
671   public static List<GitRepository> sortRepositories(@NotNull Collection<GitRepository> repositories) {
672     List<GitRepository> repos = new ArrayList<GitRepository>(repositories);
673     Collections.sort(repos, new Comparator<GitRepository>() {
674       @Override public int compare(GitRepository o1, GitRepository o2) {
675         return o1.getPresentableUrl().compareTo(o2.getPresentableUrl());
676       }
677     });
678     return repos;
679   }
680
681   @Nullable
682   public static GitRemote findRemoteByName(@NotNull GitRepository repository, @Nullable String name) {
683     if (name == null) {
684       return null;
685     }
686     for (GitRemote remote : repository.getRemotes()) {
687       if (remote.getName().equals(name)) {
688         return remote;
689       }
690     }
691     return null;
692   }
693
694   @Nullable
695   public static Pair<GitRemote, GitBranch> findMatchingRemoteBranch(GitRepository repository, GitBranch branch) throws VcsException {
696     /*
697     from man git-push:
698     git push
699                Works like git push <remote>, where <remote> is the current branch's remote (or origin, if no
700                remote is configured for the current branch).
701
702      */
703     String remoteName = branch.getTrackedRemoteName(repository.getProject(), repository.getRoot());
704     GitRemote remote;
705     if (remoteName == null) {
706       remote = findOrigin(repository.getRemotes());
707     } else {
708       remote = findRemoteByName(repository, remoteName);
709     }
710     if (remote == null) {
711       return null;
712     }
713
714     for (GitBranch remoteBranch : repository.getBranches().getRemoteBranches()) {
715       if (remoteBranch.getName().equals(remote.getName() + "/" + branch.getName())) {
716         return Pair.create(remote, remoteBranch);
717       }
718     }
719     return null;
720   }
721
722   @Nullable
723   private static GitRemote findOrigin(Collection<GitRemote> remotes) {
724     for (GitRemote remote : remotes) {
725       if (remote.getName().equals("origin")) {
726         return remote;
727       }
728     }
729     return null;
730   }
731
732   public static boolean repoContainsRemoteBranch(@NotNull GitRepository repository, @NotNull GitBranch dest) {
733     return repository.getBranches().getRemoteBranches().contains(dest);
734   }
735
736   /**
737    * Convert {@link GitBranch GitBranches} to their names, and remove remote HEAD pointers.
738    */
739   @NotNull
740   public static Collection<String> getBranchNamesWithoutRemoteHead(@NotNull Collection<GitBranch> branches) {
741     Collection<String> names = new ArrayList<String>(branches.size());
742     for (GitBranch branch : branches) {
743       if (!branch.isRemote() || !branch.getShortName().equals("HEAD")) {
744         names.add(branch.getName());
745       }
746     }
747     return names;
748   }
749
750   @NotNull
751   public static Collection<VirtualFile> getRootsFromRepositories(@NotNull Collection<GitRepository> repositories) {
752     Collection<VirtualFile> roots = new ArrayList<VirtualFile>(repositories.size());
753     for (GitRepository repository : repositories) {
754       roots.add(repository.getRoot());
755     }
756     return roots;
757   }
758
759   @NotNull
760   public static Collection<GitRepository> getRepositoriesFromRoots(@NotNull GitRepositoryManager repositoryManager,
761                                                                    @NotNull Collection<VirtualFile> roots) {
762     Collection<GitRepository> repositories = new ArrayList<GitRepository>(roots.size());
763     for (VirtualFile root : roots) {
764       GitRepository repo = repositoryManager.getRepositoryForRoot(root);
765       if (repo == null) {
766         LOG.error("Repository not found for root " + root);
767       }
768       else {
769         repositories.add(repo);
770       }
771     }
772     return repositories;
773   }
774
775   /**
776    * Returns absolute paths which have changed remotely comparing to the current branch, i.e. performs
777    * <code>git diff --name-only master..origin/master</code>
778    */
779   @NotNull
780   public static Collection<String> getPathsDiffBetweenRefs(@NotNull String beforeRef, @NotNull String afterRef, @NotNull Project project,
781                                                            @NotNull VirtualFile root) throws VcsException {
782     final GitSimpleHandler diff = new GitSimpleHandler(project, root, GitCommand.DIFF);
783     diff.addParameters("--name-only", "--pretty=format:");
784     diff.addParameters(beforeRef + ".." + afterRef);
785     diff.setNoSSH(true);
786     diff.setStdoutSuppressed(true);
787     diff.setStderrSuppressed(true);
788     diff.setSilent(true);
789     final String output = diff.run();
790
791     final Collection<String> remoteChanges = new HashSet<String>();
792     for (StringScanner s = new StringScanner(output); s.hasMoreData();) {
793       final String relative = s.line();
794       if (StringUtil.isEmptyOrSpaces(relative)) {
795         continue;
796       }
797       final String path = root.getPath() + "/" + unescapePath(relative);
798       remoteChanges.add(FilePathsHelper.convertPath(path));
799     }
800     return remoteChanges;
801   }
802
803   /**
804    * Given the list of paths converts them to the list of {@link Change Changes} found in the {@link ChangeListManager},
805    * i.e. this works only for local changes.
806    * Paths can be absolute or relative to the repository.
807    * If a path is not in the local changes, it is ignored.
808    */
809   @NotNull
810   public static List<Change> convertPathsToChanges(@NotNull GitRepository repository,
811                                                    @NotNull Collection<String> affectedPaths, boolean relativePaths) {
812     ChangeListManager changeListManager = ChangeListManager.getInstance(repository.getProject());
813     List<Change> affectedChanges = new ArrayList<Change>();
814     for (String path : affectedPaths) {
815       VirtualFile file;
816       if (relativePaths) {
817         file = repository.getRoot().findFileByRelativePath(FileUtil.toSystemIndependentName(path));
818       }
819       else {
820         file = VcsUtil.getVirtualFile(path);
821       }
822
823       if (file != null) {
824         Change change = changeListManager.getChange(file);
825         if (change != null) {
826           affectedChanges.add(change);
827         }
828       }
829     }
830     return affectedChanges;
831   }
832
833   @NotNull
834   public static GitRepositoryManager getRepositoryManager(@NotNull Project project) {
835     return ServiceManager.getService(project, GitRepositoryManager.class);
836   }
837
838   @NotNull
839   public static String getPrintableRemotes(@NotNull Collection<GitRemote> remotes) {
840     return StringUtil.join(remotes, new Function<GitRemote, String>() {
841       @Override
842       public String fun(GitRemote remote) {
843         return remote.getName() + ": [" + StringUtil.join(remote.getUrls(), ", ") + "]";
844       }
845     }, "\n");
846   }
847
848   @NotNull
849   public static String getShortHash(@NotNull String hash) {
850     if (hash.length() == 0) return "";
851     if (hash.length() == 40) return hash.substring(0, SHORT_HASH_LENGTH);
852     if (hash.length() > 40)  // revision string encoded with date too
853     {
854       return hash.substring(hash.indexOf("[") + 1, SHORT_HASH_LENGTH);
855     }
856     return hash;
857   }
858
859   @NotNull
860   public static String fileOrFolder(@NotNull VirtualFile file) {
861     if (file.isDirectory()) {
862       return "Folder";
863     }
864     else {
865       return "File";
866     }
867   }
868 }