replaced <code></code> with more concise {@code}
[idea/community.git] / plugins / git4idea / src / git4idea / GitUtil.java
1 /*
2  * Copyright 2000-2016 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.google.common.base.Predicates;
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.progress.ProgressIndicator;
23 import com.intellij.openapi.progress.Task;
24 import com.intellij.openapi.project.Project;
25 import com.intellij.openapi.ui.DialogBuilder;
26 import com.intellij.openapi.ui.ex.MultiLineLabel;
27 import com.intellij.openapi.util.io.FileUtil;
28 import com.intellij.openapi.util.text.StringUtil;
29 import com.intellij.openapi.vcs.AbstractVcsHelper;
30 import com.intellij.openapi.vcs.FilePath;
31 import com.intellij.openapi.vcs.ProjectLevelVcsManager;
32 import com.intellij.openapi.vcs.VcsException;
33 import com.intellij.openapi.vcs.changes.Change;
34 import com.intellij.openapi.vcs.changes.ChangeListManager;
35 import com.intellij.openapi.vcs.changes.ChangeListManagerEx;
36 import com.intellij.openapi.vcs.changes.ContentRevision;
37 import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
38 import com.intellij.openapi.vfs.CharsetToolkit;
39 import com.intellij.openapi.vfs.LocalFileSystem;
40 import com.intellij.openapi.vfs.VirtualFile;
41 import com.intellij.util.Consumer;
42 import com.intellij.util.Function;
43 import com.intellij.util.ObjectUtils;
44 import com.intellij.util.containers.ContainerUtil;
45 import com.intellij.util.containers.OpenTHashSet;
46 import com.intellij.util.ui.UIUtil;
47 import com.intellij.vcsUtil.VcsFileUtil;
48 import com.intellij.vcsUtil.VcsUtil;
49 import git4idea.branch.GitBranchUtil;
50 import git4idea.changes.GitChangeUtils;
51 import git4idea.changes.GitCommittedChangeList;
52 import git4idea.commands.*;
53 import git4idea.config.GitConfigUtil;
54 import git4idea.i18n.GitBundle;
55 import git4idea.repo.GitBranchTrackInfo;
56 import git4idea.repo.GitRemote;
57 import git4idea.repo.GitRepository;
58 import git4idea.repo.GitRepositoryManager;
59 import git4idea.util.GitSimplePathsBrowser;
60 import git4idea.util.GitUIUtil;
61 import git4idea.util.StringScanner;
62 import org.jetbrains.annotations.NotNull;
63 import org.jetbrains.annotations.Nullable;
64
65 import java.io.File;
66 import java.io.IOException;
67 import java.io.UnsupportedEncodingException;
68 import java.util.*;
69 import java.util.stream.Collectors;
70
71 import static com.intellij.dvcs.DvcsUtil.getShortRepositoryName;
72 import static com.intellij.dvcs.DvcsUtil.joinShortNames;
73 import static com.intellij.util.ObjectUtils.assertNotNull;
74
75 /**
76  * Git utility/helper methods
77  */
78 public class GitUtil {
79
80   public static final String DOT_GIT = ".git";
81
82   public static final String ORIGIN_HEAD = "origin/HEAD";
83   public static final String GRAFTED = "grafted";
84   public static final String REPLACED = "replaced";
85
86   public static final Function<GitRepository, VirtualFile> REPOSITORY_TO_ROOT = repository -> repository.getRoot();
87
88   public static final String HEAD = "HEAD";
89   public static final String CHERRY_PICK_HEAD = "CHERRY_PICK_HEAD";
90   public static final String MERGE_HEAD = "MERGE_HEAD";
91
92   private static final String REPO_PATH_LINK_PREFIX = "gitdir:";
93   private final static Logger LOG = Logger.getInstance(GitUtil.class);
94   private static final String HEAD_FILE = "HEAD";
95
96   /**
97    * A private constructor to suppress instance creation
98    */
99   private GitUtil() {
100     // do nothing
101   }
102
103   /**
104    * Returns the Git repository location for the given repository root directory, or null if nothing can be found.
105    * Able to find the real repository root of a submodule or of a working tree.
106    * @see #findGitDir(VirtualFile) 
107    */
108   @Nullable
109   public static File findGitDir(@NotNull File rootDir) {
110     File dotGit = new File(rootDir, DOT_GIT);
111     if (!dotGit.exists()) return null;
112     if (dotGit.isDirectory()) {
113       boolean headExists = new File(dotGit, HEAD_FILE).exists();
114       return headExists ? dotGit : null;
115     }
116
117     String content = DvcsUtil.tryLoadFileOrReturn(dotGit, null, CharsetToolkit.UTF8);
118     if (content == null) return null;
119     String pathToDir = parsePathToRepository(content);
120     return findRealRepositoryDir(rootDir.getPath(), pathToDir);
121   }
122
123   /**
124    * Returns the Git repository location for the given repository root directory, or null if nothing can be found.
125    * Able to find the real repository root of a submodule or of a working tree.
126    * <p/>
127    * More precisely: checks if there is {@code .git} directory or file directly under rootDir. <br/>
128    * If there is a directory, performs a quick check that it looks like a Git repository;<br/>
129    * if it is a file, follows the path written inside this file to find the actual repo dir.
130    */
131   @Nullable
132   public static VirtualFile findGitDir(@NotNull VirtualFile rootDir) {
133     VirtualFile dotGit = rootDir.findChild(DOT_GIT);
134     if (dotGit == null) {
135       return null;
136     }
137     if (dotGit.isDirectory()) {
138       boolean headExists = dotGit.findChild(HEAD_FILE) != null;
139       return headExists ? dotGit : null;
140     }
141
142     // if .git is a file with some specific content, it indicates a submodule or a working tree, with a link to the real repository path
143     String content = readContent(dotGit);
144     if (content == null) return null;
145     String pathToDir = parsePathToRepository(content);
146     File file = findRealRepositoryDir(rootDir.getPath(), pathToDir);
147     if (file == null) return null;
148     return VcsUtil.getVirtualFileWithRefresh(file);
149   }
150
151   @Nullable
152   private static File findRealRepositoryDir(@NotNull String rootPath, @NotNull String path) {
153     if (!FileUtil.isAbsolute(path)) {
154       String canonicalPath = FileUtil.toCanonicalPath(FileUtil.join(rootPath, path), true);
155       if (canonicalPath == null) {
156         return null;
157       }
158       path = FileUtil.toSystemIndependentName(canonicalPath);
159     }
160     File file = new File(path);
161     return file.isDirectory() ? file : null;
162   }
163
164   @NotNull
165   private static String parsePathToRepository(@NotNull String content) {
166     content = content.trim();
167     return content.startsWith(REPO_PATH_LINK_PREFIX) ? content.substring(REPO_PATH_LINK_PREFIX.length()).trim() : content;
168   }
169
170   @Nullable
171   private static String readContent(@NotNull VirtualFile dotGit) {
172     String content;
173     try {
174       content = readFile(dotGit);
175     }
176     catch (IOException e) {
177       LOG.error("Couldn't read the content of " + dotGit, e);
178       return null;
179     }
180     return content;
181   }
182
183   /**
184    * Makes 3 attempts to get the contents of the file. If all 3 fail with an IOException, rethrows the exception.
185    */
186   @NotNull
187   public static String readFile(@NotNull VirtualFile file) throws IOException {
188     final int ATTEMPTS = 3;
189     for (int attempt = 0; attempt < ATTEMPTS; attempt++) {
190       try {
191         return new String(file.contentsToByteArray());
192       }
193       catch (IOException e) {
194         LOG.info(String.format("IOException while reading %s (attempt #%s)", file, attempt));
195         if (attempt >= ATTEMPTS - 1) {
196           throw e;
197         }
198       }
199     }
200     throw new AssertionError("Shouldn't get here. Couldn't read " + file);
201   }
202
203   /**
204    * Sort files by Git root
205    *
206    * @param virtualFiles files to sort
207    * @return sorted files
208    * @throws VcsException if non git files are passed
209    */
210   @NotNull
211   public static Map<VirtualFile, List<VirtualFile>> sortFilesByGitRoot(@NotNull Collection<VirtualFile> virtualFiles) throws VcsException {
212     return sortFilesByGitRoot(virtualFiles, false);
213   }
214
215   /**
216    * Sort files by Git root
217    *
218    * @param virtualFiles files to sort
219    * @param ignoreNonGit if true, non-git files are ignored
220    * @return sorted files
221    * @throws VcsException if non git files are passed when {@code ignoreNonGit} is false
222    */
223   public static Map<VirtualFile, List<VirtualFile>> sortFilesByGitRoot(Collection<VirtualFile> virtualFiles, boolean ignoreNonGit)
224     throws VcsException {
225     Map<VirtualFile, List<VirtualFile>> result = new HashMap<>();
226     for (VirtualFile file : virtualFiles) {
227       // directory is reported only when it is a submodule or a mistakenly non-ignored nested root
228       // => it should be treated in the context of super-root
229       final VirtualFile vcsRoot = gitRootOrNull(file.isDirectory() ? file.getParent() : file);
230       if (vcsRoot == null) {
231         if (ignoreNonGit) {
232           continue;
233         }
234         else {
235           throw new VcsException("The file " + file.getPath() + " is not under Git");
236         }
237       }
238       List<VirtualFile> files = result.get(vcsRoot);
239       if (files == null) {
240         files = new ArrayList<>();
241         result.put(vcsRoot, files);
242       }
243       files.add(file);
244     }
245     return result;
246   }
247
248   /**
249    * Sort files by vcs root
250    *
251    * @param files files to sort.
252    * @return the map from root to the files under the root
253    * @throws VcsException if non git files are passed
254    */
255   public static Map<VirtualFile, List<FilePath>> sortFilePathsByGitRoot(final Collection<FilePath> files) throws VcsException {
256     return sortFilePathsByGitRoot(files, false);
257   }
258
259   /**
260    * Sort files by vcs root
261    *
262    * @param files        files to sort.
263    * @param ignoreNonGit if true, non-git files are ignored
264    * @return the map from root to the files under the root
265    * @throws VcsException if non git files are passed when {@code ignoreNonGit} is false
266    */
267   @NotNull
268   public static Map<VirtualFile, List<FilePath>> sortFilePathsByGitRoot(@NotNull Collection<FilePath> files, boolean ignoreNonGit)
269     throws VcsException {
270     Map<VirtualFile, List<FilePath>> rc = new HashMap<>();
271     for (FilePath p : files) {
272       VirtualFile root = getGitRootOrNull(p);
273       if (root == null) {
274         if (ignoreNonGit) {
275           continue;
276         }
277         else {
278           throw new VcsException("The file " + p.getPath() + " is not under Git");
279         }
280       }
281       List<FilePath> l = rc.get(root);
282       if (l == null) {
283         l = new ArrayList<>();
284         rc.put(root, l);
285       }
286       l.add(p);
287     }
288     return rc;
289   }
290
291   /**
292    * Parse UNIX timestamp as it is returned by the git
293    *
294    * @param value a value to parse
295    * @return timestamp as {@link Date} object
296    */
297   public static Date parseTimestamp(String value) {
298     final long parsed;
299     parsed = Long.parseLong(value.trim());
300     return new Date(parsed * 1000);
301   }
302
303   /**
304    * Parse UNIX timestamp returned from Git and handle {@link NumberFormatException} if one happens: return new {@link Date} and
305    * log the error properly.
306    * In some cases git output gets corrupted and this method is intended to catch the reason, why.
307    * @param value      Value to parse.
308    * @param handler    Git handler that was called to received the output.
309    * @param gitOutput  Git output.
310    * @return Parsed Date or {@code new Date} in the case of error.
311    */
312   public static Date parseTimestampWithNFEReport(String value, GitHandler handler, String gitOutput) {
313     try {
314       return parseTimestamp(value);
315     } catch (NumberFormatException e) {
316       LOG.error("annotate(). NFE. Handler: " + handler + ". Output: " + gitOutput, e);
317       return  new Date();
318     }
319   }
320
321   /**
322    * Get git roots from content roots
323    *
324    * @param roots git content roots
325    * @return a content root
326    */
327   public static Set<VirtualFile> gitRootsForPaths(final Collection<VirtualFile> roots) {
328     HashSet<VirtualFile> rc = new HashSet<>();
329     for (VirtualFile root : roots) {
330       VirtualFile gitRoot = getGitRootOrNull(VcsUtil.getFilePath(root));
331       if (gitRoot != null) {
332         rc.add(gitRoot);
333       }
334     }
335     return rc;
336   }
337
338   /**
339    * Return a git root for the file path (the parent directory with ".git" subdirectory)
340    *
341    * @param filePath a file path
342    * @return git root for the file
343    * @throws IllegalArgumentException if the file is not under git
344    * @throws VcsException             if the file is not under git
345    *
346    * @deprecated because uses the java.io.File.
347    * @use GitRepositoryManager#getRepositoryForFile().
348    */
349   public static VirtualFile getGitRoot(@NotNull FilePath filePath) throws VcsException {
350     VirtualFile root = getGitRootOrNull(filePath);
351     if (root != null) {
352       return root;
353     }
354     throw new VcsException("The file " + filePath + " is not under git.");
355   }
356
357   /**
358    * Return a git root for the file path (the parent directory with ".git" subdirectory)
359    *
360    * @param filePath a file path
361    * @return git root for the file or null if the file is not under git
362    *
363    * @deprecated because uses the java.io.File.
364    * @use GitRepositoryManager#getRepositoryForFile().
365    */
366   @Deprecated
367   @Nullable
368   public static VirtualFile getGitRootOrNull(@NotNull final FilePath filePath) {
369     File root = filePath.getIOFile();
370     while (root != null) {
371       if (isGitRoot(root)) {
372         return LocalFileSystem.getInstance().findFileByIoFile(root);
373       }
374       root = root.getParentFile();
375     }
376     return null;
377   }
378
379   public static boolean isGitRoot(@NotNull File folder) {
380     return findGitDir(folder) != null;
381   }
382
383   /**
384    * Return a git root for the file (the parent directory with ".git" subdirectory)
385    *
386    * @param file the file to check
387    * @return git root for the file
388    * @throws VcsException if the file is not under git
389    *
390    * @deprecated because uses the java.io.File.
391    * @use GitRepositoryManager#getRepositoryForFile().
392    */
393   public static VirtualFile getGitRoot(@NotNull final VirtualFile file) throws VcsException {
394     final VirtualFile root = gitRootOrNull(file);
395     if (root != null) {
396       return root;
397     }
398     else {
399       throw new VcsException("The file " + file.getPath() + " is not under git.");
400     }
401   }
402
403   /**
404    * Return a git root for the file (the parent directory with ".git" subdirectory)
405    *
406    * @param file the file to check
407    * @return git root for the file or null if the file is not not under Git
408    *
409    * @deprecated because uses the java.io.File.
410    * @use GitRepositoryManager#getRepositoryForFile().
411    */
412   @Nullable
413   public static VirtualFile gitRootOrNull(final VirtualFile file) {
414     return getGitRootOrNull(VcsUtil.getFilePath(file.getPath()));
415   }
416
417   /**
418    * Get git roots for the project. The method shows dialogs in the case when roots cannot be retrieved, so it should be called
419    * from the event dispatch thread.
420    *
421    * @param project the project
422    * @param vcs     the git Vcs
423    * @return the list of the roots
424    *
425    * @deprecated because uses the java.io.File.
426    * @use GitRepositoryManager#getRepositoryForFile().
427    */
428   @NotNull
429   public static List<VirtualFile> getGitRoots(Project project, GitVcs vcs) throws VcsException {
430     final VirtualFile[] contentRoots = ProjectLevelVcsManager.getInstance(project).getRootsUnderVcs(vcs);
431     if (contentRoots == null || contentRoots.length == 0) {
432       throw new VcsException(GitBundle.getString("repository.action.missing.roots.unconfigured.message"));
433     }
434     final List<VirtualFile> sortedRoots = DvcsUtil.sortVirtualFilesByPresentation(gitRootsForPaths(Arrays.asList(contentRoots)));
435     if (sortedRoots.size() == 0) {
436       throw new VcsException(GitBundle.getString("repository.action.missing.roots.misconfigured"));
437     }
438     return sortedRoots;
439   }
440
441
442   /**
443    * Check if the virtual file under git
444    *
445    * @param vFile a virtual file
446    * @return true if the file is under git
447    */
448   public static boolean isUnderGit(final VirtualFile vFile) {
449     return gitRootOrNull(vFile) != null;
450   }
451
452
453   /**
454    * Return committer name based on author name and committer name
455    *
456    * @param authorName    the name of author
457    * @param committerName the name of committer
458    * @return just a name if they are equal, or name that includes both author and committer
459    */
460   public static String adjustAuthorName(final String authorName, String committerName) {
461     if (!authorName.equals(committerName)) {
462       //noinspection HardCodedStringLiteral
463       committerName = authorName + ", via " + committerName;
464     }
465     return committerName;
466   }
467
468   /**
469    * Check if the file path is under git
470    *
471    * @param path the path
472    * @return true if the file path is under git
473    */
474   public static boolean isUnderGit(final FilePath path) {
475     return getGitRootOrNull(path) != null;
476   }
477
478   /**
479    * Get git roots for the selected paths
480    *
481    * @param filePaths the context paths
482    * @return a set of git roots
483    */
484   public static Set<VirtualFile> gitRoots(final Collection<FilePath> filePaths) {
485     return filePaths.stream().map(GitUtil::getGitRootOrNull).filter(Objects::nonNull).collect(Collectors.toSet());
486   }
487
488   /**
489    * Get git time (UNIX time) basing on the date object
490    *
491    * @param time the time to convert
492    * @return the time in git format
493    */
494   public static String gitTime(Date time) {
495     long t = time.getTime() / 1000;
496     return Long.toString(t);
497   }
498
499   /**
500    * Format revision number from long to 16-digit abbreviated revision
501    *
502    * @param rev the abbreviated revision number as long
503    * @return the revision string
504    */
505   public static String formatLongRev(long rev) {
506     return String.format("%015x%x", (rev >>> 4), rev & 0xF);
507   }
508
509   public static void getLocalCommittedChanges(final Project project,
510                                               final VirtualFile root,
511                                               final Consumer<GitSimpleHandler> parametersSpecifier,
512                                               final Consumer<GitCommittedChangeList> consumer, boolean skipDiffsForMerge) throws VcsException {
513     GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.LOG);
514     h.setSilent(true);
515     h.addParameters("--pretty=format:%x04%x01" + GitChangeUtils.COMMITTED_CHANGELIST_FORMAT, "--name-status");
516     parametersSpecifier.consume(h);
517
518     String output = h.run();
519     LOG.debug("getLocalCommittedChanges output: '" + output + "'");
520     StringScanner s = new StringScanner(output);
521     final StringBuilder sb = new StringBuilder();
522     boolean firstStep = true;
523     while (s.hasMoreData()) {
524       final String line = s.line();
525       final boolean lineIsAStart = line.startsWith("\u0004\u0001");
526       if ((!firstStep) && lineIsAStart) {
527         final StringScanner innerScanner = new StringScanner(sb.toString());
528         sb.setLength(0);
529         consumer.consume(GitChangeUtils.parseChangeList(project, root, innerScanner, skipDiffsForMerge, h, false, false));
530       }
531       sb.append(lineIsAStart ? line.substring(2) : line).append('\n');
532       firstStep = false;
533     }
534     if (sb.length() > 0) {
535       final StringScanner innerScanner = new StringScanner(sb.toString());
536       sb.setLength(0);
537       consumer.consume(GitChangeUtils.parseChangeList(project, root, innerScanner, skipDiffsForMerge, h, false, false));
538     }
539     if (s.hasMoreData()) {
540       throw new IllegalStateException("More input is avaialble: " + s.line());
541     }
542   }
543
544   public static List<GitCommittedChangeList> getLocalCommittedChanges(final Project project,
545                                                                    final VirtualFile root,
546                                                                    final Consumer<GitSimpleHandler> parametersSpecifier)
547     throws VcsException {
548     final List<GitCommittedChangeList> rc = new ArrayList<>();
549
550     getLocalCommittedChanges(project, root, parametersSpecifier, committedChangeList -> rc.add(committedChangeList), 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"}
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               //noinspection AssignmentToForLoopParameter
639               i--;
640               assert n == b.length;
641               // add them to string
642               final String encoding = GitConfigUtil.getFileNameEncoding();
643               try {
644                 rc.append(new String(b, encoding));
645               }
646               catch (UnsupportedEncodingException e1) {
647                 throw new IllegalStateException("The file name encoding is unsuported: " + encoding);
648               }
649             }
650             else {
651               throw new VcsException("Unknown escape sequence '\\" + path.charAt(i) + "' in the path: " + path);
652             }
653         }
654       }
655       else {
656         rc.append(c);
657       }
658     }
659     return rc.toString();
660   }
661   
662   public static boolean justOneGitRepository(Project project) {
663     if (project.isDisposed()) {
664       return true;
665     }
666     GitRepositoryManager manager = getRepositoryManager(project);
667     return !manager.moreThanOneRoot();
668   }
669
670
671   @Nullable
672   public static GitRemote findRemoteByName(@NotNull GitRepository repository, @NotNull final String name) {
673     return findRemoteByName(repository.getRemotes(), name);
674   }
675
676   @Nullable
677   public static GitRemote findRemoteByName(Collection<GitRemote> remotes, @NotNull final String name) {
678     return ContainerUtil.find(remotes, remote -> remote.getName().equals(name));
679   }
680
681   @Nullable
682   public static GitRemoteBranch findRemoteBranch(@NotNull GitRepository repository,
683                                                          @NotNull final GitRemote remote,
684                                                          @NotNull final String nameAtRemote) {
685     return ContainerUtil.find(repository.getBranches().getRemoteBranches(), remoteBranch -> remoteBranch.getRemote().equals(remote) &&
686                                                                                         remoteBranch.getNameForRemoteOperations().equals(GitBranchUtil.stripRefsPrefix(nameAtRemote)));
687   }
688
689   @NotNull
690   public static GitRemoteBranch findOrCreateRemoteBranch(@NotNull GitRepository repository,
691                                                          @NotNull GitRemote remote,
692                                                          @NotNull String branchName) {
693     GitRemoteBranch remoteBranch = findRemoteBranch(repository, remote, branchName);
694     return ObjectUtils.notNull(remoteBranch, new GitStandardRemoteBranch(remote, branchName));
695   }
696
697   @Nullable
698   public static GitRemote findOrigin(Collection<GitRemote> remotes) {
699     for (GitRemote remote : remotes) {
700       if (remote.getName().equals("origin")) {
701         return remote;
702       }
703     }
704     return null;
705   }
706
707   @NotNull
708   public static Collection<VirtualFile> getRootsFromRepositories(@NotNull Collection<GitRepository> repositories) {
709     return ContainerUtil.map(repositories, REPOSITORY_TO_ROOT);
710   }
711
712   @NotNull
713   public static Collection<GitRepository> getRepositoriesFromRoots(@NotNull GitRepositoryManager repositoryManager,
714                                                                    @NotNull Collection<VirtualFile> roots) {
715     Collection<GitRepository> repositories = new ArrayList<>(roots.size());
716     for (VirtualFile root : roots) {
717       GitRepository repo = repositoryManager.getRepositoryForRoot(root);
718       if (repo == null) {
719         LOG.error("Repository not found for root " + root);
720       }
721       else {
722         repositories.add(repo);
723       }
724     }
725     return repositories;
726   }
727
728   /**
729    * Returns absolute paths which have changed remotely comparing to the current branch, i.e. performs
730    * {@code git diff --name-only master..origin/master}
731    * <p/>
732    * Paths are absolute, Git-formatted (i.e. with forward slashes).
733    */
734   @NotNull
735   public static Collection<String> getPathsDiffBetweenRefs(@NotNull Git git, @NotNull GitRepository repository,
736                                                            @NotNull String beforeRef, @NotNull String afterRef) throws VcsException {
737     List<String> parameters = Arrays.asList("--name-only", "--pretty=format:");
738     String range = beforeRef + ".." + afterRef;
739     GitCommandResult result = git.diff(repository, parameters, range);
740     if (!result.success()) {
741       LOG.info(String.format("Couldn't get diff in range [%s] for repository [%s]", range, repository.toLogString()));
742       return Collections.emptyList();
743     }
744
745     final Collection<String> remoteChanges = new HashSet<>();
746     for (StringScanner s = new StringScanner(result.getOutputAsJoinedString()); s.hasMoreData(); ) {
747       final String relative = s.line();
748       if (StringUtil.isEmptyOrSpaces(relative)) {
749         continue;
750       }
751       final String path = repository.getRoot().getPath() + "/" + unescapePath(relative);
752       remoteChanges.add(path);
753     }
754     return remoteChanges;
755   }
756
757   @NotNull
758   public static GitRepositoryManager getRepositoryManager(@NotNull Project project) {
759     return GitRepositoryManager.getInstance(project);
760   }
761
762   @Nullable
763   public static GitRepository getRepositoryForRootOrLogError(@NotNull Project project, @NotNull VirtualFile root) {
764     GitRepositoryManager manager = getRepositoryManager(project);
765     GitRepository repository = manager.getRepositoryForRoot(root);
766     if (repository == null) {
767       LOG.error("Repository is null for root " + root);
768     }
769     return repository;
770   }
771
772   @NotNull
773   public static String getPrintableRemotes(@NotNull Collection<GitRemote> remotes) {
774     return StringUtil.join(remotes, remote -> remote.getName() + ": [" + StringUtil.join(remote.getUrls(), ", ") + "]", "\n");
775   }
776
777   /**
778    * Show changes made in the specified revision.
779    *
780    * @param project     the project
781    * @param revision    the revision number
782    * @param file        the file affected by the revision
783    * @param local       pass true to let the diff be editable, i.e. making the revision "at the right" be a local (current) revision.
784    *                    pass false to let both sides of the diff be non-editable.
785    * @param revertable  pass true to let "Revert" action be active.
786    */
787   public static void showSubmittedFiles(final Project project, final String revision, final VirtualFile file,
788                                         final boolean local, final boolean revertable) {
789     new Task.Backgroundable(project, GitBundle.message("changes.retrieving", revision)) {
790       public void run(@NotNull ProgressIndicator indicator) {
791         indicator.setIndeterminate(true);
792         try {
793           VirtualFile vcsRoot = getGitRoot(file);
794           final CommittedChangeList changeList = GitChangeUtils.getRevisionChanges(project, vcsRoot, revision, true, local, revertable);
795           if (changeList != null) {
796             UIUtil.invokeLaterIfNeeded(() -> AbstractVcsHelper.getInstance(project).showChangesListBrowser(changeList,
797                                                                                                        GitBundle.message("paths.affected.title", revision)));
798           }
799         }
800         catch (final VcsException e) {
801           UIUtil.invokeLaterIfNeeded(() -> GitUIUtil.showOperationError(project, e, "git show"));
802         }
803       }
804     }.queue();
805   }
806
807
808   /**
809    * Returns the tracking information (remote and the name of the remote branch), or null if we are not on a branch.
810    */
811   @Nullable
812   public static GitBranchTrackInfo getTrackInfoForCurrentBranch(@NotNull GitRepository repository) {
813     GitLocalBranch currentBranch = repository.getCurrentBranch();
814     if (currentBranch == null) {
815       return null;
816     }
817     return GitBranchUtil.getTrackInfoForBranch(repository, currentBranch);
818   }
819
820   @NotNull
821   public static Collection<GitRepository> getRepositoriesForFiles(@NotNull Project project, @NotNull Collection<VirtualFile> files) {
822     final GitRepositoryManager manager = getRepositoryManager(project);
823     com.google.common.base.Function<VirtualFile,GitRepository> ROOT_TO_REPO =
824       root -> root != null ? manager.getRepositoryForRoot(root) : null;
825     return Collections2.filter(Collections2.transform(sortFilesByGitRootsIgnoringOthers(files).keySet(), ROOT_TO_REPO),
826                                Predicates.notNull());
827   }
828
829   @NotNull
830   public static Map<VirtualFile, List<VirtualFile>> sortFilesByGitRootsIgnoringOthers(@NotNull Collection<VirtualFile> files) {
831     try {
832       return sortFilesByGitRoot(files, true);
833     }
834     catch (VcsException e) {
835       LOG.error("Should never happen, since we passed 'ignore non-git' parameter", e);
836       return Collections.emptyMap();
837     }
838   }
839
840   /**
841    * git diff --name-only [--cached]
842    * @return true if there is anything in the unstaged/staging area, false if the unstraed/staging area is empty.
843    * @param staged if true checks the staging area, if false checks unstaged files.
844    * @param project
845    * @param root
846    */
847   public static boolean hasLocalChanges(boolean staged, Project project, VirtualFile root) throws VcsException {
848     final GitSimpleHandler diff = new GitSimpleHandler(project, root, GitCommand.DIFF);
849     diff.addParameters("--name-only");
850     if (staged) {
851       diff.addParameters("--cached");
852     }
853     diff.setStdoutSuppressed(true);
854     diff.setStderrSuppressed(true);
855     diff.setSilent(true);
856     final String output = diff.run();
857     return !output.trim().isEmpty();
858   }
859
860   @Nullable
861   public static VirtualFile findRefreshFileOrLog(@NotNull String absolutePath) {
862     VirtualFile file = LocalFileSystem.getInstance().findFileByPath(absolutePath);
863     if (file == null) {
864       file = LocalFileSystem.getInstance().refreshAndFindFileByPath(absolutePath);
865     }
866     if (file == null) {
867       LOG.warn("VirtualFile not found for " + absolutePath);
868     }
869     return file;
870   }
871
872   @NotNull
873   public static String toAbsolute(@NotNull VirtualFile root, @NotNull String relativePath) {
874     return StringUtil.trimEnd(root.getPath(), "/") + "/" + StringUtil.trimStart(relativePath, "/");
875   }
876
877   @NotNull
878   public static Collection<String> toAbsolute(@NotNull final VirtualFile root, @NotNull Collection<String> relativePaths) {
879     return ContainerUtil.map(relativePaths, s -> toAbsolute(root, s));
880   }
881
882   /**
883    * Given the list of paths converts them to the list of {@link Change Changes} found in the {@link ChangeListManager},
884    * i.e. this works only for local changes. </br>
885    * Paths can be absolute or relative to the repository.
886    * If a path is not found in the local changes, it is ignored, but the fact is logged.
887    */
888   @NotNull
889   public static List<Change> findLocalChangesForPaths(@NotNull Project project, @NotNull VirtualFile root,
890                                                       @NotNull Collection<String> affectedPaths, boolean relativePaths) {
891     ChangeListManagerEx changeListManager = (ChangeListManagerEx)ChangeListManager.getInstance(project);
892     List<Change> affectedChanges = new ArrayList<>();
893     for (String path : affectedPaths) {
894       String absolutePath = relativePaths ? toAbsolute(root, path) : path;
895       VirtualFile file = findRefreshFileOrLog(absolutePath);
896       if (file != null) {
897         Change change = changeListManager.getChange(file);
898         if (change != null) {
899           affectedChanges.add(change);
900         }
901         else {
902           String message = "Change is not found for " + file.getPath();
903           if (changeListManager.isInUpdate()) {
904             message += " because ChangeListManager is being updated.";
905             LOG.debug(message);
906           }
907           else {
908             LOG.info(message);
909           }
910         }
911       }
912     }
913     return affectedChanges;
914   }
915
916   public static void showPathsInDialog(@NotNull Project project, @NotNull Collection<String> absolutePaths, @NotNull String title,
917                                        @Nullable String description) {
918     DialogBuilder builder = new DialogBuilder(project);
919     builder.setCenterPanel(new GitSimplePathsBrowser(project, absolutePaths));
920     if (description != null) {
921       builder.setNorthPanel(new MultiLineLabel(description));
922     }
923     builder.addOkAction();
924     builder.setTitle(title);
925     builder.show();
926   }
927
928   @NotNull
929   public static String cleanupErrorPrefixes(@NotNull String msg) {
930     final String[] PREFIXES = { "fatal:", "error:" };
931     msg = msg.trim();
932     for (String prefix : PREFIXES) {
933       if (msg.startsWith(prefix)) {
934         msg = msg.substring(prefix.length()).trim();
935       }
936     }
937     return msg;
938   }
939
940   @Nullable
941   public static GitRemote getDefaultRemote(@NotNull Collection<GitRemote> remotes) {
942     for (GitRemote remote : remotes) {
943       if (remote.getName().equals(GitRemote.ORIGIN)) {
944         return remote;
945       }
946     }
947     return null;
948   }
949
950   @NotNull
951   public static String joinToHtml(@NotNull Collection<GitRepository> repositories) {
952     return StringUtil.join(repositories, repository -> repository.getPresentableUrl(), "<br/>");
953   }
954
955   @NotNull
956   public static String mention(@NotNull GitRepository repository) {
957     return getRepositoryManager(repository.getProject()).moreThanOneRoot() ? " in " + getShortRepositoryName(repository) : "";
958   }
959
960   @NotNull
961   public static String mention(@NotNull Collection<GitRepository> repositories) {
962     return mention(repositories, -1);
963   }
964
965   @NotNull
966   public static String mention(@NotNull Collection<GitRepository> repositories, int limit) {
967     if (repositories.isEmpty()) return "";
968     return " in " + joinShortNames(repositories, limit);
969   }
970
971   public static void updateRepositories(@NotNull Collection<GitRepository> repositories) {
972     for (GitRepository repository : repositories) {
973       repository.update();
974     }
975   }
976
977   public static boolean hasGitRepositories(@NotNull Project project) {
978     return !getRepositories(project).isEmpty();
979   }
980
981   @NotNull
982   public static Collection<GitRepository> getRepositories(@NotNull Project project) {
983     return getRepositoryManager(project).getRepositories();
984   }
985
986   /**
987    * Checks if the given paths are equal only by case.
988    * It is expected that the paths are different at least by the case.
989    */
990   public static boolean isCaseOnlyChange(@NotNull String oldPath, @NotNull String newPath) {
991     if (oldPath.equalsIgnoreCase(newPath)) {
992       if (oldPath.equals(newPath)) {
993         LOG.error("Comparing perfectly equal paths: " + newPath);
994       }
995       return true;
996     }
997     return false;
998   }
999
1000   @NotNull
1001   public static String getLogString(@NotNull String root, @NotNull Collection<Change> changes) {
1002     return StringUtil.join(changes, change -> {
1003       ContentRevision after = change.getAfterRevision();
1004       ContentRevision before = change.getBeforeRevision();
1005       switch (change.getType()) {
1006         case NEW: return "A: " + getRelativePath(root, assertNotNull(after));
1007         case DELETED: return "D: " + getRelativePath(root, assertNotNull(before));
1008         case MOVED: return "M: " + getRelativePath(root, assertNotNull(before)) + " -> " + getRelativePath(root, assertNotNull(after));
1009         default: return "M: " + getRelativePath(root, assertNotNull(after));
1010       }
1011     }, ", ");
1012   }
1013
1014   @Nullable
1015   public static String getRelativePath(@NotNull String root, @NotNull ContentRevision after) {
1016     return FileUtil.getRelativePath(root, after.getFile().getPath(), File.separatorChar);
1017   }
1018
1019   /**
1020    * <p>Finds the local changes which are "the same" as the given changes.</p>
1021    * <p>The purpose of this method is to get actual local changes after some other changes were applied to the working tree
1022    * (e.g. if they were cherry-picked from a commit). Working with the original non-local changes is limited, in particular,
1023    * the difference between content revisions may be not the same as the local change.</p>
1024    * <p>"The same" here means the changes made in the same files. It is possible that there was a change made in file A in the original
1025    * commit, but there are no local changes made in file A. Such situations are ignored.</p>
1026    */
1027   @NotNull
1028   public static Collection<Change> findCorrespondentLocalChanges(@NotNull ChangeListManager changeListManager,
1029                                                                  @NotNull Collection<? extends Change> originalChanges) {
1030     OpenTHashSet<Change> allChanges = new OpenTHashSet<>(changeListManager.getAllChanges());
1031     return ContainerUtil.mapNotNull(originalChanges, allChanges::get);
1032   }
1033 }