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